mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-12 05:10:52 +02:00
widgets
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'expo-module-gradle-plugin'
|
||||
}
|
||||
|
||||
group = 'expo.modules.astrawidget'
|
||||
version = '0.1.0'
|
||||
|
||||
android {
|
||||
namespace "expo.modules.astrawidget"
|
||||
defaultConfig {
|
||||
versionCode 1
|
||||
versionName "0.1.0"
|
||||
}
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<receiver
|
||||
android:name="expo.modules.astrawidget.AstraNowPlayingWidgetProvider"
|
||||
android:exported="true"
|
||||
android:label="@string/astra_widget_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/astra_now_playing_widget_info" />
|
||||
</receiver>
|
||||
</application>
|
||||
</manifest>
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package expo.modules.astrawidget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import android.view.KeyEvent
|
||||
|
||||
class AstraNowPlayingWidgetProvider : AppWidgetProvider() {
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray,
|
||||
) {
|
||||
AstraWidgetUpdater.updateWidgets(context, appWidgetManager, appWidgetIds)
|
||||
}
|
||||
|
||||
override fun onAppWidgetOptionsChanged(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetId: Int,
|
||||
newOptions: Bundle,
|
||||
) {
|
||||
AstraWidgetUpdater.updateWidget(context, appWidgetManager, appWidgetId, newOptions)
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
super.onReceive(context, intent)
|
||||
|
||||
when (intent.action) {
|
||||
ACTION_PREVIOUS -> dispatchMediaKey(context, KeyEvent.KEYCODE_MEDIA_PREVIOUS)
|
||||
ACTION_PLAY_PAUSE -> dispatchMediaKey(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)
|
||||
ACTION_NEXT -> dispatchMediaKey(context, KeyEvent.KEYCODE_MEDIA_NEXT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun dispatchMediaKey(context: Context, keyCode: Int) {
|
||||
val audioManager =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
context.getSystemService(AudioManager::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
|
||||
} ?: return
|
||||
|
||||
val eventTime = SystemClock.uptimeMillis()
|
||||
audioManager.dispatchMediaKeyEvent(KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, keyCode, 0))
|
||||
audioManager.dispatchMediaKeyEvent(KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, keyCode, 0))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ACTION_PREFIX = "com.astra.mobile.widget"
|
||||
const val ACTION_PREVIOUS = "$ACTION_PREFIX.PREVIOUS"
|
||||
const val ACTION_PLAY_PAUSE = "$ACTION_PREFIX.PLAY_PAUSE"
|
||||
const val ACTION_NEXT = "$ACTION_PREFIX.NEXT"
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package expo.modules.astrawidget
|
||||
|
||||
import android.content.Context
|
||||
import expo.modules.kotlin.exception.Exceptions
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
import expo.modules.kotlin.records.Field
|
||||
import expo.modules.kotlin.records.Record
|
||||
|
||||
class AstraWidgetNowPlayingState : Record {
|
||||
@Field
|
||||
val title: String? = null
|
||||
|
||||
@Field
|
||||
val artist: String? = null
|
||||
|
||||
@Field
|
||||
val artworkUri: String? = null
|
||||
|
||||
@Field
|
||||
val playbackState: String = "stopped"
|
||||
|
||||
@Field
|
||||
val hasTrack: Boolean = false
|
||||
|
||||
@Field
|
||||
val recentlyPlayed: List<AstraWidgetRecentItemState> = emptyList()
|
||||
|
||||
@Field
|
||||
val replaceRecentlyPlayed: Boolean = false
|
||||
}
|
||||
|
||||
class AstraWidgetRecentItemState : Record {
|
||||
@Field
|
||||
val title: String? = null
|
||||
|
||||
@Field
|
||||
val artist: String? = null
|
||||
|
||||
@Field
|
||||
val artworkUri: String? = null
|
||||
}
|
||||
|
||||
class AstraWidgetModule : Module() {
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("AstraWidget")
|
||||
|
||||
Function("setNowPlaying") { state: AstraWidgetNowPlayingState ->
|
||||
val context = requireContext()
|
||||
val previous = AstraWidgetStateStore.load(context)
|
||||
val recentlyPlayed =
|
||||
if (state.replaceRecentlyPlayed) {
|
||||
state.recentlyPlayed
|
||||
.map {
|
||||
AstraWidgetRecentItem(
|
||||
title = it.title,
|
||||
artist = it.artist,
|
||||
artworkUri = it.artworkUri,
|
||||
)
|
||||
}
|
||||
.take(8)
|
||||
} else {
|
||||
previous.recentlyPlayed
|
||||
}
|
||||
AstraWidgetStateStore.save(
|
||||
context,
|
||||
AstraWidgetState(
|
||||
title = state.title,
|
||||
artist = state.artist,
|
||||
artworkUri = if (state.hasTrack) state.artworkUri else null,
|
||||
playbackState = state.playbackState,
|
||||
hasTrack = state.hasTrack,
|
||||
recentlyPlayed = recentlyPlayed,
|
||||
),
|
||||
)
|
||||
AstraWidgetUpdater.updateAll(context)
|
||||
}
|
||||
}
|
||||
|
||||
private fun requireContext(): Context =
|
||||
appContext.reactContext ?: throw Exceptions.ReactContextLost()
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package expo.modules.astrawidget
|
||||
|
||||
import android.content.Context
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
data class AstraWidgetState(
|
||||
val title: String?,
|
||||
val artist: String?,
|
||||
val artworkUri: String?,
|
||||
val playbackState: String,
|
||||
val hasTrack: Boolean,
|
||||
val recentlyPlayed: List<AstraWidgetRecentItem>,
|
||||
)
|
||||
|
||||
data class AstraWidgetRecentItem(
|
||||
val title: String?,
|
||||
val artist: String?,
|
||||
val artworkUri: String?,
|
||||
)
|
||||
|
||||
object AstraWidgetStateStore {
|
||||
private const val PREFS_NAME = "astra_widget_now_playing"
|
||||
private const val KEY_TITLE = "title"
|
||||
private const val KEY_ARTIST = "artist"
|
||||
private const val KEY_ARTWORK_URI = "artwork_uri"
|
||||
private const val KEY_PLAYBACK_STATE = "playback_state"
|
||||
private const val KEY_HAS_TRACK = "has_track"
|
||||
private const val KEY_RECENTLY_PLAYED = "recently_played"
|
||||
|
||||
fun save(context: Context, state: AstraWidgetState) {
|
||||
context.applicationContext
|
||||
.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putString(KEY_TITLE, state.title)
|
||||
.putString(KEY_ARTIST, state.artist)
|
||||
.putString(KEY_ARTWORK_URI, state.artworkUri)
|
||||
.putString(KEY_PLAYBACK_STATE, state.playbackState)
|
||||
.putBoolean(KEY_HAS_TRACK, state.hasTrack)
|
||||
.putString(KEY_RECENTLY_PLAYED, encodeRecentlyPlayed(state.recentlyPlayed))
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun load(context: Context): AstraWidgetState {
|
||||
val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
return AstraWidgetState(
|
||||
title = prefs.getString(KEY_TITLE, null),
|
||||
artist = prefs.getString(KEY_ARTIST, null),
|
||||
artworkUri = prefs.getString(KEY_ARTWORK_URI, null),
|
||||
playbackState = prefs.getString(KEY_PLAYBACK_STATE, "stopped") ?: "stopped",
|
||||
hasTrack = prefs.getBoolean(KEY_HAS_TRACK, false),
|
||||
recentlyPlayed = decodeRecentlyPlayed(prefs.getString(KEY_RECENTLY_PLAYED, null)),
|
||||
)
|
||||
}
|
||||
|
||||
private fun encodeRecentlyPlayed(items: List<AstraWidgetRecentItem>): String {
|
||||
val array = JSONArray()
|
||||
items.take(8).forEach { item ->
|
||||
array.put(
|
||||
JSONObject()
|
||||
.put("title", item.title)
|
||||
.put("artist", item.artist)
|
||||
.put("artworkUri", item.artworkUri),
|
||||
)
|
||||
}
|
||||
return array.toString()
|
||||
}
|
||||
|
||||
private fun decodeRecentlyPlayed(value: String?): List<AstraWidgetRecentItem> {
|
||||
if (value.isNullOrBlank()) return emptyList()
|
||||
|
||||
return runCatching {
|
||||
val array = JSONArray(value)
|
||||
buildList {
|
||||
for (index in 0 until minOf(array.length(), 8)) {
|
||||
val item = array.optJSONObject(index) ?: continue
|
||||
add(
|
||||
AstraWidgetRecentItem(
|
||||
title = item.optNullableString("title"),
|
||||
artist = item.optNullableString("artist"),
|
||||
artworkUri = item.optNullableString("artworkUri"),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}.getOrDefault(emptyList())
|
||||
}
|
||||
|
||||
private fun JSONObject.optNullableString(key: String): String? {
|
||||
if (!has(key) || isNull(key)) return null
|
||||
return optString(key).trim().takeIf { it.isNotEmpty() }
|
||||
}
|
||||
}
|
||||
+401
@@ -0,0 +1,401 @@
|
||||
package expo.modules.astrawidget
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.graphics.BitmapShader
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.graphics.Shader
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Base64
|
||||
import android.util.SizeF
|
||||
import android.view.View
|
||||
import android.widget.RemoteViews
|
||||
import java.io.File
|
||||
|
||||
object AstraWidgetUpdater {
|
||||
private const val REQUEST_OPEN_APP = 100
|
||||
private const val REQUEST_OPEN_RECENTS = 101
|
||||
|
||||
private const val ONE_ROW_MAX_HEIGHT_DP = 110
|
||||
private const val THREE_ROW_MIN_HEIGHT_DP = 300
|
||||
private const val FOUR_CELL_MIN_WIDTH_DP = 300
|
||||
private const val FIVE_CELL_MIN_WIDTH_DP = 370
|
||||
private const val ARTWORK_CORNER_RADIUS_DP = 8f
|
||||
|
||||
private val RECENT_IMAGE_IDS = intArrayOf(
|
||||
R.id.astra_widget_recent_1_image,
|
||||
R.id.astra_widget_recent_2_image,
|
||||
R.id.astra_widget_recent_3_image,
|
||||
R.id.astra_widget_recent_4_image,
|
||||
R.id.astra_widget_recent_5_image,
|
||||
R.id.astra_widget_recent_6_image,
|
||||
R.id.astra_widget_recent_7_image,
|
||||
R.id.astra_widget_recent_8_image,
|
||||
)
|
||||
|
||||
private val RECENT_LABEL_IDS = intArrayOf(
|
||||
R.id.astra_widget_recent_1_label,
|
||||
R.id.astra_widget_recent_2_label,
|
||||
R.id.astra_widget_recent_3_label,
|
||||
R.id.astra_widget_recent_4_label,
|
||||
R.id.astra_widget_recent_5_label,
|
||||
R.id.astra_widget_recent_6_label,
|
||||
R.id.astra_widget_recent_7_label,
|
||||
R.id.astra_widget_recent_8_label,
|
||||
)
|
||||
|
||||
fun updateAll(context: Context) {
|
||||
val appContext = context.applicationContext
|
||||
val manager = AppWidgetManager.getInstance(appContext)
|
||||
val component = ComponentName(appContext, AstraNowPlayingWidgetProvider::class.java)
|
||||
val appWidgetIds = manager.getAppWidgetIds(component)
|
||||
updateWidgets(appContext, manager, appWidgetIds)
|
||||
}
|
||||
|
||||
fun updateWidgets(context: Context, manager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||
if (appWidgetIds.isEmpty()) return
|
||||
|
||||
val state = AstraWidgetStateStore.load(context)
|
||||
appWidgetIds.forEach { appWidgetId ->
|
||||
val options = manager.getAppWidgetOptions(appWidgetId)
|
||||
manager.updateAppWidget(appWidgetId, buildRemoteViews(context, state, options))
|
||||
}
|
||||
}
|
||||
|
||||
fun updateWidget(context: Context, manager: AppWidgetManager, appWidgetId: Int, options: Bundle) {
|
||||
val state = AstraWidgetStateStore.load(context)
|
||||
manager.updateAppWidget(appWidgetId, buildRemoteViews(context, state, options))
|
||||
}
|
||||
|
||||
private fun buildRemoteViews(context: Context, state: AstraWidgetState, options: Bundle): RemoteViews {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
return buildResponsiveRemoteViews(context, state)
|
||||
}
|
||||
|
||||
return buildBucketRemoteViews(context, state, WidgetLayoutBucket.fromOptions(options))
|
||||
}
|
||||
|
||||
private fun buildResponsiveRemoteViews(
|
||||
context: Context,
|
||||
state: AstraWidgetState,
|
||||
): RemoteViews {
|
||||
val mapping =
|
||||
WidgetLayoutBucket.entries.associate { bucket ->
|
||||
bucket.minSize to buildBucketRemoteViews(context, state, bucket)
|
||||
}
|
||||
|
||||
return RemoteViews(mapping)
|
||||
}
|
||||
|
||||
private fun buildBucketRemoteViews(
|
||||
context: Context,
|
||||
state: AstraWidgetState,
|
||||
bucket: WidgetLayoutBucket,
|
||||
): RemoteViews {
|
||||
val views = RemoteViews(context.packageName, bucket.layoutRes)
|
||||
val title = if (state.hasTrack) state.title.cleanOrFallback("Unknown title") else "Astra"
|
||||
val artist = if (state.hasTrack) state.artist.cleanOrFallback("Unknown artist") else "Tap to open"
|
||||
val isPlaying = state.hasTrack && state.playbackState == "playing"
|
||||
|
||||
views.setTextViewText(R.id.astra_widget_title, title)
|
||||
views.setTextViewText(R.id.astra_widget_artist, artist)
|
||||
setImageView(context, views, R.id.astra_widget_art, state.artworkUri, 320)
|
||||
views.setOnClickPendingIntent(R.id.astra_widget_root, openAppPendingIntent(context))
|
||||
|
||||
if (bucket.hasPlayPause) {
|
||||
bindPlayPause(views, context, isPlaying, state.hasTrack)
|
||||
}
|
||||
if (bucket.hasNext) {
|
||||
bindControl(views, context, R.id.astra_widget_next, AstraNowPlayingWidgetProvider.ACTION_NEXT, 3, state.hasTrack)
|
||||
}
|
||||
if (bucket.hasPrevious) {
|
||||
bindControl(
|
||||
views,
|
||||
context,
|
||||
R.id.astra_widget_previous,
|
||||
AstraNowPlayingWidgetProvider.ACTION_PREVIOUS,
|
||||
1,
|
||||
state.hasTrack,
|
||||
)
|
||||
}
|
||||
if (bucket.recentCount > 0) {
|
||||
bindRecentlyPlayed(context, views, state.recentlyPlayed, bucket)
|
||||
}
|
||||
|
||||
return views
|
||||
}
|
||||
|
||||
private fun bindPlayPause(
|
||||
views: RemoteViews,
|
||||
context: Context,
|
||||
isPlaying: Boolean,
|
||||
controlsEnabled: Boolean,
|
||||
) {
|
||||
views.setImageViewResource(
|
||||
R.id.astra_widget_play_pause,
|
||||
if (isPlaying) R.drawable.astra_widget_ic_pause else R.drawable.astra_widget_ic_play,
|
||||
)
|
||||
views.setContentDescription(R.id.astra_widget_play_pause, if (isPlaying) "Pause" else "Play")
|
||||
bindControl(
|
||||
views,
|
||||
context,
|
||||
R.id.astra_widget_play_pause,
|
||||
AstraNowPlayingWidgetProvider.ACTION_PLAY_PAUSE,
|
||||
2,
|
||||
controlsEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
private fun bindControl(
|
||||
views: RemoteViews,
|
||||
context: Context,
|
||||
viewId: Int,
|
||||
action: String,
|
||||
requestCode: Int,
|
||||
controlsEnabled: Boolean,
|
||||
) {
|
||||
views.setBoolean(viewId, "setEnabled", controlsEnabled)
|
||||
views.setOnClickPendingIntent(viewId, controlPendingIntent(context, action, requestCode))
|
||||
}
|
||||
|
||||
private fun bindRecentlyPlayed(
|
||||
context: Context,
|
||||
views: RemoteViews,
|
||||
recentlyPlayed: List<AstraWidgetRecentItem>,
|
||||
bucket: WidgetLayoutBucket,
|
||||
) {
|
||||
val openRecents = openRecentlyPlayedPendingIntent(context)
|
||||
views.setOnClickPendingIntent(R.id.astra_widget_recent_container, openRecents)
|
||||
|
||||
for (index in 0 until bucket.recentCount) {
|
||||
val item = recentlyPlayed.getOrNull(index)
|
||||
val imageId = RECENT_IMAGE_IDS[index]
|
||||
setImageView(context, views, imageId, item?.artworkUri, 128)
|
||||
views.setContentDescription(imageId, item?.title.cleanOrFallback("Recently played"))
|
||||
views.setOnClickPendingIntent(imageId, openRecents)
|
||||
|
||||
if (bucket.showRecentLabels) {
|
||||
val labelId = RECENT_LABEL_IDS[index]
|
||||
views.setTextViewText(labelId, item?.title.cleanOrFallback("Recently played"))
|
||||
views.setViewVisibility(labelId, View.VISIBLE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setImageView(
|
||||
context: Context,
|
||||
views: RemoteViews,
|
||||
viewId: Int,
|
||||
uri: String?,
|
||||
maxPx: Int,
|
||||
) {
|
||||
val bitmap = decodeBitmap(context, uri, maxPx)?.let { prepareArtworkBitmap(context, it) }
|
||||
if (bitmap != null) {
|
||||
views.setImageViewBitmap(viewId, bitmap)
|
||||
} else {
|
||||
views.setImageViewResource(viewId, R.drawable.astra_widget_art_placeholder)
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareArtworkBitmap(context: Context, bitmap: Bitmap): Bitmap {
|
||||
val square = cropCenterSquare(bitmap)
|
||||
val radiusPx = ARTWORK_CORNER_RADIUS_DP * context.resources.displayMetrics.density
|
||||
return roundBitmap(square, radiusPx)
|
||||
}
|
||||
|
||||
private fun cropCenterSquare(bitmap: Bitmap): Bitmap {
|
||||
val size = minOf(bitmap.width, bitmap.height)
|
||||
if (size <= 0) return bitmap
|
||||
|
||||
val left = (bitmap.width - size) / 2
|
||||
val top = (bitmap.height - size) / 2
|
||||
return Bitmap.createBitmap(bitmap, left, top, size, size)
|
||||
}
|
||||
|
||||
private fun roundBitmap(bitmap: Bitmap, radiusPx: Float): Bitmap {
|
||||
val output = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(output)
|
||||
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
|
||||
}
|
||||
val rect = RectF(0f, 0f, bitmap.width.toFloat(), bitmap.height.toFloat())
|
||||
canvas.drawRoundRect(rect, radiusPx, radiusPx, paint)
|
||||
return output
|
||||
}
|
||||
|
||||
private fun openAppPendingIntent(context: Context): PendingIntent? {
|
||||
val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
action = Intent.ACTION_VIEW
|
||||
data = Uri.parse("trackplayer://notification.click")
|
||||
}
|
||||
|
||||
return intent?.let {
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
REQUEST_OPEN_APP,
|
||||
it,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openRecentlyPlayedPendingIntent(context: Context): PendingIntent {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("astra:///recently-played")).apply {
|
||||
setPackage(context.packageName)
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
}
|
||||
|
||||
return PendingIntent.getActivity(
|
||||
context,
|
||||
REQUEST_OPEN_RECENTS,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
}
|
||||
|
||||
private fun controlPendingIntent(context: Context, action: String, requestCode: Int): PendingIntent {
|
||||
val intent = Intent(context, AstraNowPlayingWidgetProvider::class.java).setAction(action)
|
||||
return PendingIntent.getBroadcast(
|
||||
context,
|
||||
requestCode,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
}
|
||||
|
||||
private fun decodeBitmap(context: Context, uri: String?, maxPx: Int): Bitmap? {
|
||||
if (uri.isNullOrBlank()) return null
|
||||
val bytes = runCatching { readImageBytes(context, uri) }.getOrNull() ?: return null
|
||||
if (bytes.isEmpty()) return null
|
||||
|
||||
return runCatching {
|
||||
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
BitmapFactory.decodeByteArray(bytes, 0, bytes.size, bounds)
|
||||
val largest = maxOf(bounds.outWidth, bounds.outHeight)
|
||||
val sampleSize = if (largest <= maxPx || maxPx <= 0) {
|
||||
1
|
||||
} else {
|
||||
Integer.highestOneBit(largest / maxPx).coerceAtLeast(1)
|
||||
}
|
||||
val options = BitmapFactory.Options().apply { inSampleSize = sampleSize }
|
||||
BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun readImageBytes(context: Context, value: String): ByteArray? {
|
||||
if (value.startsWith("data:image", ignoreCase = true)) {
|
||||
val encoded = value.substringAfter(',', missingDelimiterValue = "")
|
||||
if (encoded.isBlank()) return null
|
||||
return Base64.decode(encoded, Base64.DEFAULT)
|
||||
}
|
||||
|
||||
val uri = Uri.parse(value)
|
||||
return when (uri.scheme) {
|
||||
"file" -> uri.path?.let { File(it).readBytes() }
|
||||
else -> context.contentResolver.openInputStream(uri)?.use { it.readBytes() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun String?.cleanOrFallback(fallback: String): String {
|
||||
val value = this?.trim()
|
||||
return if (value.isNullOrEmpty()) fallback else value
|
||||
}
|
||||
|
||||
private enum class WidgetLayoutBucket(
|
||||
val layoutRes: Int,
|
||||
val hasPrevious: Boolean,
|
||||
val hasPlayPause: Boolean,
|
||||
val hasNext: Boolean,
|
||||
val recentCount: Int,
|
||||
val showRecentLabels: Boolean,
|
||||
val minSize: SizeF,
|
||||
) {
|
||||
Compact3x1(
|
||||
R.layout.astra_widget_3x1,
|
||||
hasPrevious = false,
|
||||
hasPlayPause = false,
|
||||
hasNext = false,
|
||||
recentCount = 0,
|
||||
showRecentLabels = false,
|
||||
minSize = SizeF(180f, 40f),
|
||||
),
|
||||
Compact4x1(
|
||||
R.layout.astra_widget_4x1,
|
||||
hasPrevious = false,
|
||||
hasPlayPause = true,
|
||||
hasNext = true,
|
||||
recentCount = 0,
|
||||
showRecentLabels = false,
|
||||
minSize = SizeF(FOUR_CELL_MIN_WIDTH_DP.toFloat(), 40f),
|
||||
),
|
||||
Wide5x1(
|
||||
R.layout.astra_widget_5x1,
|
||||
hasPrevious = true,
|
||||
hasPlayPause = true,
|
||||
hasNext = true,
|
||||
recentCount = 0,
|
||||
showRecentLabels = false,
|
||||
minSize = SizeF(FIVE_CELL_MIN_WIDTH_DP.toFloat(), 40f),
|
||||
),
|
||||
Tall3x3(
|
||||
R.layout.astra_widget_3x3,
|
||||
hasPrevious = false,
|
||||
hasPlayPause = true,
|
||||
hasNext = true,
|
||||
recentCount = 0,
|
||||
showRecentLabels = false,
|
||||
minSize = SizeF(180f, ONE_ROW_MAX_HEIGHT_DP.toFloat()),
|
||||
),
|
||||
Expanded4x2(
|
||||
R.layout.astra_widget_4x2,
|
||||
hasPrevious = true,
|
||||
hasPlayPause = true,
|
||||
hasNext = true,
|
||||
recentCount = 5,
|
||||
showRecentLabels = false,
|
||||
minSize = SizeF(FOUR_CELL_MIN_WIDTH_DP.toFloat(), ONE_ROW_MAX_HEIGHT_DP.toFloat()),
|
||||
),
|
||||
Expanded4x3(
|
||||
R.layout.astra_widget_4x3,
|
||||
hasPrevious = true,
|
||||
hasPlayPause = true,
|
||||
hasNext = true,
|
||||
recentCount = 8,
|
||||
showRecentLabels = true,
|
||||
minSize = SizeF(FOUR_CELL_MIN_WIDTH_DP.toFloat(), THREE_ROW_MIN_HEIGHT_DP.toFloat()),
|
||||
);
|
||||
|
||||
companion object {
|
||||
fun fromOptions(options: Bundle): WidgetLayoutBucket {
|
||||
val minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, FOUR_CELL_MIN_WIDTH_DP)
|
||||
val minHeight = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, ONE_ROW_MAX_HEIGHT_DP)
|
||||
|
||||
return fromSize(minWidth.toFloat(), minHeight.toFloat())
|
||||
}
|
||||
|
||||
fun fromSize(widthDp: Float, heightDp: Float): WidgetLayoutBucket {
|
||||
if (heightDp < ONE_ROW_MAX_HEIGHT_DP) {
|
||||
return when {
|
||||
widthDp >= FIVE_CELL_MIN_WIDTH_DP -> Wide5x1
|
||||
widthDp >= FOUR_CELL_MIN_WIDTH_DP -> Compact4x1
|
||||
else -> Compact3x1
|
||||
}
|
||||
}
|
||||
|
||||
if (widthDp < FOUR_CELL_MIN_WIDTH_DP) return Tall3x3
|
||||
if (heightDp < THREE_ROW_MIN_HEIGHT_DP) return Expanded4x2
|
||||
return Expanded4x3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#151b2b" />
|
||||
<stroke android:width="1dp" android:color="#2c3958" />
|
||||
<corners android:radius="10dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,5 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#101522" />
|
||||
<stroke android:width="1dp" android:color="#25304a" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,5 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#1a2133" />
|
||||
<stroke android:width="1dp" android:color="#2c3958" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
||||
@@ -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="#e2e8f4"
|
||||
android:pathData="M6,5 L15,12 L6,19 Z M17,5 L19,5 L19,19 L17,19 Z" />
|
||||
</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="#e2e8f4"
|
||||
android:pathData="M7,5 L10,5 L10,19 L7,19 Z M14,5 L17,5 L17,19 L14,19 Z" />
|
||||
</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="#e2e8f4"
|
||||
android:pathData="M8,5 L19,12 L8,19 Z" />
|
||||
</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="#e2e8f4"
|
||||
android:pathData="M5,5 L7,5 L7,19 L5,19 Z M18,5 L9,12 L18,19 Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,51 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/astra_widget_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/astra_widget_background"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingEnd="10dp"
|
||||
android:paddingBottom="7dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/astra_widget_art"
|
||||
android:layout_width="52dp"
|
||||
android:layout_height="52dp"
|
||||
android:contentDescription="Current artwork"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/astra_widget_art_placeholder" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/astra_widget_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Astra"
|
||||
android:textColor="#e2e8f4"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/astra_widget_artist"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Tap to open"
|
||||
android:textColor="#8a98b8"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,71 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/astra_widget_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/astra_widget_background"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/astra_widget_art"
|
||||
android:layout_width="76dp"
|
||||
android:layout_height="76dp"
|
||||
android:contentDescription="Current artwork"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/astra_widget_art_placeholder" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/astra_widget_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center"
|
||||
android:maxLines="1"
|
||||
android:text="Astra"
|
||||
android:textColor="#e2e8f4"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/astra_widget_artist"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="1dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center"
|
||||
android:maxLines="1"
|
||||
android:text="Tap to open"
|
||||
android:textColor="#8a98b8"
|
||||
android:textSize="11sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="34dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/astra_widget_play_pause"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="34dp"
|
||||
android:background="@drawable/astra_widget_control_background"
|
||||
android:contentDescription="Play"
|
||||
android:padding="7dp"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/astra_widget_ic_play" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/astra_widget_next"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="34dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="@drawable/astra_widget_control_background"
|
||||
android:contentDescription="Next"
|
||||
android:padding="7dp"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/astra_widget_ic_next" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,73 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/astra_widget_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/astra_widget_background"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="7dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/astra_widget_art"
|
||||
android:layout_width="52dp"
|
||||
android:layout_height="52dp"
|
||||
android:contentDescription="Current artwork"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/astra_widget_art_placeholder" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/astra_widget_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Astra"
|
||||
android:textColor="#e2e8f4"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/astra_widget_artist"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Tap to open"
|
||||
android:textColor="#8a98b8"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/astra_widget_play_pause"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="@drawable/astra_widget_control_background"
|
||||
android:contentDescription="Play"
|
||||
android:padding="9dp"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/astra_widget_ic_play" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/astra_widget_next"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="@drawable/astra_widget_control_background"
|
||||
android:contentDescription="Next"
|
||||
android:padding="9dp"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/astra_widget_ic_next" />
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,149 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/astra_widget_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/astra_widget_background"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="10dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="82dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/astra_widget_art"
|
||||
android:layout_width="82dp"
|
||||
android:layout_height="82dp"
|
||||
android:contentDescription="Current artwork"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/astra_widget_art_placeholder" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/astra_widget_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Astra"
|
||||
android:textColor="#e2e8f4"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/astra_widget_artist"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Tap to open"
|
||||
android:textColor="#8a98b8"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/astra_widget_previous"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:background="@drawable/astra_widget_control_background"
|
||||
android:contentDescription="Previous"
|
||||
android:padding="8dp"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/astra_widget_ic_previous" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/astra_widget_play_pause"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="@drawable/astra_widget_control_background"
|
||||
android:contentDescription="Play"
|
||||
android:padding="8dp"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/astra_widget_ic_play" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/astra_widget_next"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="@drawable/astra_widget_control_background"
|
||||
android:contentDescription="Next"
|
||||
android:padding="8dp"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/astra_widget_ic_next" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/astra_widget_recent_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/astra_widget_recent_1_image"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="Recently played"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/astra_widget_art_placeholder" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/astra_widget_recent_2_image"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="Recently played"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/astra_widget_art_placeholder" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/astra_widget_recent_3_image"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="Recently played"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/astra_widget_art_placeholder" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/astra_widget_recent_4_image"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="Recently played"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/astra_widget_art_placeholder" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/astra_widget_recent_5_image"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="Recently played"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/astra_widget_art_placeholder" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,324 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/astra_widget_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/astra_widget_background"
|
||||
android:orientation="vertical"
|
||||
android:padding="12dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="90dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/astra_widget_art"
|
||||
android:layout_width="90dp"
|
||||
android:layout_height="90dp"
|
||||
android:contentDescription="Current artwork"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/astra_widget_art_placeholder" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/astra_widget_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Astra"
|
||||
android:textColor="#e2e8f4"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/astra_widget_artist"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="3dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Tap to open"
|
||||
android:textColor="#8a98b8"
|
||||
android:textSize="13sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/astra_widget_previous"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="28dp"
|
||||
android:background="@drawable/astra_widget_control_background"
|
||||
android:contentDescription="Previous"
|
||||
android:padding="6dp"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/astra_widget_ic_previous" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/astra_widget_play_pause"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="28dp"
|
||||
android:layout_marginTop="3dp"
|
||||
android:background="@drawable/astra_widget_control_background"
|
||||
android:contentDescription="Play"
|
||||
android:padding="6dp"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/astra_widget_ic_play" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/astra_widget_next"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="28dp"
|
||||
android:layout_marginTop="3dp"
|
||||
android:background="@drawable/astra_widget_control_background"
|
||||
android:contentDescription="Next"
|
||||
android:padding="6dp"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/astra_widget_ic_next" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/astra_widget_recent_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
<ImageView
|
||||
android:id="@+id/astra_widget_recent_1_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="Recently played"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/astra_widget_art_placeholder" />
|
||||
<TextView
|
||||
android:id="@+id/astra_widget_recent_1_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="3dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Recent"
|
||||
android:textColor="#8a98b8"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
<ImageView
|
||||
android:id="@+id/astra_widget_recent_2_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="Recently played"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/astra_widget_art_placeholder" />
|
||||
<TextView
|
||||
android:id="@+id/astra_widget_recent_2_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="3dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Recent"
|
||||
android:textColor="#8a98b8"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
<ImageView
|
||||
android:id="@+id/astra_widget_recent_3_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="Recently played"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/astra_widget_art_placeholder" />
|
||||
<TextView
|
||||
android:id="@+id/astra_widget_recent_3_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="3dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Recent"
|
||||
android:textColor="#8a98b8"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
<ImageView
|
||||
android:id="@+id/astra_widget_recent_4_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="Recently played"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/astra_widget_art_placeholder" />
|
||||
<TextView
|
||||
android:id="@+id/astra_widget_recent_4_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="3dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Recent"
|
||||
android:textColor="#8a98b8"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
<ImageView
|
||||
android:id="@+id/astra_widget_recent_5_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="Recently played"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/astra_widget_art_placeholder" />
|
||||
<TextView
|
||||
android:id="@+id/astra_widget_recent_5_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="3dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Recent"
|
||||
android:textColor="#8a98b8"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
<ImageView
|
||||
android:id="@+id/astra_widget_recent_6_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="Recently played"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/astra_widget_art_placeholder" />
|
||||
<TextView
|
||||
android:id="@+id/astra_widget_recent_6_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="3dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Recent"
|
||||
android:textColor="#8a98b8"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
<ImageView
|
||||
android:id="@+id/astra_widget_recent_7_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="Recently played"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/astra_widget_art_placeholder" />
|
||||
<TextView
|
||||
android:id="@+id/astra_widget_recent_7_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="3dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Recent"
|
||||
android:textColor="#8a98b8"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
<ImageView
|
||||
android:id="@+id/astra_widget_recent_8_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="Recently played"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/astra_widget_art_placeholder" />
|
||||
<TextView
|
||||
android:id="@+id/astra_widget_recent_8_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="3dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Recent"
|
||||
android:textColor="#8a98b8"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,84 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/astra_widget_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/astra_widget_background"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="7dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/astra_widget_art"
|
||||
android:layout_width="52dp"
|
||||
android:layout_height="52dp"
|
||||
android:contentDescription="Current artwork"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/astra_widget_art_placeholder" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/astra_widget_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Astra"
|
||||
android:textColor="#e2e8f4"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/astra_widget_artist"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Tap to open"
|
||||
android:textColor="#8a98b8"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/astra_widget_previous"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="@drawable/astra_widget_control_background"
|
||||
android:contentDescription="Previous"
|
||||
android:padding="9dp"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/astra_widget_ic_previous" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/astra_widget_play_pause"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="@drawable/astra_widget_control_background"
|
||||
android:contentDescription="Play"
|
||||
android:padding="9dp"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/astra_widget_ic_play" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/astra_widget_next"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="@drawable/astra_widget_control_background"
|
||||
android:contentDescription="Next"
|
||||
android:padding="9dp"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/astra_widget_ic_next" />
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,4 @@
|
||||
<resources>
|
||||
<string name="astra_widget_name">Astra Now Playing</string>
|
||||
<string name="astra_widget_description">Astra playback controls</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,12 @@
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:description="@string/astra_widget_description"
|
||||
android:initialLayout="@layout/astra_widget_3x1"
|
||||
android:minWidth="180dp"
|
||||
android:minHeight="40dp"
|
||||
android:minResizeWidth="180dp"
|
||||
android:minResizeHeight="40dp"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:targetCellWidth="3"
|
||||
android:targetCellHeight="1"
|
||||
android:updatePeriodMillis="0"
|
||||
android:widgetCategory="home_screen" />
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"platforms": ["android"],
|
||||
"android": {
|
||||
"modules": ["expo.modules.astrawidget.AstraWidgetModule"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { requireOptionalNativeModule, type NativeModule } from 'expo-modules-core';
|
||||
|
||||
import type { PlaybackState } from '@/types/audio';
|
||||
|
||||
export interface AstraWidgetNowPlayingState {
|
||||
title?: string | null;
|
||||
artist?: string | null;
|
||||
artworkUri?: string | null;
|
||||
playbackState: PlaybackState;
|
||||
hasTrack: boolean;
|
||||
recentlyPlayed?: AstraWidgetRecentItem[];
|
||||
replaceRecentlyPlayed?: boolean;
|
||||
}
|
||||
|
||||
export interface AstraWidgetRecentItem {
|
||||
title?: string | null;
|
||||
artist?: string | null;
|
||||
artworkUri?: string | null;
|
||||
}
|
||||
|
||||
declare class AstraWidgetModuleType extends NativeModule {
|
||||
setNowPlaying(state: AstraWidgetNowPlayingState): void;
|
||||
}
|
||||
|
||||
const native = requireOptionalNativeModule<AstraWidgetModuleType>('AstraWidget');
|
||||
|
||||
export const AstraWidget = (native ?? {
|
||||
setNowPlaying: () => {},
|
||||
}) as AstraWidgetModuleType;
|
||||
@@ -1,4 +1,5 @@
|
||||
import TrackPlayer, { Event } from 'react-native-track-player';
|
||||
import { syncWidgetNowPlayingFromTrackPlayer } from './widgetSync';
|
||||
|
||||
/**
|
||||
* RNTP playback service — registered in `index.js`. Runs in a headless context
|
||||
@@ -6,15 +7,31 @@ import TrackPlayer, { Event } from 'react-native-track-player';
|
||||
* controls to the player. Must not depend on React or the JS UI tree.
|
||||
*/
|
||||
export async function PlaybackService(): Promise<void> {
|
||||
TrackPlayer.addEventListener(Event.RemotePlay, () => TrackPlayer.play());
|
||||
TrackPlayer.addEventListener(Event.RemotePause, () => TrackPlayer.pause());
|
||||
TrackPlayer.addEventListener(Event.RemoteStop, () => TrackPlayer.stop());
|
||||
TrackPlayer.addEventListener(Event.RemoteNext, () =>
|
||||
TrackPlayer.skipToNext().catch(() => {}),
|
||||
);
|
||||
TrackPlayer.addEventListener(Event.RemotePrevious, () =>
|
||||
TrackPlayer.skipToPrevious().catch(() => {}),
|
||||
);
|
||||
TrackPlayer.addEventListener(Event.PlaybackActiveTrackChanged, () => {
|
||||
void syncWidgetNowPlayingFromTrackPlayer();
|
||||
});
|
||||
TrackPlayer.addEventListener(Event.PlaybackState, () => {
|
||||
void syncWidgetNowPlayingFromTrackPlayer();
|
||||
});
|
||||
TrackPlayer.addEventListener(Event.RemotePlay, () => {
|
||||
void TrackPlayer.play().finally(() => syncWidgetNowPlayingFromTrackPlayer());
|
||||
});
|
||||
TrackPlayer.addEventListener(Event.RemotePause, () => {
|
||||
void TrackPlayer.pause().finally(() => syncWidgetNowPlayingFromTrackPlayer());
|
||||
});
|
||||
TrackPlayer.addEventListener(Event.RemoteStop, () => {
|
||||
void TrackPlayer.stop().finally(() => syncWidgetNowPlayingFromTrackPlayer());
|
||||
});
|
||||
TrackPlayer.addEventListener(Event.RemoteNext, () => {
|
||||
void TrackPlayer.skipToNext()
|
||||
.catch(() => {})
|
||||
.finally(() => syncWidgetNowPlayingFromTrackPlayer());
|
||||
});
|
||||
TrackPlayer.addEventListener(Event.RemotePrevious, () => {
|
||||
void TrackPlayer.skipToPrevious()
|
||||
.catch(() => {})
|
||||
.finally(() => syncWidgetNowPlayingFromTrackPlayer());
|
||||
});
|
||||
TrackPlayer.addEventListener(Event.RemoteSeek, ({ position }) =>
|
||||
TrackPlayer.seekTo(position),
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { usePlayerStore } from '@/stores/playerStore';
|
||||
import { useLibraryStore } from '@/stores/libraryStore';
|
||||
import type { PlaybackState } from '@/types/audio';
|
||||
import { rntpToTrack } from './sampleTracks';
|
||||
import { buildWidgetRecentItems, setWidgetNowPlaying } from './widgetSync';
|
||||
|
||||
const RECENT_PLAY_THRESHOLD_MS = 15_000;
|
||||
|
||||
@@ -54,6 +55,7 @@ export function usePlaybackSync(): void {
|
||||
const setProgress = usePlayerStore((s) => s.setProgress);
|
||||
const setPlaybackState = usePlayerStore((s) => s.setPlaybackState);
|
||||
const recordTrackPlayed = useLibraryStore((s) => s.recordTrackPlayed);
|
||||
const recentlyPlayedTracks = useLibraryStore((s) => s.recentlyPlayedTracks);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentTrack(activeTrack ? rntpToTrack(activeTrack) : null);
|
||||
@@ -67,6 +69,15 @@ export function usePlaybackSync(): void {
|
||||
setPlaybackState(mappedPlaybackState);
|
||||
}, [mappedPlaybackState, setPlaybackState]);
|
||||
|
||||
useEffect(() => {
|
||||
const track = activeTrack ? rntpToTrack(activeTrack) : null;
|
||||
setWidgetNowPlaying(
|
||||
track,
|
||||
mappedPlaybackState,
|
||||
buildWidgetRecentItems(recentlyPlayedTracks, track?.path),
|
||||
);
|
||||
}, [activeTrack, mappedPlaybackState, recentlyPlayedTracks]);
|
||||
|
||||
useEffect(() => {
|
||||
const path = activeTrack?.url ? String(activeTrack.url) : null;
|
||||
const now = Date.now();
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import TrackPlayer, { State, type Track as RntpTrack } from 'react-native-track-player';
|
||||
|
||||
import type { PlaybackState, Track } from '@/types/audio';
|
||||
import type { DbTrack } from '@/types/library';
|
||||
import { artworkThumbUri } from '@/library/artwork';
|
||||
import { AstraWidget, type AstraWidgetRecentItem } from '../../modules/astra-widget';
|
||||
|
||||
function mapRntpState(state?: State): PlaybackState {
|
||||
switch (state) {
|
||||
case State.Playing:
|
||||
return 'playing';
|
||||
case State.Buffering:
|
||||
case State.Loading:
|
||||
return 'loading';
|
||||
case State.Paused:
|
||||
case State.Ready:
|
||||
return 'paused';
|
||||
default:
|
||||
return 'stopped';
|
||||
}
|
||||
}
|
||||
|
||||
export function setWidgetNowPlaying(
|
||||
track: Pick<Track, 'title' | 'artist' | 'artworkData'> | null,
|
||||
playbackState: PlaybackState,
|
||||
recentlyPlayed?: AstraWidgetRecentItem[],
|
||||
): void {
|
||||
AstraWidget.setNowPlaying({
|
||||
title: track?.title ?? null,
|
||||
artist: track?.artist ?? null,
|
||||
artworkUri: track?.artworkData ?? null,
|
||||
playbackState,
|
||||
hasTrack: Boolean(track),
|
||||
...(recentlyPlayed === undefined
|
||||
? {}
|
||||
: {
|
||||
recentlyPlayed,
|
||||
replaceRecentlyPlayed: true,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export function setWidgetNowPlayingFromRntpTrack(
|
||||
track: RntpTrack | null | undefined,
|
||||
playbackState: PlaybackState,
|
||||
): void {
|
||||
setWidgetNowPlaying(
|
||||
track
|
||||
? {
|
||||
title: track.title ?? 'Unknown title',
|
||||
artist: track.artist ?? 'Unknown artist',
|
||||
artworkData: typeof track.artwork === 'string' ? track.artwork : undefined,
|
||||
}
|
||||
: null,
|
||||
playbackState,
|
||||
);
|
||||
}
|
||||
|
||||
export function buildWidgetRecentItems(
|
||||
tracks: readonly DbTrack[],
|
||||
currentPath?: string,
|
||||
): AstraWidgetRecentItem[] {
|
||||
return tracks
|
||||
.filter((track) => track.path !== currentPath)
|
||||
.slice(0, 8)
|
||||
.map((track) => ({
|
||||
title: track.title,
|
||||
artist: track.artist,
|
||||
artworkUri: track.artwork_hash ? artworkThumbUri(track.artwork_hash) : null,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function syncWidgetNowPlayingFromTrackPlayer(): Promise<void> {
|
||||
try {
|
||||
const [activeTrack, playbackState] = await Promise.all([
|
||||
TrackPlayer.getActiveTrack(),
|
||||
TrackPlayer.getPlaybackState(),
|
||||
]);
|
||||
setWidgetNowPlayingFromRntpTrack(activeTrack, mapRntpState(playbackState.state));
|
||||
} catch {
|
||||
setWidgetNowPlaying(null, 'stopped');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user