feat: android prototype (#3376)

* fix(mobile): mock webview module for Android

* fix(mobile): disable new architecture for track player workaround

* chore(mobile): add @react-native-menu/menu dependency

* fix(mobile): handle undefined index in PagerList onIndexChange

* fix(mobile): add error handling for stream fetch response

* fix(mobile): update aspect ratio handling for video components

* fix(mobile): mock Galeria component

* fixup! fix(mobile): update aspect ratio handling for video components

* chore(ci): add GitHub Actions workflow for building Android app

* fix(mobile): improve CocoaPods pre-install script to handle missing CocoaPods

* fix(mobile): move splash screen image configuration to Android-specific settings

* fix(mobile): image in android

* fix: patch react-native-track-player patch

* fix: build profile
This commit is contained in:
Whitewater 2025-04-02 20:24:06 +09:00 committed by GitHub
parent e2cc31d4ba
commit 25d253ff33
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 575 additions and 21 deletions

78
.github/workflows/build-eas-android.yml vendored Normal file
View File

@ -0,0 +1,78 @@
name: Build Android
on:
push:
branches: [dev]
paths:
- "apps/mobile/web-app/**"
- "apps/mobile/native/**"
- "apps/mobile/package.json"
- "apps/mobile/app.config.ts"
pull_request:
branches: [dev]
paths:
- "apps/mobile/web-app/**"
- "apps/mobile/native/**"
- "apps/mobile/package.json"
- "apps/mobile/app.config.ts"
workflow_dispatch:
inputs:
profile:
type: choice
default: preview
options:
- preview
- production
description: "Build profile"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build Android apk for device
if: github.secret_source != 'None'
runs-on: ubuntu-latest
steps:
- name: 📦 Checkout code
uses: actions/checkout@v4
- name: 📦 Setup pnpm
uses: pnpm/action-setup@v4
- name: 🏗 Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: "pnpm"
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: "17"
distribution: "zulu"
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: 📱 Setup EAS
uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: Install dependencies
run: pnpm install
- name: 🔨 Build Android app
working-directory: apps/mobile
run: eas build --platform android --profile ${{ github.event.inputs.profile || 'preview' }} --local --output=${{ github.workspace }}/build.apk
- name: 📤 Upload apk Artifact
uses: actions/upload-artifact@v4
with:
name: app-android
path: ${{ github.workspace }}/build.apk
retention-days: 90

View File

@ -78,6 +78,8 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
googleServicesFile: "./build/GoogleService-Info.plist",
},
android: {
// Workaround for https://github.com/doublesymmetry/react-native-track-player/issues/2293
newArchEnabled: false,
package: "is.follow",
adaptiveIcon: {
foregroundImage: adaptiveIconPath,
@ -106,6 +108,10 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
dark: {
backgroundColor: "#000000",
},
android: {
image: iconPath,
imageWidth: 200,
},
},
],
[

View File

@ -9,7 +9,7 @@
"db:generate": "drizzle-kit generate",
"dev": "npm run start",
"eas-build-post-install": "rm -rf $TMPDIR/metro-cache",
"eas-build-pre-install": "pod repo update",
"eas-build-pre-install": "command -v pod >/dev/null 2>&1 && pod repo update || echo 'CocoaPods not found, skipping pod repo update'",
"ios": "expo run:ios",
"ios:device": "expo run:ios --device",
"start": "expo start --dev-client",
@ -38,6 +38,7 @@
"@react-native-firebase/app": "21.12.3",
"@react-native-firebase/app-check": "21.12.3",
"@react-native-firebase/crashlytics": "21.12.3",
"@react-native-menu/menu": "1.2.3",
"@react-native-picker/picker": "2.11.0",
"@shopify/flash-list": "1.7.3",
"@tanstack/query-sync-storage-persister": "5.70.0",

View File

@ -0,0 +1,9 @@
import { useEffect } from "react"
import { prepareEntryRenderWebView } from "./index"
export const usePrepareEntryRenderWebView = () => {
useEffect(() => {
prepareEntryRenderWebView()
}, [])
}

View File

@ -0,0 +1,10 @@
export const SharedWebViewModule = {
load: () => {
console.warn("SharedWebViewModule.load is not implemented on Android")
},
evaluateJavaScript: () => {
console.warn("SharedWebViewModule.evaluateJavaScript is not implemented on Android")
},
}
export const prepareEntryRenderWebView = () => {}

View File

@ -1,9 +1,43 @@
import { useMemo } from "react"
import { GaleriaContext } from "./context"
import type { Galeria as GaleriaInterface } from "./index.ios"
import type { GaleriaViewProps } from "./types"
const noop = () => {}
const Galeria: typeof GaleriaInterface = Object.assign(
{},
function Galeria({
children,
urls,
theme = "dark",
ids,
}: {
children: React.ReactNode
} & Partial<Pick<GaleriaContext, "theme" | "ids" | "urls">>) {
return (
<GaleriaContext.Provider
value={useMemo(
() => ({
urls,
theme,
initialIndex: 0,
open: false,
src: "",
setOpen: noop,
ids,
}),
[urls, theme, ids],
)}
>
{children}
</GaleriaContext.Provider>
)
},
{
Image: () => null,
Image(props: GaleriaViewProps) {
return props.children
},
},
) as unknown as typeof GaleriaInterface

View File

@ -62,7 +62,10 @@ export function EntryItemSkeleton() {
return (
<View className="m-1 overflow-hidden rounded-md">
{/* Video thumbnail */}
<View className="bg-system-fill aspect-video h-32 w-full animate-pulse rounded-md" />
<View
className="bg-system-fill h-32 w-full animate-pulse rounded-md"
style={{ aspectRatio: 16 / 9 }}
/>
{/* Description and footer */}
<View className="my-2 px-2">

View File

@ -23,7 +23,7 @@ export function EntryPictureItem({ id }: { id: string }) {
if (!hasMedia) {
return (
<View className="aspect-video w-full items-center justify-center">
<View className="w-full items-center justify-center" style={{ aspectRatio: 16 / 9 }}>
<Text className="text-label text-center">No media available</Text>
</View>
)

View File

@ -30,6 +30,7 @@ export function PagerList({
const { page, pagerRef, ...rest } = usePagerView({
initialPage: viewIdIndex,
onIndexChange: (index) => {
if (index === undefined) return
selectTimeline({ type: "view", viewId: activeViews[index]!.view }, false)
},
})

View File

@ -1,15 +1,11 @@
import { useEffect } from "react"
import { prepareEntryRenderWebView } from "@/src/components/native/webview"
import { usePrepareEntryRenderWebView } from "@/src/components/native/webview/hooks"
import { Home5CuteFiIcon } from "@/src/icons/home_5_cute_fi"
import { Home5CuteReIcon } from "@/src/icons/home_5_cute_re"
import type { TabScreenComponent } from "@/src/lib/navigation/bottom-tab/types"
import { EntryList } from "@/src/modules/entry-list"
export const IndexTabScreen: TabScreenComponent = () => {
useEffect(() => {
prepareEntryRenderWebView()
}, [])
usePrepareEntryRenderWebView()
return <EntryList />
}

View File

@ -484,6 +484,10 @@ class EntrySyncServices {
ids: nextIds,
}),
})
if (!response.ok) {
console.error("Failed to fetch stream:", response.statusText, await response.text())
return
}
const reader = response.body?.getReader()
if (!reader) return

View File

@ -5,7 +5,7 @@ index 7741994191921ed86f7577f7ce589b3939d9dd49..776c09ec0ae450daa4dc9031ccd3ff4b
@@ -151,17 +151,4 @@ @interface RCT_EXTERN_REMAP_MODULE(TrackPlayerModule, RNTrackPlayer, NSObject)
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject);
-RCT_EXTERN_METHOD(getSleepTimerProgress:(RCTPromiseResolveBlock)resolve
- rejecter:(RCTPromiseRejectBlock)reject);
-
@ -20,3 +20,412 @@ index 7741994191921ed86f7577f7ce589b3939d9dd49..776c09ec0ae450daa4dc9031ccd3ff4b
- rejecter:(RCTPromiseRejectBlock)reject);
-
@end
diff --git a/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicModule.kt b/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicModule.kt
index b2409a09939164c49c0f7a16bb6d3284e8eab8fb..099caae67c52d27e451a0a5e7b11ca3645dfcd9d 100644
--- a/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicModule.kt
+++ b/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicModule.kt
@@ -251,8 +251,8 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun updateOptions(data: ReadableMap?, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun updateOptions(data: ReadableMap?, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
val options = Arguments.toBundle(data)
@@ -264,14 +264,14 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun add(data: ReadableArray?, insertBeforeIndex: Int, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun add(data: ReadableArray?, insertBeforeIndex: Int, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
try {
val tracks = readableArrayToTrackList(data);
if (insertBeforeIndex < -1 || insertBeforeIndex > musicService.tracks.size) {
callback.reject("index_out_of_bounds", "The track index is out of bounds")
- return@launch
+ return@launchInScope
}
val index = if (insertBeforeIndex == -1) musicService.tracks.size else insertBeforeIndex
musicService.add(
@@ -285,11 +285,11 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun load(data: ReadableMap?, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun load(data: ReadableMap?, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
if (data == null) {
callback.resolve(null)
- return@launch
+ return@launchInScope
}
val bundle = Arguments.toBundle(data);
if (bundle is Bundle) {
@@ -301,15 +301,15 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun move(fromIndex: Int, toIndex: Int, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun move(fromIndex: Int, toIndex: Int, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.move(fromIndex, toIndex)
callback.resolve(null)
}
@ReactMethod
- fun remove(data: ReadableArray?, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun remove(data: ReadableArray?, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
val inputIndexes = Arguments.toList(data)
if (inputIndexes != null) {
val size = musicService.tracks.size
@@ -321,7 +321,7 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
"index_out_of_bounds",
"One or more indexes was out of bounds"
)
- return@launch
+ return@launchInScope
}
indexes.add(index)
}
@@ -332,8 +332,8 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
@ReactMethod
fun updateMetadataForTrack(index: Int, map: ReadableMap?, callback: Promise) =
- scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
if (index < 0 || index >= musicService.tracks.size) {
callback.reject("index_out_of_bounds", "The index is out of bounds")
@@ -348,8 +348,8 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun updateNowPlayingMetadata(map: ReadableMap?, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun updateNowPlayingMetadata(map: ReadableMap?, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
if (musicService.tracks.isEmpty())
callback.reject("no_current_item", "There is no current item in the player")
@@ -364,8 +364,8 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun clearNowPlayingMetadata(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun clearNowPlayingMetadata(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
if (musicService.tracks.isEmpty())
callback.reject("no_current_item", "There is no current item in the player")
@@ -375,16 +375,16 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun removeUpcomingTracks(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun removeUpcomingTracks(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.removeUpcomingTracks()
callback.resolve(null)
}
@ReactMethod
- fun skip(index: Int, initialTime: Float, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun skip(index: Int, initialTime: Float, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.skip(index)
@@ -396,8 +396,8 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun skipToNext(initialTime: Float, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun skipToNext(initialTime: Float, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.skipToNext()
@@ -409,8 +409,8 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun skipToPrevious(initialTime: Float, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun skipToPrevious(initialTime: Float, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.skipToPrevious()
@@ -422,8 +422,8 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun reset(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun reset(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.stop()
delay(300) // Allow playback to stop
@@ -433,116 +433,116 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun play(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun play(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.play()
callback.resolve(null)
}
@ReactMethod
- fun pause(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun pause(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.pause()
callback.resolve(null)
}
@ReactMethod
- fun stop(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun stop(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.stop()
callback.resolve(null)
}
@ReactMethod
- fun seekTo(seconds: Float, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun seekTo(seconds: Float, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.seekTo(seconds)
callback.resolve(null)
}
@ReactMethod
- fun seekBy(offset: Float, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun seekBy(offset: Float, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.seekBy(offset)
callback.resolve(null)
}
@ReactMethod
- fun retry(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun retry(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.retry()
callback.resolve(null)
}
@ReactMethod
- fun setVolume(volume: Float, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun setVolume(volume: Float, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.setVolume(volume)
callback.resolve(null)
}
@ReactMethod
- fun getVolume(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getVolume(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
callback.resolve(musicService.getVolume())
}
@ReactMethod
- fun setRate(rate: Float, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun setRate(rate: Float, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.setRate(rate)
callback.resolve(null)
}
@ReactMethod
- fun getRate(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getRate(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
callback.resolve(musicService.getRate())
}
@ReactMethod
- fun setRepeatMode(mode: Int, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun setRepeatMode(mode: Int, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.setRepeatMode(RepeatMode.fromOrdinal(mode))
callback.resolve(null)
}
@ReactMethod
- fun getRepeatMode(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getRepeatMode(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
callback.resolve(musicService.getRepeatMode().ordinal)
}
@ReactMethod
- fun setPlayWhenReady(playWhenReady: Boolean, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun setPlayWhenReady(playWhenReady: Boolean, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.playWhenReady = playWhenReady
callback.resolve(null)
}
@ReactMethod
- fun getPlayWhenReady(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getPlayWhenReady(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
callback.resolve(musicService.playWhenReady)
}
@ReactMethod
- fun getTrack(index: Int, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getTrack(index: Int, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
if (index >= 0 && index < musicService.tracks.size) {
callback.resolve(Arguments.fromBundle(musicService.tracks[index].originalItem))
@@ -552,15 +552,15 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun getQueue(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getQueue(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
callback.resolve(Arguments.fromList(musicService.tracks.map { it.originalItem }))
}
@ReactMethod
- fun setQueue(data: ReadableArray?, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun setQueue(data: ReadableArray?, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
try {
musicService.clear()
@@ -572,16 +572,16 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun getActiveTrackIndex(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getActiveTrackIndex(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
callback.resolve(
if (musicService.tracks.isEmpty()) null else musicService.getCurrentTrackIndex()
)
}
@ReactMethod
- fun getActiveTrack(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getActiveTrack(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
callback.resolve(
if (musicService.tracks.isEmpty()) null
else Arguments.fromBundle(
@@ -591,29 +591,29 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun getDuration(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getDuration(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
callback.resolve(musicService.getDurationInSeconds())
}
@ReactMethod
- fun getBufferedPosition(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getBufferedPosition(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
callback.resolve(musicService.getBufferedPositionInSeconds())
}
@ReactMethod
- fun getPosition(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getPosition(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
callback.resolve(musicService.getPositionInSeconds())
}
@ReactMethod
- fun getProgress(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getProgress(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
var bundle = Bundle()
bundle.putDouble("duration", musicService.getDurationInSeconds());
bundle.putDouble("position", musicService.getPositionInSeconds());
@@ -622,8 +622,16 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun getPlaybackState(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getPlaybackState(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
callback.resolve(Arguments.fromBundle(musicService.getPlayerStateBundle(musicService.state)))
}
+
+ // Bridgeless interop layer tries to pass the `Job` from `scope.launch` to the JS side
+ // which causes an exception. We can work around this using a wrapper.
+ private fun launchInScope(block: suspend () -> Unit) {
+ scope.launch {
+ block()
+ }
+ }
}

View File

@ -52,7 +52,7 @@ patchedDependencies:
hash: 8a10ea43dd4d194a581d3d765a1eb3e864441b968d5d5729382a9986f710ef32
path: patches/react-native-sheet-transitions.patch
react-native-track-player:
hash: debcf90daeaf7d38c744ecb11cded73b5bfc671b8d0937865ec21307414761b3
hash: 24a9527477cf307020abf337d3f5db78cdc015636e7ab47d085a1eb5792a5ff0
path: patches/react-native-track-player.patch
workbox-precaching:
hash: 51e57c78af317e292b43fca4b72f57b247b7b1a1faa8b03c8881ddb798c7c52f
@ -815,6 +815,9 @@ importers:
'@react-native-firebase/crashlytics':
specifier: 21.12.3
version: 21.12.3(d1616443082d3617f967a48301823ad4)
'@react-native-menu/menu':
specifier: 1.2.3
version: 1.2.3(react-native@0.77.2(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1)
'@react-native-picker/picker':
specifier: 2.11.0
version: 2.11.0(react-native@0.77.2(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1)
@ -1027,7 +1030,7 @@ importers:
version: 15.11.2(react-native@0.77.2(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1)
react-native-track-player:
specifier: 4.1.1
version: 4.1.1(patch_hash=debcf90daeaf7d38c744ecb11cded73b5bfc671b8d0937865ec21307414761b3)(react-native@0.77.2(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1)
version: 4.1.1(patch_hash=24a9527477cf307020abf337d3f5db78cdc015636e7ab47d085a1eb5792a5ff0)(react-native@0.77.2(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1)
react-native-uikit-colors:
specifier: 0.3.10
version: 0.3.10(0d6f50eaea763e7ce5838e09437a33ba)
@ -1057,7 +1060,7 @@ importers:
version: 3.1.1(react@18.3.1)
zeego:
specifier: 3.0.6
version: 3.0.6(10862d462bdf72b2f4c999770e7a8205)
version: 3.0.6(9cf8a8a5ccce31468c34b8be336f174a)
zod:
specifier: 3.24.2
version: 3.24.2
@ -5257,8 +5260,8 @@ packages:
expo:
optional: true
'@react-native-menu/menu@1.2.2':
resolution: {integrity: sha512-Uk65PAhwNkCVBAqJu5t2H9biV+m0JLwJc7m3v2X2A/W8SFJmUqYabBsLH4fOWKI3a7kkR9QDT6HruliIKSfM8w==}
'@react-native-menu/menu@1.2.3':
resolution: {integrity: sha512-sEfiVIivsa0lSelFm9Wbm/RAi+XoEHc75GGhjwvSrj9KSCVvNNXwr9F8l42e1t/lzYvVYzmkYxLG6VKxrDYJiw==}
peerDependencies:
react: 18.3.1
react-native: '*'
@ -20153,7 +20156,7 @@ snapshots:
optionalDependencies:
expo: 52.0.41(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.77.2(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.13.5(react-native@0.77.2(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1))(react-native@0.77.2(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1)(utf-8-validate@6.0.5)
'@react-native-menu/menu@1.2.2(react-native@0.77.2(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1)':
'@react-native-menu/menu@1.2.3(react-native@0.77.2(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1)':
dependencies:
react: 18.3.1
react-native: 0.77.2(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5)
@ -29364,7 +29367,7 @@ snapshots:
react-native: 0.77.2(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5)
warn-once: 0.1.1
react-native-track-player@4.1.1(patch_hash=debcf90daeaf7d38c744ecb11cded73b5bfc671b8d0937865ec21307414761b3)(react-native@0.77.2(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1):
react-native-track-player@4.1.1(patch_hash=24a9527477cf307020abf337d3f5db78cdc015636e7ab47d085a1eb5792a5ff0)(react-native@0.77.2(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1):
dependencies:
react: 18.3.1
react-native: 0.77.2(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5)
@ -32129,11 +32132,11 @@ snapshots:
yoga-wasm-web@0.3.3: {}
zeego@3.0.6(10862d462bdf72b2f4c999770e7a8205):
zeego@3.0.6(9cf8a8a5ccce31468c34b8be336f174a):
dependencies:
'@radix-ui/react-context-menu': 2.2.6(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-dropdown-menu': 2.1.6(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@react-native-menu/menu': 1.2.2(react-native@0.77.2(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1)
'@react-native-menu/menu': 1.2.3(react-native@0.77.2(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1)
react: 18.3.1
react-native: 0.77.2(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5)
react-native-ios-context-menu: 3.1.0(react-native-ios-utilities@5.1.3(react-native@0.77.2(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1))(react-native@0.77.2(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1)