feat(ui): add PlayerBottomSheet and PlayerViewModel
This commit is contained in:
171
app/src/main/java/com/radiola/ui/player/PlayerBottomSheet.kt
Normal file
171
app/src/main/java/com/radiola/ui/player/PlayerBottomSheet.kt
Normal file
@@ -0,0 +1,171 @@
|
||||
package com.radiola.ui.player
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import coil.compose.AsyncImage
|
||||
import com.composables.icons.lucide.Lucide
|
||||
import com.composables.icons.lucide.Pause
|
||||
import com.composables.icons.lucide.Play
|
||||
import com.radiola.domain.model.DeeplinkService
|
||||
import com.radiola.domain.model.Station
|
||||
import com.radiola.domain.model.Track
|
||||
|
||||
@Composable
|
||||
fun PlayerBottomSheet(
|
||||
station: Station?,
|
||||
track: Track?,
|
||||
isPlaying: Boolean,
|
||||
onPlayPause: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: PlayerViewModel = hiltViewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val enabledServices by viewModel.enabledServices.collectAsState()
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(Color(0xFF1a1a2e), Color(0xFF121212))
|
||||
)
|
||||
)
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
station?.let {
|
||||
Text(
|
||||
text = it.name,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = Color(0xFF888888)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(240.dp)
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(
|
||||
Brush.linearGradient(listOf(Color(0xFF667eea), Color(0xFF764ba2)))
|
||||
)
|
||||
) {
|
||||
AsyncImage(
|
||||
model = track?.coverUrl ?: station?.coverUrl,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = track?.song ?: "",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
maxLines = 1
|
||||
)
|
||||
Text(
|
||||
text = track?.artist ?: "",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = Color(0xFF888888),
|
||||
maxLines = 1
|
||||
)
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
LazyRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentPadding = PaddingValues(horizontal = 8.dp)
|
||||
) {
|
||||
items(enabledServices) { service ->
|
||||
DeeplinkButton(
|
||||
service = service,
|
||||
onClick = {
|
||||
track?.let { t ->
|
||||
val url = viewModel.getDeeplinkUrl(t, service)
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
ControlButton(size = 56.dp, onClick = { })
|
||||
ControlButton(
|
||||
size = 72.dp,
|
||||
isPlay = true,
|
||||
isPlaying = isPlaying,
|
||||
onClick = onPlayPause
|
||||
)
|
||||
ControlButton(size = 56.dp, onClick = { })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeeplinkButton(
|
||||
service: DeeplinkService,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(48.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(Color(0xFF2A2A2A))
|
||||
.clickable(onClick = onClick),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = service.displayName.take(2),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = Color(0xFF888888)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ControlButton(
|
||||
size: androidx.compose.ui.unit.Dp,
|
||||
isPlay: Boolean = false,
|
||||
isPlaying: Boolean = false,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(size)
|
||||
.clip(CircleShape)
|
||||
.background(if (isPlay) Color.White else Color(0xFF2A2A2A))
|
||||
.clickable(onClick = onClick),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (isPlay) {
|
||||
Icon(
|
||||
imageVector = if (isPlaying) Lucide.Pause else Lucide.Play,
|
||||
contentDescription = if (isPlaying) "Pause" else "Play",
|
||||
tint = Color.Black,
|
||||
modifier = Modifier.size(size * 0.4f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
66
app/src/main/java/com/radiola/ui/player/PlayerViewModel.kt
Normal file
66
app/src/main/java/com/radiola/ui/player/PlayerViewModel.kt
Normal file
@@ -0,0 +1,66 @@
|
||||
package com.radiola.ui.player
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.radiola.domain.model.DeeplinkService
|
||||
import com.radiola.domain.model.Station
|
||||
import com.radiola.domain.model.Track
|
||||
import com.radiola.domain.repository.SettingsRepository
|
||||
import com.radiola.domain.usecase.GetNowPlayingUseCase
|
||||
import com.radiola.domain.usecase.SearchTrackInServiceUseCase
|
||||
import com.radiola.service.PlayerController
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class PlayerViewModel @Inject constructor(
|
||||
private val playerController: PlayerController,
|
||||
private val getNowPlayingUseCase: GetNowPlayingUseCase,
|
||||
private val searchTrackInServiceUseCase: SearchTrackInServiceUseCase,
|
||||
private val settingsRepository: SettingsRepository
|
||||
) : ViewModel() {
|
||||
|
||||
val isPlaying: StateFlow<Boolean> = playerController.isPlaying
|
||||
val currentStationPrefix: StateFlow<String?> = playerController.currentStationPrefix
|
||||
|
||||
private val _currentTrack = MutableStateFlow<Track?>(null)
|
||||
val currentTrack: StateFlow<Track?> = _currentTrack.asStateFlow()
|
||||
|
||||
private val _enabledServices = MutableStateFlow<List<DeeplinkService>>(emptyList())
|
||||
val enabledServices: StateFlow<List<DeeplinkService>> = _enabledServices.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
settingsRepository.getEnabledDeeplinkServices().collect { ids ->
|
||||
_enabledServices.value = DeeplinkService.entries.filter { it.serviceId in ids }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun play(station: Station) {
|
||||
playerController.play(station.streamUrl, station.prefix)
|
||||
viewModelScope.launch {
|
||||
getNowPlayingUseCase(station.prefix).collect { track ->
|
||||
_currentTrack.value = track
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
playerController.pause()
|
||||
}
|
||||
|
||||
fun resume(station: Station) {
|
||||
if (playerController.currentStationPrefix.value == station.prefix) {
|
||||
playerController.exoPlayer.play()
|
||||
} else {
|
||||
play(station)
|
||||
}
|
||||
}
|
||||
|
||||
fun getDeeplinkUrl(track: Track, service: DeeplinkService): String {
|
||||
return searchTrackInServiceUseCase(track, service)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user