mirror of
https://github.com/Myzel394/Alibi.git
synced 2025-06-18 23:05:26 +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-tooling-preview'
|
||||
implementation 'androidx.compose.material3:material3'
|
||||
implementation "androidx.compose.material:material-icons-extended:1.4.3"
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
@ -79,4 +80,7 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1"
|
||||
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
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 androidx.datastore.dataStore
|
||||
import app.myzel394.locationtest.db.AppSettingsSerializer
|
||||
import app.myzel394.locationtest.ui.Navigation
|
||||
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() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
LocationTestTheme {
|
||||
// A surface container using the 'background' color from the theme
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
AudioRecorder()
|
||||
}
|
||||
Navigation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
|
||||
fun setAudioRecorderSettings(audioRecorderSettings: AudioRecorderSettings): AppSettings {
|
||||
return copy(audioRecorderSettings = audioRecorderSettings)
|
||||
}
|
||||
|
||||
companion object {
|
||||
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.result.contract.ActivityResultContracts
|
||||
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.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.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.navigation.NavController
|
||||
import app.myzel394.locationtest.services.RecorderService
|
||||
import app.myzel394.locationtest.ui.enums.Screen
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AudioRecorder() {
|
||||
fun AudioRecorder(
|
||||
navController: NavController,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val launcher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.RequestPermission(),
|
||||
@ -50,48 +64,72 @@ fun AudioRecorder() {
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
Button(
|
||||
onClick = {
|
||||
// Check audio recording permission
|
||||
if (context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != android.content.pm.PackageManager.PERMISSION_GRANTED) {
|
||||
launcher.launch(Manifest.permission.RECORD_AUDIO)
|
||||
|
||||
return@Button
|
||||
}
|
||||
|
||||
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)
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(text = "Audio Recorder")
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
navController.navigate(Screen.Settings.route)
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Settings,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
) {padding ->
|
||||
Row(
|
||||
modifier = Modifier.padding(padding),
|
||||
) {
|
||||
Text(text = if (isRecording) "Stop" else "Start")
|
||||
}
|
||||
if (!isRecording && service != null)
|
||||
Button(
|
||||
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 {
|
||||
setDataSource(path)
|
||||
prepare()
|
||||
return@Button
|
||||
}
|
||||
|
||||
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