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
|
package xyz.cottongin.radio247
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
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.BackHandler
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import xyz.cottongin.radio247.ui.navigation.Screen
|
import xyz.cottongin.radio247.ui.navigation.Screen
|
||||||
import xyz.cottongin.radio247.ui.screens.nowplaying.NowPlayingScreen
|
import xyz.cottongin.radio247.ui.screens.nowplaying.NowPlayingScreen
|
||||||
import xyz.cottongin.radio247.ui.screens.settings.SettingsScreen
|
import xyz.cottongin.radio247.ui.screens.settings.SettingsScreen
|
||||||
@@ -18,6 +26,17 @@ class MainActivity : ComponentActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
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 {
|
Radio247Theme {
|
||||||
var currentScreen by remember { mutableStateOf<Screen>(Screen.StationList) }
|
var currentScreen by remember { mutableStateOf<Screen>(Screen.StationList) }
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import android.app.Application
|
|||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import xyz.cottongin.radio247.data.db.RadioDatabase
|
import xyz.cottongin.radio247.data.db.RadioDatabase
|
||||||
import xyz.cottongin.radio247.data.prefs.RadioPreferences
|
import xyz.cottongin.radio247.data.prefs.RadioPreferences
|
||||||
|
import xyz.cottongin.radio247.metadata.AlbumArtResolver
|
||||||
import xyz.cottongin.radio247.service.RadioController
|
import xyz.cottongin.radio247.service.RadioController
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
class RadioApplication : Application() {
|
class RadioApplication : Application() {
|
||||||
val database: RadioDatabase by lazy {
|
val database: RadioDatabase by lazy {
|
||||||
@@ -19,4 +21,12 @@ class RadioApplication : Application() {
|
|||||||
val controller: RadioController by lazy {
|
val controller: RadioController by lazy {
|
||||||
RadioController(this)
|
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
|
package xyz.cottongin.radio247.ui.screens.nowplaying
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
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.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
import xyz.cottongin.radio247.R
|
import xyz.cottongin.radio247.R
|
||||||
import xyz.cottongin.radio247.audio.IcyMetadata
|
import xyz.cottongin.radio247.audio.IcyMetadata
|
||||||
import xyz.cottongin.radio247.service.PlaybackState
|
import xyz.cottongin.radio247.service.PlaybackState
|
||||||
@@ -54,6 +56,7 @@ fun NowPlayingScreen(
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
val playbackState by viewModel.playbackState.collectAsState()
|
val playbackState by viewModel.playbackState.collectAsState()
|
||||||
|
val artworkUrl by viewModel.artworkUrl.collectAsState()
|
||||||
val sessionElapsed by viewModel.sessionElapsed.collectAsState()
|
val sessionElapsed by viewModel.sessionElapsed.collectAsState()
|
||||||
val connectionElapsed by viewModel.connectionElapsed.collectAsState()
|
val connectionElapsed by viewModel.connectionElapsed.collectAsState()
|
||||||
val estimatedLatencyMs by viewModel.estimatedLatencyMs.collectAsState()
|
val estimatedLatencyMs by viewModel.estimatedLatencyMs.collectAsState()
|
||||||
@@ -110,9 +113,18 @@ fun NowPlayingScreen(
|
|||||||
shape = MaterialTheme.shapes.medium
|
shape = MaterialTheme.shapes.medium
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
|
if (artworkUrl != null) {
|
||||||
|
AsyncImage(
|
||||||
|
model = artworkUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.drawable.ic_radio_placeholder),
|
painter = painterResource(R.drawable.ic_radio_placeholder),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
@@ -120,6 +132,7 @@ fun NowPlayingScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = station.name,
|
text = station.name,
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ import androidx.lifecycle.AndroidViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import xyz.cottongin.radio247.RadioApplication
|
import xyz.cottongin.radio247.RadioApplication
|
||||||
import xyz.cottongin.radio247.service.PlaybackState
|
import xyz.cottongin.radio247.service.PlaybackState
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
@@ -29,9 +32,14 @@ class NowPlayingViewModel(application: Application) : AndroidViewModel(applicati
|
|||||||
0
|
0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val _artworkUrl = MutableStateFlow<String?>(null)
|
||||||
|
val artworkUrl: StateFlow<String?> = _artworkUrl.asStateFlow()
|
||||||
|
|
||||||
val sessionElapsed: StateFlow<Long>
|
val sessionElapsed: StateFlow<Long>
|
||||||
val connectionElapsed: StateFlow<Long>
|
val connectionElapsed: StateFlow<Long>
|
||||||
|
|
||||||
|
private var artworkResolveJob: Job? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val ticker = flow {
|
val ticker = flow {
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -54,6 +62,37 @@ class NowPlayingViewModel(application: Application) : AndroidViewModel(applicati
|
|||||||
else -> 0L
|
else -> 0L
|
||||||
}
|
}
|
||||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 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() {
|
fun stop() {
|
||||||
|
|||||||
Reference in New Issue
Block a user