Merge branch 'develop' into feature/sync-play

This commit is contained in:
Alex Kim
2026-06-05 17:12:36 +10:00
77 changed files with 6865 additions and 1258 deletions

View File

@@ -1,4 +1,9 @@
import { BottomSheetModal } from "@gorhom/bottom-sheet";
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import { useNavigation } from "expo-router";
import { useAtom } from "jotai";
import { useEffect, useMemo, useRef, useState } from "react";
@@ -7,6 +12,7 @@ import { Alert, Platform, ScrollView, View } from "react-native";
import { Pressable } from "react-native-gesture-handler";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { toast } from "sonner-native";
import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import ActiveDownloads from "@/components/downloads/ActiveDownloads";
@@ -101,7 +107,7 @@ export default function DownloadsPage() {
navigation.setOptions({
headerRight: () => (
<Pressable
onPress={bottomSheetModalRef.current?.present}
onPress={() => bottomSheetModalRef.current?.present()}
className='px-2'
>
<DownloadSize items={downloadedFiles?.map((f) => f.item) || []} />
@@ -116,7 +122,7 @@ export default function DownloadsPage() {
}
}, [showMigration]);
const _deleteMovies = () =>
const deleteMovies = () =>
deleteFileByType("Movie")
.then(() =>
toast.success(
@@ -127,7 +133,7 @@ export default function DownloadsPage() {
writeToLog("ERROR", reason);
toast.error(t("home.downloads.toasts.failed_to_delete_all_movies"));
});
const _deleteShows = () =>
const deleteShows = () =>
deleteFileByType("Episode")
.then(() =>
toast.success(
@@ -138,7 +144,7 @@ export default function DownloadsPage() {
writeToLog("ERROR", reason);
toast.error(t("home.downloads.toasts.failed_to_delete_all_tvseries"));
});
const _deleteOtherMedia = () =>
const deleteOtherMedia = () =>
Promise.all(
otherMedia
.filter((item) => item.item.Type)
@@ -162,6 +168,9 @@ export default function DownloadsPage() {
),
);
const deleteAllMedia = async () =>
await Promise.all([deleteMovies(), deleteShows(), deleteOtherMedia()]);
return (
<OfflineModeProvider isOffline={true}>
<ScrollView
@@ -256,6 +265,42 @@ export default function DownloadsPage() {
)}
</View>
</ScrollView>
<BottomSheetModal
ref={bottomSheetModalRef}
enableDynamicSizing
handleIndicatorStyle={{
backgroundColor: "white",
}}
backgroundStyle={{
backgroundColor: "#171717",
}}
backdropComponent={(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
)}
>
<BottomSheetView>
<View className='p-4 space-y-4 mb-4'>
<Button color='purple' onPress={deleteMovies}>
{t("home.downloads.delete_all_movies_button")}
</Button>
<Button color='purple' onPress={deleteShows}>
{t("home.downloads.delete_all_tvseries_button")}
</Button>
{otherMedia.length > 0 && (
<Button color='purple' onPress={deleteOtherMedia}>
{t("home.downloads.delete_all_other_media_button")}
</Button>
)}
<Button color='red' onPress={deleteAllMedia}>
{t("home.downloads.delete_all_button")}
</Button>
</View>
</BottomSheetView>
</BottomSheetModal>
</OfflineModeProvider>
);
}

View File

@@ -59,17 +59,19 @@ function SettingsMobile() {
<QuickConnect className='mb-4' />
<View className='mb-4'>
<ListGroup title={t("pairing.pair_with_phone_title")}>
<ListItem
onPress={() =>
router.push("/(auth)/(tabs)/(home)/companion-login")
}
title={t("pairing.pair_with_phone")}
textColor='blue'
/>
</ListGroup>
</View>
{Platform.OS !== "ios" && (
<View className='mb-4'>
<ListGroup title={t("pairing.pair_with_phone_title")}>
<ListItem
onPress={() =>
router.push("/(auth)/(tabs)/(home)/companion-login")
}
title={t("pairing.pair_with_phone")}
textColor='blue'
/>
</ListGroup>
</View>
)}
<View className='mb-4'>
<AppLanguageSelector />

View File

@@ -114,7 +114,7 @@ export default function StreamystatsPage() {
};
const handleRefreshFromServer = useCallback(async () => {
const newPluginSettings = await refreshStreamyfinPluginSettings(true);
const newPluginSettings = await refreshStreamyfinPluginSettings();
// Update local state with new values
const newUrl = newPluginSettings?.streamyStatsServerUrl?.value || "";
setUrl(newUrl);

View File

@@ -6,6 +6,7 @@ import {
BottomSheetTextInput,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import type { BottomSheetModalMethods } from "@gorhom/bottom-sheet/lib/typescript/types";
import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { useLocalSearchParams, useNavigation } from "expo-router";
@@ -76,7 +77,7 @@ const MobilePage: React.FC = () => {
const [issueMessage, setIssueMessage] = useState<string>();
const [requestBody, _setRequestBody] = useState<MediaRequestBody>();
const [issueTypeDropdownOpen, setIssueTypeDropdownOpen] = useState(false);
const advancedReqModalRef = useRef<BottomSheetModal>(null);
const advancedReqModalRef = useRef<BottomSheetModalMethods>(null);
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const {

View File

@@ -166,7 +166,7 @@ export default function IndexLayout() {
open={dropdownOpen}
onOpenChange={setDropdownOpen}
trigger={
<View className='pl-1.5'>
<View>
<Ionicons
name='ellipsis-horizontal-outline'
size={24}

View File

@@ -40,6 +40,8 @@ const Layout = () => {
keyboardDismissMode='none'
screenOptions={{
tabBarBounces: true,
tabBarActiveTintColor: "#FFFFFF",
tabBarInactiveTintColor: "#9CA3AF",
tabBarLabelStyle: {
fontSize: TAB_LABEL_FONT_SIZE,
fontWeight: "600",

View File

@@ -291,6 +291,11 @@ export default function DirectPlayerPage() {
};
if (itemId) {
setItem(null);
setDownloadedItem(null);
// Clear the previous episode's stream so the loader gate stays closed
// until the new item's stream resolves (avoids a stale MPV source frame).
setStream(null);
fetchItemData();
}
}, [itemId, offline, api, user?.Id]);
@@ -333,6 +338,12 @@ export default function DirectPlayerPage() {
return null;
}
// Ensure item matches the current itemId to avoid race conditions
if (item.Id !== itemId) {
setStreamStatus({ isLoading: false, isError: false });
return null;
}
let result: Stream | null = null;
if (offline && downloadedItem?.mediaSource) {
const url = downloadedItem.videoFilePath;
@@ -405,6 +416,7 @@ export default function DirectPlayerPage() {
item,
user?.Id,
downloadedItem,
offline,
]);
useEffect(() => {
@@ -538,21 +550,15 @@ export default function DirectPlayerPage() {
if (!item?.Id || !stream?.sessionId || offline || !api) return;
const currentTimeInTicks = msToTicks(progress.get());
await getPlaystateApi(api).onPlaybackStopped({
itemId: item.Id,
mediaSourceId: mediaSourceId,
positionTicks: currentTimeInTicks,
playSessionId: stream.sessionId,
await getPlaystateApi(api).reportPlaybackStopped({
playbackStopInfo: {
ItemId: item.Id,
MediaSourceId: mediaSourceId,
PositionTicks: currentTimeInTicks,
PlaySessionId: stream.sessionId,
},
});
}, [
api,
item,
mediaSourceId,
stream,
progress,
offline,
revalidateProgressCache,
]);
}, [api, item, mediaSourceId, stream, progress, offline]);
const stop = useCallback(() => {
// Update URL with final playback position before stopping
@@ -570,9 +576,10 @@ export default function DirectPlayerPage() {
useEffect(() => {
const beforeRemoveListener = navigation.addListener("beforeRemove", stop);
return () => {
reportPlaybackStopped();
beforeRemoveListener();
};
}, [navigation, stop]);
}, [navigation, stop, reportPlaybackStopped]);
const currentPlayStateInfo = useCallback(():
| PlaybackProgressInfo