Compare commits

..

4 Commits

Author SHA1 Message Date
Gauvain
947d2e4ff3 fix(hooks): correct useMemo dependency arrays
Two memos read values missing from their dependency arrays:
- TrackSheet: the MediaStreams filter uses streamType (add it).
- LibraryItemCard: the route memo uses api (add it).
Prevents stale memoized values when those inputs change (e.g. api after re-login).
2026-06-01 01:00:12 +02:00
Felix Schneider
6b7ee0514f feat(i18n): add new translations for action sheet options (#1475)
Some checks are pending
🏗️ Build Apps / 🤖 Build Android APK (Phone) (push) Waiting to run
🏗️ Build Apps / 🤖 Build Android APK (TV) (push) Waiting to run
🏗️ Build Apps / 🍎 Build iOS IPA (Phone) (push) Waiting to run
🏗️ Build Apps / 🍎 Build iOS IPA (Phone - Unsigned) (push) Waiting to run
🏗️ Build Apps / 🍎 Build tvOS IPA (push) Waiting to run
🏗️ Build Apps / 🍎 Build tvOS IPA (Unsigned) (push) Waiting to run
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Waiting to run
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Waiting to run
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Waiting to run
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Waiting to run
🌐 Translation Sync / sync-translations (push) Waiting to run
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Waiting to run
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Waiting to run
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Waiting to run
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Waiting to run
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Waiting to run
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Waiting to run
🚦 Security & Quality Gate / 🔍 Lint & Test (typecheck) (push) Waiting to run
Co-authored-by: lance chant <13349722+lancechant@users.noreply.github.com>
Co-authored-by: Gauvain <contact@uruk.dev>
2026-05-31 23:45:45 +02:00
Fredrik Burmester
c663bd0413 fix(jellyseerr): correct RequestModal ref type to fix typecheck
advancedReqModalRef was typed as BottomSheetModal but RequestModal's
forwardRef expects BottomSheetModalMethods, causing a TS2322 error
that broke the Security & Quality Gate typecheck on develop.
2026-05-31 22:10:15 +02:00
Alex
52e6f56220 fix(auth): clear stored user on logout to prevent empty home on relaunch (#1622) 2026-05-31 21:52:41 +02:00
36 changed files with 3006 additions and 11940 deletions

View File

@@ -1,3 +1,9 @@
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import { useNavigation } from "expo-router"; import { useNavigation } from "expo-router";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
@@ -18,11 +24,6 @@ import { useDownload } from "@/providers/DownloadProvider";
import { type DownloadedItem } from "@/providers/Downloads/types"; import { type DownloadedItem } from "@/providers/Downloads/types";
import { OfflineModeProvider } from "@/providers/OfflineModeProvider"; import { OfflineModeProvider } from "@/providers/OfflineModeProvider";
import { queueAtom } from "@/utils/atoms/queue"; import { queueAtom } from "@/utils/atoms/queue";
import {
type BottomSheetMethods,
BottomSheetModal,
BottomSheetView,
} from "@/utils/expoUiBottomSheet";
import { writeToLog } from "@/utils/log"; import { writeToLog } from "@/utils/log";
export default function DownloadsPage() { export default function DownloadsPage() {
@@ -31,7 +32,7 @@ export default function DownloadsPage() {
const [_queue, _setQueue] = useAtom(queueAtom); const [_queue, _setQueue] = useAtom(queueAtom);
const { downloadedItems, deleteFileByType, deleteAllFiles } = useDownload(); const { downloadedItems, deleteFileByType, deleteAllFiles } = useDownload();
const router = useRouter(); const router = useRouter();
const bottomSheetModalRef = useRef<BottomSheetMethods>(null); const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const [showMigration, setShowMigration] = useState(false); const [showMigration, setShowMigration] = useState(false);
@@ -267,13 +268,19 @@ export default function DownloadsPage() {
<BottomSheetModal <BottomSheetModal
ref={bottomSheetModalRef} ref={bottomSheetModalRef}
enableDynamicSizing enableDynamicSizing
enablePanDownToClose
handleIndicatorStyle={{ handleIndicatorStyle={{
backgroundColor: "white", backgroundColor: "white",
}} }}
backgroundStyle={{ backgroundStyle={{
backgroundColor: "#171717", backgroundColor: "#171717",
}} }}
backdropComponent={(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
)}
> >
<BottomSheetView> <BottomSheetView>
<View className='p-4 space-y-4 mb-4'> <View className='p-4 space-y-4 mb-4'>

View File

@@ -1,4 +1,12 @@
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetTextInput,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import type { BottomSheetModalMethods } from "@gorhom/bottom-sheet/lib/typescript/types";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image"; import { Image } from "expo-image";
import { useLocalSearchParams, useNavigation } from "expo-router"; import { useLocalSearchParams, useNavigation } from "expo-router";
@@ -24,12 +32,6 @@ import { ItemActions } from "@/components/series/SeriesActions";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useJellyseerr } from "@/hooks/useJellyseerr";
import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest"; import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest";
import {
type BottomSheetMethods,
BottomSheetModal,
BottomSheetTextInput,
BottomSheetView,
} from "@/utils/expoUiBottomSheet";
import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants"; import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants";
import { import {
type IssueType, type IssueType,
@@ -75,8 +77,8 @@ const MobilePage: React.FC = () => {
const [issueMessage, setIssueMessage] = useState<string>(); const [issueMessage, setIssueMessage] = useState<string>();
const [requestBody, _setRequestBody] = useState<MediaRequestBody>(); const [requestBody, _setRequestBody] = useState<MediaRequestBody>();
const [issueTypeDropdownOpen, setIssueTypeDropdownOpen] = useState(false); const [issueTypeDropdownOpen, setIssueTypeDropdownOpen] = useState(false);
const advancedReqModalRef = useRef<BottomSheetMethods>(null); const advancedReqModalRef = useRef<BottomSheetModalMethods>(null);
const bottomSheetModalRef = useRef<BottomSheetMethods>(null); const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const { const {
data: details, data: details,
@@ -142,6 +144,17 @@ const MobilePage: React.FC = () => {
} }
}, [jellyseerrApi, pendingRequest, refetch, t]); }, [jellyseerrApi, pendingRequest, refetch, t]);
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
),
[],
);
const submitIssue = useCallback(() => { const submitIssue = useCallback(() => {
if (result.id && issueType && issueMessage && details) { if (result.id && issueType && issueMessage && details) {
jellyseerrApi jellyseerrApi
@@ -466,7 +479,6 @@ const MobilePage: React.FC = () => {
// This is till it's fixed because the menu isn't selectable on TV // This is till it's fixed because the menu isn't selectable on TV
<BottomSheetModal <BottomSheetModal
ref={bottomSheetModalRef} ref={bottomSheetModalRef}
enablePanDownToClose
enableDynamicSizing enableDynamicSizing
handleIndicatorStyle={{ handleIndicatorStyle={{
backgroundColor: "white", backgroundColor: "white",
@@ -474,6 +486,8 @@ const MobilePage: React.FC = () => {
backgroundStyle={{ backgroundStyle={{
backgroundColor: "#171717", backgroundColor: "#171717",
}} }}
backdropComponent={renderBackdrop}
stackBehavior='push'
onDismiss={handleIssueModalDismiss} onDismiss={handleIssueModalDismiss}
> >
<BottomSheetView> <BottomSheetView>

View File

@@ -1,5 +1,6 @@
import { ExpoAvRoutePickerView } from "@douglowder/expo-av-route-picker-view"; import { ExpoAvRoutePickerView } from "@douglowder/expo-av-route-picker-view";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
import type { import type {
BaseItemDto, BaseItemDto,
MediaSourceInfo, MediaSourceInfo,
@@ -221,129 +222,133 @@ export default function NowPlayingScreen() {
if (!currentTrack) { if (!currentTrack) {
return ( return (
<BottomSheetModalProvider>
<View
className='flex-1 bg-[#121212] items-center justify-center'
style={{
paddingTop: Platform.OS === "android" ? insets.top : 0,
paddingBottom: Platform.OS === "android" ? insets.bottom : 0,
}}
>
<Text className='text-neutral-500'>No track playing</Text>
</View>
</BottomSheetModalProvider>
);
}
return (
<BottomSheetModalProvider>
<View <View
className='flex-1 bg-[#121212] items-center justify-center' className='flex-1 bg-[#121212]'
style={{ style={{
paddingTop: Platform.OS === "android" ? insets.top : 0, paddingTop: Platform.OS === "android" ? insets.top : 0,
paddingBottom: Platform.OS === "android" ? insets.bottom : 0, paddingBottom: Platform.OS === "android" ? insets.bottom : 0,
}} }}
> >
<Text className='text-neutral-500'>No track playing</Text> {/* Header */}
</View> <View className='flex-row items-center justify-between px-4 pt-3 pb-2'>
);
}
return (
<View
className='flex-1 bg-[#121212]'
style={{
paddingTop: Platform.OS === "android" ? insets.top : 0,
paddingBottom: Platform.OS === "android" ? insets.bottom : 0,
}}
>
{/* Header */}
<View className='flex-row items-center justify-between px-4 pt-3 pb-2'>
<TouchableOpacity
onPress={handleClose}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
className='p-2'
>
<Ionicons name='chevron-down' size={28} color='white' />
</TouchableOpacity>
<View className='flex-row'>
<TouchableOpacity <TouchableOpacity
onPress={() => setViewMode("player")} onPress={handleClose}
className='px-3 py-1' hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
className='p-2'
> >
<Text <Ionicons name='chevron-down' size={28} color='white' />
className={
viewMode === "player"
? "text-white font-semibold"
: "text-neutral-500"
}
>
Now Playing
</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity
onPress={() => setViewMode("queue")} <View className='flex-row'>
className='px-3 py-1' <TouchableOpacity
> onPress={() => setViewMode("player")}
<Text className='px-3 py-1'
className={
viewMode === "queue"
? "text-white font-semibold"
: "text-neutral-500"
}
> >
Queue ({queue.length}) <Text
</Text> className={
</TouchableOpacity> viewMode === "player"
? "text-white font-semibold"
: "text-neutral-500"
}
>
Now Playing
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => setViewMode("queue")}
className='px-3 py-1'
>
<Text
className={
viewMode === "queue"
? "text-white font-semibold"
: "text-neutral-500"
}
>
Queue ({queue.length})
</Text>
</TouchableOpacity>
</View>
{/* Empty placeholder to balance header layout */}
<View className='p-2' style={{ width: 44 }} />
</View> </View>
{/* Empty placeholder to balance header layout */}
<View className='p-2' style={{ width: 44 }} /> {viewMode === "player" ? (
<PlayerView
api={api}
currentTrack={currentTrack}
imageUrl={imageUrl}
sliderProgress={sliderProgress}
sliderMin={sliderMin}
sliderMax={sliderMax}
progressText={progressText}
remainingText={remainingText}
isPlaying={isPlaying}
isLoading={isLoading}
repeatMode={repeatMode}
shuffleEnabled={shuffleEnabled}
canGoNext={canGoNext}
canGoPrevious={canGoPrevious}
onSliderComplete={handleSliderComplete}
onTogglePlayPause={togglePlayPause}
onNext={next}
onPrevious={previous}
onCycleRepeat={cycleRepeatMode}
onToggleShuffle={toggleShuffle}
getRepeatIcon={getRepeatIcon}
mediaSource={mediaSource}
isTranscoding={isTranscoding}
isFavorite={isFavorite}
onToggleFavorite={toggleFavorite}
onOptionsPress={handleOptionsPress}
isCastConnected={isCastConnected}
/>
) : (
<QueueView
api={api}
queue={queue}
queueIndex={queueIndex}
onJumpToIndex={jumpToIndex}
onRemoveFromQueue={removeFromQueue}
onReorderQueue={reorderQueue}
/>
)}
<TrackOptionsSheet
open={trackOptionsOpen}
setOpen={setTrackOptionsOpen}
track={currentTrack}
onAddToPlaylist={handleAddToPlaylist}
/>
<PlaylistPickerSheet
open={playlistPickerOpen}
setOpen={setPlaylistPickerOpen}
trackToAdd={currentTrack}
onCreateNew={handleCreateNewPlaylist}
/>
<CreatePlaylistModal
open={createPlaylistOpen}
setOpen={setCreatePlaylistOpen}
initialTrackId={currentTrack?.Id}
/>
</View> </View>
</BottomSheetModalProvider>
{viewMode === "player" ? (
<PlayerView
api={api}
currentTrack={currentTrack}
imageUrl={imageUrl}
sliderProgress={sliderProgress}
sliderMin={sliderMin}
sliderMax={sliderMax}
progressText={progressText}
remainingText={remainingText}
isPlaying={isPlaying}
isLoading={isLoading}
repeatMode={repeatMode}
shuffleEnabled={shuffleEnabled}
canGoNext={canGoNext}
canGoPrevious={canGoPrevious}
onSliderComplete={handleSliderComplete}
onTogglePlayPause={togglePlayPause}
onNext={next}
onPrevious={previous}
onCycleRepeat={cycleRepeatMode}
onToggleShuffle={toggleShuffle}
getRepeatIcon={getRepeatIcon}
mediaSource={mediaSource}
isTranscoding={isTranscoding}
isFavorite={isFavorite}
onToggleFavorite={toggleFavorite}
onOptionsPress={handleOptionsPress}
isCastConnected={isCastConnected}
/>
) : (
<QueueView
api={api}
queue={queue}
queueIndex={queueIndex}
onJumpToIndex={jumpToIndex}
onRemoveFromQueue={removeFromQueue}
onReorderQueue={reorderQueue}
/>
)}
<TrackOptionsSheet
open={trackOptionsOpen}
setOpen={setTrackOptionsOpen}
track={currentTrack}
onAddToPlaylist={handleAddToPlaylist}
/>
<PlaylistPickerSheet
open={playlistPickerOpen}
setOpen={setPlaylistPickerOpen}
trackToAdd={currentTrack}
onCreateNew={handleCreateNewPlaylist}
/>
<CreatePlaylistModal
open={createPlaylistOpen}
setOpen={setCreatePlaylistOpen}
initialTrackId={currentTrack?.Id}
/>
</View>
); );
} }

View File

@@ -1,5 +1,6 @@
import "@/augmentations"; import "@/augmentations";
import { ActionSheetProvider } from "@expo/react-native-action-sheet"; import { ActionSheetProvider } from "@expo/react-native-action-sheet";
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
import NetInfo from "@react-native-community/netinfo"; import NetInfo from "@react-native-community/netinfo";
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister"; import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
import { onlineManager, QueryClient } from "@tanstack/react-query"; import { onlineManager, QueryClient } from "@tanstack/react-query";
@@ -411,125 +412,127 @@ function Layout() {
<DownloadProvider> <DownloadProvider>
<MusicPlayerProvider> <MusicPlayerProvider>
<GlobalModalProvider> <GlobalModalProvider>
<IntroSheetProvider> <BottomSheetModalProvider>
<ThemeProvider value={DarkTheme}> <IntroSheetProvider>
<SystemBars style='light' hidden={false} /> <ThemeProvider value={DarkTheme}>
<Stack initialRouteName='(auth)/(tabs)'> <SystemBars style='light' hidden={false} />
<Stack.Screen <Stack initialRouteName='(auth)/(tabs)'>
name='(auth)/(tabs)' <Stack.Screen
options={{ name='(auth)/(tabs)'
headerShown: false, options={{
title: "", headerShown: false,
header: () => null, title: "",
header: () => null,
}}
/>
<Stack.Screen
name='(auth)/player'
options={{
headerShown: false,
title: "",
header: () => null,
}}
/>
<Stack.Screen
name='(auth)/now-playing'
options={{
headerShown: false,
presentation: "modal",
gestureEnabled: true,
}}
/>
<Stack.Screen
name='login'
options={{
headerShown: true,
title: "",
headerTransparent: Platform.OS === "ios",
}}
/>
<Stack.Screen name='+not-found' />
<Stack.Screen
name='(auth)/tv-option-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='(auth)/tv-subtitle-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='(auth)/tv-request-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='(auth)/tv-season-select-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='(auth)/tv-series-season-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='tv-account-action-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='tv-account-select-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='(auth)/tv-user-switch-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
</Stack>
<Toaster
duration={4000}
toastOptions={{
style: {
backgroundColor: "#262626",
borderColor: "#363639",
borderWidth: 1,
},
titleStyle: {
color: "white",
},
}} }}
closeButton
/> />
<Stack.Screen {!Platform.isTV && <GlobalModal />}
name='(auth)/player' </ThemeProvider>
options={{ </IntroSheetProvider>
headerShown: false, </BottomSheetModalProvider>
title: "",
header: () => null,
}}
/>
<Stack.Screen
name='(auth)/now-playing'
options={{
headerShown: false,
presentation: "modal",
gestureEnabled: true,
}}
/>
<Stack.Screen
name='login'
options={{
headerShown: true,
title: "",
headerTransparent: Platform.OS === "ios",
}}
/>
<Stack.Screen name='+not-found' />
<Stack.Screen
name='(auth)/tv-option-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='(auth)/tv-subtitle-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='(auth)/tv-request-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='(auth)/tv-season-select-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='(auth)/tv-series-season-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='tv-account-action-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='tv-account-select-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
<Stack.Screen
name='(auth)/tv-user-switch-modal'
options={{
headerShown: false,
presentation: "transparentModal",
animation: "fade",
}}
/>
</Stack>
<Toaster
duration={4000}
toastOptions={{
style: {
backgroundColor: "#262626",
borderColor: "#363639",
borderWidth: 1,
},
titleStyle: {
color: "white",
},
}}
closeButton
/>
{!Platform.isTV && <GlobalModal />}
</ThemeProvider>
</IntroSheetProvider>
</GlobalModalProvider> </GlobalModalProvider>
</MusicPlayerProvider> </MusicPlayerProvider>
</DownloadProvider> </DownloadProvider>

13767
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,10 @@
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import type React from "react"; import type React from "react";
import { useCallback, useEffect, useMemo, useRef } from "react"; import { useCallback, useEffect, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -6,11 +12,6 @@ import { Alert, Platform, TouchableOpacity, View } from "react-native";
import { Swipeable } from "react-native-gesture-handler"; import { Swipeable } from "react-native-gesture-handler";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Colors } from "@/constants/Colors"; import { Colors } from "@/constants/Colors";
import {
type BottomSheetMethods,
BottomSheetModal,
BottomSheetView,
} from "@/utils/expoUiBottomSheet";
import { import {
deleteAccountCredential, deleteAccountCredential,
type SavedServer, type SavedServer,
@@ -38,7 +39,7 @@ export const AccountsSheet: React.FC<AccountsSheetProps> = ({
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const bottomSheetModalRef = useRef<BottomSheetMethods>(null); const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const isAndroid = Platform.OS === "android"; const isAndroid = Platform.OS === "android";
const snapPoints = useMemo( const snapPoints = useMemo(
@@ -63,6 +64,17 @@ export const AccountsSheet: React.FC<AccountsSheetProps> = ({
[setOpen], [setOpen],
); );
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
),
[],
);
const handleDeleteAccount = async (account: SavedServerAccount) => { const handleDeleteAccount = async (account: SavedServerAccount) => {
if (!server) return; if (!server) return;
@@ -106,16 +118,15 @@ export const AccountsSheet: React.FC<AccountsSheetProps> = ({
); );
if (!server) return null; if (!server) return null;
if (Platform.isTV) return null;
return ( return (
<BottomSheetModal <BottomSheetModal
ref={bottomSheetModalRef} ref={bottomSheetModalRef}
enablePanDownToClose
snapPoints={snapPoints} snapPoints={snapPoints}
onChange={handleSheetChanges} onChange={handleSheetChanges}
handleIndicatorStyle={{ backgroundColor: "white" }} handleIndicatorStyle={{ backgroundColor: "white" }}
backgroundStyle={{ backgroundColor: "#171717" }} backgroundStyle={{ backgroundColor: "#171717" }}
backdropComponent={renderBackdrop}
> >
<BottomSheetView <BottomSheetView
style={{ style={{

View File

@@ -1,4 +1,10 @@
import Ionicons from "@expo/vector-icons/Ionicons"; import Ionicons from "@expo/vector-icons/Ionicons";
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import type { import type {
BaseItemDto, BaseItemDto,
MediaSourceInfo, MediaSourceInfo,
@@ -17,11 +23,6 @@ import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { queueAtom } from "@/utils/atoms/queue"; import { queueAtom } from "@/utils/atoms/queue";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import {
type BottomSheetMethods,
BottomSheetModal,
BottomSheetView,
} from "@/utils/expoUiBottomSheet";
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"; import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
import { getDownloadUrl } from "@/utils/jellyfin/media/getDownloadUrl"; import { getDownloadUrl } from "@/utils/jellyfin/media/getDownloadUrl";
import { AudioTrackSelector } from "./AudioTrackSelector"; import { AudioTrackSelector } from "./AudioTrackSelector";
@@ -89,7 +90,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
[user], [user],
); );
const bottomSheetModalRef = useRef<BottomSheetMethods>(null); const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const handlePresentModalPress = useCallback(() => { const handlePresentModalPress = useCallback(() => {
bottomSheetModalRef.current?.present(); bottomSheetModalRef.current?.present();
@@ -316,6 +317,17 @@ export const DownloadItems: React.FC<DownloadProps> = ({
} }
}, [closeModal, initiateDownload, itemsToDownload, userCanDownload]); }, [closeModal, initiateDownload, itemsToDownload, userCanDownload]);
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
),
[],
);
const renderButtonContent = () => { const renderButtonContent = () => {
// For single item downloads, show progress if item is being processed // For single item downloads, show progress if item is being processed
// For multi-item downloads (season/series), show progress only if 2+ items are in progress or queued // For multi-item downloads (season/series), show progress only if 2+ items are in progress or queued
@@ -363,8 +375,6 @@ export const DownloadItems: React.FC<DownloadProps> = ({
} }
}; };
if (Platform.isTV) return null;
return ( return (
<View {...props}> <View {...props}>
<RoundButton size={size} onPress={onButtonPress}> <RoundButton size={size} onPress={onButtonPress}>
@@ -380,7 +390,10 @@ export const DownloadItems: React.FC<DownloadProps> = ({
backgroundColor: "#171717", backgroundColor: "#171717",
}} }}
onChange={handleSheetChanges} onChange={handleSheetChanges}
backdropComponent={renderBackdrop}
enablePanDownToClose enablePanDownToClose
enableDismissOnClose
android_keyboardInputMode='adjustResize'
keyboardBehavior='interactive' keyboardBehavior='interactive'
keyboardBlurBehavior='restore' keyboardBlurBehavior='restore'
> >

View File

@@ -1,7 +1,10 @@
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
} from "@gorhom/bottom-sheet";
import { useCallback, useEffect } from "react"; import { useCallback, useEffect } from "react";
import { Platform } from "react-native";
import { useGlobalModal } from "@/providers/GlobalModalProvider"; import { useGlobalModal } from "@/providers/GlobalModalProvider";
import { BottomSheetModal } from "@/utils/expoUiBottomSheet";
/** /**
* GlobalModal Component * GlobalModal Component
@@ -9,7 +12,8 @@ import { BottomSheetModal } from "@/utils/expoUiBottomSheet";
* This component renders a global bottom sheet modal that can be controlled * This component renders a global bottom sheet modal that can be controlled
* from anywhere in the app using the useGlobalModal hook. * from anywhere in the app using the useGlobalModal hook.
* *
* Place this component at the root level of your app (in _layout.tsx). * Place this component at the root level of your app (in _layout.tsx)
* after BottomSheetModalProvider.
*/ */
export const GlobalModal = () => { export const GlobalModal = () => {
const { hideModal, modalState, modalRef, isVisible } = useGlobalModal(); const { hideModal, modalState, modalRef, isVisible } = useGlobalModal();
@@ -29,6 +33,17 @@ export const GlobalModal = () => {
[hideModal], [hideModal],
); );
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
),
[],
);
const defaultOptions = { const defaultOptions = {
enableDynamicSizing: true, enableDynamicSizing: true,
enablePanDownToClose: true, enablePanDownToClose: true,
@@ -43,8 +58,6 @@ export const GlobalModal = () => {
// Merge default options with provided options // Merge default options with provided options
const modalOptions = { ...defaultOptions, ...modalState.options }; const modalOptions = { ...defaultOptions, ...modalState.options };
if (Platform.isTV) return null;
return ( return (
<BottomSheetModal <BottomSheetModal
ref={modalRef} ref={modalRef}
@@ -52,9 +65,12 @@ export const GlobalModal = () => {
? { snapPoints: modalOptions.snapPoints } ? { snapPoints: modalOptions.snapPoints }
: { enableDynamicSizing: modalOptions.enableDynamicSizing })} : { enableDynamicSizing: modalOptions.enableDynamicSizing })}
onChange={handleSheetChanges} onChange={handleSheetChanges}
backdropComponent={renderBackdrop}
handleIndicatorStyle={modalOptions.handleIndicatorStyle} handleIndicatorStyle={modalOptions.handleIndicatorStyle}
backgroundStyle={modalOptions.backgroundStyle} backgroundStyle={modalOptions.backgroundStyle}
enablePanDownToClose={modalOptions.enablePanDownToClose} enablePanDownToClose={modalOptions.enablePanDownToClose}
enableDismissOnClose
stackBehavior='push'
style={{ zIndex: 1000 }} style={{ zIndex: 1000 }}
> >
{modalState.content} {modalState.content}

View File

@@ -1,4 +1,10 @@
import { Feather, Ionicons } from "@expo/vector-icons"; import { Feather, Ionicons } from "@expo/vector-icons";
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetScrollView,
} from "@gorhom/bottom-sheet";
import { Image } from "expo-image"; import { Image } from "expo-image";
import { forwardRef, useCallback, useImperativeHandle, useRef } from "react"; import { forwardRef, useCallback, useImperativeHandle, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -7,11 +13,6 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Button } from "@/components/Button"; import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import {
type BottomSheetMethods,
BottomSheetModal,
BottomSheetScrollView,
} from "@/utils/expoUiBottomSheet";
import { storage } from "@/utils/mmkv"; import { storage } from "@/utils/mmkv";
export interface IntroSheetRef { export interface IntroSheetRef {
@@ -20,7 +21,7 @@ export interface IntroSheetRef {
} }
export const IntroSheet = forwardRef<IntroSheetRef>((_, ref) => { export const IntroSheet = forwardRef<IntroSheetRef>((_, ref) => {
const bottomSheetRef = useRef<BottomSheetMethods>(null); const bottomSheetRef = useRef<BottomSheetModal>(null);
const { t } = useTranslation(); const { t } = useTranslation();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const router = useRouter(); const router = useRouter();
@@ -35,6 +36,17 @@ export const IntroSheet = forwardRef<IntroSheetRef>((_, ref) => {
}, },
})); }));
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
),
[],
);
const handleDismiss = useCallback(() => { const handleDismiss = useCallback(() => {
bottomSheetRef.current?.dismiss(); bottomSheetRef.current?.dismiss();
}, []); }, []);
@@ -44,13 +56,11 @@ export const IntroSheet = forwardRef<IntroSheetRef>((_, ref) => {
router.push("/settings"); router.push("/settings");
}, []); }, []);
if (Platform.isTV) return null;
return ( return (
<BottomSheetModal <BottomSheetModal
ref={bottomSheetRef} ref={bottomSheetRef}
enablePanDownToClose
enableDynamicSizing enableDynamicSizing
backdropComponent={renderBackdrop}
backgroundStyle={{ backgroundColor: "#171717" }} backgroundStyle={{ backgroundColor: "#171717" }}
handleIndicatorStyle={{ backgroundColor: "#737373" }} handleIndicatorStyle={{ backgroundColor: "#737373" }}
> >

View File

@@ -1,4 +1,10 @@
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetScrollView,
} from "@gorhom/bottom-sheet";
import type { import type {
MediaSourceInfo, MediaSourceInfo,
MediaStream, MediaStream,
@@ -6,13 +12,8 @@ import type {
import type React from "react"; import type React from "react";
import { useMemo, useRef } from "react"; import { useMemo, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Platform, TouchableOpacity, View } from "react-native"; import { TouchableOpacity, View } from "react-native";
import { formatBitrate } from "@/utils/bitrate"; import { formatBitrate } from "@/utils/bitrate";
import {
type BottomSheetMethods,
BottomSheetModal,
BottomSheetScrollView,
} from "@/utils/expoUiBottomSheet";
import { Badge } from "./Badge"; import { Badge } from "./Badge";
import { Text } from "./common/Text"; import { Text } from "./common/Text";
@@ -21,20 +22,9 @@ interface Props {
} }
export const ItemTechnicalDetails: React.FC<Props> = ({ source }) => { export const ItemTechnicalDetails: React.FC<Props> = ({ source }) => {
const bottomSheetModalRef = useRef<BottomSheetMethods>(null); const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const { t } = useTranslation(); const { t } = useTranslation();
if (Platform.isTV) {
return (
<View className='px-4 mt-2 mb-4'>
<Text className='text-lg font-bold mb-4'>{t("item_card.video")}</Text>
<View className='flex flex-row space-x-2'>
<VideoStreamInfo source={source} />
</View>
</View>
);
}
return ( return (
<View className='px-4 mt-2 mb-4'> <View className='px-4 mt-2 mb-4'>
<Text className='text-lg font-bold mb-4'>{t("item_card.video")}</Text> <Text className='text-lg font-bold mb-4'>{t("item_card.video")}</Text>
@@ -46,27 +36,27 @@ export const ItemTechnicalDetails: React.FC<Props> = ({ source }) => {
</TouchableOpacity> </TouchableOpacity>
<BottomSheetModal <BottomSheetModal
ref={bottomSheetModalRef} ref={bottomSheetModalRef}
enableDynamicSizing snapPoints={["80%"]}
enablePanDownToClose
handleIndicatorStyle={{ handleIndicatorStyle={{
backgroundColor: "white", backgroundColor: "white",
}} }}
backgroundStyle={{ backgroundStyle={{
backgroundColor: "#171717", backgroundColor: "#171717",
}} }}
backdropComponent={(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
)}
> >
<BottomSheetScrollView> <BottomSheetScrollView>
<View className='flex flex-col space-y-2 p-4'> <View className='flex flex-col space-y-2 p-4 mb-4'>
<View className='flex-row items-center justify-between mb-2'>
<Text className='text-lg font-bold'>{t("item_card.video")}</Text>
<TouchableOpacity
onPress={() => bottomSheetModalRef.current?.dismiss()}
hitSlop={12}
>
<Ionicons name='close' size={24} color='white' />
</TouchableOpacity>
</View>
<View> <View>
<Text className='text-lg font-bold mb-4'>
{t("item_card.video")}
</Text>
<View className='flex flex-row space-x-2'> <View className='flex flex-row space-x-2'>
<VideoStreamInfo source={source} /> <VideoStreamInfo source={source} />
</View> </View>

View File

@@ -1,3 +1,9 @@
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import type React from "react"; import type React from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -11,11 +17,6 @@ import {
} from "react-native"; } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useHaptic } from "@/hooks/useHaptic"; import { useHaptic } from "@/hooks/useHaptic";
import {
type BottomSheetMethods,
BottomSheetModal,
BottomSheetView,
} from "@/utils/expoUiBottomSheet";
import { verifyAccountPIN } from "@/utils/secureCredentials"; import { verifyAccountPIN } from "@/utils/secureCredentials";
import { Button } from "./Button"; import { Button } from "./Button";
import { Text } from "./common/Text"; import { Text } from "./common/Text";
@@ -42,7 +43,7 @@ export const PINEntryModal: React.FC<PINEntryModalProps> = ({
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const bottomSheetModalRef = useRef<BottomSheetMethods>(null); const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const [pinCode, setPinCode] = useState(""); const [pinCode, setPinCode] = useState("");
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isVerifying, setIsVerifying] = useState(false); const [isVerifying, setIsVerifying] = useState(false);
@@ -77,6 +78,17 @@ export const PINEntryModal: React.FC<PINEntryModalProps> = ({
[onClose], [onClose],
); );
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
),
[],
);
const shake = () => { const shake = () => {
Animated.sequence([ Animated.sequence([
Animated.timing(shakeAnimation, { Animated.timing(shakeAnimation, {
@@ -147,18 +159,18 @@ export const PINEntryModal: React.FC<PINEntryModalProps> = ({
]); ]);
}; };
if (Platform.isTV) return null;
return ( return (
<BottomSheetModal <BottomSheetModal
ref={bottomSheetModalRef} ref={bottomSheetModalRef}
enablePanDownToClose
snapPoints={snapPoints} snapPoints={snapPoints}
onChange={handleSheetChanges} onChange={handleSheetChanges}
handleIndicatorStyle={{ backgroundColor: "white" }} handleIndicatorStyle={{ backgroundColor: "white" }}
backgroundStyle={{ backgroundColor: "#171717" }} backgroundStyle={{ backgroundColor: "#171717" }}
backdropComponent={renderBackdrop}
keyboardBehavior={isAndroid ? "fillParent" : "interactive"} keyboardBehavior={isAndroid ? "fillParent" : "interactive"}
keyboardBlurBehavior='restore' keyboardBlurBehavior='restore'
android_keyboardInputMode='adjustResize'
topInset={isAndroid ? 0 : undefined}
> >
<BottomSheetView <BottomSheetView
style={{ style={{

View File

@@ -1,15 +1,16 @@
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetTextInput,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import type React from "react"; import type React from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ActivityIndicator, Platform, View } from "react-native"; import { ActivityIndicator, Platform, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useHaptic } from "@/hooks/useHaptic"; import { useHaptic } from "@/hooks/useHaptic";
import {
type BottomSheetMethods,
BottomSheetModal,
BottomSheetTextInput,
BottomSheetView,
} from "@/utils/expoUiBottomSheet";
import { Button } from "./Button"; import { Button } from "./Button";
import { Text } from "./common/Text"; import { Text } from "./common/Text";
@@ -28,7 +29,7 @@ export const PasswordEntryModal: React.FC<PasswordEntryModalProps> = ({
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const bottomSheetModalRef = useRef<BottomSheetMethods>(null); const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -61,6 +62,17 @@ export const PasswordEntryModal: React.FC<PasswordEntryModalProps> = ({
[onClose], [onClose],
); );
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
),
[],
);
const handleSubmit = async () => { const handleSubmit = async () => {
if (!password) { if (!password) {
setError(t("password.enter_password")); setError(t("password.enter_password"));
@@ -81,18 +93,18 @@ export const PasswordEntryModal: React.FC<PasswordEntryModalProps> = ({
} }
}; };
if (Platform.isTV) return null;
return ( return (
<BottomSheetModal <BottomSheetModal
ref={bottomSheetModalRef} ref={bottomSheetModalRef}
enablePanDownToClose
snapPoints={snapPoints} snapPoints={snapPoints}
onChange={handleSheetChanges} onChange={handleSheetChanges}
handleIndicatorStyle={{ backgroundColor: "white" }} handleIndicatorStyle={{ backgroundColor: "white" }}
backgroundStyle={{ backgroundColor: "#171717" }} backgroundStyle={{ backgroundColor: "#171717" }}
backdropComponent={renderBackdrop}
keyboardBehavior={isAndroid ? "fillParent" : "interactive"} keyboardBehavior={isAndroid ? "fillParent" : "interactive"}
keyboardBlurBehavior='restore' keyboardBlurBehavior='restore'
android_keyboardInputMode='adjustResize'
topInset={isAndroid ? 0 : undefined}
> >
<BottomSheetView <BottomSheetView
style={{ style={{

View File

@@ -1,4 +1,5 @@
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { BottomSheetScrollView } from "@gorhom/bottom-sheet";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { import {
type LayoutChangeEvent, type LayoutChangeEvent,
@@ -10,7 +11,6 @@ import {
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { useGlobalModal } from "@/providers/GlobalModalProvider"; import { useGlobalModal } from "@/providers/GlobalModalProvider";
import { BottomSheetScrollView } from "@/utils/expoUiBottomSheet";
// @expo/ui's SwiftUI native module (ExpoUI) does not exist in tvOS builds. // @expo/ui's SwiftUI native module (ExpoUI) does not exist in tvOS builds.
// A static top-level import evaluates requireNativeModule('ExpoUI') at module // A static top-level import evaluates requireNativeModule('ExpoUI') at module

View File

@@ -1,5 +1,6 @@
import { useActionSheet } from "@expo/react-native-action-sheet"; import { useActionSheet } from "@expo/react-native-action-sheet";
import { Feather, Ionicons } from "@expo/vector-icons"; import { Feather, Ionicons } from "@expo/vector-icons";
import { BottomSheetView } from "@gorhom/bottom-sheet";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { useAtom, useAtomValue } from "jotai"; import { useAtom, useAtomValue } from "jotai";
import { useCallback, useEffect } from "react"; import { useCallback, useEffect } from "react";
@@ -31,7 +32,6 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useOfflineMode } from "@/providers/OfflineModeProvider"; import { useOfflineMode } from "@/providers/OfflineModeProvider";
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor"; import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { BottomSheetView } from "@/utils/expoUiBottomSheet";
import { getParentBackdropImageUrl } from "@/utils/jellyfin/image/getParentBackdropImageUrl"; import { getParentBackdropImageUrl } from "@/utils/jellyfin/image/getParentBackdropImageUrl";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";

View File

@@ -39,21 +39,19 @@ export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
if (Platform.OS === "ios") { if (Platform.OS === "ios") {
return ( return (
<Pressable onPress={handlePress} {...(viewProps as any)}> <Pressable
<BlurView onPress={handlePress}
intensity={50} className={`rounded-full ${buttonSize} flex items-center justify-center ${fillColorClass}`}
tint='systemChromeMaterial' {...(viewProps as any)}
className={`rounded-full overflow-hidden ${buttonSize} flex items-center justify-center ${fillColorClass}`} >
> {icon ? (
{icon ? ( <Ionicons
<Ionicons name={icon}
name={icon} size={size === "large" ? 22 : 18}
size={size === "large" ? 22 : 18} color={color === "white" ? "white" : "#9334E9"}
color={color === "white" ? "white" : "#9334E9"} />
/> ) : null}
) : null} {children ? children : null}
{children ? children : null}
</BlurView>
</Pressable> </Pressable>
); );
} }

View File

@@ -1,14 +1,15 @@
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import type React from "react"; import type React from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Platform, TouchableOpacity, View } from "react-native"; import { Platform, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import {
type BottomSheetMethods,
BottomSheetModal,
BottomSheetView,
} from "@/utils/expoUiBottomSheet";
import type { AccountSecurityType } from "@/utils/secureCredentials"; import type { AccountSecurityType } from "@/utils/secureCredentials";
import { Button } from "./Button"; import { Button } from "./Button";
import { Text } from "./common/Text"; import { Text } from "./common/Text";
@@ -57,7 +58,7 @@ export const SaveAccountModal: React.FC<SaveAccountModalProps> = ({
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const bottomSheetModalRef = useRef<BottomSheetMethods>(null); const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const [selectedType, setSelectedType] = useState<AccountSecurityType>("none"); const [selectedType, setSelectedType] = useState<AccountSecurityType>("none");
const [pinCode, setPinCode] = useState(""); const [pinCode, setPinCode] = useState("");
const [pinError, setPinError] = useState<string | null>(null); const [pinError, setPinError] = useState<string | null>(null);
@@ -92,6 +93,17 @@ export const SaveAccountModal: React.FC<SaveAccountModalProps> = ({
setPinError(null); setPinError(null);
}; };
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
),
[],
);
const handleOptionSelect = (type: AccountSecurityType) => { const handleOptionSelect = (type: AccountSecurityType) => {
setSelectedType(type); setSelectedType(type);
setPinCode(""); setPinCode("");
@@ -123,18 +135,18 @@ export const SaveAccountModal: React.FC<SaveAccountModalProps> = ({
return true; return true;
}; };
if (Platform.isTV) return null;
return ( return (
<BottomSheetModal <BottomSheetModal
ref={bottomSheetModalRef} ref={bottomSheetModalRef}
enablePanDownToClose
snapPoints={snapPoints} snapPoints={snapPoints}
onChange={handleSheetChanges} onChange={handleSheetChanges}
handleIndicatorStyle={{ backgroundColor: "white" }} handleIndicatorStyle={{ backgroundColor: "white" }}
backgroundStyle={{ backgroundColor: "#171717" }} backgroundStyle={{ backgroundColor: "#171717" }}
backdropComponent={renderBackdrop}
keyboardBehavior={isAndroid ? "fillParent" : "interactive"} keyboardBehavior={isAndroid ? "fillParent" : "interactive"}
keyboardBlurBehavior='restore' keyboardBlurBehavior='restore'
android_keyboardInputMode='adjustResize'
topInset={isAndroid ? 0 : undefined}
> >
<BottomSheetView <BottomSheetView
style={{ style={{

View File

@@ -26,7 +26,7 @@ export const TrackSheet: React.FC<Props> = ({
const streams = useMemo( const streams = useMemo(
() => source?.MediaStreams?.filter((x) => x.Type === streamType), () => source?.MediaStreams?.filter((x) => x.Type === streamType),
[source], [source, streamType],
); );
const selectedSteam = useMemo( const selectedSteam = useMemo(

View File

@@ -2,6 +2,7 @@ import { useActionSheet } from "@expo/react-native-action-sheet";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useSegments } from "expo-router"; import { useSegments } from "expo-router";
import { type PropsWithChildren, useCallback } from "react"; import { type PropsWithChildren, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { import {
Platform, Platform,
TouchableOpacity, TouchableOpacity,
@@ -149,6 +150,7 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
children, children,
...props ...props
}) => { }) => {
const { t } = useTranslation();
const segments = useSegments(); const segments = useSegments();
const { showActionSheetWithOptions } = useActionSheet(); const { showActionSheetWithOptions } = useActionSheet();
const markAsPlayedStatus = useMarkAsPlayed([item]); const markAsPlayedStatus = useMarkAsPlayed([item]);
@@ -182,11 +184,13 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
return; return;
const options: string[] = [ const options: string[] = [
"Mark as Played", t("common.mark_as_played"),
"Mark as Not Played", t("common.mark_as_not_played"),
isFavorite ? "Unmark as Favorite" : "Mark as Favorite", isFavorite
...(isOffline ? ["Delete Download"] : []), ? t("music.track_options.remove_from_favorites")
"Cancel", : t("music.track_options.add_to_favorites"),
...(isOffline ? [t("home.downloads.delete_download")] : []),
t("common.cancel"),
]; ];
const cancelButtonIndex = options.length - 1; const cancelButtonIndex = options.length - 1;
const destructiveButtonIndex = isOffline const destructiveButtonIndex = isOffline
@@ -219,6 +223,7 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
isOffline, isOffline,
deleteFile, deleteFile,
item.Id, item.Id,
t,
]); ]);
if ( if (

View File

@@ -1,10 +1,15 @@
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetScrollView,
} from "@gorhom/bottom-sheet";
import { isEqual } from "lodash"; import { isEqual } from "lodash";
import type React from "react"; import type React from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
Platform,
StyleSheet, StyleSheet,
TouchableOpacity, TouchableOpacity,
View, View,
@@ -12,11 +17,6 @@ import {
} from "react-native"; } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import {
type BottomSheetMethods,
BottomSheetModal,
BottomSheetScrollView,
} from "@/utils/expoUiBottomSheet";
import { Button } from "../Button"; import { Button } from "../Button";
import { Input } from "../common/Input"; import { Input } from "../common/Input";
@@ -75,7 +75,7 @@ export const FilterSheet = <T,>({
disableSearch = false, disableSearch = false,
multiple = false, multiple = false,
}: Props<T>) => { }: Props<T>) => {
const bottomSheetModalRef = useRef<BottomSheetMethods>(null); const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const snapPoints = useMemo(() => ["85%"], []); const snapPoints = useMemo(() => ["85%"], []);
const { t } = useTranslation(); const { t } = useTranslation();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
@@ -143,15 +143,24 @@ export const FilterSheet = <T,>({
return data; return data;
}, [search, filteredData, data]); }, [search, filteredData, data]);
if (Platform.isTV) return null; const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
),
[],
);
return ( return (
<BottomSheetModal <BottomSheetModal
ref={bottomSheetModalRef} ref={bottomSheetModalRef}
enablePanDownToClose
index={0} index={0}
snapPoints={snapPoints} snapPoints={snapPoints}
onChange={handleSheetChanges} onChange={handleSheetChanges}
backdropComponent={renderBackdrop}
handleIndicatorStyle={{ handleIndicatorStyle={{
backgroundColor: "white", backgroundColor: "white",
}} }}

View File

@@ -1,3 +1,4 @@
import { BottomSheetTextInput } from "@gorhom/bottom-sheet";
import React, { useCallback, useImperativeHandle, useRef } from "react"; import React, { useCallback, useImperativeHandle, useRef } from "react";
import { import {
type StyleProp, type StyleProp,
@@ -7,7 +8,6 @@ import {
View, View,
type ViewStyle, type ViewStyle,
} from "react-native"; } from "react-native";
import { BottomSheetTextInput } from "@/utils/expoUiBottomSheet";
interface PinInputProps interface PinInputProps
extends Omit<TextInputProps, "value" | "onChangeText" | "style"> { extends Omit<TextInputProps, "value" | "onChangeText" | "style"> {

View File

@@ -1,22 +1,23 @@
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import type { BottomSheetModalMethods } from "@gorhom/bottom-sheet/lib/typescript/types";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { forwardRef, useCallback, useMemo, useState } from "react"; import { forwardRef, useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Platform, View, type ViewProps } from "react-native"; import { View, type ViewProps } from "react-native";
import { Button } from "@/components/Button"; import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { PlatformDropdown } from "@/components/PlatformDropdown"; import { PlatformDropdown } from "@/components/PlatformDropdown";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useJellyseerr } from "@/hooks/useJellyseerr";
import {
type BottomSheetMethods,
BottomSheetModal,
BottomSheetView,
} from "@/utils/expoUiBottomSheet";
import type { import type {
QualityProfile, QualityProfile,
RootFolder, RootFolder,
Tag, Tag,
} from "@/utils/jellyseerr/server/api/servarr/base"; } from "@/utils/jellyseerr/server/api/servarr/base";
import type { MediaType } from "@/utils/jellyseerr/server/constants/media"; import type { MediaType } from "@/utils/jellyseerr/server/constants/media";
import type { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; import type { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
import { writeDebugLog } from "@/utils/log"; import { writeDebugLog } from "@/utils/log";
@@ -33,7 +34,7 @@ interface Props {
} }
const RequestModal = forwardRef< const RequestModal = forwardRef<
BottomSheetMethods, BottomSheetModalMethods,
Props & Omit<ViewProps, "id"> Props & Omit<ViewProps, "id">
>( >(
( (
@@ -282,13 +283,11 @@ const RequestModal = forwardRef<
defaultTags, defaultTags,
]); ]);
if (Platform.isTV) return null;
return ( return (
<BottomSheetModal <BottomSheetModal
ref={ref} ref={ref}
enablePanDownToClose
enableDynamicSizing enableDynamicSizing
enableDismissOnClose
onDismiss={handleDismiss} onDismiss={handleDismiss}
handleIndicatorStyle={{ handleIndicatorStyle={{
backgroundColor: "white", backgroundColor: "white",
@@ -296,6 +295,14 @@ const RequestModal = forwardRef<
backgroundStyle={{ backgroundStyle={{
backgroundColor: "#171717", backgroundColor: "#171717",
}} }}
backdropComponent={(sheetProps: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...sheetProps}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
)}
stackBehavior='push'
> >
<BottomSheetView> <BottomSheetView>
<View className='flex flex-col space-y-4 px-4 pb-8 pt-2'> <View className='flex flex-col space-y-4 px-4 pb-8 pt-2'>

View File

@@ -51,7 +51,7 @@ export const LibraryItemCard: React.FC<Props> = ({ library, ...props }) => {
api, api,
item: library, item: library,
}), }),
[library], [api, library],
); );
const itemType = useMemo(() => { const itemType = useMemo(() => {

View File

@@ -1,3 +1,10 @@
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetTextInput,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import React, { import React, {
useCallback, useCallback,
useEffect, useEffect,
@@ -6,17 +13,11 @@ import React, {
useState, useState,
} from "react"; } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ActivityIndicator, Keyboard, Platform } from "react-native"; import { ActivityIndicator, Keyboard } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Button } from "@/components/Button"; import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { useCreatePlaylist } from "@/hooks/usePlaylistMutations"; import { useCreatePlaylist } from "@/hooks/usePlaylistMutations";
import {
type BottomSheetMethods,
BottomSheetModal,
BottomSheetTextInput,
BottomSheetView,
} from "@/utils/expoUiBottomSheet";
interface Props { interface Props {
open: boolean; open: boolean;
@@ -31,7 +32,7 @@ export const CreatePlaylistModal: React.FC<Props> = ({
onPlaylistCreated, onPlaylistCreated,
initialTrackId, initialTrackId,
}) => { }) => {
const bottomSheetModalRef = useRef<BottomSheetMethods>(null); const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { t } = useTranslation(); const { t } = useTranslation();
const createPlaylist = useCreatePlaylist(); const createPlaylist = useCreatePlaylist();
@@ -58,6 +59,17 @@ export const CreatePlaylistModal: React.FC<Props> = ({
[setOpen], [setOpen],
); );
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
),
[],
);
const handleCreate = useCallback(async () => { const handleCreate = useCallback(async () => {
if (!name.trim()) return; if (!name.trim()) return;
@@ -74,15 +86,13 @@ export const CreatePlaylistModal: React.FC<Props> = ({
const isValid = name.trim().length > 0; const isValid = name.trim().length > 0;
if (Platform.isTV) return null;
return ( return (
<BottomSheetModal <BottomSheetModal
ref={bottomSheetModalRef} ref={bottomSheetModalRef}
enablePanDownToClose
index={0} index={0}
snapPoints={snapPoints} snapPoints={snapPoints}
onChange={handleSheetChanges} onChange={handleSheetChanges}
backdropComponent={renderBackdrop}
handleIndicatorStyle={{ handleIndicatorStyle={{
backgroundColor: "white", backgroundColor: "white",
}} }}

View File

@@ -1,23 +1,18 @@
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import React, { useCallback, useEffect, useMemo, useRef } from "react"; import React, { useCallback, useEffect, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import { Alert, StyleSheet, TouchableOpacity, View } from "react-native";
Alert,
Platform,
StyleSheet,
TouchableOpacity,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { useDeletePlaylist } from "@/hooks/usePlaylistMutations"; import { useDeletePlaylist } from "@/hooks/usePlaylistMutations";
import {
type BottomSheetMethods,
BottomSheetModal,
BottomSheetView,
} from "@/utils/expoUiBottomSheet";
interface Props { interface Props {
open: boolean; open: boolean;
@@ -30,7 +25,7 @@ export const PlaylistOptionsSheet: React.FC<Props> = ({
setOpen, setOpen,
playlist, playlist,
}) => { }) => {
const bottomSheetModalRef = useRef<BottomSheetMethods>(null); const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const router = useRouter(); const router = useRouter();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -52,6 +47,17 @@ export const PlaylistOptionsSheet: React.FC<Props> = ({
[setOpen], [setOpen],
); );
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
),
[],
);
const handleDeletePlaylist = useCallback(() => { const handleDeletePlaylist = useCallback(() => {
if (!playlist?.Id) return; if (!playlist?.Id) return;
@@ -83,15 +89,14 @@ export const PlaylistOptionsSheet: React.FC<Props> = ({
}, [playlist, deletePlaylist, setOpen, router, t]); }, [playlist, deletePlaylist, setOpen, router, t]);
if (!playlist) return null; if (!playlist) return null;
if (Platform.isTV) return null;
return ( return (
<BottomSheetModal <BottomSheetModal
ref={bottomSheetModalRef} ref={bottomSheetModalRef}
enablePanDownToClose
index={0} index={0}
snapPoints={snapPoints} snapPoints={snapPoints}
onChange={handleSheetChanges} onChange={handleSheetChanges}
backdropComponent={renderBackdrop}
handleIndicatorStyle={{ handleIndicatorStyle={{
backgroundColor: "white", backgroundColor: "white",
}} }}

View File

@@ -1,4 +1,10 @@
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetScrollView,
} from "@gorhom/bottom-sheet";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
@@ -14,7 +20,6 @@ import React, {
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
ActivityIndicator, ActivityIndicator,
Platform,
StyleSheet, StyleSheet,
TouchableOpacity, TouchableOpacity,
View, View,
@@ -24,11 +29,6 @@ import { Input } from "@/components/common/Input";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { useAddToPlaylist } from "@/hooks/usePlaylistMutations"; import { useAddToPlaylist } from "@/hooks/usePlaylistMutations";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import {
type BottomSheetMethods,
BottomSheetModal,
BottomSheetScrollView,
} from "@/utils/expoUiBottomSheet";
interface Props { interface Props {
open: boolean; open: boolean;
@@ -43,7 +43,7 @@ export const PlaylistPickerSheet: React.FC<Props> = ({
trackToAdd, trackToAdd,
onCreateNew, onCreateNew,
}) => { }) => {
const bottomSheetModalRef = useRef<BottomSheetMethods>(null); const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom); const [user] = useAtom(userAtom);
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
@@ -101,6 +101,17 @@ export const PlaylistPickerSheet: React.FC<Props> = ({
[setOpen], [setOpen],
); );
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
),
[],
);
const handleSelectPlaylist = useCallback( const handleSelectPlaylist = useCallback(
async (playlist: BaseItemDto) => { async (playlist: BaseItemDto) => {
if (!trackToAdd?.Id || !playlist.Id) return; if (!trackToAdd?.Id || !playlist.Id) return;
@@ -131,15 +142,13 @@ export const PlaylistPickerSheet: React.FC<Props> = ({
[api], [api],
); );
if (Platform.isTV) return null;
return ( return (
<BottomSheetModal <BottomSheetModal
ref={bottomSheetModalRef} ref={bottomSheetModalRef}
enablePanDownToClose
index={0} index={0}
snapPoints={snapPoints} snapPoints={snapPoints}
onChange={handleSheetChanges} onChange={handleSheetChanges}
backdropComponent={renderBackdrop}
handleIndicatorStyle={{ handleIndicatorStyle={{
backgroundColor: "white", backgroundColor: "white",
}} }}

View File

@@ -1,14 +1,15 @@
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import React, { useCallback, useEffect, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next";
import { Platform, StyleSheet, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text";
import { import {
type BottomSheetMethods, BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal, BottomSheetModal,
BottomSheetView, BottomSheetView,
} from "@/utils/expoUiBottomSheet"; } from "@gorhom/bottom-sheet";
import React, { useCallback, useEffect, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next";
import { StyleSheet, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text";
export type PlaylistSortOption = "SortName" | "DateCreated"; export type PlaylistSortOption = "SortName" | "DateCreated";
@@ -42,7 +43,7 @@ export const PlaylistSortSheet: React.FC<Props> = ({
sortOrder, sortOrder,
onSortChange, onSortChange,
}) => { }) => {
const bottomSheetModalRef = useRef<BottomSheetMethods>(null); const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -62,6 +63,17 @@ export const PlaylistSortSheet: React.FC<Props> = ({
[setOpen], [setOpen],
); );
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
),
[],
);
const handleSortSelect = useCallback( const handleSortSelect = useCallback(
(option: PlaylistSortOption) => { (option: PlaylistSortOption) => {
// If selecting same option, toggle order; otherwise use sensible default // If selecting same option, toggle order; otherwise use sensible default
@@ -81,15 +93,13 @@ export const PlaylistSortSheet: React.FC<Props> = ({
[sortBy, sortOrder, onSortChange, setOpen], [sortBy, sortOrder, onSortChange, setOpen],
); );
if (Platform.isTV) return null;
return ( return (
<BottomSheetModal <BottomSheetModal
ref={bottomSheetModalRef} ref={bottomSheetModalRef}
enablePanDownToClose
index={0} index={0}
snapPoints={snapPoints} snapPoints={snapPoints}
onChange={handleSheetChanges} onChange={handleSheetChanges}
backdropComponent={renderBackdrop}
handleIndicatorStyle={{ handleIndicatorStyle={{
backgroundColor: "white", backgroundColor: "white",
}} }}

View File

@@ -1,4 +1,10 @@
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { Image } from "expo-image"; import { Image } from "expo-image";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
@@ -12,7 +18,6 @@ import React, {
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
ActivityIndicator, ActivityIndicator,
Platform,
StyleSheet, StyleSheet,
TouchableOpacity, TouchableOpacity,
View, View,
@@ -31,11 +36,6 @@ import {
} from "@/providers/AudioStorage"; } from "@/providers/AudioStorage";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useMusicPlayer } from "@/providers/MusicPlayerProvider"; import { useMusicPlayer } from "@/providers/MusicPlayerProvider";
import {
type BottomSheetMethods,
BottomSheetModal,
BottomSheetView,
} from "@/utils/expoUiBottomSheet";
import { getAudioStreamUrl } from "@/utils/jellyfin/audio/getAudioStreamUrl"; import { getAudioStreamUrl } from "@/utils/jellyfin/audio/getAudioStreamUrl";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
@@ -56,7 +56,7 @@ export const TrackOptionsSheet: React.FC<Props> = ({
playlistId, playlistId,
onRemoveFromPlaylist, onRemoveFromPlaylist,
}) => { }) => {
const bottomSheetModalRef = useRef<BottomSheetMethods>(null); const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom); const [user] = useAtom(userAtom);
const router = useRouter(); const router = useRouter();
@@ -128,6 +128,17 @@ export const TrackOptionsSheet: React.FC<Props> = ({
[setOpen], [setOpen],
); );
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
),
[],
);
const handlePlayNext = useCallback(() => { const handlePlayNext = useCallback(() => {
if (track) { if (track) {
playNext(track); playNext(track);
@@ -216,14 +227,13 @@ export const TrackOptionsSheet: React.FC<Props> = ({
const hasAlbum = !!(track?.AlbumId || track?.ParentId); const hasAlbum = !!(track?.AlbumId || track?.ParentId);
if (!track) return null; if (!track) return null;
if (Platform.isTV) return null;
return ( return (
<BottomSheetModal <BottomSheetModal
ref={bottomSheetModalRef} ref={bottomSheetModalRef}
enablePanDownToClose
enableDynamicSizing enableDynamicSizing
onChange={handleSheetChanges} onChange={handleSheetChanges}
backdropComponent={renderBackdrop}
handleIndicatorStyle={{ handleIndicatorStyle={{
backgroundColor: "white", backgroundColor: "white",
}} }}

View File

@@ -1,9 +1,14 @@
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import type React from "react"; import type React from "react";
import { useCallback, useEffect, useRef } from "react"; import { useCallback, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
Platform,
StyleSheet, StyleSheet,
TouchableOpacity, TouchableOpacity,
View, View,
@@ -11,11 +16,6 @@ import {
} from "react-native"; } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import {
type BottomSheetMethods,
BottomSheetModal,
BottomSheetView,
} from "@/utils/expoUiBottomSheet";
interface LibraryOptions { interface LibraryOptions {
display: "row" | "list"; display: "row" | "list";
@@ -132,7 +132,7 @@ export const LibraryOptionsSheet: React.FC<Props> = ({
updateSettings, updateSettings,
disabled = false, disabled = false,
}) => { }) => {
const bottomSheetModalRef = useRef<BottomSheetMethods>(null); const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const { t } = useTranslation(); const { t } = useTranslation();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
@@ -161,14 +161,25 @@ export const LibraryOptionsSheet: React.FC<Props> = ({
[setOpen], [setOpen],
); );
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
),
[],
);
if (disabled) return null; if (disabled) return null;
if (Platform.isTV) return null;
return ( return (
<BottomSheetModal <BottomSheetModal
ref={bottomSheetModalRef} ref={bottomSheetModalRef}
enableDynamicSizing enableDynamicSizing
onChange={handleSheetChanges} onChange={handleSheetChanges}
backdropComponent={renderBackdrop}
handleIndicatorStyle={{ handleIndicatorStyle={{
backgroundColor: "white", backgroundColor: "white",
}} }}
@@ -176,6 +187,7 @@ export const LibraryOptionsSheet: React.FC<Props> = ({
backgroundColor: "#171717", backgroundColor: "#171717",
}} }}
enablePanDownToClose enablePanDownToClose
enableDismissOnClose
> >
<BottomSheetView> <BottomSheetView>
<View <View

View File

@@ -1,3 +1,9 @@
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import { getQuickConnectApi } from "@jellyfin/sdk/lib/utils/api"; import { getQuickConnectApi } from "@jellyfin/sdk/lib/utils/api";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import type React from "react"; import type React from "react";
@@ -6,11 +12,6 @@ import { useTranslation } from "react-i18next";
import { Alert, Platform, View, type ViewProps } from "react-native"; import { Alert, Platform, View, type ViewProps } from "react-native";
import { useHaptic } from "@/hooks/useHaptic"; import { useHaptic } from "@/hooks/useHaptic";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import {
type BottomSheetMethods,
BottomSheetModal,
BottomSheetView,
} from "@/utils/expoUiBottomSheet";
import { Button } from "../Button"; import { Button } from "../Button";
import { Text } from "../common/Text"; import { Text } from "../common/Text";
import { PinInput } from "../inputs/PinInput"; import { PinInput } from "../inputs/PinInput";
@@ -24,7 +25,7 @@ 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 [quickConnectCode, setQuickConnectCode] = useState<string>();
const bottomSheetModalRef = useRef<BottomSheetMethods>(null); const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const successHapticFeedback = useHaptic("success"); const successHapticFeedback = useHaptic("success");
const errorHapticFeedback = useHaptic("error"); const errorHapticFeedback = useHaptic("error");
const snapPoints = useMemo( const snapPoints = useMemo(
@@ -35,6 +36,17 @@ export const QuickConnect: React.FC<Props> = ({ ...props }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
),
[],
);
const authorizeQuickConnect = useCallback(async () => { const authorizeQuickConnect = useCallback(async () => {
if (quickConnectCode) { if (quickConnectCode) {
try { try {
@@ -85,7 +97,6 @@ export const QuickConnect: React.FC<Props> = ({ ...props }) => {
<BottomSheetModal <BottomSheetModal
ref={bottomSheetModalRef} ref={bottomSheetModalRef}
enablePanDownToClose
snapPoints={snapPoints} snapPoints={snapPoints}
handleIndicatorStyle={{ handleIndicatorStyle={{
backgroundColor: "white", backgroundColor: "white",
@@ -93,8 +104,11 @@ export const QuickConnect: React.FC<Props> = ({ ...props }) => {
backgroundStyle={{ backgroundStyle={{
backgroundColor: "#171717", backgroundColor: "#171717",
}} }}
backdropComponent={renderBackdrop}
keyboardBehavior={isAndroid ? "fillParent" : "interactive"} keyboardBehavior={isAndroid ? "fillParent" : "interactive"}
keyboardBlurBehavior='restore' keyboardBlurBehavior='restore'
android_keyboardInputMode='adjustResize'
topInset={isAndroid ? 0 : undefined}
> >
<BottomSheetView> <BottomSheetView>
<View className='flex flex-col space-y-4 px-4 pb-8 pt-2'> <View className='flex flex-col space-y-4 px-4 pb-8 pt-2'>

View File

@@ -1,4 +1,10 @@
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import React, { import React, {
forwardRef, forwardRef,
@@ -10,7 +16,6 @@ import React, {
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
ActivityIndicator, ActivityIndicator,
Platform,
StyleSheet, StyleSheet,
TouchableOpacity, TouchableOpacity,
View, View,
@@ -26,11 +31,6 @@ import {
useItemInWatchlists, useItemInWatchlists,
useMyWatchlistsQuery, useMyWatchlistsQuery,
} from "@/hooks/useWatchlists"; } from "@/hooks/useWatchlists";
import {
type BottomSheetMethods,
BottomSheetModal,
BottomSheetView,
} from "@/utils/expoUiBottomSheet";
import type { StreamystatsWatchlist } from "@/utils/streamystats/types"; import type { StreamystatsWatchlist } from "@/utils/streamystats/types";
export interface WatchlistSheetRef { export interface WatchlistSheetRef {
@@ -263,7 +263,7 @@ const WatchlistSheetContent: React.FC<WatchlistSheetContentProps> = ({
export const WatchlistSheet = forwardRef<WatchlistSheetRef, object>( export const WatchlistSheet = forwardRef<WatchlistSheetRef, object>(
(_props, ref) => { (_props, ref) => {
const bottomSheetModalRef = useRef<BottomSheetMethods>(null); const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const [currentItem, setCurrentItem] = React.useState<BaseItemDto | null>( const [currentItem, setCurrentItem] = React.useState<BaseItemDto | null>(
null, null,
); );
@@ -283,13 +283,23 @@ export const WatchlistSheet = forwardRef<WatchlistSheetRef, object>(
bottomSheetModalRef.current?.dismiss(); bottomSheetModalRef.current?.dismiss();
}, []); }, []);
if (Platform.isTV) return null; const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
),
[],
);
return ( return (
<BottomSheetModal <BottomSheetModal
ref={bottomSheetModalRef} ref={bottomSheetModalRef}
enablePanDownToClose
enableDynamicSizing enableDynamicSizing
maxDynamicContentSize={600}
backdropComponent={renderBackdrop}
handleIndicatorStyle={{ handleIndicatorStyle={{
backgroundColor: "white", backgroundColor: "white",
}} }}

View File

@@ -32,6 +32,7 @@
"@expo/react-native-action-sheet": "^4.1.1", "@expo/react-native-action-sheet": "^4.1.1",
"@expo/ui": "~56.0.14", "@expo/ui": "~56.0.14",
"@expo/vector-icons": "^15.0.3", "@expo/vector-icons": "^15.0.3",
"@gorhom/bottom-sheet": "5.2.14",
"@jellyfin/sdk": "^0.13.0", "@jellyfin/sdk": "^0.13.0",
"@react-native-community/netinfo": "^12.0.0", "@react-native-community/netinfo": "^12.0.0",
"@react-navigation/material-top-tabs": "7.4.28", "@react-navigation/material-top-tabs": "7.4.28",

View File

@@ -1,3 +1,4 @@
import type { BottomSheetModal } from "@gorhom/bottom-sheet";
import type React from "react"; import type React from "react";
import { import {
createContext, createContext,
@@ -10,7 +11,6 @@ import {
} from "react"; } from "react";
import { BackHandler, Platform } from "react-native"; import { BackHandler, Platform } from "react-native";
import type { BottomSheetMethods } from "@/utils/expoUiBottomSheet";
interface ModalOptions { interface ModalOptions {
enableDynamicSizing?: boolean; enableDynamicSizing?: boolean;
@@ -30,7 +30,7 @@ interface GlobalModalContextType {
hideModal: () => void; hideModal: () => void;
isVisible: boolean; isVisible: boolean;
modalState: GlobalModalState; modalState: GlobalModalState;
modalRef: React.RefObject<BottomSheetMethods | null>; modalRef: React.RefObject<BottomSheetModal | null>;
} }
const GlobalModalContext = createContext<GlobalModalContextType | undefined>( const GlobalModalContext = createContext<GlobalModalContextType | undefined>(
@@ -57,7 +57,7 @@ export const GlobalModalProvider: React.FC<GlobalModalProviderProps> = ({
options: undefined, options: undefined,
}); });
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const modalRef = useRef<BottomSheetMethods>(null); const modalRef = useRef<BottomSheetModal>(null);
const showModal = useCallback( const showModal = useCallback(
(content: ReactNode, options?: ModalOptions) => { (content: ReactNode, options?: ModalOptions) => {

View File

@@ -69,6 +69,13 @@ const initialApi = (() => {
const initialUser = (() => { const initialUser = (() => {
try { try {
// Only return a stored user if we also have a token. Otherwise the
// user atom would be populated while the api atom is null (e.g. after
// a logout that left stale user JSON in storage), which causes
// useProtectedRoute to keep us inside the (auth) group instead of
// redirecting to /login.
const token = storage.getString("token");
if (!token) return null;
const userStr = storage.getString("user"); const userStr = storage.getString("user");
if (userStr) { if (userStr) {
return JSON.parse(userStr) as UserDto; return JSON.parse(userStr) as UserDto;
@@ -402,6 +409,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
); );
storage.remove("token"); storage.remove("token");
storage.remove("user");
clearTVDiscoverySafely(); clearTVDiscoverySafely();
setUser(null); setUser(null);
setApi(null); setApi(null);

View File

@@ -456,6 +456,7 @@
"new_app_version_requires_re_download_description": "Die neue App-Version erfordert das erneute Herunterladen von Filmen und Serien. Bitte lösche alle heruntergeladenen Elemente und starte den Download erneut.", "new_app_version_requires_re_download_description": "Die neue App-Version erfordert das erneute Herunterladen von Filmen und Serien. Bitte lösche alle heruntergeladenen Elemente und starte den Download erneut.",
"back": "Zurück", "back": "Zurück",
"delete": "Löschen", "delete": "Löschen",
"delete_download": "Download löschen",
"something_went_wrong": "Etwas ist schiefgelaufen", "something_went_wrong": "Etwas ist schiefgelaufen",
"could_not_get_stream_url_from_jellyfin": "Konnte keine Stream-URL von Jellyfin erhalten", "could_not_get_stream_url_from_jellyfin": "Konnte keine Stream-URL von Jellyfin erhalten",
"eta": "ETA {{eta}}", "eta": "ETA {{eta}}",
@@ -498,6 +499,8 @@
"audio": "Audio", "audio": "Audio",
"subtitle": "Untertitel", "subtitle": "Untertitel",
"play": "Abspielen", "play": "Abspielen",
"mark_as_played": "Als gesehen markieren",
"mark_as_not_played": "Als ungesehen markieren",
"none": "Keine", "none": "Keine",
"track": "Spur", "track": "Spur",
"cancel": "Abbrechen", "cancel": "Abbrechen",

View File

@@ -534,6 +534,7 @@
"new_app_version_requires_re_download_description": "The new update requires content to be downloaded again. Please remove all downloaded content and try again.", "new_app_version_requires_re_download_description": "The new update requires content to be downloaded again. Please remove all downloaded content and try again.",
"back": "Back", "back": "Back",
"delete": "Delete", "delete": "Delete",
"delete_download": "Delete Download",
"something_went_wrong": "Something Went Wrong", "something_went_wrong": "Something Went Wrong",
"could_not_get_stream_url_from_jellyfin": "Could not get the stream URL from Jellyfin", "could_not_get_stream_url_from_jellyfin": "Could not get the stream URL from Jellyfin",
"eta": "ETA {{eta}}", "eta": "ETA {{eta}}",
@@ -577,6 +578,8 @@
"audio": "Audio", "audio": "Audio",
"subtitle": "Subtitle", "subtitle": "Subtitle",
"play": "Play", "play": "Play",
"mark_as_played": "Mark as Played",
"mark_as_not_played": "Mark as not Played",
"none": "None", "none": "None",
"track": "Track", "track": "Track",
"cancel": "Cancel", "cancel": "Cancel",

View File

@@ -1,40 +0,0 @@
import { Platform } from "react-native";
/**
* TV-safe re-exports of `@expo/ui/community/bottom-sheet`.
*
* `@expo/ui` resolves its SwiftUI bridge at module load via
* `requireNativeModule('ExpoUI')`. That native module does not exist on tvOS,
* so a static top-level import from any route file crashes the whole route
* tree (expo-router eagerly loads every route).
*
* We `require()` the module lazily and only when *not* on tvOS. On TV the
* exported components are `undefined`, which is fine because every call site
* must early-return (`if (Platform.isTV) return null;`) before rendering a
* bottom sheet.
*
* Usage:
*
* import {
* BottomSheetModal,
* BottomSheetView,
* type BottomSheetMethods,
* } from "@/utils/expoUiBottomSheet";
*
* const ref = useRef<BottomSheetMethods>(null);
*
* if (Platform.isTV) return null;
* return <BottomSheetModal ref={ref}>...</BottomSheetModal>;
*/
type BottomSheetMod = typeof import("@expo/ui/community/bottom-sheet");
const mod: BottomSheetMod = Platform.isTV
? ({} as BottomSheetMod)
: (require("@expo/ui/community/bottom-sheet") as BottomSheetMod);
export const BottomSheetModal = mod.BottomSheetModal;
export const BottomSheetView = mod.BottomSheetView;
export const BottomSheetScrollView = mod.BottomSheetScrollView;
export const BottomSheetTextInput = mod.BottomSheetTextInput;
export type { BottomSheetMethods } from "@expo/ui/community/bottom-sheet";