mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-05-31 19:18:26 +01:00
Compare commits
9 Commits
renovate/x
...
chore/migr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6055503a5 | ||
|
|
ebc86473ff | ||
|
|
c981f59a50 | ||
|
|
27f6f6b056 | ||
|
|
62fc6f9a70 | ||
|
|
8cf9a8d584 | ||
|
|
eb8dd51b4e | ||
|
|
ea5a999f21 | ||
|
|
dffcdef945 |
8
.github/workflows/build-apps.yml
vendored
8
.github/workflows/build-apps.yml
vendored
@@ -219,7 +219,7 @@ jobs:
|
||||
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
|
||||
with:
|
||||
# renovate: datasource=custom.xcode depName=xcode versioning=loose
|
||||
xcode-version: "26.5"
|
||||
xcode-version: "26.4"
|
||||
|
||||
- name: 🏗️ Setup EAS
|
||||
uses: expo/expo-github-action@b184ff86a3c926240f1b6db41764c83a01c02eef # main
|
||||
@@ -284,7 +284,7 @@ jobs:
|
||||
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
|
||||
with:
|
||||
# renovate: datasource=custom.xcode depName=xcode versioning=loose
|
||||
xcode-version: "26.5"
|
||||
xcode-version: "26.4"
|
||||
|
||||
- name: 🚀 Build iOS app
|
||||
env:
|
||||
@@ -344,7 +344,7 @@ jobs:
|
||||
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
|
||||
with:
|
||||
# renovate: datasource=custom.xcode depName=xcode versioning=loose
|
||||
xcode-version: "26.5"
|
||||
xcode-version: "26.4"
|
||||
|
||||
- name: 🏗️ Setup EAS
|
||||
uses: expo/expo-github-action@b184ff86a3c926240f1b6db41764c83a01c02eef # main
|
||||
@@ -412,7 +412,7 @@ jobs:
|
||||
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
|
||||
with:
|
||||
# renovate: datasource=custom.xcode depName=xcode versioning=loose
|
||||
xcode-version: "26.5"
|
||||
xcode-version: "26.4"
|
||||
|
||||
- name: 🚀 Build iOS app
|
||||
env:
|
||||
|
||||
132
.github/workflows/release.yml
vendored
Normal file
132
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
name: 🚀 Release (EAS Build + Submit)
|
||||
|
||||
# Cloud EAS build + auto-submit for iOS, tvOS and Android on merge to main.
|
||||
# A manual approval gate (the `production` GitHub Environment) pauses the run
|
||||
# before any build/submit starts. Configure required reviewers on that
|
||||
# environment in repo Settings → Environments → production.
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
approve:
|
||||
name: 🔐 Approve release
|
||||
runs-on: ubuntu-24.04
|
||||
environment: production
|
||||
steps:
|
||||
- name: ✅ Release approved
|
||||
run: echo "Release approved for ${{ github.sha }}"
|
||||
|
||||
release:
|
||||
name: 🚀 ${{ matrix.name }}
|
||||
needs: approve
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: 🍎 iOS
|
||||
platform: ios
|
||||
profile: production
|
||||
- name: 📺 tvOS
|
||||
platform: ios
|
||||
profile: production_tv
|
||||
- name: 🤖 Android
|
||||
platform: android
|
||||
profile: production
|
||||
|
||||
steps:
|
||||
- name: 📥 Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
show-progress: false
|
||||
|
||||
- name: 🍞 Setup Bun
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: 💾 Cache Bun dependencies
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-cache
|
||||
|
||||
- name: 📦 Install dependencies and reload submodules
|
||||
run: |
|
||||
bun install --frozen-lockfile
|
||||
bun run submodule-reload
|
||||
|
||||
- name: 🏗️ Setup EAS
|
||||
uses: expo/expo-github-action@b184ff86a3c926240f1b6db41764c83a01c02eef # main
|
||||
with:
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
eas-cache: true
|
||||
|
||||
# tvOS uses local credentials (EAS can't manage tvOS provisioning
|
||||
# remotely, including the TopShelf extension target). Restore the
|
||||
# gitignored credentials.json + cert + profiles from secrets so the
|
||||
# cloud build can sign with `credentialsSource: local`.
|
||||
- name: 🔐 Restore tvOS signing credentials
|
||||
if: matrix.profile == 'production_tv'
|
||||
env:
|
||||
EAS_CREDENTIALS_JSON: ${{ secrets.EAS_CREDENTIALS_JSON }}
|
||||
TVOS_DIST_CERT_P12_BASE64: ${{ secrets.TVOS_DIST_CERT_P12_BASE64 }}
|
||||
TVOS_APP_PROFILE_BASE64: ${{ secrets.TVOS_APP_PROFILE_BASE64 }}
|
||||
TVOS_TOPSHELF_PROFILE_BASE64: ${{ secrets.TVOS_TOPSHELF_PROFILE_BASE64 }}
|
||||
run: |
|
||||
mkdir -p certs profiles
|
||||
printf '%s' "$EAS_CREDENTIALS_JSON" > credentials.json
|
||||
echo "$TVOS_DIST_CERT_P12_BASE64" | base64 -d > certs/distribution.p12
|
||||
echo "$TVOS_APP_PROFILE_BASE64" | base64 -d > profiles/Streamyfin_tvOS_App_Store.mobileprovision
|
||||
echo "$TVOS_TOPSHELF_PROFILE_BASE64" | base64 -d > profiles/Streamyfin_TopShelf_tvOS_App_Store.mobileprovision
|
||||
|
||||
# iOS + tvOS submit upload to App Store Connect with an ASC API key.
|
||||
# EAS reads it from EXPO_ASC_API_KEY_PATH / EXPO_ASC_KEY_ID /
|
||||
# EXPO_ASC_ISSUER_ID (set on the build step below). Write the .p8,
|
||||
# tolerating either raw-PEM or base64-encoded secret content.
|
||||
- name: 🔐 Restore App Store Connect API key
|
||||
if: matrix.platform == 'ios'
|
||||
env:
|
||||
APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }}
|
||||
run: |
|
||||
if printf '%s' "$APPLE_KEY_CONTENT" | grep -q "BEGIN PRIVATE KEY"; then
|
||||
printf '%s' "$APPLE_KEY_CONTENT" > "$RUNNER_TEMP/asc_api_key.p8"
|
||||
else
|
||||
printf '%s' "$APPLE_KEY_CONTENT" | base64 -d > "$RUNNER_TEMP/asc_api_key.p8"
|
||||
fi
|
||||
|
||||
# Android submit needs a Google Play service account JSON. eas.json's
|
||||
# submit.production.android.serviceAccountKeyPath points at this file.
|
||||
- name: 🔐 Restore Google Play service account
|
||||
if: matrix.platform == 'android'
|
||||
env:
|
||||
GOOGLE_SERVICE_ACCOUNT_KEY: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }}
|
||||
run: printf '%s' "$GOOGLE_SERVICE_ACCOUNT_KEY" > google-service-account.json
|
||||
|
||||
- name: 🚀 Build & submit (${{ matrix.name }})
|
||||
env:
|
||||
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
|
||||
# Consumed by eas submit for iOS/tvOS; ignored for Android.
|
||||
EXPO_ASC_API_KEY_PATH: ${{ runner.temp }}/asc_api_key.p8
|
||||
EXPO_ASC_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
|
||||
EXPO_ASC_ISSUER_ID: ${{ secrets.APPLE_KEY_ISSUER_ID }}
|
||||
run: |
|
||||
eas build \
|
||||
--platform ${{ matrix.platform }} \
|
||||
--profile ${{ matrix.profile }} \
|
||||
--auto-submit \
|
||||
--non-interactive
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -18,6 +18,9 @@ web-build/
|
||||
/androidmobile
|
||||
/androidtv
|
||||
|
||||
# Gradle caches (top-level + per-module native projects)
|
||||
**/.gradle/
|
||||
|
||||
# Module-specific Builds
|
||||
modules/mpv-player/android/build
|
||||
modules/player/android
|
||||
@@ -76,3 +79,6 @@ build/
|
||||
.claude/
|
||||
.agents/skills/**
|
||||
skills-lock.json
|
||||
|
||||
# CI-injected Google Play service account key (written at build time)
|
||||
google-service-account.json
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { BottomSheetModal } from "@gorhom/bottom-sheet";
|
||||
import { useNavigation } from "expo-router";
|
||||
import { useAtom } from "jotai";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
@@ -7,6 +6,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";
|
||||
@@ -18,6 +18,11 @@ import { useDownload } from "@/providers/DownloadProvider";
|
||||
import { type DownloadedItem } from "@/providers/Downloads/types";
|
||||
import { OfflineModeProvider } from "@/providers/OfflineModeProvider";
|
||||
import { queueAtom } from "@/utils/atoms/queue";
|
||||
import {
|
||||
type BottomSheetMethods,
|
||||
BottomSheetModal,
|
||||
BottomSheetView,
|
||||
} from "@/utils/expoUiBottomSheet";
|
||||
import { writeToLog } from "@/utils/log";
|
||||
|
||||
export default function DownloadsPage() {
|
||||
@@ -26,7 +31,7 @@ export default function DownloadsPage() {
|
||||
const [_queue, _setQueue] = useAtom(queueAtom);
|
||||
const { downloadedItems, deleteFileByType, deleteAllFiles } = useDownload();
|
||||
const router = useRouter();
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
const bottomSheetModalRef = useRef<BottomSheetMethods>(null);
|
||||
|
||||
const [showMigration, setShowMigration] = useState(false);
|
||||
|
||||
@@ -101,7 +106,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 +121,7 @@ export default function DownloadsPage() {
|
||||
}
|
||||
}, [showMigration]);
|
||||
|
||||
const _deleteMovies = () =>
|
||||
const deleteMovies = () =>
|
||||
deleteFileByType("Movie")
|
||||
.then(() =>
|
||||
toast.success(
|
||||
@@ -127,7 +132,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 +143,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 +167,9 @@ export default function DownloadsPage() {
|
||||
),
|
||||
);
|
||||
|
||||
const deleteAllMedia = async () =>
|
||||
await Promise.all([deleteMovies(), deleteShows(), deleteOtherMedia()]);
|
||||
|
||||
return (
|
||||
<OfflineModeProvider isOffline={true}>
|
||||
<ScrollView
|
||||
@@ -256,6 +264,36 @@ export default function DownloadsPage() {
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetModalRef}
|
||||
enableDynamicSizing
|
||||
enablePanDownToClose
|
||||
handleIndicatorStyle={{
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
backgroundStyle={{
|
||||
backgroundColor: "#171717",
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import {
|
||||
BottomSheetBackdrop,
|
||||
type BottomSheetBackdropProps,
|
||||
BottomSheetModal,
|
||||
BottomSheetTextInput,
|
||||
BottomSheetView,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Image } from "expo-image";
|
||||
import { useLocalSearchParams, useNavigation } from "expo-router";
|
||||
@@ -31,6 +24,12 @@ import { ItemActions } from "@/components/series/SeriesActions";
|
||||
import useRouter from "@/hooks/useAppRouter";
|
||||
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
||||
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 {
|
||||
type IssueType,
|
||||
@@ -76,8 +75,8 @@ 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 bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
const advancedReqModalRef = useRef<BottomSheetMethods>(null);
|
||||
const bottomSheetModalRef = useRef<BottomSheetMethods>(null);
|
||||
|
||||
const {
|
||||
data: details,
|
||||
@@ -143,17 +142,6 @@ const MobilePage: React.FC = () => {
|
||||
}
|
||||
}, [jellyseerrApi, pendingRequest, refetch, t]);
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: BottomSheetBackdropProps) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const submitIssue = useCallback(() => {
|
||||
if (result.id && issueType && issueMessage && details) {
|
||||
jellyseerrApi
|
||||
@@ -478,6 +466,7 @@ const MobilePage: React.FC = () => {
|
||||
// This is till it's fixed because the menu isn't selectable on TV
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetModalRef}
|
||||
enablePanDownToClose
|
||||
enableDynamicSizing
|
||||
handleIndicatorStyle={{
|
||||
backgroundColor: "white",
|
||||
@@ -485,8 +474,6 @@ const MobilePage: React.FC = () => {
|
||||
backgroundStyle={{
|
||||
backgroundColor: "#171717",
|
||||
}}
|
||||
backdropComponent={renderBackdrop}
|
||||
stackBehavior='push'
|
||||
onDismiss={handleIssueModalDismiss}
|
||||
>
|
||||
<BottomSheetView>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ExpoAvRoutePickerView } from "@douglowder/expo-av-route-picker-view";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
|
||||
import type {
|
||||
BaseItemDto,
|
||||
MediaSourceInfo,
|
||||
@@ -222,133 +221,129 @@ export default function NowPlayingScreen() {
|
||||
|
||||
if (!currentTrack) {
|
||||
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
|
||||
className='flex-1 bg-[#121212]'
|
||||
className='flex-1 bg-[#121212] items-center justify-center'
|
||||
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
|
||||
onPress={() => setViewMode("player")}
|
||||
className='px-3 py-1'
|
||||
>
|
||||
<Text
|
||||
className={
|
||||
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>
|
||||
|
||||
{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}
|
||||
/>
|
||||
<Text className='text-neutral-500'>No track playing</Text>
|
||||
</View>
|
||||
</BottomSheetModalProvider>
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
onPress={() => setViewMode("player")}
|
||||
className='px-3 py-1'
|
||||
>
|
||||
<Text
|
||||
className={
|
||||
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>
|
||||
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
237
app/_layout.tsx
237
app/_layout.tsx
@@ -1,6 +1,5 @@
|
||||
import "@/augmentations";
|
||||
import { ActionSheetProvider } from "@expo/react-native-action-sheet";
|
||||
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
|
||||
import { onlineManager, QueryClient } from "@tanstack/react-query";
|
||||
@@ -412,127 +411,125 @@ function Layout() {
|
||||
<DownloadProvider>
|
||||
<MusicPlayerProvider>
|
||||
<GlobalModalProvider>
|
||||
<BottomSheetModalProvider>
|
||||
<IntroSheetProvider>
|
||||
<ThemeProvider value={DarkTheme}>
|
||||
<SystemBars style='light' hidden={false} />
|
||||
<Stack initialRouteName='(auth)/(tabs)'>
|
||||
<Stack.Screen
|
||||
name='(auth)/(tabs)'
|
||||
options={{
|
||||
headerShown: false,
|
||||
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",
|
||||
},
|
||||
<IntroSheetProvider>
|
||||
<ThemeProvider value={DarkTheme}>
|
||||
<SystemBars style='light' hidden={false} />
|
||||
<Stack initialRouteName='(auth)/(tabs)'>
|
||||
<Stack.Screen
|
||||
name='(auth)/(tabs)'
|
||||
options={{
|
||||
headerShown: false,
|
||||
title: "",
|
||||
header: () => null,
|
||||
}}
|
||||
closeButton
|
||||
/>
|
||||
{!Platform.isTV && <GlobalModal />}
|
||||
</ThemeProvider>
|
||||
</IntroSheetProvider>
|
||||
</BottomSheetModalProvider>
|
||||
<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
|
||||
/>
|
||||
{!Platform.isTV && <GlobalModal />}
|
||||
</ThemeProvider>
|
||||
</IntroSheetProvider>
|
||||
</GlobalModalProvider>
|
||||
</MusicPlayerProvider>
|
||||
</DownloadProvider>
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import {
|
||||
BottomSheetBackdrop,
|
||||
type BottomSheetBackdropProps,
|
||||
BottomSheetModal,
|
||||
BottomSheetView,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import type React from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -12,6 +6,11 @@ import { Alert, Platform, TouchableOpacity, View } from "react-native";
|
||||
import { Swipeable } from "react-native-gesture-handler";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Colors } from "@/constants/Colors";
|
||||
import {
|
||||
type BottomSheetMethods,
|
||||
BottomSheetModal,
|
||||
BottomSheetView,
|
||||
} from "@/utils/expoUiBottomSheet";
|
||||
import {
|
||||
deleteAccountCredential,
|
||||
type SavedServer,
|
||||
@@ -39,7 +38,7 @@ export const AccountsSheet: React.FC<AccountsSheetProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const insets = useSafeAreaInsets();
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
const bottomSheetModalRef = useRef<BottomSheetMethods>(null);
|
||||
|
||||
const isAndroid = Platform.OS === "android";
|
||||
const snapPoints = useMemo(
|
||||
@@ -64,17 +63,6 @@ export const AccountsSheet: React.FC<AccountsSheetProps> = ({
|
||||
[setOpen],
|
||||
);
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: BottomSheetBackdropProps) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleDeleteAccount = async (account: SavedServerAccount) => {
|
||||
if (!server) return;
|
||||
|
||||
@@ -118,15 +106,16 @@ export const AccountsSheet: React.FC<AccountsSheetProps> = ({
|
||||
);
|
||||
|
||||
if (!server) return null;
|
||||
if (Platform.isTV) return null;
|
||||
|
||||
return (
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetModalRef}
|
||||
enablePanDownToClose
|
||||
snapPoints={snapPoints}
|
||||
onChange={handleSheetChanges}
|
||||
handleIndicatorStyle={{ backgroundColor: "white" }}
|
||||
backgroundStyle={{ backgroundColor: "#171717" }}
|
||||
backdropComponent={renderBackdrop}
|
||||
>
|
||||
<BottomSheetView
|
||||
style={{
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import Ionicons from "@expo/vector-icons/Ionicons";
|
||||
import {
|
||||
BottomSheetBackdrop,
|
||||
type BottomSheetBackdropProps,
|
||||
BottomSheetModal,
|
||||
BottomSheetView,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import type {
|
||||
BaseItemDto,
|
||||
MediaSourceInfo,
|
||||
@@ -23,6 +17,11 @@ import { useDownload } from "@/providers/DownloadProvider";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { queueAtom } from "@/utils/atoms/queue";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import {
|
||||
type BottomSheetMethods,
|
||||
BottomSheetModal,
|
||||
BottomSheetView,
|
||||
} from "@/utils/expoUiBottomSheet";
|
||||
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
|
||||
import { getDownloadUrl } from "@/utils/jellyfin/media/getDownloadUrl";
|
||||
import { AudioTrackSelector } from "./AudioTrackSelector";
|
||||
@@ -90,7 +89,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
||||
[user],
|
||||
);
|
||||
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
const bottomSheetModalRef = useRef<BottomSheetMethods>(null);
|
||||
|
||||
const handlePresentModalPress = useCallback(() => {
|
||||
bottomSheetModalRef.current?.present();
|
||||
@@ -317,17 +316,6 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
||||
}
|
||||
}, [closeModal, initiateDownload, itemsToDownload, userCanDownload]);
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: BottomSheetBackdropProps) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const renderButtonContent = () => {
|
||||
// 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
|
||||
@@ -375,6 +363,8 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
if (Platform.isTV) return null;
|
||||
|
||||
return (
|
||||
<View {...props}>
|
||||
<RoundButton size={size} onPress={onButtonPress}>
|
||||
@@ -390,10 +380,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
||||
backgroundColor: "#171717",
|
||||
}}
|
||||
onChange={handleSheetChanges}
|
||||
backdropComponent={renderBackdrop}
|
||||
enablePanDownToClose
|
||||
enableDismissOnClose
|
||||
android_keyboardInputMode='adjustResize'
|
||||
keyboardBehavior='interactive'
|
||||
keyboardBlurBehavior='restore'
|
||||
>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import {
|
||||
BottomSheetBackdrop,
|
||||
type BottomSheetBackdropProps,
|
||||
BottomSheetModal,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import { useGlobalModal } from "@/providers/GlobalModalProvider";
|
||||
import { BottomSheetModal } from "@/utils/expoUiBottomSheet";
|
||||
|
||||
/**
|
||||
* GlobalModal Component
|
||||
@@ -12,8 +9,7 @@ import { useGlobalModal } from "@/providers/GlobalModalProvider";
|
||||
* This component renders a global bottom sheet modal that can be controlled
|
||||
* from anywhere in the app using the useGlobalModal hook.
|
||||
*
|
||||
* Place this component at the root level of your app (in _layout.tsx)
|
||||
* after BottomSheetModalProvider.
|
||||
* Place this component at the root level of your app (in _layout.tsx).
|
||||
*/
|
||||
export const GlobalModal = () => {
|
||||
const { hideModal, modalState, modalRef, isVisible } = useGlobalModal();
|
||||
@@ -33,17 +29,6 @@ export const GlobalModal = () => {
|
||||
[hideModal],
|
||||
);
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: BottomSheetBackdropProps) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const defaultOptions = {
|
||||
enableDynamicSizing: true,
|
||||
enablePanDownToClose: true,
|
||||
@@ -58,6 +43,8 @@ export const GlobalModal = () => {
|
||||
// Merge default options with provided options
|
||||
const modalOptions = { ...defaultOptions, ...modalState.options };
|
||||
|
||||
if (Platform.isTV) return null;
|
||||
|
||||
return (
|
||||
<BottomSheetModal
|
||||
ref={modalRef}
|
||||
@@ -65,12 +52,9 @@ export const GlobalModal = () => {
|
||||
? { snapPoints: modalOptions.snapPoints }
|
||||
: { enableDynamicSizing: modalOptions.enableDynamicSizing })}
|
||||
onChange={handleSheetChanges}
|
||||
backdropComponent={renderBackdrop}
|
||||
handleIndicatorStyle={modalOptions.handleIndicatorStyle}
|
||||
backgroundStyle={modalOptions.backgroundStyle}
|
||||
enablePanDownToClose={modalOptions.enablePanDownToClose}
|
||||
enableDismissOnClose
|
||||
stackBehavior='push'
|
||||
style={{ zIndex: 1000 }}
|
||||
>
|
||||
{modalState.content}
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import { Feather, Ionicons } from "@expo/vector-icons";
|
||||
import {
|
||||
BottomSheetBackdrop,
|
||||
type BottomSheetBackdropProps,
|
||||
BottomSheetModal,
|
||||
BottomSheetScrollView,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import { Image } from "expo-image";
|
||||
import { forwardRef, useCallback, useImperativeHandle, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -13,6 +7,11 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Button } from "@/components/Button";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import useRouter from "@/hooks/useAppRouter";
|
||||
import {
|
||||
type BottomSheetMethods,
|
||||
BottomSheetModal,
|
||||
BottomSheetScrollView,
|
||||
} from "@/utils/expoUiBottomSheet";
|
||||
import { storage } from "@/utils/mmkv";
|
||||
|
||||
export interface IntroSheetRef {
|
||||
@@ -21,7 +20,7 @@ export interface IntroSheetRef {
|
||||
}
|
||||
|
||||
export const IntroSheet = forwardRef<IntroSheetRef>((_, ref) => {
|
||||
const bottomSheetRef = useRef<BottomSheetModal>(null);
|
||||
const bottomSheetRef = useRef<BottomSheetMethods>(null);
|
||||
const { t } = useTranslation();
|
||||
const insets = useSafeAreaInsets();
|
||||
const router = useRouter();
|
||||
@@ -36,17 +35,6 @@ export const IntroSheet = forwardRef<IntroSheetRef>((_, ref) => {
|
||||
},
|
||||
}));
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: BottomSheetBackdropProps) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
bottomSheetRef.current?.dismiss();
|
||||
}, []);
|
||||
@@ -56,11 +44,13 @@ export const IntroSheet = forwardRef<IntroSheetRef>((_, ref) => {
|
||||
router.push("/settings");
|
||||
}, []);
|
||||
|
||||
if (Platform.isTV) return null;
|
||||
|
||||
return (
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetRef}
|
||||
enablePanDownToClose
|
||||
enableDynamicSizing
|
||||
backdropComponent={renderBackdrop}
|
||||
backgroundStyle={{ backgroundColor: "#171717" }}
|
||||
handleIndicatorStyle={{ backgroundColor: "#737373" }}
|
||||
>
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import {
|
||||
BottomSheetBackdrop,
|
||||
type BottomSheetBackdropProps,
|
||||
BottomSheetModal,
|
||||
BottomSheetScrollView,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import type {
|
||||
MediaSourceInfo,
|
||||
MediaStream,
|
||||
@@ -12,8 +6,13 @@ import type {
|
||||
import type React from "react";
|
||||
import { useMemo, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import { Platform, TouchableOpacity, View } from "react-native";
|
||||
import { formatBitrate } from "@/utils/bitrate";
|
||||
import {
|
||||
type BottomSheetMethods,
|
||||
BottomSheetModal,
|
||||
BottomSheetScrollView,
|
||||
} from "@/utils/expoUiBottomSheet";
|
||||
import { Badge } from "./Badge";
|
||||
import { Text } from "./common/Text";
|
||||
|
||||
@@ -22,9 +21,20 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ItemTechnicalDetails: React.FC<Props> = ({ source }) => {
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
const bottomSheetModalRef = useRef<BottomSheetMethods>(null);
|
||||
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 (
|
||||
<View className='px-4 mt-2 mb-4'>
|
||||
<Text className='text-lg font-bold mb-4'>{t("item_card.video")}</Text>
|
||||
@@ -36,27 +46,27 @@ export const ItemTechnicalDetails: React.FC<Props> = ({ source }) => {
|
||||
</TouchableOpacity>
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetModalRef}
|
||||
snapPoints={["80%"]}
|
||||
enableDynamicSizing
|
||||
enablePanDownToClose
|
||||
handleIndicatorStyle={{
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
backgroundStyle={{
|
||||
backgroundColor: "#171717",
|
||||
}}
|
||||
backdropComponent={(props: BottomSheetBackdropProps) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<BottomSheetScrollView>
|
||||
<View className='flex flex-col space-y-2 p-4 mb-4'>
|
||||
<View className='flex flex-col space-y-2 p-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>
|
||||
<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>
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
import {
|
||||
BottomSheetBackdrop,
|
||||
type BottomSheetBackdropProps,
|
||||
BottomSheetModal,
|
||||
BottomSheetView,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import type React from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -17,6 +11,11 @@ import {
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useHaptic } from "@/hooks/useHaptic";
|
||||
import {
|
||||
type BottomSheetMethods,
|
||||
BottomSheetModal,
|
||||
BottomSheetView,
|
||||
} from "@/utils/expoUiBottomSheet";
|
||||
import { verifyAccountPIN } from "@/utils/secureCredentials";
|
||||
import { Button } from "./Button";
|
||||
import { Text } from "./common/Text";
|
||||
@@ -43,7 +42,7 @@ export const PINEntryModal: React.FC<PINEntryModalProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const insets = useSafeAreaInsets();
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
const bottomSheetModalRef = useRef<BottomSheetMethods>(null);
|
||||
const [pinCode, setPinCode] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isVerifying, setIsVerifying] = useState(false);
|
||||
@@ -78,17 +77,6 @@ export const PINEntryModal: React.FC<PINEntryModalProps> = ({
|
||||
[onClose],
|
||||
);
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: BottomSheetBackdropProps) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const shake = () => {
|
||||
Animated.sequence([
|
||||
Animated.timing(shakeAnimation, {
|
||||
@@ -159,18 +147,18 @@ export const PINEntryModal: React.FC<PINEntryModalProps> = ({
|
||||
]);
|
||||
};
|
||||
|
||||
if (Platform.isTV) return null;
|
||||
|
||||
return (
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetModalRef}
|
||||
enablePanDownToClose
|
||||
snapPoints={snapPoints}
|
||||
onChange={handleSheetChanges}
|
||||
handleIndicatorStyle={{ backgroundColor: "white" }}
|
||||
backgroundStyle={{ backgroundColor: "#171717" }}
|
||||
backdropComponent={renderBackdrop}
|
||||
keyboardBehavior={isAndroid ? "fillParent" : "interactive"}
|
||||
keyboardBlurBehavior='restore'
|
||||
android_keyboardInputMode='adjustResize'
|
||||
topInset={isAndroid ? 0 : undefined}
|
||||
>
|
||||
<BottomSheetView
|
||||
style={{
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import {
|
||||
BottomSheetBackdrop,
|
||||
type BottomSheetBackdropProps,
|
||||
BottomSheetModal,
|
||||
BottomSheetTextInput,
|
||||
BottomSheetView,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import type React from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ActivityIndicator, Platform, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useHaptic } from "@/hooks/useHaptic";
|
||||
import {
|
||||
type BottomSheetMethods,
|
||||
BottomSheetModal,
|
||||
BottomSheetTextInput,
|
||||
BottomSheetView,
|
||||
} from "@/utils/expoUiBottomSheet";
|
||||
import { Button } from "./Button";
|
||||
import { Text } from "./common/Text";
|
||||
|
||||
@@ -29,7 +28,7 @@ export const PasswordEntryModal: React.FC<PasswordEntryModalProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const insets = useSafeAreaInsets();
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
const bottomSheetModalRef = useRef<BottomSheetMethods>(null);
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -62,17 +61,6 @@ export const PasswordEntryModal: React.FC<PasswordEntryModalProps> = ({
|
||||
[onClose],
|
||||
);
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: BottomSheetBackdropProps) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!password) {
|
||||
setError(t("password.enter_password"));
|
||||
@@ -93,18 +81,18 @@ export const PasswordEntryModal: React.FC<PasswordEntryModalProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
if (Platform.isTV) return null;
|
||||
|
||||
return (
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetModalRef}
|
||||
enablePanDownToClose
|
||||
snapPoints={snapPoints}
|
||||
onChange={handleSheetChanges}
|
||||
handleIndicatorStyle={{ backgroundColor: "white" }}
|
||||
backgroundStyle={{ backgroundColor: "#171717" }}
|
||||
backdropComponent={renderBackdrop}
|
||||
keyboardBehavior={isAndroid ? "fillParent" : "interactive"}
|
||||
keyboardBlurBehavior='restore'
|
||||
android_keyboardInputMode='adjustResize'
|
||||
topInset={isAndroid ? 0 : undefined}
|
||||
>
|
||||
<BottomSheetView
|
||||
style={{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { BottomSheetScrollView } from "@gorhom/bottom-sheet";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
type LayoutChangeEvent,
|
||||
@@ -11,6 +10,7 @@ import {
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { useGlobalModal } from "@/providers/GlobalModalProvider";
|
||||
import { BottomSheetScrollView } from "@/utils/expoUiBottomSheet";
|
||||
|
||||
// @expo/ui's SwiftUI native module (ExpoUI) does not exist in tvOS builds.
|
||||
// A static top-level import evaluates requireNativeModule('ExpoUI') at module
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useActionSheet } from "@expo/react-native-action-sheet";
|
||||
import { Feather, Ionicons } from "@expo/vector-icons";
|
||||
import { BottomSheetView } from "@gorhom/bottom-sheet";
|
||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { useCallback, useEffect } from "react";
|
||||
@@ -32,6 +31,7 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
||||
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { BottomSheetView } from "@/utils/expoUiBottomSheet";
|
||||
import { getParentBackdropImageUrl } from "@/utils/jellyfin/image/getParentBackdropImageUrl";
|
||||
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||
|
||||
@@ -39,19 +39,21 @@ export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
return (
|
||||
<Pressable
|
||||
onPress={handlePress}
|
||||
className={`rounded-full ${buttonSize} flex items-center justify-center ${fillColorClass}`}
|
||||
{...(viewProps as any)}
|
||||
>
|
||||
{icon ? (
|
||||
<Ionicons
|
||||
name={icon}
|
||||
size={size === "large" ? 22 : 18}
|
||||
color={color === "white" ? "white" : "#9334E9"}
|
||||
/>
|
||||
) : null}
|
||||
{children ? children : null}
|
||||
<Pressable onPress={handlePress} {...(viewProps as any)}>
|
||||
<BlurView
|
||||
intensity={50}
|
||||
tint='systemChromeMaterial'
|
||||
className={`rounded-full overflow-hidden ${buttonSize} flex items-center justify-center ${fillColorClass}`}
|
||||
>
|
||||
{icon ? (
|
||||
<Ionicons
|
||||
name={icon}
|
||||
size={size === "large" ? 22 : 18}
|
||||
color={color === "white" ? "white" : "#9334E9"}
|
||||
/>
|
||||
) : null}
|
||||
{children ? children : null}
|
||||
</BlurView>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import {
|
||||
BottomSheetBackdrop,
|
||||
type BottomSheetBackdropProps,
|
||||
BottomSheetModal,
|
||||
BottomSheetView,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import type React from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Platform, TouchableOpacity, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import {
|
||||
type BottomSheetMethods,
|
||||
BottomSheetModal,
|
||||
BottomSheetView,
|
||||
} from "@/utils/expoUiBottomSheet";
|
||||
import type { AccountSecurityType } from "@/utils/secureCredentials";
|
||||
import { Button } from "./Button";
|
||||
import { Text } from "./common/Text";
|
||||
@@ -58,7 +57,7 @@ export const SaveAccountModal: React.FC<SaveAccountModalProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const insets = useSafeAreaInsets();
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
const bottomSheetModalRef = useRef<BottomSheetMethods>(null);
|
||||
const [selectedType, setSelectedType] = useState<AccountSecurityType>("none");
|
||||
const [pinCode, setPinCode] = useState("");
|
||||
const [pinError, setPinError] = useState<string | null>(null);
|
||||
@@ -93,17 +92,6 @@ export const SaveAccountModal: React.FC<SaveAccountModalProps> = ({
|
||||
setPinError(null);
|
||||
};
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: BottomSheetBackdropProps) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleOptionSelect = (type: AccountSecurityType) => {
|
||||
setSelectedType(type);
|
||||
setPinCode("");
|
||||
@@ -135,18 +123,18 @@ export const SaveAccountModal: React.FC<SaveAccountModalProps> = ({
|
||||
return true;
|
||||
};
|
||||
|
||||
if (Platform.isTV) return null;
|
||||
|
||||
return (
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetModalRef}
|
||||
enablePanDownToClose
|
||||
snapPoints={snapPoints}
|
||||
onChange={handleSheetChanges}
|
||||
handleIndicatorStyle={{ backgroundColor: "white" }}
|
||||
backgroundStyle={{ backgroundColor: "#171717" }}
|
||||
backdropComponent={renderBackdrop}
|
||||
keyboardBehavior={isAndroid ? "fillParent" : "interactive"}
|
||||
keyboardBlurBehavior='restore'
|
||||
android_keyboardInputMode='adjustResize'
|
||||
topInset={isAndroid ? 0 : undefined}
|
||||
>
|
||||
<BottomSheetView
|
||||
style={{
|
||||
|
||||
@@ -37,11 +37,12 @@ export const ProgressBar: React.FC<ProgressBarProps> = ({ item }) => {
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
width: `${progress}%`,
|
||||
backgroundColor: Platform.isTV ? "#ffffff" : undefined,
|
||||
}}
|
||||
className={`absolute bottom-0 left-0 h-1 w-full ${Platform.isTV ? "" : "bg-purple-600"}`}
|
||||
style={
|
||||
Platform.isTV
|
||||
? { width: `${progress}%`, backgroundColor: "#ffffff" }
|
||||
: { width: `${progress}%` }
|
||||
}
|
||||
className={`absolute bottom-0 left-0 h-1 ${Platform.isTV ? "" : "bg-purple-600"}`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import {
|
||||
BottomSheetBackdrop,
|
||||
type BottomSheetBackdropProps,
|
||||
BottomSheetModal,
|
||||
BottomSheetScrollView,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import { isEqual } from "lodash";
|
||||
import type React from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
@@ -17,6 +12,11 @@ import {
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import {
|
||||
type BottomSheetMethods,
|
||||
BottomSheetModal,
|
||||
BottomSheetScrollView,
|
||||
} from "@/utils/expoUiBottomSheet";
|
||||
import { Button } from "../Button";
|
||||
import { Input } from "../common/Input";
|
||||
|
||||
@@ -75,7 +75,7 @@ export const FilterSheet = <T,>({
|
||||
disableSearch = false,
|
||||
multiple = false,
|
||||
}: Props<T>) => {
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
const bottomSheetModalRef = useRef<BottomSheetMethods>(null);
|
||||
const snapPoints = useMemo(() => ["85%"], []);
|
||||
const { t } = useTranslation();
|
||||
const insets = useSafeAreaInsets();
|
||||
@@ -143,24 +143,15 @@ export const FilterSheet = <T,>({
|
||||
return data;
|
||||
}, [search, filteredData, data]);
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: BottomSheetBackdropProps) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
if (Platform.isTV) return null;
|
||||
|
||||
return (
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetModalRef}
|
||||
enablePanDownToClose
|
||||
index={0}
|
||||
snapPoints={snapPoints}
|
||||
onChange={handleSheetChanges}
|
||||
backdropComponent={renderBackdrop}
|
||||
handleIndicatorStyle={{
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { BottomSheetTextInput } from "@gorhom/bottom-sheet";
|
||||
import React, { useCallback, useImperativeHandle, useRef } from "react";
|
||||
import {
|
||||
type StyleProp,
|
||||
@@ -8,6 +7,7 @@ import {
|
||||
View,
|
||||
type ViewStyle,
|
||||
} from "react-native";
|
||||
import { BottomSheetTextInput } from "@/utils/expoUiBottomSheet";
|
||||
|
||||
interface PinInputProps
|
||||
extends Omit<TextInputProps, "value" | "onChangeText" | "style"> {
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
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 { forwardRef, useCallback, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View, type ViewProps } from "react-native";
|
||||
import { Platform, View, type ViewProps } from "react-native";
|
||||
import { Button } from "@/components/Button";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { PlatformDropdown } from "@/components/PlatformDropdown";
|
||||
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
||||
import {
|
||||
type BottomSheetMethods,
|
||||
BottomSheetModal,
|
||||
BottomSheetView,
|
||||
} from "@/utils/expoUiBottomSheet";
|
||||
import type {
|
||||
QualityProfile,
|
||||
RootFolder,
|
||||
Tag,
|
||||
} from "@/utils/jellyseerr/server/api/servarr/base";
|
||||
|
||||
import type { MediaType } from "@/utils/jellyseerr/server/constants/media";
|
||||
import type { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
|
||||
import { writeDebugLog } from "@/utils/log";
|
||||
@@ -34,7 +33,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const RequestModal = forwardRef<
|
||||
BottomSheetModalMethods,
|
||||
BottomSheetMethods,
|
||||
Props & Omit<ViewProps, "id">
|
||||
>(
|
||||
(
|
||||
@@ -283,11 +282,13 @@ const RequestModal = forwardRef<
|
||||
defaultTags,
|
||||
]);
|
||||
|
||||
if (Platform.isTV) return null;
|
||||
|
||||
return (
|
||||
<BottomSheetModal
|
||||
ref={ref}
|
||||
enablePanDownToClose
|
||||
enableDynamicSizing
|
||||
enableDismissOnClose
|
||||
onDismiss={handleDismiss}
|
||||
handleIndicatorStyle={{
|
||||
backgroundColor: "white",
|
||||
@@ -295,14 +296,6 @@ const RequestModal = forwardRef<
|
||||
backgroundStyle={{
|
||||
backgroundColor: "#171717",
|
||||
}}
|
||||
backdropComponent={(sheetProps: BottomSheetBackdropProps) => (
|
||||
<BottomSheetBackdrop
|
||||
{...sheetProps}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
)}
|
||||
stackBehavior='push'
|
||||
>
|
||||
<BottomSheetView>
|
||||
<View className='flex flex-col space-y-4 px-4 pb-8 pt-2'>
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
import {
|
||||
BottomSheetBackdrop,
|
||||
type BottomSheetBackdropProps,
|
||||
BottomSheetModal,
|
||||
BottomSheetTextInput,
|
||||
BottomSheetView,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
@@ -13,11 +6,17 @@ import React, {
|
||||
useState,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ActivityIndicator, Keyboard } from "react-native";
|
||||
import { ActivityIndicator, Keyboard, Platform } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Button } from "@/components/Button";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { useCreatePlaylist } from "@/hooks/usePlaylistMutations";
|
||||
import {
|
||||
type BottomSheetMethods,
|
||||
BottomSheetModal,
|
||||
BottomSheetTextInput,
|
||||
BottomSheetView,
|
||||
} from "@/utils/expoUiBottomSheet";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
@@ -32,7 +31,7 @@ export const CreatePlaylistModal: React.FC<Props> = ({
|
||||
onPlaylistCreated,
|
||||
initialTrackId,
|
||||
}) => {
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
const bottomSheetModalRef = useRef<BottomSheetMethods>(null);
|
||||
const insets = useSafeAreaInsets();
|
||||
const { t } = useTranslation();
|
||||
const createPlaylist = useCreatePlaylist();
|
||||
@@ -59,17 +58,6 @@ export const CreatePlaylistModal: React.FC<Props> = ({
|
||||
[setOpen],
|
||||
);
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: BottomSheetBackdropProps) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleCreate = useCallback(async () => {
|
||||
if (!name.trim()) return;
|
||||
|
||||
@@ -86,13 +74,15 @@ export const CreatePlaylistModal: React.FC<Props> = ({
|
||||
|
||||
const isValid = name.trim().length > 0;
|
||||
|
||||
if (Platform.isTV) return null;
|
||||
|
||||
return (
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetModalRef}
|
||||
enablePanDownToClose
|
||||
index={0}
|
||||
snapPoints={snapPoints}
|
||||
onChange={handleSheetChanges}
|
||||
backdropComponent={renderBackdrop}
|
||||
handleIndicatorStyle={{
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
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 React, { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Alert, StyleSheet, TouchableOpacity, View } from "react-native";
|
||||
import {
|
||||
Alert,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import useRouter from "@/hooks/useAppRouter";
|
||||
import { useDeletePlaylist } from "@/hooks/usePlaylistMutations";
|
||||
import {
|
||||
type BottomSheetMethods,
|
||||
BottomSheetModal,
|
||||
BottomSheetView,
|
||||
} from "@/utils/expoUiBottomSheet";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
@@ -25,7 +30,7 @@ export const PlaylistOptionsSheet: React.FC<Props> = ({
|
||||
setOpen,
|
||||
playlist,
|
||||
}) => {
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
const bottomSheetModalRef = useRef<BottomSheetMethods>(null);
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { t } = useTranslation();
|
||||
@@ -47,17 +52,6 @@ export const PlaylistOptionsSheet: React.FC<Props> = ({
|
||||
[setOpen],
|
||||
);
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: BottomSheetBackdropProps) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleDeletePlaylist = useCallback(() => {
|
||||
if (!playlist?.Id) return;
|
||||
|
||||
@@ -89,14 +83,15 @@ export const PlaylistOptionsSheet: React.FC<Props> = ({
|
||||
}, [playlist, deletePlaylist, setOpen, router, t]);
|
||||
|
||||
if (!playlist) return null;
|
||||
if (Platform.isTV) return null;
|
||||
|
||||
return (
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetModalRef}
|
||||
enablePanDownToClose
|
||||
index={0}
|
||||
snapPoints={snapPoints}
|
||||
onChange={handleSheetChanges}
|
||||
backdropComponent={renderBackdrop}
|
||||
handleIndicatorStyle={{
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
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 { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
@@ -20,6 +14,7 @@ import React, {
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
@@ -29,6 +24,11 @@ import { Input } from "@/components/common/Input";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { useAddToPlaylist } from "@/hooks/usePlaylistMutations";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import {
|
||||
type BottomSheetMethods,
|
||||
BottomSheetModal,
|
||||
BottomSheetScrollView,
|
||||
} from "@/utils/expoUiBottomSheet";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
@@ -43,7 +43,7 @@ export const PlaylistPickerSheet: React.FC<Props> = ({
|
||||
trackToAdd,
|
||||
onCreateNew,
|
||||
}) => {
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
const bottomSheetModalRef = useRef<BottomSheetMethods>(null);
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
const insets = useSafeAreaInsets();
|
||||
@@ -101,17 +101,6 @@ export const PlaylistPickerSheet: React.FC<Props> = ({
|
||||
[setOpen],
|
||||
);
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: BottomSheetBackdropProps) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSelectPlaylist = useCallback(
|
||||
async (playlist: BaseItemDto) => {
|
||||
if (!trackToAdd?.Id || !playlist.Id) return;
|
||||
@@ -142,13 +131,15 @@ export const PlaylistPickerSheet: React.FC<Props> = ({
|
||||
[api],
|
||||
);
|
||||
|
||||
if (Platform.isTV) return null;
|
||||
|
||||
return (
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetModalRef}
|
||||
enablePanDownToClose
|
||||
index={0}
|
||||
snapPoints={snapPoints}
|
||||
onChange={handleSheetChanges}
|
||||
backdropComponent={renderBackdrop}
|
||||
handleIndicatorStyle={{
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import {
|
||||
BottomSheetBackdrop,
|
||||
type BottomSheetBackdropProps,
|
||||
BottomSheetModal,
|
||||
BottomSheetView,
|
||||
} 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 { Platform, StyleSheet, TouchableOpacity, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import {
|
||||
type BottomSheetMethods,
|
||||
BottomSheetModal,
|
||||
BottomSheetView,
|
||||
} from "@/utils/expoUiBottomSheet";
|
||||
|
||||
export type PlaylistSortOption = "SortName" | "DateCreated";
|
||||
|
||||
@@ -43,7 +42,7 @@ export const PlaylistSortSheet: React.FC<Props> = ({
|
||||
sortOrder,
|
||||
onSortChange,
|
||||
}) => {
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
const bottomSheetModalRef = useRef<BottomSheetMethods>(null);
|
||||
const insets = useSafeAreaInsets();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -63,17 +62,6 @@ export const PlaylistSortSheet: React.FC<Props> = ({
|
||||
[setOpen],
|
||||
);
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: BottomSheetBackdropProps) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSortSelect = useCallback(
|
||||
(option: PlaylistSortOption) => {
|
||||
// If selecting same option, toggle order; otherwise use sensible default
|
||||
@@ -93,13 +81,15 @@ export const PlaylistSortSheet: React.FC<Props> = ({
|
||||
[sortBy, sortOrder, onSortChange, setOpen],
|
||||
);
|
||||
|
||||
if (Platform.isTV) return null;
|
||||
|
||||
return (
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetModalRef}
|
||||
enablePanDownToClose
|
||||
index={0}
|
||||
snapPoints={snapPoints}
|
||||
onChange={handleSheetChanges}
|
||||
backdropComponent={renderBackdrop}
|
||||
handleIndicatorStyle={{
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
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 { Image } from "expo-image";
|
||||
import { useAtom } from "jotai";
|
||||
@@ -18,6 +12,7 @@ import React, {
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
@@ -36,6 +31,11 @@ import {
|
||||
} from "@/providers/AudioStorage";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { useMusicPlayer } from "@/providers/MusicPlayerProvider";
|
||||
import {
|
||||
type BottomSheetMethods,
|
||||
BottomSheetModal,
|
||||
BottomSheetView,
|
||||
} from "@/utils/expoUiBottomSheet";
|
||||
import { getAudioStreamUrl } from "@/utils/jellyfin/audio/getAudioStreamUrl";
|
||||
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
||||
|
||||
@@ -56,7 +56,7 @@ export const TrackOptionsSheet: React.FC<Props> = ({
|
||||
playlistId,
|
||||
onRemoveFromPlaylist,
|
||||
}) => {
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
const bottomSheetModalRef = useRef<BottomSheetMethods>(null);
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
const router = useRouter();
|
||||
@@ -128,17 +128,6 @@ export const TrackOptionsSheet: React.FC<Props> = ({
|
||||
[setOpen],
|
||||
);
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: BottomSheetBackdropProps) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const handlePlayNext = useCallback(() => {
|
||||
if (track) {
|
||||
playNext(track);
|
||||
@@ -227,13 +216,14 @@ export const TrackOptionsSheet: React.FC<Props> = ({
|
||||
const hasAlbum = !!(track?.AlbumId || track?.ParentId);
|
||||
|
||||
if (!track) return null;
|
||||
if (Platform.isTV) return null;
|
||||
|
||||
return (
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetModalRef}
|
||||
enablePanDownToClose
|
||||
enableDynamicSizing
|
||||
onChange={handleSheetChanges}
|
||||
backdropComponent={renderBackdrop}
|
||||
handleIndicatorStyle={{
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import {
|
||||
BottomSheetBackdrop,
|
||||
type BottomSheetBackdropProps,
|
||||
BottomSheetModal,
|
||||
BottomSheetView,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import type React from "react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
@@ -16,6 +11,11 @@ import {
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import {
|
||||
type BottomSheetMethods,
|
||||
BottomSheetModal,
|
||||
BottomSheetView,
|
||||
} from "@/utils/expoUiBottomSheet";
|
||||
|
||||
interface LibraryOptions {
|
||||
display: "row" | "list";
|
||||
@@ -132,7 +132,7 @@ export const LibraryOptionsSheet: React.FC<Props> = ({
|
||||
updateSettings,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
const bottomSheetModalRef = useRef<BottomSheetMethods>(null);
|
||||
const { t } = useTranslation();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
@@ -161,25 +161,14 @@ export const LibraryOptionsSheet: React.FC<Props> = ({
|
||||
[setOpen],
|
||||
);
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: BottomSheetBackdropProps) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
if (disabled) return null;
|
||||
if (Platform.isTV) return null;
|
||||
|
||||
return (
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetModalRef}
|
||||
enableDynamicSizing
|
||||
onChange={handleSheetChanges}
|
||||
backdropComponent={renderBackdrop}
|
||||
handleIndicatorStyle={{
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
@@ -187,7 +176,6 @@ export const LibraryOptionsSheet: React.FC<Props> = ({
|
||||
backgroundColor: "#171717",
|
||||
}}
|
||||
enablePanDownToClose
|
||||
enableDismissOnClose
|
||||
>
|
||||
<BottomSheetView>
|
||||
<View
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
import {
|
||||
BottomSheetBackdrop,
|
||||
type BottomSheetBackdropProps,
|
||||
BottomSheetModal,
|
||||
BottomSheetView,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import { getQuickConnectApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useAtom } from "jotai";
|
||||
import type React from "react";
|
||||
@@ -12,6 +6,11 @@ import { useTranslation } from "react-i18next";
|
||||
import { Alert, Platform, View, type ViewProps } from "react-native";
|
||||
import { useHaptic } from "@/hooks/useHaptic";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import {
|
||||
type BottomSheetMethods,
|
||||
BottomSheetModal,
|
||||
BottomSheetView,
|
||||
} from "@/utils/expoUiBottomSheet";
|
||||
import { Button } from "../Button";
|
||||
import { Text } from "../common/Text";
|
||||
import { PinInput } from "../inputs/PinInput";
|
||||
@@ -25,7 +24,7 @@ export const QuickConnect: React.FC<Props> = ({ ...props }) => {
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
const [quickConnectCode, setQuickConnectCode] = useState<string>();
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
const bottomSheetModalRef = useRef<BottomSheetMethods>(null);
|
||||
const successHapticFeedback = useHaptic("success");
|
||||
const errorHapticFeedback = useHaptic("error");
|
||||
const snapPoints = useMemo(
|
||||
@@ -36,17 +35,6 @@ export const QuickConnect: React.FC<Props> = ({ ...props }) => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: BottomSheetBackdropProps) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const authorizeQuickConnect = useCallback(async () => {
|
||||
if (quickConnectCode) {
|
||||
try {
|
||||
@@ -97,6 +85,7 @@ export const QuickConnect: React.FC<Props> = ({ ...props }) => {
|
||||
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetModalRef}
|
||||
enablePanDownToClose
|
||||
snapPoints={snapPoints}
|
||||
handleIndicatorStyle={{
|
||||
backgroundColor: "white",
|
||||
@@ -104,11 +93,8 @@ export const QuickConnect: React.FC<Props> = ({ ...props }) => {
|
||||
backgroundStyle={{
|
||||
backgroundColor: "#171717",
|
||||
}}
|
||||
backdropComponent={renderBackdrop}
|
||||
keyboardBehavior={isAndroid ? "fillParent" : "interactive"}
|
||||
keyboardBlurBehavior='restore'
|
||||
android_keyboardInputMode='adjustResize'
|
||||
topInset={isAndroid ? 0 : undefined}
|
||||
>
|
||||
<BottomSheetView>
|
||||
<View className='flex flex-col space-y-4 px-4 pb-8 pt-2'>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { formatTimeString } from "@/utils/time";
|
||||
@@ -16,6 +17,8 @@ export const TimeDisplay: FC<TimeDisplayProps> = ({
|
||||
currentTime,
|
||||
remainingTime,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getFinishTime = () => {
|
||||
const now = new Date();
|
||||
// remainingTime is in ms
|
||||
@@ -37,7 +40,7 @@ export const TimeDisplay: FC<TimeDisplayProps> = ({
|
||||
-{formatTimeString(remainingTime, "ms")}
|
||||
</Text>
|
||||
<Text className='text-[10px] text-neutral-500 opacity-70'>
|
||||
ends at {getFinishTime()}
|
||||
{t("player.ends_at", { time: getFinishTime() })}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
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 React, {
|
||||
forwardRef,
|
||||
@@ -16,6 +10,7 @@ import React, {
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
@@ -31,6 +26,11 @@ import {
|
||||
useItemInWatchlists,
|
||||
useMyWatchlistsQuery,
|
||||
} from "@/hooks/useWatchlists";
|
||||
import {
|
||||
type BottomSheetMethods,
|
||||
BottomSheetModal,
|
||||
BottomSheetView,
|
||||
} from "@/utils/expoUiBottomSheet";
|
||||
import type { StreamystatsWatchlist } from "@/utils/streamystats/types";
|
||||
|
||||
export interface WatchlistSheetRef {
|
||||
@@ -263,7 +263,7 @@ const WatchlistSheetContent: React.FC<WatchlistSheetContentProps> = ({
|
||||
|
||||
export const WatchlistSheet = forwardRef<WatchlistSheetRef, object>(
|
||||
(_props, ref) => {
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
const bottomSheetModalRef = useRef<BottomSheetMethods>(null);
|
||||
const [currentItem, setCurrentItem] = React.useState<BaseItemDto | null>(
|
||||
null,
|
||||
);
|
||||
@@ -283,23 +283,13 @@ export const WatchlistSheet = forwardRef<WatchlistSheetRef, object>(
|
||||
bottomSheetModalRef.current?.dismiss();
|
||||
}, []);
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: BottomSheetBackdropProps) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
if (Platform.isTV) return null;
|
||||
|
||||
return (
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetModalRef}
|
||||
enablePanDownToClose
|
||||
enableDynamicSizing
|
||||
maxDynamicContentSize={600}
|
||||
backdropComponent={renderBackdrop}
|
||||
handleIndicatorStyle={{
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
|
||||
11
eas.json
11
eas.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"cli": {
|
||||
"version": ">= 9.1.0",
|
||||
"version": ">= 16.0.0",
|
||||
"appVersionSource": "remote"
|
||||
},
|
||||
"build": {
|
||||
@@ -52,6 +52,7 @@
|
||||
}
|
||||
},
|
||||
"production": {
|
||||
"bun": "1.3.5",
|
||||
"environment": "production",
|
||||
"autoIncrement": true,
|
||||
"android": {
|
||||
@@ -59,6 +60,7 @@
|
||||
}
|
||||
},
|
||||
"production-apk": {
|
||||
"bun": "1.3.5",
|
||||
"environment": "production",
|
||||
"autoIncrement": true,
|
||||
"android": {
|
||||
@@ -67,6 +69,7 @@
|
||||
}
|
||||
},
|
||||
"production-apk-tv": {
|
||||
"bun": "1.3.5",
|
||||
"environment": "production",
|
||||
"autoIncrement": true,
|
||||
"android": {
|
||||
@@ -78,6 +81,7 @@
|
||||
}
|
||||
},
|
||||
"production_tv": {
|
||||
"bun": "1.3.5",
|
||||
"environment": "production",
|
||||
"autoIncrement": true,
|
||||
"env": {
|
||||
@@ -93,6 +97,11 @@
|
||||
"ios": {
|
||||
"appleTeamId": "MWD5K362T8",
|
||||
"ascAppId": "6593660679"
|
||||
},
|
||||
"android": {
|
||||
"serviceAccountKeyPath": "./google-service-account.json",
|
||||
"track": "internal",
|
||||
"releaseStatus": "completed"
|
||||
}
|
||||
},
|
||||
"production_tv": {
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
"@expo/react-native-action-sheet": "^4.1.1",
|
||||
"@expo/ui": "~56.0.14",
|
||||
"@expo/vector-icons": "^15.0.3",
|
||||
"@gorhom/bottom-sheet": "5.2.8",
|
||||
"@jellyfin/sdk": "^0.13.0",
|
||||
"@react-native-community/netinfo": "^12.0.0",
|
||||
"@react-navigation/material-top-tabs": "7.4.28",
|
||||
"@react-navigation/native": "^7.2.5",
|
||||
"@shopify/flash-list": "2.0.2",
|
||||
"@tanstack/query-sync-storage-persister": "^5.100.14",
|
||||
@@ -104,6 +104,7 @@
|
||||
"react-native-safe-area-context": "~5.7.0",
|
||||
"react-native-screens": "4.25.2",
|
||||
"react-native-svg": "15.15.4",
|
||||
"react-native-tab-view": "4.3.0",
|
||||
"react-native-text-ticker": "^1.15.0",
|
||||
"react-native-track-player": "github:lovegaoshi/react-native-track-player#APM",
|
||||
"react-native-udp": "^4.1.7",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { BottomSheetModal } from "@gorhom/bottom-sheet";
|
||||
import type React from "react";
|
||||
import {
|
||||
createContext,
|
||||
@@ -11,6 +10,7 @@ import {
|
||||
} from "react";
|
||||
|
||||
import { BackHandler, Platform } from "react-native";
|
||||
import type { BottomSheetMethods } from "@/utils/expoUiBottomSheet";
|
||||
|
||||
interface ModalOptions {
|
||||
enableDynamicSizing?: boolean;
|
||||
@@ -30,7 +30,7 @@ interface GlobalModalContextType {
|
||||
hideModal: () => void;
|
||||
isVisible: boolean;
|
||||
modalState: GlobalModalState;
|
||||
modalRef: React.RefObject<BottomSheetModal | null>;
|
||||
modalRef: React.RefObject<BottomSheetMethods | null>;
|
||||
}
|
||||
|
||||
const GlobalModalContext = createContext<GlobalModalContextType | undefined>(
|
||||
@@ -57,7 +57,7 @@ export const GlobalModalProvider: React.FC<GlobalModalProviderProps> = ({
|
||||
options: undefined,
|
||||
});
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const modalRef = useRef<BottomSheetModal>(null);
|
||||
const modalRef = useRef<BottomSheetMethods>(null);
|
||||
|
||||
const showModal = useCallback(
|
||||
(content: ReactNode, options?: ModalOptions) => {
|
||||
|
||||
@@ -608,7 +608,8 @@
|
||||
"downloaded_file_message": "Heruntergeladene Datei abspielen?",
|
||||
"downloaded_file_yes": "Ja",
|
||||
"downloaded_file_no": "Nein",
|
||||
"downloaded_file_cancel": "Abbrechen"
|
||||
"downloaded_file_cancel": "Abbrechen",
|
||||
"ends_at": "Endet um {{time}}"
|
||||
},
|
||||
"item_card": {
|
||||
"next_up": "Als Nächstes",
|
||||
|
||||
@@ -698,7 +698,7 @@
|
||||
"downloaded_file_no": "No",
|
||||
"downloaded_file_cancel": "Cancel",
|
||||
"swipe_down_settings": "Swipe down for settings",
|
||||
"ends_at": "ends at",
|
||||
"ends_at": "Ends at {{time}}",
|
||||
"search_subtitles": "Search Subtitles",
|
||||
"subtitle_tracks": "Tracks",
|
||||
"subtitle_search": "Search & Download",
|
||||
|
||||
40
utils/expoUiBottomSheet.ts
Normal file
40
utils/expoUiBottomSheet.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
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";
|
||||
Reference in New Issue
Block a user