diff --git a/app/src/main/java/com/radiola/ui/player/PlayerBottomSheet.kt b/app/src/main/java/com/radiola/ui/player/PlayerBottomSheet.kt new file mode 100644 index 0000000..839bc8e --- /dev/null +++ b/app/src/main/java/com/radiola/ui/player/PlayerBottomSheet.kt @@ -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) + ) + } + } +} diff --git a/app/src/main/java/com/radiola/ui/player/PlayerViewModel.kt b/app/src/main/java/com/radiola/ui/player/PlayerViewModel.kt new file mode 100644 index 0000000..475dc9d --- /dev/null +++ b/app/src/main/java/com/radiola/ui/player/PlayerViewModel.kt @@ -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 = playerController.isPlaying + val currentStationPrefix: StateFlow = playerController.currentStationPrefix + + private val _currentTrack = MutableStateFlow(null) + val currentTrack: StateFlow = _currentTrack.asStateFlow() + + private val _enabledServices = MutableStateFlow>(emptyList()) + val enabledServices: StateFlow> = _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) + } +}