Improved Playing recording dialog
This commit is contained in:
@ -99,4 +99,5 @@ dependencies {
|
|||||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||||
implementation(libs.androidx.compose.material.icons.core)
|
implementation(libs.androidx.compose.material.icons.core)
|
||||||
implementation(libs.androidx.compose.material.icons.extended)
|
implementation(libs.androidx.compose.material.icons.extended)
|
||||||
|
implementation(libs.androidx.compose.runtime)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.WindowInsets
|
|||||||
import androidx.compose.foundation.layout.asPaddingValues
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBars
|
import androidx.compose.foundation.layout.statusBars
|
||||||
@ -29,18 +30,26 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.automirrored.filled.CallMade
|
import androidx.compose.material.icons.automirrored.filled.CallMade
|
||||||
import androidx.compose.material.icons.automirrored.filled.CallReceived
|
import androidx.compose.material.icons.automirrored.filled.CallReceived
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@ -58,6 +67,7 @@ import com.tutpro.baresip.CallRow.Details
|
|||||||
import com.tutpro.baresip.CustomElements.AlertDialog
|
import com.tutpro.baresip.CustomElements.AlertDialog
|
||||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -260,10 +270,16 @@ private fun Duration(ctx: Context, detail: Details, durationText: String) {
|
|||||||
val mediaPlayer = remember { MediaPlayer() }
|
val mediaPlayer = remember { MediaPlayer() }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
AlertDialog(
|
PlaybackDialog(
|
||||||
showDialog = showDialog,
|
showDialog = showDialog,
|
||||||
title = stringResource(R.string.playing_recording),
|
mediaPlayer = mediaPlayer,
|
||||||
message = "",
|
onStop = {
|
||||||
|
if (mediaPlayer.isPlaying) {
|
||||||
|
mediaPlayer.stop()
|
||||||
|
}
|
||||||
|
mediaPlayer.reset()
|
||||||
|
showDialog.value = false
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
val isRawState = recording[0] != "" && recording[1] != ""
|
val isRawState = recording[0] != "" && recording[1] != ""
|
||||||
@ -284,13 +300,15 @@ private fun Duration(ctx: Context, detail: Details, durationText: String) {
|
|||||||
if (isRawState) {
|
if (isRawState) {
|
||||||
val fileIn = File(recording[0])
|
val fileIn = File(recording[0])
|
||||||
val fileOut = File(recording[1])
|
val fileOut = File(recording[1])
|
||||||
val mergedFileName = "merged_${fileIn.nameWithoutExtension}_${fileOut.nameWithoutExtension}.wav"
|
val mergedFileName =
|
||||||
|
"merged_${fileIn.nameWithoutExtension}_${fileOut.nameWithoutExtension}.wav"
|
||||||
val mergedFile = File(BaresipService.filesPath + "/tmp", mergedFileName)
|
val mergedFile = File(BaresipService.filesPath + "/tmp", mergedFileName)
|
||||||
|
|
||||||
if (mergedFile.exists()) {
|
if (mergedFile.exists()) {
|
||||||
Log.d(TAG, "Using already merged file: ${mergedFile.name}")
|
Log.d(TAG, "Using already merged file: ${mergedFile.name}")
|
||||||
finalFile = mergedFile
|
finalFile = mergedFile
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
File(BaresipService.filesPath + "/tmp").mkdirs()
|
File(BaresipService.filesPath + "/tmp").mkdirs()
|
||||||
if (Utils.mergeWavFiles(fileIn, fileOut, mergedFile)) {
|
if (Utils.mergeWavFiles(fileIn, fileOut, mergedFile)) {
|
||||||
finalFile = mergedFile
|
finalFile = mergedFile
|
||||||
@ -315,7 +333,8 @@ private fun Duration(ctx: Context, detail: Details, durationText: String) {
|
|||||||
Log.w(TAG, "MergeWav: Failed to delete original files: ${e.message}")
|
Log.w(TAG, "MergeWav: Failed to delete original files: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
// We are already in merged state
|
// We are already in merged state
|
||||||
Log.d(TAG, "Using already merged file: ${recording[0]}")
|
Log.d(TAG, "Using already merged file: ${recording[0]}")
|
||||||
finalFile = File(recording[0])
|
finalFile = File(recording[0])
|
||||||
@ -350,12 +369,14 @@ private fun Duration(ctx: Context, detail: Details, durationText: String) {
|
|||||||
Log.e(TAG, "Playback failed: $e")
|
Log.e(TAG, "Playback failed: $e")
|
||||||
Toast.makeText(ctx, "Playback error", Toast.LENGTH_SHORT).show()
|
Toast.makeText(ctx, "Playback error", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Toast.makeText(ctx, "Failed to process audio file", Toast.LENGTH_SHORT).show()
|
Toast.makeText(ctx, "Failed to process audio file", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
mediaPlayer.stop()
|
mediaPlayer.stop()
|
||||||
mediaPlayer.reset()
|
mediaPlayer.reset()
|
||||||
showDialog.value = false
|
showDialog.value = false
|
||||||
@ -367,3 +388,73 @@ private fun Duration(ctx: Context, detail: Details, durationText: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PlaybackDialog(
|
||||||
|
showDialog: MutableState<Boolean>,
|
||||||
|
mediaPlayer: MediaPlayer,
|
||||||
|
onStop: () -> Unit
|
||||||
|
) {
|
||||||
|
if (showDialog.value) {
|
||||||
|
// State to hold progress (0.0f to 1.0f)
|
||||||
|
var currentProgress by remember { mutableFloatStateOf(0f) }
|
||||||
|
|
||||||
|
// Formatted time strings
|
||||||
|
var currentPositionText by remember { mutableStateOf("00:00") }
|
||||||
|
var totalDurationText by remember { mutableStateOf("00:00") }
|
||||||
|
|
||||||
|
// Update progress every 100ms
|
||||||
|
LaunchedEffect(showDialog.value) {
|
||||||
|
if (mediaPlayer.isPlaying) {
|
||||||
|
val duration = mediaPlayer.duration
|
||||||
|
totalDurationText = DateUtils.formatElapsedTime(duration / 1000L)
|
||||||
|
|
||||||
|
while (mediaPlayer.isPlaying) {
|
||||||
|
val current = mediaPlayer.currentPosition
|
||||||
|
currentProgress = if (duration > 0) current.toFloat() / duration.toFloat() else 0f
|
||||||
|
currentPositionText = DateUtils.formatElapsedTime(current / 1000L)
|
||||||
|
delay(100) // Poll every 100ms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
// If user clicks outside, stop playback
|
||||||
|
onStop()
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(R.string.playing_recording))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = { currentProgress },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(8.dp),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(text = currentPositionText, style = MaterialTheme.typography.bodySmall)
|
||||||
|
Text(text = totalDurationText, style = MaterialTheme.typography.bodySmall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
onStop()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.stop))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -261,6 +261,7 @@
|
|||||||
<string name="time">Time</string>
|
<string name="time">Time</string>
|
||||||
<string name="calls_duration">Duration</string>
|
<string name="calls_duration">Duration</string>
|
||||||
<string name="playing_recording">Playing recording …</string>
|
<string name="playing_recording">Playing recording …</string>
|
||||||
|
<string name="stop">Stop</string>
|
||||||
<string name="calls_add_delete_question">Do you want to add \'%1$s\' to contacts or delete
|
<string name="calls_add_delete_question">Do you want to add \'%1$s\' to contacts or delete
|
||||||
%2$s from call history\?</string>
|
%2$s from call history\?</string>
|
||||||
<string name="calls_delete_question">Do you want to delete \'%1$s\' %2$s from call history\?</string>
|
<string name="calls_delete_question">Do you want to delete \'%1$s\' %2$s from call history\?</string>
|
||||||
|
|||||||
@ -21,6 +21,7 @@ runtimeLivedata = "1.10.0"
|
|||||||
composeMaterial3 = "1.4.0"
|
composeMaterial3 = "1.4.0"
|
||||||
navigationRuntimeAndroid = "2.9.6"
|
navigationRuntimeAndroid = "2.9.6"
|
||||||
composeMaterialIcons = "1.7.8"
|
composeMaterialIcons = "1.7.8"
|
||||||
|
runtime = "1.10.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
|
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
|
||||||
@ -47,6 +48,7 @@ androidx-compose-material3 = { group = "androidx.compose.material3", name = "mat
|
|||||||
androidx-navigation-runtime-android = { group = "androidx.navigation", name = "navigation-runtime-android", version.ref = "navigationRuntimeAndroid" }
|
androidx-navigation-runtime-android = { group = "androidx.navigation", name = "navigation-runtime-android", version.ref = "navigationRuntimeAndroid" }
|
||||||
androidx-compose-material-icons-core = { group = "androidx.compose.material", name = "material-icons-core", version.ref = "composeMaterialIcons" }
|
androidx-compose-material-icons-core = { group = "androidx.compose.material", name = "material-icons-core", version.ref = "composeMaterialIcons" }
|
||||||
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "composeMaterialIcons" }
|
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "composeMaterialIcons" }
|
||||||
|
androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "runtime" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
|
|||||||
Reference in New Issue
Block a user