diff --git a/app/src/main/java/com/dmitryzenevich/remoteaudiocontrol/presentation/Extentions.kt b/app/src/main/java/com/dmitryzenevich/remoteaudiocontrol/presentation/Extentions.kt deleted file mode 100644 index 4f6c42c..0000000 --- a/app/src/main/java/com/dmitryzenevich/remoteaudiocontrol/presentation/Extentions.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.dmitryzenevich.remoteaudiocontrol.presentation - -import kotlin.math.roundToInt - -fun Float.classicRound(): Float = times(100).roundToInt().div(100f) diff --git a/app/src/main/java/com/dmitryzenevich/remoteaudiocontrol/presentation/MainActivity.kt b/app/src/main/java/com/dmitryzenevich/remoteaudiocontrol/presentation/MainActivity.kt index e8d3461..f90778f 100644 --- a/app/src/main/java/com/dmitryzenevich/remoteaudiocontrol/presentation/MainActivity.kt +++ b/app/src/main/java/com/dmitryzenevich/remoteaudiocontrol/presentation/MainActivity.kt @@ -1,14 +1,9 @@ package com.dmitryzenevich.remoteaudiocontrol.presentation import android.os.Bundle -import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.runtime.* import com.dmitryzenevich.remoteaudiocontrol.presentation.theme.RemoteAudioControlTheme -import androidx.lifecycle.viewmodel.compose.viewModel import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -23,23 +18,3 @@ class MainActivity : ComponentActivity() { } } } - -@Composable -fun MainScreen(viewModel: MainViewModel = viewModel()) { - val uiState by viewModel.uiState.collectAsState() - - LazyRow { - items( - items = uiState.volumes, - key = { item -> item.pid } - ) { itemState -> - VolumeItem( - volumeItemState = itemState, - onValueChanged = { newValue: Float -> - Log.i("debug", "new value: $newValue") - viewModel.onVolumeChanged(itemState, newValue.toInt()) - } - ) - } - } -} diff --git a/app/src/main/java/com/dmitryzenevich/remoteaudiocontrol/presentation/MainScreen.kt b/app/src/main/java/com/dmitryzenevich/remoteaudiocontrol/presentation/MainScreen.kt new file mode 100644 index 0000000..6abee80 --- /dev/null +++ b/app/src/main/java/com/dmitryzenevich/remoteaudiocontrol/presentation/MainScreen.kt @@ -0,0 +1,31 @@ +package com.dmitryzenevich.remoteaudiocontrol.presentation + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier + +@Composable +fun MainScreen(viewModel: MainViewModel = androidx.lifecycle.viewmodel.compose.viewModel()) { + val uiState by viewModel.uiState.collectAsState() + + LazyRow( + modifier = Modifier.fillMaxSize() + ) { + items( + items = uiState.volumes, + key = { item -> item.pid } + ) { itemState -> + VolumeItem( + volumeItemState = itemState, + onValueChanged = { newValue: Float -> + viewModel.onVolumeChanged(itemState, newValue.toInt()) + }, + onMuteClick = { viewModel.onMuteClick(itemState) } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dmitryzenevich/remoteaudiocontrol/presentation/MainScreenUiState.kt b/app/src/main/java/com/dmitryzenevich/remoteaudiocontrol/presentation/MainScreenUiState.kt new file mode 100644 index 0000000..f9b339a --- /dev/null +++ b/app/src/main/java/com/dmitryzenevich/remoteaudiocontrol/presentation/MainScreenUiState.kt @@ -0,0 +1,19 @@ +package com.dmitryzenevich.remoteaudiocontrol.presentation + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.snapshots.SnapshotStateList + +data class MainScreenUiState( + val volumes: SnapshotStateList = mutableStateListOf(), + val isError: Boolean = false +) + +data class VolumeItemState( + val pid: Long, + val name: String = "", + val volume: MutableState = mutableStateOf(0), + val isMuted: MutableState = mutableStateOf(false), + val isActive: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/com/dmitryzenevich/remoteaudiocontrol/presentation/MainViewModel.kt b/app/src/main/java/com/dmitryzenevich/remoteaudiocontrol/presentation/MainViewModel.kt index 8fe22ba..0257a31 100644 --- a/app/src/main/java/com/dmitryzenevich/remoteaudiocontrol/presentation/MainViewModel.kt +++ b/app/src/main/java/com/dmitryzenevich/remoteaudiocontrol/presentation/MainViewModel.kt @@ -1,11 +1,6 @@ package com.dmitryzenevich.remoteaudiocontrol.presentation import android.util.Log -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.dmitryzenevich.remoteaudiocontrol.data.SocketRepositoryImpl @@ -15,6 +10,8 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import javax.inject.Inject +const val NOT_BLOCKED = -1L + @HiltViewModel class MainViewModel @Inject constructor( private val socketRepositoryImpl: SocketRepositoryImpl @@ -23,7 +20,9 @@ class MainViewModel @Inject constructor( private val _uiState = MutableStateFlow(MainScreenUiState()) val uiState = _uiState.asStateFlow() - private var commandJob: Job? = null + @Volatile + private var blockingPid: Long = NOT_BLOCKED + private var blockingJob: Job? = null init { viewModelScope.launch { @@ -33,7 +32,6 @@ class MainViewModel @Inject constructor( .filterNotNull() .onEach { Log.i(javaClass.simpleName, "received: $it") - // TODO: test it withContext(Dispatchers.Main) { proceedEvent(it) } } .catch { Log.e(javaClass.simpleName, "Fetch event error", it) } @@ -49,11 +47,7 @@ class MainViewModel @Inject constructor( is NewSessionEvent -> addIfNotExist(event) is MuteStateChangedEvent -> { volumes.find { event.PID == it.pid }?.let { item -> - val newItem = item.copy(isMuted = event.isMuted) - volumes.set( - index = volumes.indexOfFirst { it.pid == newItem.pid }, - element = newItem - ) + item.isMuted.value = event.isMuted } } is SetNameEvent -> { @@ -75,28 +69,40 @@ class MainViewModel @Inject constructor( } } is VolumeChangedEvent -> { + if (blockingPid == event.PID) return + volumes.find { event.PID == it.pid }?.let { item -> -// val newItem = item.copy(volume = event.volume) -// volumes.set( -// index = volumes.indexOfFirst { it.pid == newItem.pid }, -// element = newItem -// ) item.volume.value = event.volume + blockVolumeEvent(event.PID) } } UnknownEvent -> _uiState.value.isError // TODO: implement error state } } + private fun blockVolumeEvent(eventPid: Long) { + blockingJob?.cancel() + blockingJob = viewModelScope.launch { + blockingPid = eventPid + delay(200) + blockingPid = NOT_BLOCKED + } + } + fun onVolumeChanged(volumeItemState: VolumeItemState, newVolume: Int) { Log.i(javaClass.simpleName, "old volume: ${volumeItemState.volume}, newVolume: $newVolume") volumeItemState.volume.value = newVolume sendCommand(SetVolumeCommand(volumeItemState.pid, newVolume)) } + fun onMuteClick(volumeItemState: VolumeItemState) { + Log.i(javaClass.simpleName, "new checked: ${!volumeItemState.isMuted.value}") + volumeItemState.isMuted.value = !volumeItemState.isMuted.value + sendCommand(MuteToggleCommand(volumeItemState.pid)) + } + private fun sendCommand(command: Command) { - commandJob?.cancel() - commandJob = viewModelScope.launch { socketRepositoryImpl.sendCommand(command) } + viewModelScope.launch { socketRepositoryImpl.sendCommand(command) } } private fun addIfNotExist(event: NewSessionEvent) { @@ -104,22 +110,6 @@ class MainViewModel @Inject constructor( _uiState.value.volumes.add(event.toVolumeItemState()) } } - } -class MainScreenUiState( - val volumes: SnapshotStateList = mutableStateListOf(), - val isError: Boolean = false -) - -data class VolumeItemState( - val pid: Long, - val name: String = "", - val volume: MutableState = mutableStateOf(0), - val isMuted: Boolean = false, - val isActive: Boolean = false -) - fun NewSessionEvent.toVolumeItemState() = VolumeItemState(pid = PID) - -//fun VolumeChangedEvent.toVolumeItemState() = VolumeItemState(pid = PID, volume = volume) \ No newline at end of file diff --git a/app/src/main/java/com/dmitryzenevich/remoteaudiocontrol/presentation/VolumeItem.kt b/app/src/main/java/com/dmitryzenevich/remoteaudiocontrol/presentation/VolumeItem.kt index 69a0bc5..599dcd1 100644 --- a/app/src/main/java/com/dmitryzenevich/remoteaudiocontrol/presentation/VolumeItem.kt +++ b/app/src/main/java/com/dmitryzenevich/remoteaudiocontrol/presentation/VolumeItem.kt @@ -1,50 +1,66 @@ package com.dmitryzenevich.remoteaudiocontrol.presentation -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.material3.Slider -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.layout +import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.dp +import com.dmitryzenevich.remoteaudiocontrol.R @Composable fun VolumeItem( volumeItemState: VolumeItemState, - onValueChanged: (Float) -> Unit + onValueChanged: (Float) -> Unit, + onMuteClick: () -> Unit, ) { Column( modifier = Modifier - .padding(8.dp) - .wrapContentSize(), - horizontalAlignment = Alignment.CenterHorizontally + .fillMaxHeight() + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top ) { - Text( - text = "${volumeItemState.name}\nVolume: ${volumeItemState.volume.value}" - ) + Text(text = volumeItemState.name) + Text(text = volumeItemState.volume.value.toString()) VerticalSlider( value = volumeItemState.volume.value.toFloat(), - onValueChanged = onValueChanged + onValueChanged = onValueChanged, + modifier = Modifier + .fillMaxWidth() + .weight(1f) ) + IconButton(onClick = onMuteClick) { + if (volumeItemState.isMuted.value) + Icon( + painter = painterResource(R.drawable.ic_volume_off), + contentDescription = "Muted" + ) + else + Icon( + painter = painterResource(R.drawable.ic_volume_up), + contentDescription = "Resumed" + ) + } } } @Composable fun VerticalSlider( value: Float, - onValueChanged: (Float) -> Unit + onValueChanged: (Float) -> Unit, + modifier: Modifier = Modifier ) { Slider( value = value, valueRange = 0f..100f, onValueChange = onValueChanged, - modifier = Modifier + modifier = modifier .graphicsLayer { rotationZ = 270f transformOrigin = TransformOrigin(0f, 0f) @@ -62,6 +78,6 @@ fun VerticalSlider( placeable.place(-placeable.width, 0) } } - .padding(horizontal = 24.dp) + .padding(horizontal = 8.dp) ) } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_volume_off.xml b/app/src/main/res/drawable/ic_volume_off.xml new file mode 100644 index 0000000..73dc595 --- /dev/null +++ b/app/src/main/res/drawable/ic_volume_off.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_volume_up.xml b/app/src/main/res/drawable/ic_volume_up.xml new file mode 100644 index 0000000..30dff4b --- /dev/null +++ b/app/src/main/res/drawable/ic_volume_up.xml @@ -0,0 +1,5 @@ + + +