mirror of
https://github.com/Myzel394/Alibi.git
synced 2025-06-19 07:15:25 +02:00
feat: Add batch duration settings to SettingsScreen
This commit is contained in:
parent
752901e5c4
commit
ab1fca418c
@ -59,6 +59,7 @@ dependencies {
|
|||||||
implementation 'androidx.compose.ui:ui-graphics'
|
implementation 'androidx.compose.ui:ui-graphics'
|
||||||
implementation 'androidx.compose.ui:ui-tooling-preview'
|
implementation 'androidx.compose.ui:ui-tooling-preview'
|
||||||
implementation 'androidx.compose.material3:material3'
|
implementation 'androidx.compose.material3:material3'
|
||||||
|
implementation "androidx.compose.material:material-icons-extended:1.4.3"
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||||
@ -79,4 +80,7 @@ dependencies {
|
|||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1"
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1"
|
||||||
|
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
|
||||||
|
|
||||||
|
implementation 'com.maxkeppeler.sheets-compose-dialogs:core:1.2.0'
|
||||||
|
implementation 'com.maxkeppeler.sheets-compose-dialogs:duration:1.2.0'
|
||||||
}
|
}
|
@ -1,47 +1,27 @@
|
|||||||
package app.myzel394.locationtest
|
package app.myzel394.locationtest
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.datastore.dataStore
|
||||||
import androidx.compose.material3.MaterialTheme
|
import app.myzel394.locationtest.db.AppSettingsSerializer
|
||||||
import androidx.compose.material3.Surface
|
import app.myzel394.locationtest.ui.Navigation
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import app.myzel394.locationtest.ui.screens.AudioRecorder
|
|
||||||
import app.myzel394.locationtest.ui.theme.LocationTestTheme
|
import app.myzel394.locationtest.ui.theme.LocationTestTheme
|
||||||
|
|
||||||
|
const val SETTINGS_FILE = "settings.json"
|
||||||
|
val Context.dataStore by dataStore(
|
||||||
|
fileName = SETTINGS_FILE,
|
||||||
|
serializer = AppSettingsSerializer()
|
||||||
|
)
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
setContent {
|
||||||
LocationTestTheme {
|
LocationTestTheme {
|
||||||
// A surface container using the 'background' color from the theme
|
Navigation()
|
||||||
Surface(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
color = MaterialTheme.colorScheme.background
|
|
||||||
) {
|
|
||||||
AudioRecorder()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
|
||||||
Text(
|
|
||||||
text = "Hello $name!",
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
fun GreetingPreview() {
|
|
||||||
LocationTestTheme {
|
|
||||||
Greeting("Android")
|
|
||||||
}
|
|
||||||
}
|
|
@ -15,6 +15,10 @@ data class AppSettings(
|
|||||||
return copy(showAdvancedSettings = showAdvancedSettings)
|
return copy(showAdvancedSettings = showAdvancedSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setAudioRecorderSettings(audioRecorderSettings: AudioRecorderSettings): AppSettings {
|
||||||
|
return copy(audioRecorderSettings = audioRecorderSettings)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getDefaultInstance(): AppSettings = AppSettings()
|
fun getDefaultInstance(): AppSettings = AppSettings()
|
||||||
}
|
}
|
||||||
|
29
app/src/main/java/app/myzel394/locationtest/ui/Navigation.kt
Normal file
29
app/src/main/java/app/myzel394/locationtest/ui/Navigation.kt
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package app.myzel394.locationtest.ui
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import app.myzel394.locationtest.ui.enums.Screen
|
||||||
|
import app.myzel394.locationtest.ui.screens.AudioRecorder
|
||||||
|
import app.myzel394.locationtest.ui.screens.SettingsScreen
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Navigation() {
|
||||||
|
val navController = rememberNavController()
|
||||||
|
|
||||||
|
NavHost(navController = navController, startDestination = Screen.AudioRecorder.route) {
|
||||||
|
composable(Screen.AudioRecorder.route) {
|
||||||
|
AudioRecorder(
|
||||||
|
navController = navController,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable(
|
||||||
|
route = Screen.Settings.route
|
||||||
|
) {
|
||||||
|
SettingsScreen(
|
||||||
|
navController = navController,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package app.myzel394.locationtest.ui.enums
|
||||||
|
|
||||||
|
sealed class Screen(val route: String) {
|
||||||
|
object AudioRecorder : Screen("audio-recorder")
|
||||||
|
object Settings : Screen("settings")
|
||||||
|
|
||||||
|
fun withArgs(vararg args: String): String {
|
||||||
|
return buildString {
|
||||||
|
append(route)
|
||||||
|
args.forEach { arg ->
|
||||||
|
append("/$arg")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -10,16 +10,30 @@ import android.os.IBinder
|
|||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.navigation.NavController
|
||||||
import app.myzel394.locationtest.services.RecorderService
|
import app.myzel394.locationtest.services.RecorderService
|
||||||
|
import app.myzel394.locationtest.ui.enums.Screen
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AudioRecorder() {
|
fun AudioRecorder(
|
||||||
|
navController: NavController,
|
||||||
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val launcher = rememberLauncherForActivityResult(
|
val launcher = rememberLauncherForActivityResult(
|
||||||
ActivityResultContracts.RequestPermission(),
|
ActivityResultContracts.RequestPermission(),
|
||||||
@ -50,48 +64,72 @@ fun AudioRecorder() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Scaffold(
|
||||||
Button(
|
topBar = {
|
||||||
onClick = {
|
TopAppBar(
|
||||||
// Check audio recording permission
|
title = {
|
||||||
if (context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != android.content.pm.PackageManager.PERMISSION_GRANTED) {
|
Text(text = "Audio Recorder")
|
||||||
launcher.launch(Manifest.permission.RECORD_AUDIO)
|
},
|
||||||
|
actions = {
|
||||||
return@Button
|
IconButton(
|
||||||
}
|
onClick = {
|
||||||
|
navController.navigate(Screen.Settings.route)
|
||||||
if (isRecording) {
|
},
|
||||||
Intent(context, RecorderService::class.java).also { intent ->
|
) {
|
||||||
intent.action = RecorderService.Actions.STOP.toString()
|
Icon(
|
||||||
|
Icons.Default.Settings,
|
||||||
context.startService(intent)
|
contentDescription = null
|
||||||
}
|
)
|
||||||
} else {
|
|
||||||
Intent(context, RecorderService::class.java).also { intent ->
|
|
||||||
intent.action = RecorderService.Actions.START.toString()
|
|
||||||
|
|
||||||
ContextCompat.startForegroundService(context, intent)
|
|
||||||
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
)
|
||||||
|
},
|
||||||
|
) {padding ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(padding),
|
||||||
) {
|
) {
|
||||||
Text(text = if (isRecording) "Stop" else "Start")
|
|
||||||
}
|
|
||||||
if (!isRecording && service != null)
|
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
val path = service!!.concatenateAudios()
|
// Check audio recording permission
|
||||||
|
if (context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != android.content.pm.PackageManager.PERMISSION_GRANTED) {
|
||||||
|
launcher.launch(Manifest.permission.RECORD_AUDIO)
|
||||||
|
|
||||||
val player = MediaPlayer().apply {
|
return@Button
|
||||||
setDataSource(path)
|
|
||||||
prepare()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
player.start()
|
if (isRecording) {
|
||||||
|
Intent(context, RecorderService::class.java).also { intent ->
|
||||||
|
intent.action = RecorderService.Actions.STOP.toString()
|
||||||
|
|
||||||
|
context.startService(intent)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Intent(context, RecorderService::class.java).also { intent ->
|
||||||
|
intent.action = RecorderService.Actions.START.toString()
|
||||||
|
|
||||||
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Text(text = "Convert")
|
Text(text = if (isRecording) "Stop" else "Start")
|
||||||
}
|
}
|
||||||
|
if (!isRecording && service != null)
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
val path = service!!.concatenateAudios()
|
||||||
|
|
||||||
|
val player = MediaPlayer().apply {
|
||||||
|
setDataSource(path)
|
||||||
|
prepare()
|
||||||
|
}
|
||||||
|
|
||||||
|
player.start()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(text = "Convert")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,167 @@
|
|||||||
|
package app.myzel394.locationtest.ui.screens
|
||||||
|
|
||||||
|
import android.app.ProgressDialog.show
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Mic
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LargeTopAppBar
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.SwitchColors
|
||||||
|
import androidx.compose.material3.SwitchDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.datastore.dataStore
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import app.myzel394.locationtest.dataStore
|
||||||
|
import app.myzel394.locationtest.db.AppSettings
|
||||||
|
import app.myzel394.locationtest.ui.components.GlobalSwitch
|
||||||
|
import app.myzel394.locationtest.ui.utils.formatDuration
|
||||||
|
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
|
||||||
|
import com.maxkeppeler.sheets.duration.DurationDialog
|
||||||
|
import com.maxkeppeler.sheets.duration.models.DurationConfig
|
||||||
|
import com.maxkeppeler.sheets.duration.models.DurationFormat
|
||||||
|
import com.maxkeppeler.sheets.duration.models.DurationSelection
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.intellij.lang.annotations.JdkConstants.HorizontalAlignment
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SettingsScreen(
|
||||||
|
navController: NavController
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
LargeTopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(text = "Settings")
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = navController::popBackStack) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.ArrowBack,
|
||||||
|
contentDescription = "Back"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(padding),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val dataStore = LocalContext.current.dataStore
|
||||||
|
val settings = dataStore
|
||||||
|
.data
|
||||||
|
.collectAsState(initial = AppSettings.getDefaultInstance())
|
||||||
|
.value
|
||||||
|
|
||||||
|
GlobalSwitch(
|
||||||
|
label = "Advanced Settings",
|
||||||
|
checked = settings.showAdvancedSettings,
|
||||||
|
onCheckedChange = {
|
||||||
|
scope.launch {
|
||||||
|
dataStore.updateData {
|
||||||
|
it.setShowAdvancedSettings(it.showAdvancedSettings.not())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
AnimatedVisibility(visible = settings.showAdvancedSettings) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Mic,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Batch duration",
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "Record a single batch for this duration. Alibi records multiple batches and deletes the oldest one. When exporting the audio, all batches will be merged together",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
|
val showDurationPicker = rememberUseCaseState()
|
||||||
|
|
||||||
|
DurationDialog(
|
||||||
|
state = showDurationPicker,
|
||||||
|
selection = DurationSelection { newTimeInSeconds ->
|
||||||
|
scope.launch {
|
||||||
|
dataStore.updateData {
|
||||||
|
it.setAudioRecorderSettings(
|
||||||
|
it.audioRecorderSettings.setIntervalDuration(newTimeInSeconds * 1000L)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
config = DurationConfig(
|
||||||
|
timeFormat = DurationFormat.HH_MM_SS,
|
||||||
|
currentTime = settings.audioRecorderSettings.intervalDuration / 1000,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
onClick = showDurationPicker::show,
|
||||||
|
colors = ButtonDefaults.filledTonalButtonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = formatDuration(settings.audioRecorderSettings.intervalDuration),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package app.myzel394.locationtest.ui.utils
|
||||||
|
|
||||||
|
import kotlin.math.floor
|
||||||
|
|
||||||
|
|
||||||
|
fun formatDuration(durationInMilliseconds: Long): String {
|
||||||
|
if (durationInMilliseconds < 1000) {
|
||||||
|
return "00:00.$durationInMilliseconds"
|
||||||
|
}
|
||||||
|
|
||||||
|
val totalSeconds = durationInMilliseconds / 1000
|
||||||
|
|
||||||
|
if (totalSeconds < 60) {
|
||||||
|
return "00:${totalSeconds.toString().padStart(2, '0')}"
|
||||||
|
}
|
||||||
|
|
||||||
|
val minutes = floor(totalSeconds / 60.0).toInt()
|
||||||
|
val seconds = totalSeconds - (minutes * 60)
|
||||||
|
|
||||||
|
return "${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}"
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user