feat: wire album art to UI and handle Android 13+ notification permission
Made-with: Cursor
This commit is contained in:
@@ -1,13 +1,21 @@
|
||||
package xyz.cottongin.radio247
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import xyz.cottongin.radio247.ui.navigation.Screen
|
||||
import xyz.cottongin.radio247.ui.screens.nowplaying.NowPlayingScreen
|
||||
import xyz.cottongin.radio247.ui.screens.settings.SettingsScreen
|
||||
@@ -18,6 +26,17 @@ class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
val requestNotificationPermission = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.RequestPermission()
|
||||
) { _ -> }
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(Unit) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
requestNotificationPermission.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
}
|
||||
Radio247Theme {
|
||||
var currentScreen by remember { mutableStateOf<Screen>(Screen.StationList) }
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ import android.app.Application
|
||||
import androidx.room.Room
|
||||
import xyz.cottongin.radio247.data.db.RadioDatabase
|
||||
import xyz.cottongin.radio247.data.prefs.RadioPreferences
|
||||
import xyz.cottongin.radio247.metadata.AlbumArtResolver
|
||||
import xyz.cottongin.radio247.service.RadioController
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class RadioApplication : Application() {
|
||||
val database: RadioDatabase by lazy {
|
||||
@@ -19,4 +21,12 @@ class RadioApplication : Application() {
|
||||
val controller: RadioController by lazy {
|
||||
RadioController(this)
|
||||
}
|
||||
|
||||
val okHttpClient: OkHttpClient by lazy {
|
||||
OkHttpClient.Builder().build()
|
||||
}
|
||||
|
||||
val albumArtResolver: AlbumArtResolver by lazy {
|
||||
AlbumArtResolver(okHttpClient)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package xyz.cottongin.radio247.ui.screens.nowplaying
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -38,6 +39,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil3.compose.AsyncImage
|
||||
import xyz.cottongin.radio247.R
|
||||
import xyz.cottongin.radio247.audio.IcyMetadata
|
||||
import xyz.cottongin.radio247.service.PlaybackState
|
||||
@@ -54,6 +56,7 @@ fun NowPlayingScreen(
|
||||
)
|
||||
) {
|
||||
val playbackState by viewModel.playbackState.collectAsState()
|
||||
val artworkUrl by viewModel.artworkUrl.collectAsState()
|
||||
val sessionElapsed by viewModel.sessionElapsed.collectAsState()
|
||||
val connectionElapsed by viewModel.connectionElapsed.collectAsState()
|
||||
val estimatedLatencyMs by viewModel.estimatedLatencyMs.collectAsState()
|
||||
@@ -110,14 +113,24 @@ fun NowPlayingScreen(
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_radio_placeholder),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(120.dp)
|
||||
)
|
||||
if (artworkUrl != null) {
|
||||
AsyncImage(
|
||||
model = artworkUrl,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
} else {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_radio_placeholder),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(120.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,12 @@ import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import xyz.cottongin.radio247.RadioApplication
|
||||
import xyz.cottongin.radio247.service.PlaybackState
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
@@ -29,9 +32,14 @@ class NowPlayingViewModel(application: Application) : AndroidViewModel(applicati
|
||||
0
|
||||
)
|
||||
|
||||
private val _artworkUrl = MutableStateFlow<String?>(null)
|
||||
val artworkUrl: StateFlow<String?> = _artworkUrl.asStateFlow()
|
||||
|
||||
val sessionElapsed: StateFlow<Long>
|
||||
val connectionElapsed: StateFlow<Long>
|
||||
|
||||
private var artworkResolveJob: Job? = null
|
||||
|
||||
init {
|
||||
val ticker = flow {
|
||||
while (true) {
|
||||
@@ -54,6 +62,37 @@ class NowPlayingViewModel(application: Application) : AndroidViewModel(applicati
|
||||
else -> 0L
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0L)
|
||||
|
||||
viewModelScope.launch {
|
||||
playbackState.collect { state ->
|
||||
val (station, metadata) = when (state) {
|
||||
is PlaybackState.Playing -> state.station to state.metadata
|
||||
is PlaybackState.Reconnecting -> state.station to state.metadata
|
||||
PlaybackState.Idle -> null to null
|
||||
}
|
||||
when (state) {
|
||||
is PlaybackState.Playing,
|
||||
is PlaybackState.Reconnecting -> {
|
||||
artworkResolveJob?.cancel()
|
||||
artworkResolveJob = viewModelScope.launch {
|
||||
val url = station?.let { s ->
|
||||
app.albumArtResolver.resolve(
|
||||
artist = metadata?.artist,
|
||||
title = metadata?.title,
|
||||
icyStreamUrl = metadata?.streamUrl,
|
||||
stationArtworkUrl = s.defaultArtworkUrl
|
||||
)
|
||||
}
|
||||
_artworkUrl.value = url
|
||||
}
|
||||
}
|
||||
PlaybackState.Idle -> {
|
||||
artworkResolveJob?.cancel()
|
||||
_artworkUrl.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
|
||||
Reference in New Issue
Block a user