mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-04 04:58:30 +01:00
Compare commits
188 Commits
revert-377
...
chore/expo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4d9552401 | ||
|
|
c74336a1a1 | ||
|
|
17257ece85 | ||
|
|
d5c634b74b | ||
|
|
933f3f2f7c | ||
|
|
252fc4387b | ||
|
|
3e299e2136 | ||
|
|
01cab2277e | ||
|
|
e4f4e861e0 | ||
|
|
4d665013f0 | ||
|
|
9aa4ea4a2e | ||
|
|
93ae03f55c | ||
|
|
b311ac98a7 | ||
|
|
83d425b2fb | ||
|
|
007fbdd0a3 | ||
|
|
37df999db5 | ||
|
|
72b9675df4 | ||
|
|
7a30a63335 | ||
|
|
0ff0fab3f4 | ||
|
|
d9d9b0ee00 | ||
|
|
fdaa69a787 | ||
|
|
ed5403e597 | ||
|
|
e6f290b85f | ||
|
|
aa20d9c701 | ||
|
|
e7128afb32 | ||
|
|
a24b126539 | ||
|
|
e1fe20db86 | ||
|
|
cd9f6aa8bd | ||
|
|
747bd1b416 | ||
|
|
364ce46fe5 | ||
|
|
5703279b46 | ||
|
|
4022ccb213 | ||
|
|
3a836462f5 | ||
|
|
8a5f24002f | ||
|
|
c30f9860ee | ||
|
|
94c170e3d2 | ||
|
|
cd8aba32d8 | ||
|
|
15f3ddf612 | ||
|
|
90f20f6e46 | ||
|
|
ea1f45bbaf | ||
|
|
7e62c9bc9a | ||
|
|
23f9e9dfae | ||
|
|
580e12b605 | ||
|
|
ff4c5f28af | ||
|
|
1b931ea348 | ||
|
|
49c0437f81 | ||
|
|
d81ae94ce8 | ||
|
|
7c77c70024 | ||
|
|
b28c4a56f3 | ||
|
|
2495a318eb | ||
|
|
7832ea4d0a | ||
|
|
4a0a51ef1d | ||
|
|
8cc551d906 | ||
|
|
c8da365a00 | ||
|
|
74b7cbc530 | ||
|
|
a14063a736 | ||
|
|
a3307a90a3 | ||
|
|
a2145fd7e8 | ||
|
|
cab5e4d980 | ||
|
|
ab603e6997 | ||
|
|
957348fe19 | ||
|
|
444bd040b0 | ||
|
|
d0ae63235d | ||
|
|
1727125ea7 | ||
|
|
dc498d62d8 | ||
|
|
455bf08213 | ||
|
|
0f974ef2a3 | ||
|
|
2d9aaccfe0 | ||
|
|
2c6823eb53 | ||
|
|
9dfcc01f17 | ||
|
|
38aad9610b | ||
|
|
54af64abef | ||
|
|
e1720a00da | ||
|
|
882d0ea188 | ||
|
|
f3b539232f | ||
|
|
33ea657a5c | ||
|
|
75820adcbc | ||
|
|
76cdb2b3f8 | ||
|
|
0a2ea33635 | ||
|
|
aad6093852 | ||
|
|
c553cff9d1 | ||
|
|
dcd458bd3d | ||
|
|
05dc61d17d | ||
|
|
e4de11127f | ||
|
|
2dc49735f4 | ||
|
|
0ebacd4bd3 | ||
|
|
14c8c1aaed | ||
|
|
2da774272d | ||
|
|
ef42207174 | ||
|
|
efa5638b12 | ||
|
|
c63cea891d | ||
|
|
4e80f58823 | ||
|
|
cfe39d504c | ||
|
|
cf43d1a657 | ||
|
|
cbe3b18226 | ||
|
|
b637a0f7d2 | ||
|
|
a0ce7cc6d0 | ||
|
|
a640df30bc | ||
|
|
062e6e6c23 | ||
|
|
d709e3b13e | ||
|
|
b232bebd73 | ||
|
|
90ef8ef6f9 | ||
|
|
0df6b8e2a0 | ||
|
|
f48b26076d | ||
|
|
c86a8438e5 | ||
|
|
faa2baae68 | ||
|
|
ed42371353 | ||
|
|
24277135a8 | ||
|
|
23d9cd36d1 | ||
|
|
b243524a7d | ||
|
|
8288682e68 | ||
|
|
58ec915699 | ||
|
|
480abb216d | ||
|
|
249109a94e | ||
|
|
eb7fa93f9b | ||
|
|
e8fd322d30 | ||
|
|
cad03a3566 | ||
|
|
9baa4063bd | ||
|
|
41db34ed8e | ||
|
|
5aba66ce05 | ||
|
|
79407ccd70 | ||
|
|
9a93b3b3bb | ||
|
|
2b846a1aca | ||
|
|
55d61172f4 | ||
|
|
57173a62dc | ||
|
|
78f65be09d | ||
|
|
fc4a11d916 | ||
|
|
cf2beb8299 | ||
|
|
49d157a95a | ||
|
|
a061f9f480 | ||
|
|
0fb6f2fb30 | ||
|
|
0773f773ba | ||
|
|
39bb3a9370 | ||
|
|
b79e534692 | ||
|
|
e9336e9a67 | ||
|
|
adfde1a7cd | ||
|
|
cab6257fb2 | ||
|
|
3f0f0090af | ||
|
|
b278632581 | ||
|
|
5ee1a9cabb | ||
|
|
2169bea031 | ||
|
|
95cf252349 | ||
|
|
636a27246f | ||
|
|
a488c68633 | ||
|
|
7342b7eb92 | ||
|
|
8370519758 | ||
|
|
85e21edbf1 | ||
|
|
8d4115f5a0 | ||
|
|
c5d7a6729b | ||
|
|
db4046267f | ||
|
|
b506871c46 | ||
|
|
734678b1d5 | ||
|
|
68e98bbb94 | ||
|
|
d84ed558f3 | ||
|
|
ad39e8e10a | ||
|
|
29bba04fdd | ||
|
|
5a24957e88 | ||
|
|
39f2735756 | ||
|
|
53ea1cc899 | ||
|
|
5dc86d4765 | ||
|
|
d13731c28f | ||
|
|
459ca3245b | ||
|
|
0d1fb87284 | ||
|
|
495742c52c | ||
|
|
894305e126 | ||
|
|
ed993d07ce | ||
|
|
dc9008f31c | ||
|
|
f92fee4158 | ||
|
|
e23387a384 | ||
|
|
bb141cad57 | ||
|
|
e833b4bc68 | ||
|
|
34fc26ed18 | ||
|
|
40b8410390 | ||
|
|
723233381c | ||
|
|
602de34824 | ||
|
|
9b1f2a98e5 | ||
|
|
946de97580 | ||
|
|
f2eadabf6a | ||
|
|
373d83a0d5 | ||
|
|
2c0ba18b49 | ||
|
|
3e8e8e1163 | ||
|
|
fe9c73a8f0 | ||
|
|
4f62391027 | ||
|
|
53b5fdda87 | ||
|
|
c0b71eb73d | ||
|
|
9b4590c876 | ||
|
|
4b18bad3bc | ||
|
|
752cb1cdc6 |
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -4,7 +4,9 @@ title: "[Bug]: "
|
|||||||
labels:
|
labels:
|
||||||
- ["❌ bug"]
|
- ["❌ bug"]
|
||||||
projects:
|
projects:
|
||||||
- ["streamyfin/3"]
|
- ["fredrikburmester/5"]
|
||||||
|
assignees:
|
||||||
|
- fredrikburmester
|
||||||
|
|
||||||
body:
|
body:
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@@ -43,7 +45,7 @@ body:
|
|||||||
label: Version
|
label: Version
|
||||||
description: What version of Streamyfin are you running?
|
description: What version of Streamyfin are you running?
|
||||||
options:
|
options:
|
||||||
- 0.24.0
|
- 0.23.0
|
||||||
- 0.22.0
|
- 0.22.0
|
||||||
- 0.21.0
|
- 0.21.0
|
||||||
- older
|
- older
|
||||||
|
|||||||
3
.github/ISSUE_TEMPLATE/feature_request.md
vendored
3
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -4,8 +4,7 @@ about: Suggest an idea for this project
|
|||||||
title: ''
|
title: ''
|
||||||
labels: '✨ enhancement'
|
labels: '✨ enhancement'
|
||||||
assignees: ''
|
assignees: ''
|
||||||
projects:
|
|
||||||
- streamyfin/3
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
**Describe the solution you'd like**
|
||||||
|
|||||||
49
.github/workflows/build-ios.yaml
vendored
49
.github/workflows/build-ios.yaml
vendored
@@ -1,49 +0,0 @@
|
|||||||
name: Automatic Build and Deploy
|
|
||||||
|
|
||||||
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: appstore
|
|
||||||
#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
|
|
||||||
@@ -13,7 +13,7 @@ Welcome to Streamyfin, a simple and user-friendly Jellyfin client built with Exp
|
|||||||
|
|
||||||
## 🌟 Features
|
## 🌟 Features
|
||||||
|
|
||||||
- 🚀 **Skip Intro / Credits Support**
|
- 🚀 **Skp intro / credits support**
|
||||||
- 🖼️ **Trickplay images**: The new golden standard for chapter previews when seeking.
|
- 🖼️ **Trickplay images**: The new golden standard for chapter previews when seeking.
|
||||||
- 🔊 **Background audio**: Stream music in the background, even when locking the phone.
|
- 🔊 **Background audio**: Stream music in the background, even when locking the phone.
|
||||||
- 📥 **Download media** (Experimental): Save your media locally and watch it offline.
|
- 📥 **Download media** (Experimental): Save your media locally and watch it offline.
|
||||||
|
|||||||
7
app.json
7
app.json
@@ -2,7 +2,7 @@
|
|||||||
"expo": {
|
"expo": {
|
||||||
"name": "Streamyfin",
|
"name": "Streamyfin",
|
||||||
"slug": "streamyfin",
|
"slug": "streamyfin",
|
||||||
"version": "0.24.0",
|
"version": "0.23.0",
|
||||||
"orientation": "default",
|
"orientation": "default",
|
||||||
"icon": "./assets/images/icon.png",
|
"icon": "./assets/images/icon.png",
|
||||||
"scheme": "streamyfin",
|
"scheme": "streamyfin",
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"jsEngine": "hermes",
|
"jsEngine": "hermes",
|
||||||
"versionCode": 50,
|
"versionCode": 49,
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"foregroundImage": "./assets/images/adaptive_icon.png"
|
"foregroundImage": "./assets/images/adaptive_icon.png"
|
||||||
},
|
},
|
||||||
@@ -130,6 +130,7 @@
|
|||||||
},
|
},
|
||||||
"updates": {
|
"updates": {
|
||||||
"url": "https://u.expo.dev/e79219d1-797f-4fbe-9fa1-cfd360690a68"
|
"url": "https://u.expo.dev/e79219d1-797f-4fbe-9fa1-cfd360690a68"
|
||||||
}
|
},
|
||||||
|
"newArchEnabled": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { SubtitleToggles } from "@/components/settings/SubtitleToggles";
|
|||||||
import { UserInfo } from "@/components/settings/UserInfo";
|
import { UserInfo } from "@/components/settings/UserInfo";
|
||||||
import { useJellyfin } from "@/providers/JellyfinProvider";
|
import { useJellyfin } from "@/providers/JellyfinProvider";
|
||||||
import { clearLogs } from "@/utils/log";
|
import { clearLogs } from "@/utils/log";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import * as Haptics from "expo-haptics";
|
||||||
import { useNavigation, useRouter } from "expo-router";
|
import { useNavigation, useRouter } from "expo-router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { ScrollView, TouchableOpacity, View } from "react-native";
|
import { ScrollView, TouchableOpacity, View } from "react-native";
|
||||||
@@ -23,11 +23,10 @@ export default function settings() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { logout } = useJellyfin();
|
const { logout } = useJellyfin();
|
||||||
const successHapticFeedback = useHaptic("success");
|
|
||||||
|
|
||||||
const onClearLogsClicked = async () => {
|
const onClearLogsClicked = async () => {
|
||||||
clearLogs();
|
clearLogs();
|
||||||
successHapticFeedback();
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||||
};
|
};
|
||||||
|
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
getUserLibraryApi,
|
getUserLibraryApi,
|
||||||
} from "@jellyfin/sdk/lib/utils/api";
|
} from "@jellyfin/sdk/lib/utils/api";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import * as Haptics from "expo-haptics";
|
||||||
import { useFocusEffect, useGlobalSearchParams } from "expo-router";
|
import { useFocusEffect, useGlobalSearchParams } from "expo-router";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import React, {
|
import React, {
|
||||||
@@ -68,11 +68,9 @@ export default function page() {
|
|||||||
const { getDownloadedItem } = useDownload();
|
const { getDownloadedItem } = useDownload();
|
||||||
const revalidateProgressCache = useInvalidatePlaybackProgressCache();
|
const revalidateProgressCache = useInvalidatePlaybackProgressCache();
|
||||||
|
|
||||||
const lightHapticFeedback = useHaptic("light");
|
|
||||||
|
|
||||||
const setShowControls = useCallback((show: boolean) => {
|
const setShowControls = useCallback((show: boolean) => {
|
||||||
_setShowControls(show);
|
_setShowControls(show);
|
||||||
lightHapticFeedback();
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -177,7 +175,7 @@ export default function page() {
|
|||||||
const togglePlay = useCallback(async () => {
|
const togglePlay = useCallback(async () => {
|
||||||
if (!api) return;
|
if (!api) return;
|
||||||
|
|
||||||
lightHapticFeedback();
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
await videoRef.current?.pause();
|
await videoRef.current?.pause();
|
||||||
|
|
||||||
@@ -437,6 +435,7 @@ export default function page() {
|
|||||||
position: "relative",
|
position: "relative",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
|
opacity: showControls ? (Platform.OS === "android" ? 0.7 : 0.5) : 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<VlcPlayerView
|
<VlcPlayerView
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
getUserLibraryApi,
|
getUserLibraryApi,
|
||||||
} from "@jellyfin/sdk/lib/utils/api";
|
} from "@jellyfin/sdk/lib/utils/api";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import * as Haptics from "expo-haptics";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
@@ -45,8 +45,6 @@ export default function page() {
|
|||||||
const isSeeking = useSharedValue(false);
|
const isSeeking = useSharedValue(false);
|
||||||
const cacheProgress = useSharedValue(0);
|
const cacheProgress = useSharedValue(0);
|
||||||
|
|
||||||
const lightHapticFeedback = useHaptic("light");
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
itemId,
|
itemId,
|
||||||
audioIndex: audioIndexStr,
|
audioIndex: audioIndexStr,
|
||||||
@@ -126,7 +124,7 @@ export default function page() {
|
|||||||
|
|
||||||
const togglePlay = useCallback(
|
const togglePlay = useCallback(
|
||||||
async (ticks: number) => {
|
async (ticks: number) => {
|
||||||
lightHapticFeedback();
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
videoRef.current?.pause();
|
videoRef.current?.pause();
|
||||||
await getPlaystateApi(api!).onPlaybackProgress({
|
await getPlaystateApi(api!).onPlaybackProgress({
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
getUserLibraryApi,
|
getUserLibraryApi,
|
||||||
} from "@jellyfin/sdk/lib/utils/api";
|
} from "@jellyfin/sdk/lib/utils/api";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import * as Haptics from "expo-haptics";
|
||||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import React, {
|
import React, {
|
||||||
@@ -48,7 +48,6 @@ const Player = () => {
|
|||||||
|
|
||||||
const firstTime = useRef(true);
|
const firstTime = useRef(true);
|
||||||
const revalidateProgressCache = useInvalidatePlaybackProgressCache();
|
const revalidateProgressCache = useInvalidatePlaybackProgressCache();
|
||||||
const lightHapticFeedback = useHaptic("light");
|
|
||||||
|
|
||||||
const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
|
const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
|
||||||
const [showControls, _setShowControls] = useState(true);
|
const [showControls, _setShowControls] = useState(true);
|
||||||
@@ -59,7 +58,7 @@ const Player = () => {
|
|||||||
|
|
||||||
const setShowControls = useCallback((show: boolean) => {
|
const setShowControls = useCallback((show: boolean) => {
|
||||||
_setShowControls(show);
|
_setShowControls(show);
|
||||||
lightHapticFeedback();
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const progress = useSharedValue(0);
|
const progress = useSharedValue(0);
|
||||||
@@ -168,7 +167,7 @@ const Player = () => {
|
|||||||
const videoSource = useVideoSource(item, api, poster, stream?.url);
|
const videoSource = useVideoSource(item, api, poster, stream?.url);
|
||||||
|
|
||||||
const togglePlay = useCallback(async () => {
|
const togglePlay = useCallback(async () => {
|
||||||
lightHapticFeedback();
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
videoRef.current?.pause();
|
videoRef.current?.pause();
|
||||||
await getPlaystateApi(api!).onPlaybackProgress({
|
await getPlaystateApi(api!).onPlaybackProgress({
|
||||||
@@ -388,6 +387,7 @@ const Player = () => {
|
|||||||
position: "relative",
|
position: "relative",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
|
opacity: showControls ? 0.5 : 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{videoSource ? (
|
{videoSource ? (
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import { useGlobalSearchParams } from "expo-router";
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
||||||
import { Alert, Dimensions, View } from "react-native";
|
|
||||||
import YoutubePlayer, { PLAYER_STATES } from "react-native-youtube-iframe";
|
|
||||||
|
|
||||||
export default function page() {
|
|
||||||
const searchParams = useGlobalSearchParams();
|
|
||||||
|
|
||||||
const { url } = searchParams as { url: string };
|
|
||||||
|
|
||||||
const videoId = useMemo(() => {
|
|
||||||
return url.split("v=")[1];
|
|
||||||
}, [url]);
|
|
||||||
|
|
||||||
const [playing, setPlaying] = useState(false);
|
|
||||||
|
|
||||||
const onStateChange = useCallback((state: PLAYER_STATES) => {
|
|
||||||
if (state === "ended") {
|
|
||||||
setPlaying(false);
|
|
||||||
Alert.alert("video has finished playing!");
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const togglePlaying = useCallback(() => {
|
|
||||||
setPlaying((prev) => !prev);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
togglePlaying();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const screenWidth = Dimensions.get("screen").width;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View className="flex flex-col bg-black items-center justify-center h-full">
|
|
||||||
<YoutubePlayer
|
|
||||||
height={300}
|
|
||||||
play={playing}
|
|
||||||
videoId={videoId}
|
|
||||||
onChangeState={onStateChange}
|
|
||||||
width={screenWidth}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -319,7 +319,7 @@ function Layout() {
|
|||||||
<BottomSheetModalProvider>
|
<BottomSheetModalProvider>
|
||||||
<SystemBars style="light" hidden={false} />
|
<SystemBars style="light" hidden={false} />
|
||||||
<ThemeProvider value={DarkTheme}>
|
<ThemeProvider value={DarkTheme}>
|
||||||
<Stack initialRouteName="/home">
|
<Stack>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="(auth)/(tabs)"
|
name="(auth)/(tabs)"
|
||||||
options={{
|
options={{
|
||||||
@@ -336,14 +336,6 @@ function Layout() {
|
|||||||
header: () => null,
|
header: () => null,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
|
||||||
name="(auth)/trailer/page"
|
|
||||||
options={{
|
|
||||||
headerShown: false,
|
|
||||||
presentation: "modal",
|
|
||||||
title: "",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="login"
|
name="login"
|
||||||
options={{
|
options={{
|
||||||
|
|||||||
230
app/login.tsx
230
app/login.tsx
@@ -189,129 +189,133 @@ const Login: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
if (api?.basePath) {
|
||||||
<SafeAreaView style={{ flex: 1, paddingBottom: 16 }}>
|
return (
|
||||||
<KeyboardAvoidingView
|
<SafeAreaView style={{ flex: 1 }}>
|
||||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
<KeyboardAvoidingView
|
||||||
>
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||||
{api?.basePath ? (
|
style={{ flex: 1, height: "100%" }}
|
||||||
<>
|
>
|
||||||
<View className="flex flex-col h-full relative items-center justify-center">
|
<View className="flex flex-col h-full relative items-center justify-center">
|
||||||
<View className="px-4 -mt-20 w-full">
|
<View className="px-4 -mt-20 w-full">
|
||||||
<View className="flex flex-col space-y-2">
|
<View className="flex flex-col space-y-2">
|
||||||
<Text className="text-2xl font-bold -mb-2">
|
<Text className="text-2xl font-bold -mb-2">
|
||||||
Log in
|
|
||||||
<>
|
|
||||||
{serverName ? (
|
|
||||||
<>
|
|
||||||
{" to "}
|
|
||||||
<Text className="text-purple-600">{serverName}</Text>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
</Text>
|
|
||||||
<Text className="text-xs text-neutral-400">
|
|
||||||
{api.basePath}
|
|
||||||
</Text>
|
|
||||||
<Input
|
|
||||||
placeholder="Username"
|
|
||||||
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="Password"
|
|
||||||
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>
|
|
||||||
|
|
||||||
<View className="absolute bottom-0 left-0 w-full px-4 mb-2">
|
|
||||||
<Button
|
|
||||||
color="black"
|
|
||||||
onPress={handleQuickConnect}
|
|
||||||
className="w-full mb-2"
|
|
||||||
>
|
|
||||||
Use Quick Connect
|
|
||||||
</Button>
|
|
||||||
<Button onPress={handleLogin} loading={loading}>
|
|
||||||
Log in
|
Log in
|
||||||
</Button>
|
<>
|
||||||
</View>
|
{serverName ? (
|
||||||
</View>
|
<>
|
||||||
</>
|
{" to "}
|
||||||
) : (
|
<Text className="text-purple-600">{serverName}</Text>
|
||||||
<>
|
</>
|
||||||
<View className="flex flex-col h-full relative items-center justify-center w-full">
|
) : null}
|
||||||
<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">
|
|
||||||
Enter the URL to your Jellyfin server
|
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text className="text-xs text-neutral-400">{api.basePath}</Text>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Server URL"
|
placeholder="Username"
|
||||||
onChangeText={setServerURL}
|
onChangeText={(text) =>
|
||||||
value={serverURL}
|
setCredentials({ ...credentials, username: text })
|
||||||
keyboardType="url"
|
}
|
||||||
|
value={credentials.username}
|
||||||
|
autoFocus
|
||||||
|
secureTextEntry={false}
|
||||||
|
keyboardType="default"
|
||||||
returnKeyType="done"
|
returnKeyType="done"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
textContentType="URL"
|
textContentType="username"
|
||||||
|
clearButtonMode="while-editing"
|
||||||
maxLength={500}
|
maxLength={500}
|
||||||
/>
|
/>
|
||||||
<Text className="text-xs text-neutral-500 ml-4">
|
|
||||||
Make sure to include http or https
|
<Input
|
||||||
</Text>
|
className="mb-2"
|
||||||
<PreviousServersList
|
placeholder="Password"
|
||||||
onServerSelect={(s) => {
|
onChangeText={(text) =>
|
||||||
handleConnect(s.address);
|
setCredentials({ ...credentials, password: text })
|
||||||
}}
|
}
|
||||||
|
value={credentials.password}
|
||||||
|
secureTextEntry
|
||||||
|
keyboardType="default"
|
||||||
|
returnKeyType="done"
|
||||||
|
autoCapitalize="none"
|
||||||
|
textContentType="password"
|
||||||
|
clearButtonMode="while-editing"
|
||||||
|
maxLength={500}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View className="mb-2 absolute bottom-0 left-0 w-full px-4">
|
|
||||||
<Button
|
<Text className="text-red-600 mb-2">{error}</Text>
|
||||||
loading={loadingServerCheck}
|
|
||||||
disabled={loadingServerCheck}
|
|
||||||
onPress={async () => await handleConnect(serverURL)}
|
|
||||||
className="w-full grow"
|
|
||||||
>
|
|
||||||
Connect
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</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"
|
||||||
|
>
|
||||||
|
Use Quick Connect
|
||||||
|
</Button>
|
||||||
|
<Button onPress={handleLogin} loading={loading}>
|
||||||
|
Log in
|
||||||
|
</Button>
|
||||||
|
</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">
|
||||||
|
Enter the URL to your Jellyfin server
|
||||||
|
</Text>
|
||||||
|
<Input
|
||||||
|
placeholder="Server URL"
|
||||||
|
onChangeText={setServerURL}
|
||||||
|
value={serverURL}
|
||||||
|
keyboardType="url"
|
||||||
|
returnKeyType="done"
|
||||||
|
autoCapitalize="none"
|
||||||
|
textContentType="URL"
|
||||||
|
maxLength={500}
|
||||||
|
/>
|
||||||
|
<Text className="text-xs text-neutral-500 ml-4">
|
||||||
|
Make sure to include http or https
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import * as Haptics from "expo-haptics";
|
||||||
import React, { PropsWithChildren, ReactNode, useMemo } from "react";
|
import React, { PropsWithChildren, ReactNode, useMemo } from "react";
|
||||||
import { Text, TouchableOpacity, View } from "react-native";
|
import { Text, TouchableOpacity, View } from "react-native";
|
||||||
import { Loader } from "./Loader";
|
import { Loader } from "./Loader";
|
||||||
@@ -43,8 +43,6 @@ export const Button: React.FC<PropsWithChildren<ButtonProps>> = ({
|
|||||||
}
|
}
|
||||||
}, [color]);
|
}, [color]);
|
||||||
|
|
||||||
const lightHapticFeedback = useHaptic("light");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
className={`
|
className={`
|
||||||
@@ -56,7 +54,7 @@ export const Button: React.FC<PropsWithChildren<ButtonProps>> = ({
|
|||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (!loading && !disabled && onPress) {
|
if (!loading && !disabled && onPress) {
|
||||||
onPress();
|
onPress();
|
||||||
lightHapticFeedback();
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={disabled || loading}
|
disabled={disabled || loading}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import Animated, {
|
|||||||
import { Button } from "./Button";
|
import { Button } from "./Button";
|
||||||
import { SelectedOptions } from "./ItemContent";
|
import { SelectedOptions } from "./ItemContent";
|
||||||
import { chromecastProfile } from "@/utils/profiles/chromecast";
|
import { chromecastProfile } from "@/utils/profiles/chromecast";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import * as Haptics from "expo-haptics";
|
||||||
|
|
||||||
interface Props extends React.ComponentProps<typeof Button> {
|
interface Props extends React.ComponentProps<typeof Button> {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
@@ -64,7 +64,6 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
const widthProgress = useSharedValue(0);
|
const widthProgress = useSharedValue(0);
|
||||||
const colorChangeProgress = useSharedValue(0);
|
const colorChangeProgress = useSharedValue(0);
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
const lightHapticFeedback = useHaptic("light");
|
|
||||||
|
|
||||||
const goToPlayer = useCallback(
|
const goToPlayer = useCallback(
|
||||||
(q: string, bitrateValue: number | undefined) => {
|
(q: string, bitrateValue: number | undefined) => {
|
||||||
@@ -80,7 +79,7 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
const onPress = useCallback(async () => {
|
const onPress = useCallback(async () => {
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
|
|
||||||
lightHapticFeedback();
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
|
|
||||||
const queryParams = new URLSearchParams({
|
const queryParams = new URLSearchParams({
|
||||||
itemId: item.Id!,
|
itemId: item.Id!,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
TouchableOpacityProps,
|
TouchableOpacityProps,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import * as Haptics from "expo-haptics";
|
||||||
|
|
||||||
interface Props extends TouchableOpacityProps {
|
interface Props extends TouchableOpacityProps {
|
||||||
onPress?: () => void;
|
onPress?: () => void;
|
||||||
@@ -29,11 +29,10 @@ export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const buttonSize = size === "large" ? "h-10 w-10" : "h-9 w-9";
|
const buttonSize = size === "large" ? "h-10 w-10" : "h-9 w-9";
|
||||||
const fillColorClass = fillColor === "primary" ? "bg-purple-600" : "";
|
const fillColorClass = fillColor === "primary" ? "bg-purple-600" : "";
|
||||||
const lightHapticFeedback = useHaptic("light");
|
|
||||||
|
|
||||||
const handlePress = () => {
|
const handlePress = () => {
|
||||||
if (hapticFeedback) {
|
if (hapticFeedback) {
|
||||||
lightHapticFeedback();
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
}
|
}
|
||||||
onPress?.();
|
onPress?.();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import renderer from 'react-test-renderer';
|
|
||||||
|
|
||||||
import { ThemedText } from '../ThemedText';
|
|
||||||
|
|
||||||
it(`renders correctly`, () => {
|
|
||||||
const tree = renderer.create(<ThemedText>Snapshot test!</ThemedText>).toJSON();
|
|
||||||
|
|
||||||
expect(tree).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`renders correctly 1`] = `
|
|
||||||
<Text
|
|
||||||
style={
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"color": "#11181C",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fontSize": 16,
|
|
||||||
"lineHeight": 24,
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Snapshot test!
|
|
||||||
</Text>
|
|
||||||
`;
|
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import {useRouter, useSegments} from "expo-router";
|
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
||||||
import React, {PropsWithChildren, useCallback, useMemo} from "react";
|
import {
|
||||||
import {TouchableOpacity, TouchableOpacityProps} from "react-native";
|
hasPermission,
|
||||||
import * as ContextMenu from "zeego/context-menu";
|
Permission,
|
||||||
import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search";
|
} from "@/utils/jellyseerr/server/lib/permissions";
|
||||||
import {useJellyseerr} from "@/hooks/useJellyseerr";
|
import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search";
|
||||||
import {hasPermission, Permission} from "@/utils/jellyseerr/server/lib/permissions";
|
import { useRouter, useSegments } from "expo-router";
|
||||||
import {MediaType} from "@/utils/jellyseerr/server/constants/media";
|
import React, { PropsWithChildren, useCallback, useMemo } from "react";
|
||||||
|
import { TouchableOpacity, TouchableOpacityProps } from "react-native";
|
||||||
|
|
||||||
interface Props extends TouchableOpacityProps {
|
interface Props extends TouchableOpacityProps {
|
||||||
result: MovieResult | TvResult;
|
result: MovieResult | TvResult;
|
||||||
@@ -26,78 +27,49 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const segments = useSegments();
|
const segments = useSegments();
|
||||||
const {jellyseerrApi, jellyseerrUser, requestMedia} = useJellyseerr()
|
const { jellyseerrApi, jellyseerrUser, requestMedia } = useJellyseerr();
|
||||||
|
|
||||||
const from = segments[2];
|
const from = segments[2];
|
||||||
|
|
||||||
const autoApprove = useMemo(() => {
|
const autoApprove = useMemo(() => {
|
||||||
return jellyseerrUser && hasPermission(
|
return (
|
||||||
Permission.AUTO_APPROVE,
|
jellyseerrUser &&
|
||||||
jellyseerrUser.permissions,
|
hasPermission(Permission.AUTO_APPROVE, jellyseerrUser.permissions, {
|
||||||
{type: 'or'}
|
type: "or",
|
||||||
)
|
})
|
||||||
}, [jellyseerrApi, jellyseerrUser])
|
);
|
||||||
|
}, [jellyseerrApi, jellyseerrUser]);
|
||||||
|
|
||||||
const request = useCallback(() =>
|
const request = useCallback(
|
||||||
|
() =>
|
||||||
requestMedia(mediaTitle, {
|
requestMedia(mediaTitle, {
|
||||||
mediaId: result.id,
|
mediaId: result.id,
|
||||||
mediaType: result.mediaType
|
mediaType: result.mediaType,
|
||||||
}
|
}),
|
||||||
),
|
|
||||||
[jellyseerrApi, result]
|
[jellyseerrApi, result]
|
||||||
)
|
);
|
||||||
|
|
||||||
if (from === "(home)" || from === "(search)" || from === "(libraries)")
|
if (from === "(home)" || from === "(search)" || from === "(libraries)")
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ContextMenu.Root>
|
<TouchableOpacity
|
||||||
<ContextMenu.Trigger>
|
onPress={() => {
|
||||||
<TouchableOpacity
|
router.push({
|
||||||
onPress={() => {
|
pathname: `/(auth)/(tabs)/${from}/jellyseerr/page`,
|
||||||
// @ts-ignore
|
params: {
|
||||||
router.push({pathname: `/(auth)/(tabs)/${from}/jellyseerr/page`, params: {...result, mediaTitle, releaseYear, canRequest, posterSrc}});
|
...result,
|
||||||
}}
|
mediaTitle,
|
||||||
{...props}
|
releaseYear,
|
||||||
>
|
// @ts-expect-error
|
||||||
{children}
|
canRequest,
|
||||||
</TouchableOpacity>
|
posterSrc,
|
||||||
</ContextMenu.Trigger>
|
},
|
||||||
<ContextMenu.Content
|
});
|
||||||
avoidCollisions
|
}}
|
||||||
alignOffset={0}
|
{...props}
|
||||||
collisionPadding={0}
|
>
|
||||||
loop={false}
|
{children}
|
||||||
key={"content"}
|
</TouchableOpacity>
|
||||||
>
|
|
||||||
<ContextMenu.Label key="label-1">Actions</ContextMenu.Label>
|
|
||||||
{canRequest && result.mediaType === MediaType.MOVIE && (
|
|
||||||
<ContextMenu.Item
|
|
||||||
key="item-1"
|
|
||||||
onSelect={() => {
|
|
||||||
if (autoApprove) {
|
|
||||||
request()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
shouldDismissMenuOnSelect
|
|
||||||
>
|
|
||||||
<ContextMenu.ItemTitle key="item-1-title">Request</ContextMenu.ItemTitle>
|
|
||||||
<ContextMenu.ItemIcon
|
|
||||||
ios={{
|
|
||||||
name: "arrow.down.to.line",
|
|
||||||
pointSize: 18,
|
|
||||||
weight: "semibold",
|
|
||||||
scale: "medium",
|
|
||||||
hierarchicalColor: {
|
|
||||||
dark: "purple",
|
|
||||||
light: "purple",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
androidIconName="download"
|
|
||||||
/>
|
|
||||||
</ContextMenu.Item>
|
|
||||||
)}
|
|
||||||
</ContextMenu.Content>
|
|
||||||
</ContextMenu.Root>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
import { useRouter, useSegments } from "expo-router";
|
import { useRouter, useSegments } from "expo-router";
|
||||||
import { PropsWithChildren } from "react";
|
import { PropsWithChildren } from "react";
|
||||||
import { TouchableOpacity, TouchableOpacityProps } from "react-native";
|
import { TouchableOpacity, TouchableOpacityProps } from "react-native";
|
||||||
import * as ContextMenu from "zeego/context-menu";
|
|
||||||
|
|
||||||
interface Props extends TouchableOpacityProps {
|
interface Props extends TouchableOpacityProps {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
@@ -16,8 +15,6 @@ export const itemRouter = (
|
|||||||
item: BaseItemDto | BaseItemPerson,
|
item: BaseItemDto | BaseItemPerson,
|
||||||
from: string
|
from: string
|
||||||
) => {
|
) => {
|
||||||
console.log(item.Type, item?.CollectionType);
|
|
||||||
|
|
||||||
if ("CollectionType" in item && item.CollectionType === "livetv") {
|
if ("CollectionType" in item && item.CollectionType === "livetv") {
|
||||||
return `/(auth)/(tabs)/${from}/livetv`;
|
return `/(auth)/(tabs)/${from}/livetv`;
|
||||||
}
|
}
|
||||||
@@ -80,78 +77,15 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
from === "(favorites)"
|
from === "(favorites)"
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<ContextMenu.Root>
|
<TouchableOpacity
|
||||||
<ContextMenu.Trigger>
|
onPress={() => {
|
||||||
<TouchableOpacity
|
const url = itemRouter(item, from);
|
||||||
onPress={() => {
|
// @ts-ignore
|
||||||
const url = itemRouter(item, from);
|
router.push(url);
|
||||||
// @ts-ignore
|
}}
|
||||||
router.push(url);
|
{...props}
|
||||||
}}
|
>
|
||||||
{...props}
|
{children}
|
||||||
>
|
</TouchableOpacity>
|
||||||
{children}
|
|
||||||
</TouchableOpacity>
|
|
||||||
</ContextMenu.Trigger>
|
|
||||||
<ContextMenu.Content
|
|
||||||
avoidCollisions
|
|
||||||
alignOffset={0}
|
|
||||||
collisionPadding={0}
|
|
||||||
loop={false}
|
|
||||||
key={"content"}
|
|
||||||
>
|
|
||||||
<ContextMenu.Label key="label-1">Actions</ContextMenu.Label>
|
|
||||||
<ContextMenu.Item
|
|
||||||
key="item-1"
|
|
||||||
onSelect={() => {
|
|
||||||
markAsPlayedStatus(true);
|
|
||||||
}}
|
|
||||||
shouldDismissMenuOnSelect
|
|
||||||
>
|
|
||||||
<ContextMenu.ItemTitle key="item-1-title">
|
|
||||||
Mark as watched
|
|
||||||
</ContextMenu.ItemTitle>
|
|
||||||
<ContextMenu.ItemIcon
|
|
||||||
ios={{
|
|
||||||
name: "checkmark.circle", // Changed to "checkmark.circle" which represents "watched"
|
|
||||||
pointSize: 18,
|
|
||||||
weight: "semibold",
|
|
||||||
scale: "medium",
|
|
||||||
hierarchicalColor: {
|
|
||||||
dark: "green", // Changed to green for "watched"
|
|
||||||
light: "green",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
androidIconName="checkmark-circle"
|
|
||||||
></ContextMenu.ItemIcon>
|
|
||||||
</ContextMenu.Item>
|
|
||||||
<ContextMenu.Item
|
|
||||||
key="item-2"
|
|
||||||
onSelect={() => {
|
|
||||||
markAsPlayedStatus(false);
|
|
||||||
}}
|
|
||||||
shouldDismissMenuOnSelect
|
|
||||||
destructive
|
|
||||||
>
|
|
||||||
<ContextMenu.ItemTitle key="item-2-title">
|
|
||||||
Mark as not watched
|
|
||||||
</ContextMenu.ItemTitle>
|
|
||||||
<ContextMenu.ItemIcon
|
|
||||||
ios={{
|
|
||||||
name: "eye.slash", // Changed to "eye.slash" which represents "not watched"
|
|
||||||
pointSize: 18, // Adjusted for better visibility
|
|
||||||
weight: "semibold",
|
|
||||||
scale: "medium",
|
|
||||||
hierarchicalColor: {
|
|
||||||
dark: "red", // Changed to red for "not watched"
|
|
||||||
light: "red",
|
|
||||||
},
|
|
||||||
// Removed paletteColors as it's not necessary in this case
|
|
||||||
}}
|
|
||||||
androidIconName="eye-slash"
|
|
||||||
></ContextMenu.ItemIcon>
|
|
||||||
</ContextMenu.Item>
|
|
||||||
</ContextMenu.Content>
|
|
||||||
</ContextMenu.Root>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import * as Haptics from "expo-haptics";
|
||||||
import React, { useCallback, useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import { TouchableOpacity, TouchableOpacityProps, View } from "react-native";
|
import { TouchableOpacity, TouchableOpacityProps, View } from "react-native";
|
||||||
import {
|
import {
|
||||||
@@ -26,7 +26,6 @@ export const EpisodeCard: React.FC<EpisodeCardProps> = ({ item, ...props }) => {
|
|||||||
const { deleteFile } = useDownload();
|
const { deleteFile } = useDownload();
|
||||||
const { openFile } = useDownloadedFileOpener();
|
const { openFile } = useDownloadedFileOpener();
|
||||||
const { showActionSheetWithOptions } = useActionSheet();
|
const { showActionSheetWithOptions } = useActionSheet();
|
||||||
const successHapticFeedback = useHaptic("success");
|
|
||||||
|
|
||||||
const base64Image = useMemo(() => {
|
const base64Image = useMemo(() => {
|
||||||
return storage.getString(item.Id!);
|
return storage.getString(item.Id!);
|
||||||
@@ -42,7 +41,7 @@ export const EpisodeCard: React.FC<EpisodeCardProps> = ({ item, ...props }) => {
|
|||||||
const handleDeleteFile = useCallback(() => {
|
const handleDeleteFile = useCallback(() => {
|
||||||
if (item.Id) {
|
if (item.Id) {
|
||||||
deleteFile(item.Id);
|
deleteFile(item.Id);
|
||||||
successHapticFeedback();
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||||
}
|
}
|
||||||
}, [deleteFile, item.Id]);
|
}, [deleteFile, item.Id]);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
useActionSheet,
|
useActionSheet,
|
||||||
} from "@expo/react-native-action-sheet";
|
} from "@expo/react-native-action-sheet";
|
||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import * as Haptics from "expo-haptics";
|
||||||
import React, { useCallback, useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import { TouchableOpacity, View } from "react-native";
|
import { TouchableOpacity, View } from "react-native";
|
||||||
|
|
||||||
@@ -28,7 +28,6 @@ export const MovieCard: React.FC<MovieCardProps> = ({ item }) => {
|
|||||||
const { deleteFile } = useDownload();
|
const { deleteFile } = useDownload();
|
||||||
const { openFile } = useDownloadedFileOpener();
|
const { openFile } = useDownloadedFileOpener();
|
||||||
const { showActionSheetWithOptions } = useActionSheet();
|
const { showActionSheetWithOptions } = useActionSheet();
|
||||||
const successHapticFeedback = useHaptic("success");
|
|
||||||
|
|
||||||
const handleOpenFile = useCallback(() => {
|
const handleOpenFile = useCallback(() => {
|
||||||
openFile(item);
|
openFile(item);
|
||||||
@@ -44,7 +43,7 @@ export const MovieCard: React.FC<MovieCardProps> = ({ item }) => {
|
|||||||
const handleDeleteFile = useCallback(() => {
|
const handleDeleteFile = useCallback(() => {
|
||||||
if (item.Id) {
|
if (item.Id) {
|
||||||
deleteFile(item.Id);
|
deleteFile(item.Id);
|
||||||
successHapticFeedback();
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||||
}
|
}
|
||||||
}, [deleteFile, item.Id]);
|
}, [deleteFile, item.Id]);
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { itemRouter, TouchableItemRouter } from "../common/TouchableItemRouter";
|
|||||||
import { Loader } from "../Loader";
|
import { Loader } from "../Loader";
|
||||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||||
import { useRouter, useSegments } from "expo-router";
|
import { useRouter, useSegments } from "expo-router";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import * as Haptics from "expo-haptics";
|
||||||
|
|
||||||
interface Props extends ViewProps {}
|
interface Props extends ViewProps {}
|
||||||
|
|
||||||
@@ -128,7 +128,6 @@ const RenderItem: React.FC<{ item: BaseItemDto }> = ({ item }) => {
|
|||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const screenWidth = Dimensions.get("screen").width;
|
const screenWidth = Dimensions.get("screen").width;
|
||||||
const lightHapticFeedback = useHaptic("light");
|
|
||||||
|
|
||||||
const uri = useMemo(() => {
|
const uri = useMemo(() => {
|
||||||
if (!api) return null;
|
if (!api) return null;
|
||||||
@@ -154,7 +153,7 @@ const RenderItem: React.FC<{ item: BaseItemDto }> = ({ item }) => {
|
|||||||
const handleRoute = useCallback(() => {
|
const handleRoute = useCallback(() => {
|
||||||
if (!from) return;
|
if (!from) return;
|
||||||
const url = itemRouter(item, from);
|
const url = itemRouter(item, from);
|
||||||
lightHapticFeedback();
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (url) router.push(url);
|
if (url) router.push(url);
|
||||||
}, [item, from]);
|
}, [item, from]);
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr";
|
import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr";
|
||||||
import { userAtom } from "@/providers/JellyfinProvider";
|
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
|
||||||
import { useMutation } from "@tanstack/react-query";
|
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { View } from "react-native";
|
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 { Text } from "../common/Text";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
import { useCallback, useRef, useState } from "react";
|
||||||
|
import { Input } from "../common/Input";
|
||||||
import { ListItem } from "../list/ListItem";
|
import { ListItem } from "../list/ListItem";
|
||||||
|
import { Loader } from "../Loader";
|
||||||
|
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 { ListGroup } from "../list/ListGroup";
|
||||||
|
|
||||||
export const JellyseerrSettings = () => {
|
export const JellyseerrSettings = () => {
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -178,15 +178,6 @@ export const OtherSettings: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem title="Disable Haptic Feedback">
|
|
||||||
<Switch
|
|
||||||
value={settings.disableHapticFeedback}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
updateSettings({ disableHapticFeedback: value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,115 +1,59 @@
|
|||||||
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 { useHaptic } from "@/hooks/useHaptic";
|
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import React, { useCallback, useRef, useState } from "react";
|
|
||||||
import { Alert, View, ViewProps } from "react-native";
|
import { Alert, View, ViewProps } from "react-native";
|
||||||
import { Button } from "../Button";
|
|
||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
|
||||||
import { ListItem } from "../list/ListItem";
|
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 { getQuickConnectApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
|
import * as Haptics from "expo-haptics";
|
||||||
|
|
||||||
interface Props extends ViewProps {}
|
interface Props extends ViewProps {}
|
||||||
|
|
||||||
export const QuickConnect: React.FC<Props> = ({ ...props }) => {
|
export const QuickConnect: React.FC<Props> = ({ ...props }) => {
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const [quickConnectCode, setQuickConnectCode] = useState<string>();
|
|
||||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
|
||||||
const successHapticFeedback = useHaptic("success");
|
|
||||||
const errorHapticFeedback = useHaptic("error");
|
|
||||||
|
|
||||||
const renderBackdrop = useCallback(
|
const openQuickConnectAuthCodeInput = () => {
|
||||||
(props: BottomSheetBackdropProps) => (
|
Alert.prompt(
|
||||||
<BottomSheetBackdrop
|
"Quick connect",
|
||||||
{...props}
|
"Enter the quick connect code",
|
||||||
disappearsOnIndex={-1}
|
async (text) => {
|
||||||
appearsOnIndex={0}
|
if (text) {
|
||||||
/>
|
try {
|
||||||
),
|
const res = await getQuickConnectApi(api!).authorizeQuickConnect({
|
||||||
[]
|
code: text,
|
||||||
);
|
userId: user?.Id,
|
||||||
|
});
|
||||||
const authorizeQuickConnect = useCallback(async () => {
|
if (res.status === 200) {
|
||||||
if (quickConnectCode) {
|
Haptics.notificationAsync(
|
||||||
try {
|
Haptics.NotificationFeedbackType.Success
|
||||||
const res = await getQuickConnectApi(api!).authorizeQuickConnect({
|
);
|
||||||
code: quickConnectCode,
|
Alert.alert("Success", "Quick connect authorized");
|
||||||
userId: user?.Id,
|
} else {
|
||||||
});
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
||||||
if (res.status === 200) {
|
Alert.alert("Error", "Invalid code");
|
||||||
successHapticFeedback();
|
}
|
||||||
Alert.alert("Success", "Quick connect authorized");
|
} catch (e) {
|
||||||
setQuickConnectCode(undefined);
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
||||||
bottomSheetModalRef?.current?.close();
|
Alert.alert("Error", "Invalid code");
|
||||||
} else {
|
}
|
||||||
errorHapticFeedback();
|
|
||||||
Alert.alert("Error", "Invalid code");
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
errorHapticFeedback();
|
|
||||||
Alert.alert("Error", "Invalid code");
|
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
}, [api, user, quickConnectCode]);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View {...props}>
|
<View {...props}>
|
||||||
<ListGroup title={"Quick Connect"}>
|
<ListGroup title={"Quick Connect"}>
|
||||||
<ListItem
|
<ListItem
|
||||||
onPress={() => bottomSheetModalRef?.current?.present()}
|
onPress={openQuickConnectAuthCodeInput}
|
||||||
title="Authorize Quick Connect"
|
title="Authorize Quick Connect"
|
||||||
textColor="blue"
|
textColor="blue"
|
||||||
/>
|
></ListItem>
|
||||||
</ListGroup>
|
</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useDownload } from "@/providers/DownloadProvider";
|
|||||||
import { clearLogs } from "@/utils/log";
|
import { clearLogs } from "@/utils/log";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import * as FileSystem from "expo-file-system";
|
import * as FileSystem from "expo-file-system";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import * as Haptics from "expo-haptics";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import * as Progress from "react-native-progress";
|
import * as Progress from "react-native-progress";
|
||||||
import { toast } from "sonner-native";
|
import { toast } from "sonner-native";
|
||||||
@@ -13,8 +13,6 @@ import { ListItem } from "../list/ListItem";
|
|||||||
|
|
||||||
export const StorageSettings = () => {
|
export const StorageSettings = () => {
|
||||||
const { deleteAllFiles, appSizeUsage } = useDownload();
|
const { deleteAllFiles, appSizeUsage } = useDownload();
|
||||||
const successHapticFeedback = useHaptic("success");
|
|
||||||
const errorHapticFeedback = useHaptic("error");
|
|
||||||
|
|
||||||
const { data: size, isLoading: appSizeLoading } = useQuery({
|
const { data: size, isLoading: appSizeLoading } = useQuery({
|
||||||
queryKey: ["appSize", appSizeUsage],
|
queryKey: ["appSize", appSizeUsage],
|
||||||
@@ -31,9 +29,9 @@ export const StorageSettings = () => {
|
|||||||
const onDeleteClicked = async () => {
|
const onDeleteClicked = async () => {
|
||||||
try {
|
try {
|
||||||
await deleteAllFiles();
|
await deleteAllFiles();
|
||||||
successHapticFeedback();
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorHapticFeedback();
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
||||||
toast.error("Error deleting files");
|
toast.error("Error deleting files");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
BaseItemDto,
|
BaseItemDto,
|
||||||
MediaSourceInfo,
|
MediaSourceInfo,
|
||||||
} from "@jellyfin/sdk/lib/generated-client";
|
} from "@jellyfin/sdk/lib/generated-client";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import * as Haptics from "expo-haptics";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
@@ -157,12 +157,10 @@ export const Controls: React.FC<Props> = ({
|
|||||||
isVlc
|
isVlc
|
||||||
);
|
);
|
||||||
|
|
||||||
const lightHapticFeedback = useHaptic("light");
|
|
||||||
|
|
||||||
const goToPreviousItem = useCallback(() => {
|
const goToPreviousItem = useCallback(() => {
|
||||||
if (!previousItem || !settings) return;
|
if (!previousItem || !settings) return;
|
||||||
|
|
||||||
lightHapticFeedback();
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
|
|
||||||
const previousIndexes: previousIndexes = {
|
const previousIndexes: previousIndexes = {
|
||||||
subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined,
|
subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined,
|
||||||
@@ -200,7 +198,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
const goToNextItem = useCallback(() => {
|
const goToNextItem = useCallback(() => {
|
||||||
if (!nextItem || !settings) return;
|
if (!nextItem || !settings) return;
|
||||||
|
|
||||||
lightHapticFeedback();
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
|
|
||||||
const previousIndexes: previousIndexes = {
|
const previousIndexes: previousIndexes = {
|
||||||
subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined,
|
subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined,
|
||||||
@@ -328,7 +326,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
const handleSkipBackward = useCallback(async () => {
|
const handleSkipBackward = useCallback(async () => {
|
||||||
if (!settings?.rewindSkipTime) return;
|
if (!settings?.rewindSkipTime) return;
|
||||||
wasPlayingRef.current = isPlaying;
|
wasPlayingRef.current = isPlaying;
|
||||||
lightHapticFeedback();
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
try {
|
try {
|
||||||
const curr = progress.value;
|
const curr = progress.value;
|
||||||
if (curr !== undefined) {
|
if (curr !== undefined) {
|
||||||
@@ -346,7 +344,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
const handleSkipForward = useCallback(async () => {
|
const handleSkipForward = useCallback(async () => {
|
||||||
if (!settings?.forwardSkipTime) return;
|
if (!settings?.forwardSkipTime) return;
|
||||||
wasPlayingRef.current = isPlaying;
|
wasPlayingRef.current = isPlaying;
|
||||||
lightHapticFeedback();
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
try {
|
try {
|
||||||
const curr = progress.value;
|
const curr = progress.value;
|
||||||
if (curr !== undefined) {
|
if (curr !== undefined) {
|
||||||
@@ -363,7 +361,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
|
|
||||||
const toggleIgnoreSafeAreas = useCallback(() => {
|
const toggleIgnoreSafeAreas = useCallback(() => {
|
||||||
setIgnoreSafeAreas((prev) => !prev);
|
setIgnoreSafeAreas((prev) => !prev);
|
||||||
lightHapticFeedback();
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const memoizedRenderBubble = useCallback(() => {
|
const memoizedRenderBubble = useCallback(() => {
|
||||||
@@ -442,7 +440,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
const gotoItem = await getItemById(api, itemId);
|
const gotoItem = await getItemById(api, itemId);
|
||||||
if (!settings || !gotoItem) return;
|
if (!settings || !gotoItem) return;
|
||||||
|
|
||||||
lightHapticFeedback();
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
|
|
||||||
const previousIndexes: previousIndexes = {
|
const previousIndexes: previousIndexes = {
|
||||||
subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined,
|
subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined,
|
||||||
@@ -499,6 +497,33 @@ 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
|
<Pressable
|
||||||
onPressIn={() => {
|
onPressIn={() => {
|
||||||
toggleControls();
|
toggleControls();
|
||||||
@@ -507,8 +532,6 @@ export const Controls: React.FC<Props> = ({
|
|||||||
position: "absolute",
|
position: "absolute",
|
||||||
width: Dimensions.get("window").width,
|
width: Dimensions.get("window").width,
|
||||||
height: Dimensions.get("window").height,
|
height: Dimensions.get("window").height,
|
||||||
backgroundColor: "black",
|
|
||||||
opacity: showControls ? 0.5 : 0,
|
|
||||||
}}
|
}}
|
||||||
></Pressable>
|
></Pressable>
|
||||||
|
|
||||||
@@ -518,82 +541,61 @@ export const Controls: React.FC<Props> = ({
|
|||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: settings?.safeAreaInControlsEnabled ? insets.top : 0,
|
top: settings?.safeAreaInControlsEnabled ? insets.top : 0,
|
||||||
right: settings?.safeAreaInControlsEnabled ? insets.right : 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,
|
opacity: showControls ? 1 : 0,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
pointerEvents={showControls ? "auto" : "none"}
|
pointerEvents={showControls ? "auto" : "none"}
|
||||||
className={`flex flex-row w-full p-4 `}
|
className={`flex flex-row items-center space-x-2 z-10 p-4 `}
|
||||||
>
|
>
|
||||||
<View className="mr-auto">
|
{item?.Type === "Episode" && !offline && (
|
||||||
<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
|
<TouchableOpacity
|
||||||
onPress={async () => {
|
onPress={() => {
|
||||||
lightHapticFeedback();
|
switchOnEpisodeMode();
|
||||||
router.back();
|
|
||||||
}}
|
}}
|
||||||
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
|
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" />
|
<Ionicons name="list" size={24} color="white" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
)}
|
||||||
|
{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
|
||||||
|
|||||||
@@ -118,7 +118,14 @@ const DropdownView: React.FC<DropdownViewProps> = ({ showControls }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
zIndex: 1000,
|
||||||
|
opacity: showControls ? 1 : 0,
|
||||||
|
}}
|
||||||
|
className="p-4"
|
||||||
|
>
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
<TouchableOpacity className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2">
|
<TouchableOpacity className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2">
|
||||||
|
|||||||
4
eas.json
4
eas.json
@@ -22,13 +22,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"production": {
|
"production": {
|
||||||
"channel": "0.24.0",
|
"channel": "0.23.0",
|
||||||
"android": {
|
"android": {
|
||||||
"image": "latest"
|
"image": "latest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"production-apk": {
|
"production-apk": {
|
||||||
"channel": "0.24.0",
|
"channel": "0.23.0",
|
||||||
"android": {
|
"android": {
|
||||||
"buildType": "apk",
|
"buildType": "apk",
|
||||||
"image": "latest"
|
"image": "latest"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { apiAtom } from "@/providers/JellyfinProvider";
|
|||||||
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
|
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
|
||||||
import { writeToLog } from "@/utils/log";
|
import { writeToLog } from "@/utils/log";
|
||||||
import { msToSeconds, secondsToMs } from "@/utils/time";
|
import { msToSeconds, secondsToMs } from "@/utils/time";
|
||||||
import { useHaptic } from "./useHaptic";
|
import * as Haptics from "expo-haptics";
|
||||||
|
|
||||||
interface CreditTimestamps {
|
interface CreditTimestamps {
|
||||||
Introduction: {
|
Introduction: {
|
||||||
@@ -29,7 +29,6 @@ export const useCreditSkipper = (
|
|||||||
) => {
|
) => {
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [showSkipCreditButton, setShowSkipCreditButton] = useState(false);
|
const [showSkipCreditButton, setShowSkipCreditButton] = useState(false);
|
||||||
const lightHapticFeedback = useHaptic("light");
|
|
||||||
|
|
||||||
if (isVlc) {
|
if (isVlc) {
|
||||||
currentTime = msToSeconds(currentTime);
|
currentTime = msToSeconds(currentTime);
|
||||||
@@ -80,7 +79,7 @@ export const useCreditSkipper = (
|
|||||||
if (!creditTimestamps) return;
|
if (!creditTimestamps) return;
|
||||||
console.log(`Skipping credits to ${creditTimestamps.Credits.End}`);
|
console.log(`Skipping credits to ${creditTimestamps.Credits.End}`);
|
||||||
try {
|
try {
|
||||||
lightHapticFeedback();
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
wrappedSeek(creditTimestamps.Credits.End);
|
wrappedSeek(creditTimestamps.Credits.End);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
play();
|
play();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from "@jellyfin/sdk/lib/generated-client";
|
} from "@jellyfin/sdk/lib/generated-client";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
// Used only for initial play settings.
|
// Used only for intial play settings.
|
||||||
const useDefaultPlaySettings = (
|
const useDefaultPlaySettings = (
|
||||||
item: BaseItemDto,
|
item: BaseItemDto,
|
||||||
settings: Settings | null
|
settings: Settings | null
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
import { useCallback, useMemo } from "react";
|
|
||||||
import { Platform } from "react-native";
|
|
||||||
import * as Haptics from "expo-haptics";
|
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
|
||||||
|
|
||||||
export type HapticFeedbackType =
|
|
||||||
| "light"
|
|
||||||
| "medium"
|
|
||||||
| "heavy"
|
|
||||||
| "selection"
|
|
||||||
| "success"
|
|
||||||
| "warning"
|
|
||||||
| "error";
|
|
||||||
|
|
||||||
export const useHaptic = (feedbackType: HapticFeedbackType = "selection") => {
|
|
||||||
const [settings] = useSettings();
|
|
||||||
|
|
||||||
const createHapticHandler = useCallback(
|
|
||||||
(type: Haptics.ImpactFeedbackStyle) => {
|
|
||||||
return Platform.OS === "web" ? () => {} : () => Haptics.impactAsync(type);
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
const createNotificationFeedback = useCallback(
|
|
||||||
(type: Haptics.NotificationFeedbackType) => {
|
|
||||||
return Platform.OS === "web"
|
|
||||||
? () => {}
|
|
||||||
: () => Haptics.notificationAsync(type);
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const hapticHandlers = useMemo(
|
|
||||||
() => ({
|
|
||||||
light: createHapticHandler(Haptics.ImpactFeedbackStyle.Light),
|
|
||||||
medium: createHapticHandler(Haptics.ImpactFeedbackStyle.Medium),
|
|
||||||
heavy: createHapticHandler(Haptics.ImpactFeedbackStyle.Heavy),
|
|
||||||
selection: Platform.OS === "web" ? () => {} : Haptics.selectionAsync,
|
|
||||||
success: createNotificationFeedback(
|
|
||||||
Haptics.NotificationFeedbackType.Success
|
|
||||||
),
|
|
||||||
warning: createNotificationFeedback(
|
|
||||||
Haptics.NotificationFeedbackType.Warning
|
|
||||||
),
|
|
||||||
error: createNotificationFeedback(Haptics.NotificationFeedbackType.Error),
|
|
||||||
}),
|
|
||||||
[createHapticHandler, createNotificationFeedback]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (settings?.disableHapticFeedback) {
|
|
||||||
return () => {};
|
|
||||||
}
|
|
||||||
return hapticHandlers[feedbackType];
|
|
||||||
};
|
|
||||||
@@ -5,7 +5,7 @@ import { apiAtom } from "@/providers/JellyfinProvider";
|
|||||||
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
|
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
|
||||||
import { writeToLog } from "@/utils/log";
|
import { writeToLog } from "@/utils/log";
|
||||||
import { msToSeconds, secondsToMs } from "@/utils/time";
|
import { msToSeconds, secondsToMs } from "@/utils/time";
|
||||||
import { useHaptic } from "./useHaptic";
|
import * as Haptics from "expo-haptics";
|
||||||
|
|
||||||
interface IntroTimestamps {
|
interface IntroTimestamps {
|
||||||
EpisodeId: string;
|
EpisodeId: string;
|
||||||
@@ -33,7 +33,6 @@ export const useIntroSkipper = (
|
|||||||
if (isVlc) {
|
if (isVlc) {
|
||||||
currentTime = msToSeconds(currentTime);
|
currentTime = msToSeconds(currentTime);
|
||||||
}
|
}
|
||||||
const lightHapticFeedback = useHaptic("light");
|
|
||||||
|
|
||||||
const wrappedSeek = (seconds: number) => {
|
const wrappedSeek = (seconds: number) => {
|
||||||
if (isVlc) {
|
if (isVlc) {
|
||||||
@@ -79,7 +78,7 @@ export const useIntroSkipper = (
|
|||||||
const skipIntro = useCallback(() => {
|
const skipIntro = useCallback(() => {
|
||||||
if (!introTimestamps) return;
|
if (!introTimestamps) return;
|
||||||
try {
|
try {
|
||||||
lightHapticFeedback();
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
wrappedSeek(introTimestamps.IntroEnd);
|
wrappedSeek(introTimestamps.IntroEnd);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
play();
|
play();
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ import { markAsNotPlayed } from "@/utils/jellyfin/playstate/markAsNotPlayed";
|
|||||||
import { markAsPlayed } from "@/utils/jellyfin/playstate/markAsPlayed";
|
import { markAsPlayed } from "@/utils/jellyfin/playstate/markAsPlayed";
|
||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useHaptic } from "./useHaptic";
|
import * as Haptics from "expo-haptics";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
|
|
||||||
export const useMarkAsPlayed = (item: BaseItemDto) => {
|
export const useMarkAsPlayed = (item: BaseItemDto) => {
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const lightHapticFeedback = useHaptic("light");
|
|
||||||
|
|
||||||
const invalidateQueries = () => {
|
const invalidateQueries = () => {
|
||||||
const queriesToInvalidate = [
|
const queriesToInvalidate = [
|
||||||
@@ -30,7 +29,7 @@ export const useMarkAsPlayed = (item: BaseItemDto) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const markAsPlayedStatus = async (played: boolean) => {
|
const markAsPlayedStatus = async (played: boolean) => {
|
||||||
lightHapticFeedback();
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
|
|
||||||
// Optimistic update
|
// Optimistic update
|
||||||
queryClient.setQueryData(
|
queryClient.setQueryData(
|
||||||
|
|||||||
102
package.json
102
package.json
@@ -4,102 +4,100 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"submodule-reload": "git submodule update --init --remote --recursive",
|
"submodule-reload": "git submodule update --init --remote --recursive",
|
||||||
|
"clean": "echo y | expo prebuild --clean",
|
||||||
"start": "bun run submodule-reload && expo start",
|
"start": "bun run submodule-reload && expo start",
|
||||||
"reset-project": "node ./scripts/reset-project.js",
|
"reset-project": "node ./scripts/reset-project.js",
|
||||||
"android": "bun run submodule-reload && expo run:android",
|
"android": "bun run submodule-reload && expo run:android",
|
||||||
"ios": "bun run submodule-reload && expo run:ios",
|
"ios": "bun run submodule-reload && expo run:ios",
|
||||||
"web": "bun run submodule-reload && expo start --web",
|
"web": "bun run submodule-reload && expo start --web",
|
||||||
"test": "jest --watchAll",
|
|
||||||
"lint": "expo lint",
|
"lint": "expo lint",
|
||||||
"postinstall": "patch-package"
|
"postinstall": "patch-package"
|
||||||
},
|
},
|
||||||
"jest": {
|
|
||||||
"preset": "jest-expo"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bottom-tabs/react-navigation": "^0.7.1",
|
"@bottom-tabs/react-navigation": "0.8.0",
|
||||||
|
"react-native-bottom-tabs": "0.8.0",
|
||||||
|
"@react-navigation/material-top-tabs": "^7.1.0",
|
||||||
|
"@react-navigation/native": "^7.0.14",
|
||||||
"@config-plugins/ffmpeg-kit-react-native": "^8.0.0",
|
"@config-plugins/ffmpeg-kit-react-native": "^8.0.0",
|
||||||
"@expo/react-native-action-sheet": "^4.1.0",
|
"@expo/react-native-action-sheet": "^4.1.0",
|
||||||
"@expo/vector-icons": "^14.0.4",
|
"@expo/vector-icons": "^14.0.4",
|
||||||
"@futurejj/react-native-visibility-sensor": "^1.3.5",
|
"@futurejj/react-native-visibility-sensor": "^1.3.5",
|
||||||
"@gorhom/bottom-sheet": "^4.6.4",
|
"@gorhom/bottom-sheet": "^5.0.6",
|
||||||
"@jellyfin/sdk": "^0.11.0",
|
"@jellyfin/sdk": "^0.11.0",
|
||||||
"@kesha-antonov/react-native-background-downloader": "3.1.2",
|
"@kesha-antonov/react-native-background-downloader": "3.2.6",
|
||||||
"@react-native-async-storage/async-storage": "1.23.1",
|
"@react-native-async-storage/async-storage": "1.23.1",
|
||||||
"@react-native-community/netinfo": "11.3.1",
|
"@react-native-community/netinfo": "11.4.1",
|
||||||
"@react-native-menu/menu": "^1.1.6",
|
"@react-native-menu/menu": "^1.1.6",
|
||||||
"@react-navigation/material-top-tabs": "^6.6.14",
|
"@shopify/flash-list": "1.7.1",
|
||||||
"@react-navigation/native": "^6.1.18",
|
|
||||||
"@shopify/flash-list": "1.6.4",
|
|
||||||
"@tanstack/react-query": "^5.59.20",
|
"@tanstack/react-query": "^5.59.20",
|
||||||
"@types/lodash": "^4.17.13",
|
"@types/lodash": "^4.17.13",
|
||||||
"@types/react-native-vector-icons": "^6.4.18",
|
"@types/react-native-vector-icons": "^6.4.18",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"add": "^2.0.6",
|
"add": "^2.0.6",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"expo": "~51.0.39",
|
"expo": "^52.0.0",
|
||||||
"expo-asset": "~10.0.10",
|
"expo-asset": "~11.0.2",
|
||||||
"expo-background-fetch": "~12.0.1",
|
"expo-background-fetch": "~13.0.4",
|
||||||
"expo-blur": "~13.0.2",
|
"expo-blur": "~14.0.3",
|
||||||
"expo-brightness": "~12.0.1",
|
"expo-brightness": "~13.0.3",
|
||||||
"expo-build-properties": "~0.12.5",
|
"expo-build-properties": "~0.13.2",
|
||||||
"expo-constants": "~16.0.2",
|
"expo-constants": "~17.0.4",
|
||||||
"expo-dev-client": "~4.0.29",
|
"expo-dev-client": "~5.0.10",
|
||||||
"expo-device": "~6.0.2",
|
"expo-device": "~7.0.2",
|
||||||
"expo-font": "~12.0.10",
|
"expo-font": "~13.0.3",
|
||||||
"expo-haptics": "~13.0.1",
|
"expo-haptics": "~14.0.1",
|
||||||
"expo-image": "~1.13.0",
|
"expo-image": "~2.0.4",
|
||||||
"expo-keep-awake": "~13.0.2",
|
"expo-keep-awake": "~14.0.2",
|
||||||
"expo-linear-gradient": "~13.0.2",
|
"expo-linear-gradient": "~14.0.2",
|
||||||
"expo-linking": "~6.3.1",
|
"expo-linking": "~7.0.4",
|
||||||
"expo-network": "~6.0.1",
|
"expo-network": "~7.0.5",
|
||||||
"expo-notifications": "~0.28.19",
|
"expo-notifications": "~0.29.12",
|
||||||
"expo-router": "~3.5.24",
|
"expo-router": "~4.0.17",
|
||||||
"expo-screen-orientation": "~7.0.5",
|
"expo-screen-orientation": "~8.0.4",
|
||||||
"expo-sensors": "~13.0.9",
|
"expo-sensors": "~14.0.2",
|
||||||
"expo-splash-screen": "~0.27.7",
|
"expo-splash-screen": "~0.29.21",
|
||||||
"expo-status-bar": "~1.12.1",
|
"expo-status-bar": "~2.0.1",
|
||||||
"expo-system-ui": "^3.0.7",
|
"expo-system-ui": "~4.0.7",
|
||||||
"expo-task-manager": "~11.8.2",
|
"expo-task-manager": "~12.0.4",
|
||||||
"expo-updates": "~0.25.27",
|
"expo-updates": "~0.26.13",
|
||||||
"expo-web-browser": "~13.0.3",
|
"expo-web-browser": "~14.0.2",
|
||||||
"ffmpeg-kit-react-native": "^6.0.2",
|
"ffmpeg-kit-react-native": "^6.0.2",
|
||||||
"install": "^0.13.0",
|
"install": "^0.13.0",
|
||||||
"jotai": "^2.10.1",
|
"jotai": "^2.10.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"nativewind": "^2.0.11",
|
"nativewind": "^2.0.11",
|
||||||
"react": "18.2.0",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.3.1",
|
||||||
"react-native": "0.74.5",
|
"react-native": "0.76.6",
|
||||||
"react-native-awesome-slider": "^2.5.6",
|
"react-native-awesome-slider": "^2.5.6",
|
||||||
"react-native-bottom-tabs": "0.7.1",
|
|
||||||
"react-native-circular-progress": "^1.4.1",
|
"react-native-circular-progress": "^1.4.1",
|
||||||
"react-native-compressor": "^1.9.0",
|
"react-native-compressor": "^1.9.0",
|
||||||
|
"react-native-country-flag": "^2.0.2",
|
||||||
"react-native-device-info": "^14.0.1",
|
"react-native-device-info": "^14.0.1",
|
||||||
"react-native-edge-to-edge": "^1.1.3",
|
"react-native-edge-to-edge": "^1.1.3",
|
||||||
"react-native-gesture-handler": "~2.16.1",
|
"react-native-gesture-handler": "~2.20.2",
|
||||||
"react-native-get-random-values": "^1.11.0",
|
"react-native-get-random-values": "^1.11.0",
|
||||||
"react-native-google-cast": "^4.8.3",
|
"react-native-google-cast": "^4.8.3",
|
||||||
"react-native-image-colors": "^2.4.0",
|
"react-native-image-colors": "^2.4.0",
|
||||||
"react-native-ios-context-menu": "^2.5.2",
|
"react-native-ios-context-menu": "^2.5.2",
|
||||||
"react-native-ios-utilities": "^4.5.1",
|
"react-native-ios-utilities": "4.5.3",
|
||||||
"react-native-mmkv": "^2.12.2",
|
"react-native-mmkv": "^2.12.2",
|
||||||
"react-native-pager-view": "6.3.0",
|
"react-native-pager-view": "6.5.1",
|
||||||
"react-native-progress": "^5.0.1",
|
"react-native-progress": "^5.0.1",
|
||||||
"react-native-reanimated": "~3.10.1",
|
"react-native-reanimated": "~3.16.1",
|
||||||
"react-native-reanimated-carousel": "4.0.0-canary.22",
|
"react-native-reanimated-carousel": "4.0.0-canary.22",
|
||||||
"react-native-safe-area-context": "4.10.5",
|
"react-native-safe-area-context": "4.12.0",
|
||||||
"react-native-screens": "3.31.1",
|
"react-native-screens": "~4.4.0",
|
||||||
"react-native-svg": "15.2.0",
|
"react-native-svg": "15.8.0",
|
||||||
"react-native-tab-view": "^3.5.2",
|
"react-native-tab-view": "^3.5.2",
|
||||||
|
"react-native-udp": "^4.1.7",
|
||||||
"react-native-uitextview": "^1.4.0",
|
"react-native-uitextview": "^1.4.0",
|
||||||
"react-native-url-polyfill": "^2.0.0",
|
"react-native-url-polyfill": "^2.0.0",
|
||||||
"react-native-uuid": "^2.0.2",
|
"react-native-uuid": "^2.0.2",
|
||||||
"react-native-video": "^6.7.0",
|
"react-native-video": "^6.7.0",
|
||||||
"react-native-volume-manager": "^1.10.0",
|
"react-native-volume-manager": "^1.10.0",
|
||||||
"react-native-web": "~0.19.13",
|
"react-native-web": "~0.19.13",
|
||||||
"react-native-webview": "13.8.6",
|
"react-native-webview": "13.12.5",
|
||||||
"react-native-youtube-iframe": "^2.3.0",
|
|
||||||
"sonner-native": "^0.14.2",
|
"sonner-native": "^0.14.2",
|
||||||
"tailwindcss": "3.3.2",
|
"tailwindcss": "3.3.2",
|
||||||
"use-debounce": "^10.0.4",
|
"use-debounce": "^10.0.4",
|
||||||
@@ -110,10 +108,8 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.26.0",
|
"@babel/core": "^7.26.0",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/react": "~18.2.79",
|
"@types/react": "~18.3.12",
|
||||||
"@types/react-test-renderer": "^18.0.7",
|
"@types/react-test-renderer": "^18.0.7",
|
||||||
"jest": "^29.2.1",
|
|
||||||
"jest-expo": "~51.0.4",
|
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"postinstall-postinstall": "^2.1.0",
|
"postinstall-postinstall": "^2.1.0",
|
||||||
"react-test-renderer": "18.2.0",
|
"react-test-renderer": "18.2.0",
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ import useImageStorage from "@/hooks/useImageStorage";
|
|||||||
import { storage } from "@/utils/mmkv";
|
import { storage } from "@/utils/mmkv";
|
||||||
import useDownloadHelper from "@/utils/download";
|
import useDownloadHelper from "@/utils/download";
|
||||||
import { FileInfo } from "expo-file-system";
|
import { FileInfo } from "expo-file-system";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import * as Haptics from "expo-haptics";
|
||||||
import * as Application from "expo-application";
|
import * as Application from "expo-application";
|
||||||
|
|
||||||
export type DownloadedItem = {
|
export type DownloadedItem = {
|
||||||
@@ -78,8 +78,6 @@ function useDownloadProvider() {
|
|||||||
|
|
||||||
const [processes, setProcesses] = useAtom<JobStatus[]>(processesAtom);
|
const [processes, setProcesses] = useAtom<JobStatus[]>(processesAtom);
|
||||||
|
|
||||||
const successHapticFeedback = useHaptic("success");
|
|
||||||
|
|
||||||
const authHeader = useMemo(() => {
|
const authHeader = useMemo(() => {
|
||||||
return api?.accessToken;
|
return api?.accessToken;
|
||||||
}, [api]);
|
}, [api]);
|
||||||
@@ -534,7 +532,9 @@ function useDownloadProvider() {
|
|||||||
if (i.Id) return deleteFile(i.Id);
|
if (i.Id) return deleteFile(i.Id);
|
||||||
return;
|
return;
|
||||||
})
|
})
|
||||||
).then(() => successHapticFeedback());
|
).then(() =>
|
||||||
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const cleanCacheDirectory = async () => {
|
const cleanCacheDirectory = async () => {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
setJellyfin(
|
setJellyfin(
|
||||||
() =>
|
() =>
|
||||||
new Jellyfin({
|
new Jellyfin({
|
||||||
clientInfo: { name: "Streamyfin", version: "0.24.0" },
|
clientInfo: { name: "Streamyfin", version: "0.23.0" },
|
||||||
deviceInfo: {
|
deviceInfo: {
|
||||||
name: deviceName,
|
name: deviceName,
|
||||||
id,
|
id,
|
||||||
@@ -92,7 +92,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
return {
|
return {
|
||||||
authorization: `MediaBrowser Client="Streamyfin", Device=${
|
authorization: `MediaBrowser Client="Streamyfin", Device=${
|
||||||
Platform.OS === "android" ? "Android" : "iOS"
|
Platform.OS === "android" ? "Android" : "iOS"
|
||||||
}, DeviceId="${deviceId}", Version="0.24.0"`,
|
}, DeviceId="${deviceId}", Version="0.23.0"`,
|
||||||
};
|
};
|
||||||
}, [deviceId]);
|
}, [deviceId]);
|
||||||
|
|
||||||
|
|||||||
0
svenska_kyrkan.sql
Normal file
0
svenska_kyrkan.sql
Normal file
@@ -84,7 +84,6 @@ export type Settings = {
|
|||||||
downloadMethod: "optimized" | "remux";
|
downloadMethod: "optimized" | "remux";
|
||||||
autoDownload: boolean;
|
autoDownload: boolean;
|
||||||
showCustomMenuLinks: boolean;
|
showCustomMenuLinks: boolean;
|
||||||
disableHapticFeedback: boolean;
|
|
||||||
subtitleSize: number;
|
subtitleSize: number;
|
||||||
remuxConcurrentLimit: 1 | 2 | 3 | 4;
|
remuxConcurrentLimit: 1 | 2 | 3 | 4;
|
||||||
safeAreaInControlsEnabled: boolean;
|
safeAreaInControlsEnabled: boolean;
|
||||||
@@ -123,7 +122,6 @@ const loadSettings = (): Settings => {
|
|||||||
downloadMethod: "remux",
|
downloadMethod: "remux",
|
||||||
autoDownload: false,
|
autoDownload: false,
|
||||||
showCustomMenuLinks: false,
|
showCustomMenuLinks: false,
|
||||||
disableHapticFeedback: false,
|
|
||||||
subtitleSize: Platform.OS === "ios" ? 60 : 100,
|
subtitleSize: Platform.OS === "ios" ? 60 : 100,
|
||||||
remuxConcurrentLimit: 1,
|
remuxConcurrentLimit: 1,
|
||||||
safeAreaInControlsEnabled: true,
|
safeAreaInControlsEnabled: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user