diff --git a/.github/workflows/build-eas-android.yml b/.github/workflows/build-eas-android.yml new file mode 100644 index 000000000..6a807e17c --- /dev/null +++ b/.github/workflows/build-eas-android.yml @@ -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 diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 8ec355d49..968b7c2f8 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -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, + }, }, ], [ diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 543d42d43..e6bad0f79 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -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", diff --git a/apps/mobile/src/components/native/webview/hooks.ts b/apps/mobile/src/components/native/webview/hooks.ts new file mode 100644 index 000000000..7d7373dfc --- /dev/null +++ b/apps/mobile/src/components/native/webview/hooks.ts @@ -0,0 +1,9 @@ +import { useEffect } from "react" + +import { prepareEntryRenderWebView } from "./index" + +export const usePrepareEntryRenderWebView = () => { + useEffect(() => { + prepareEntryRenderWebView() + }, []) +} diff --git a/apps/mobile/src/components/native/webview/index.android.ts b/apps/mobile/src/components/native/webview/index.android.ts new file mode 100644 index 000000000..5fb384e6f --- /dev/null +++ b/apps/mobile/src/components/native/webview/index.android.ts @@ -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 = () => {} diff --git a/apps/mobile/src/components/ui/image/galeria/index.tsx b/apps/mobile/src/components/ui/image/galeria/index.tsx index 7884c59d2..8a96f95d2 100644 --- a/apps/mobile/src/components/ui/image/galeria/index.tsx +++ b/apps/mobile/src/components/ui/image/galeria/index.tsx @@ -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>) { + return ( + ({ + urls, + theme, + initialIndex: 0, + open: false, + src: "", + setOpen: noop, + ids, + }), + [urls, theme, ids], + )} + > + {children} + + ) + }, { - Image: () => null, + Image(props: GaleriaViewProps) { + return props.children + }, }, ) as unknown as typeof GaleriaInterface diff --git a/apps/mobile/src/modules/entry-list/EntryListContentVideo.tsx b/apps/mobile/src/modules/entry-list/EntryListContentVideo.tsx index c597aec75..09ff48502 100644 --- a/apps/mobile/src/modules/entry-list/EntryListContentVideo.tsx +++ b/apps/mobile/src/modules/entry-list/EntryListContentVideo.tsx @@ -62,7 +62,10 @@ export function EntryItemSkeleton() { return ( {/* Video thumbnail */} - + {/* Description and footer */} diff --git a/apps/mobile/src/modules/entry-list/templates/EntryPictureItem.tsx b/apps/mobile/src/modules/entry-list/templates/EntryPictureItem.tsx index 1f7ec482e..33423e5cb 100644 --- a/apps/mobile/src/modules/entry-list/templates/EntryPictureItem.tsx +++ b/apps/mobile/src/modules/entry-list/templates/EntryPictureItem.tsx @@ -23,7 +23,7 @@ export function EntryPictureItem({ id }: { id: string }) { if (!hasMedia) { return ( - + No media available ) diff --git a/apps/mobile/src/modules/screen/PageList.tsx b/apps/mobile/src/modules/screen/PageList.tsx index 60109b21e..1ea8e80ea 100644 --- a/apps/mobile/src/modules/screen/PageList.tsx +++ b/apps/mobile/src/modules/screen/PageList.tsx @@ -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) }, }) diff --git a/apps/mobile/src/screens/(stack)/(tabs)/index.tsx b/apps/mobile/src/screens/(stack)/(tabs)/index.tsx index 61b9715b9..5ffe80de3 100644 --- a/apps/mobile/src/screens/(stack)/(tabs)/index.tsx +++ b/apps/mobile/src/screens/(stack)/(tabs)/index.tsx @@ -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 } diff --git a/apps/mobile/src/store/entry/store.ts b/apps/mobile/src/store/entry/store.ts index 291fea8f9..39e5a39ef 100644 --- a/apps/mobile/src/store/entry/store.ts +++ b/apps/mobile/src/store/entry/store.ts @@ -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 diff --git a/patches/react-native-track-player.patch b/patches/react-native-track-player.patch index 20b27140e..8e6cd2df2 100644 --- a/patches/react-native-track-player.patch +++ b/patches/react-native-track-player.patch @@ -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() ++ } ++ } + } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 41b2be499..382aca9be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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)