mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-12 05:10:52 +02:00
android auto more stuff
This commit is contained in:
+4
@@ -116,6 +116,10 @@ class AstraCarCommandService : HeadlessJsTaskService() {
|
||||
)
|
||||
}
|
||||
|
||||
fun startFavoriteAction(context: Context) {
|
||||
start(context, Intent(context, AstraCarCommandService::class.java).putExtra(EXTRA_COMMAND, AstraCarFavoriteAction.COMMAND))
|
||||
}
|
||||
|
||||
fun startPlayFromMediaId(context: Context, mediaId: String?) {
|
||||
val media = AstraCarMediaIds.decode(mediaId) ?: return
|
||||
start(
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package expo.modules.astracar
|
||||
|
||||
object AstraCarFavoriteAction {
|
||||
const val TOGGLE = "astra.favorite.toggle"
|
||||
const val COMMAND = "toggleFavorite"
|
||||
}
|
||||
@@ -51,6 +51,14 @@ class AstraCarMediaService : MediaBrowserServiceCompat() {
|
||||
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
|
||||
AstraCarCommandService.startPlayFromSearch(this@AstraCarMediaService, query, extras)
|
||||
}
|
||||
|
||||
override fun onCustomAction(action: String?, extras: Bundle?) {
|
||||
if (action == AstraCarFavoriteAction.TOGGLE) {
|
||||
AstraCarCommandService.startFavoriteAction(this@AstraCarMediaService)
|
||||
} else {
|
||||
super.onCustomAction(action, extras)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
applyAstraState(AstraCarNowPlayingStore.load(this@AstraCarMediaService))
|
||||
|
||||
@@ -37,6 +37,12 @@ class AstraCarNowPlayingRecord : Record {
|
||||
|
||||
@Field
|
||||
val position: Double? = null
|
||||
|
||||
@Field
|
||||
val trackPath: String? = null
|
||||
|
||||
@Field
|
||||
val isFavorite: Boolean = false
|
||||
}
|
||||
|
||||
class AstraCarModule : Module() {
|
||||
@@ -56,6 +62,8 @@ class AstraCarModule : Module() {
|
||||
hasTrack = state.hasTrack,
|
||||
durationSeconds = state.duration,
|
||||
positionSeconds = state.position,
|
||||
trackPath = state.trackPath,
|
||||
isFavorite = state.isFavorite,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ data class AstraCarNowPlayingState(
|
||||
val hasTrack: Boolean,
|
||||
val durationSeconds: Double?,
|
||||
val positionSeconds: Double?,
|
||||
val trackPath: String?,
|
||||
val isFavorite: Boolean,
|
||||
)
|
||||
|
||||
object AstraCarNowPlayingStore {
|
||||
@@ -94,9 +96,19 @@ object AstraCarNowPlayingStore {
|
||||
((state.positionSeconds ?: 0.0) * 1000).toLong(),
|
||||
if (playbackState == PlaybackStateCompat.STATE_PLAYING) 1f else 0f,
|
||||
)
|
||||
.apply {
|
||||
favoriteAction(state)?.let(::addCustomAction)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun favoriteAction(state: AstraCarNowPlayingState): PlaybackStateCompat.CustomAction? {
|
||||
if (!state.hasTrack || state.trackPath.isNullOrBlank()) return null
|
||||
val label = if (state.isFavorite) "Unfavorite" else "Favorite"
|
||||
val icon = if (state.isFavorite) R.drawable.ic_astra_favorite else R.drawable.ic_astra_favorite_border
|
||||
return PlaybackStateCompat.CustomAction.Builder(AstraCarFavoriteAction.TOGGLE, label, icon).build()
|
||||
}
|
||||
|
||||
private fun encode(state: AstraCarNowPlayingState): String =
|
||||
JSONObject()
|
||||
.put("title", state.title)
|
||||
@@ -107,6 +119,8 @@ object AstraCarNowPlayingStore {
|
||||
.put("hasTrack", state.hasTrack)
|
||||
.put("durationSeconds", state.durationSeconds)
|
||||
.put("positionSeconds", state.positionSeconds)
|
||||
.put("trackPath", state.trackPath)
|
||||
.put("isFavorite", state.isFavorite)
|
||||
.toString()
|
||||
|
||||
private fun decode(value: String?): AstraCarNowPlayingState {
|
||||
@@ -122,6 +136,8 @@ object AstraCarNowPlayingStore {
|
||||
hasTrack = json.optBoolean("hasTrack", false),
|
||||
durationSeconds = json.optDoubleOrNull("durationSeconds"),
|
||||
positionSeconds = json.optDoubleOrNull("positionSeconds"),
|
||||
trackPath = json.optNullableString("trackPath"),
|
||||
isFavorite = json.optBoolean("isFavorite", false),
|
||||
)
|
||||
}.getOrDefault(emptyState())
|
||||
}
|
||||
@@ -136,6 +152,8 @@ object AstraCarNowPlayingStore {
|
||||
hasTrack = false,
|
||||
durationSeconds = null,
|
||||
positionSeconds = null,
|
||||
trackPath = null,
|
||||
isFavorite = false,
|
||||
)
|
||||
|
||||
private fun JSONObject.optNullableString(key: String): String? {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M16.5,3c-1.74,0 -3.41,0.81 -4.5,2.09C10.91,3.81 9.24,3 7.5,3 4.42,3 2,5.42 2,8.5c0,3.78 3.4,6.86 8.55,11.54L12,21.35l1.45,-1.32C18.6,15.36 22,12.28 22,8.5 22,5.42 19.58,3 16.5,3zM12.1,18.55l-0.1,0.1 -0.1,-0.1C7.14,14.24 4,11.39 4,8.5 4,6.5 5.5,5 7.5,5c1.54,0 3.04,0.99 3.57,2.36h1.87C13.46,5.99 14.96,5 16.5,5c2,0 3.5,1.5 3.5,3.5 0,2.89 -3.14,5.74 -7.9,10.05z" />
|
||||
</vector>
|
||||
@@ -16,6 +16,8 @@ export interface AstraCarNowPlayingState {
|
||||
hasTrack: boolean;
|
||||
duration?: number | null;
|
||||
position?: number | null;
|
||||
trackPath?: string | null;
|
||||
isFavorite?: boolean;
|
||||
}
|
||||
|
||||
declare class AstraCarModuleType extends NativeModule {
|
||||
|
||||
@@ -2,6 +2,7 @@ import TrackPlayer, { State, type Track as RntpTrack } from 'react-native-track-
|
||||
|
||||
import type { PlaybackState, Track } from '@/types/audio';
|
||||
import { AstraCar } from '../../modules/astra-car';
|
||||
import { usePlaylistStore } from '@/stores/playlistStore';
|
||||
|
||||
function mapRntpState(state?: State): PlaybackState {
|
||||
switch (state) {
|
||||
@@ -20,7 +21,7 @@ function mapRntpState(state?: State): PlaybackState {
|
||||
|
||||
type CarNowPlayingTrack = Pick<
|
||||
Track,
|
||||
'title' | 'artist' | 'album' | 'artworkData' | 'duration' | 'sourceType' | 'sourceId' | 'artworkSourceId'
|
||||
'path' | 'title' | 'artist' | 'album' | 'artworkData' | 'duration' | 'sourceType' | 'sourceId' | 'artworkSourceId'
|
||||
>;
|
||||
|
||||
/** True when the track should use its remote server cover (no local cache to serve). */
|
||||
@@ -51,6 +52,7 @@ export function setCarNowPlaying(
|
||||
// Android Auto loads art only from content:// URIs, so we pass structured identity
|
||||
// (local hash or remote source+id) and let the native module build the content URI.
|
||||
const remote = isRemoteArt(track);
|
||||
const trackPath = track?.path ?? null;
|
||||
AstraCar.setNowPlaying({
|
||||
title: track?.title ?? null,
|
||||
artist: track?.artist ?? null,
|
||||
@@ -62,6 +64,8 @@ export function setCarNowPlaying(
|
||||
hasTrack: Boolean(track),
|
||||
duration: duration ?? track?.duration ?? null,
|
||||
position: position ?? null,
|
||||
trackPath,
|
||||
isFavorite: trackPath ? usePlaylistStore.getState().favoritePaths.has(trackPath) : false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -74,6 +78,7 @@ export function setCarNowPlayingFromRntpTrack(
|
||||
setCarNowPlaying(
|
||||
track
|
||||
? {
|
||||
path: typeof track.astraPath === 'string' ? track.astraPath : String(track.url ?? track.id),
|
||||
title: track.title ?? 'Unknown title',
|
||||
artist: track.artist ?? 'Unknown artist',
|
||||
album: track.album ?? '',
|
||||
|
||||
@@ -15,9 +15,11 @@ import { buildArtistList, filterTracksByArtist } from '@/library/artistGrouping'
|
||||
import { dbTrackToTrack } from '@/library/trackAdapter';
|
||||
import { playForCar, playTracksForCar, pause, seekTo, skipToNext, skipToPrevious } from '@/audio/playbackController';
|
||||
import { syncCarNowPlayingFromTrackPlayer } from '@/audio/carSync';
|
||||
import TrackPlayer, { type Track as RntpTrack } from 'react-native-track-player';
|
||||
import { useAudioSettingsStore } from '@/stores/audioSettingsStore';
|
||||
import { useEQStore } from '@/stores/eqStore';
|
||||
import { useLibraryStore } from '@/stores/libraryStore';
|
||||
import { usePlaylistStore } from '@/stores/playlistStore';
|
||||
import { useRemoteSourcesStore } from '@/stores/remoteSourcesStore';
|
||||
import { useSettingsStore } from '@/stores/settingsStore';
|
||||
import type { DbTrack } from '@/types/library';
|
||||
@@ -53,6 +55,7 @@ async function initializeForCar(): Promise<void> {
|
||||
initPromise = (async () => {
|
||||
await useSettingsStore.getState().load();
|
||||
await useLibraryStore.getState().initialize();
|
||||
await usePlaylistStore.getState().refresh();
|
||||
await useRemoteSourcesStore.getState().init();
|
||||
await Promise.all([
|
||||
useEQStore.getState().load(),
|
||||
@@ -92,6 +95,9 @@ export async function handleAstraCarCommand(payload: CarCommandPayload): Promise
|
||||
case 'seek':
|
||||
if (typeof payload.position === 'number') await seekTo(payload.position);
|
||||
break;
|
||||
case 'toggleFavorite':
|
||||
await handleFavoriteCommand();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -102,6 +108,19 @@ export async function handleAstraCarCommand(payload: CarCommandPayload): Promise
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFavoriteCommand(): Promise<void> {
|
||||
const activeTrack = await TrackPlayer.getActiveTrack();
|
||||
const path = rntpTrackPath(activeTrack);
|
||||
if (!path) return;
|
||||
await usePlaylistStore.getState().toggleFavorite({ path });
|
||||
}
|
||||
|
||||
function rntpTrackPath(track: RntpTrack | null | undefined): string | null {
|
||||
if (!track) return null;
|
||||
if (typeof track.astraPath === 'string' && track.astraPath.length > 0) return track.astraPath;
|
||||
return typeof track.url === 'string' && track.url.length > 0 ? track.url : null;
|
||||
}
|
||||
|
||||
async function playMedia(media: CarMediaPayload): Promise<void> {
|
||||
const db = await openLibraryDb();
|
||||
const resolved = await resolveMediaTracks(db, media);
|
||||
|
||||
Reference in New Issue
Block a user