diff --git a/.github/workflows/release-app-google-play.yaml b/.github/workflows/release-app-google-play.yaml index e61b53f..9c0ab5d 100644 --- a/.github/workflows/release-app-google-play.yaml +++ b/.github/workflows/release-app-google-play.yaml @@ -41,4 +41,4 @@ jobs: track: production status: inProgress inAppUpdatePriority: 2 - userFraction: 0.33 + userFraction: 0.2 diff --git a/app/build.gradle b/app/build.gradle index 9ae6af4..963fcad 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,8 +35,8 @@ android { applicationId "app.myzel394.alibi" minSdk 24 targetSdk 34 - versionCode 14 - versionName "0.5.1" + versionCode 15 + versionName "0.5.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -94,9 +94,9 @@ android { dependencies { implementation 'androidx.core:core-ktx:1.13.1' implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0') - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.3' - implementation 'androidx.activity:activity-compose:1.9.0' - implementation 'androidx.activity:activity-ktx:1.9.0' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.4' + implementation 'androidx.activity:activity-compose:1.9.1' + implementation 'androidx.activity:activity-ktx:1.9.1' implementation platform('androidx.compose:compose-bom:2024.06.00') implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.ui:ui-graphics' @@ -105,7 +105,7 @@ dependencies { implementation "androidx.compose.material:material-icons-extended:1.6.8" implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.documentfile:documentfile:1.0.1' - implementation 'androidx.lifecycle:lifecycle-service:2.8.3' + implementation 'androidx.lifecycle:lifecycle-service:2.8.4' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.2.1' diff --git a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt index cfd47e9..2d49749 100644 --- a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt +++ b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt @@ -29,6 +29,8 @@ data class AppSettings( val theme: Theme = Theme.SYSTEM, val lastRecording: RecordingInformation? = null, + val filenameFormat: FilenameFormat = FilenameFormat.DATETIME_RELATIVE_START, + /// Recording information // 30 minutes val maxDuration: Long = 15 * 60 * 1000L, @@ -67,6 +69,10 @@ data class AppSettings( return copy(lastRecording = lastRecording) } + fun setFilenameFormat(filenameFormat: FilenameFormat): AppSettings { + return copy(filenameFormat = filenameFormat) + } + fun setMaxDuration(duration: Long): AppSettings { if (duration < 60 * 1000L || duration > 10 * 24 * 60 * 60 * 1000L) { throw Exception("Max duration must be between 1 minute and 10 days") @@ -124,14 +130,20 @@ data class AppSettings( )) } + fun exportToString(): String { + return Json.encodeToString(serializer(), this) + } + enum class Theme { SYSTEM, LIGHT, DARK, } - fun exportToString(): String { - return Json.encodeToString(serializer(), this) + enum class FilenameFormat { + DATETIME_ABSOLUTE_START, + DATETIME_RELATIVE_START, + DATETIME_NOW, } companion object { @@ -151,6 +163,7 @@ data class RecordingInformation( val folderPath: String, @Serializable(with = LocalDateTimeSerializer::class) val recordingStart: LocalDateTime, + val batchesAmount: Int, val maxDuration: Long, val intervalDuration: Long, val fileExtension: String, @@ -165,6 +178,23 @@ data class RecordingInformation( .hasRecordingsAvailable() } + fun getStartDateForFilename(filenameFormat: AppSettings.FilenameFormat): LocalDateTime { + return when (filenameFormat) { + AppSettings.FilenameFormat.DATETIME_ABSOLUTE_START -> recordingStart + AppSettings.FilenameFormat.DATETIME_RELATIVE_START -> LocalDateTime.now().minusSeconds( + getFullDuration() / 1000 + ) + + AppSettings.FilenameFormat.DATETIME_NOW -> LocalDateTime.now() + } + } + + fun getFullDuration(): Long { + // This is not accurate, since the last batch may be shorter than the others + // but it's good enough + return intervalDuration * batchesAmount - (intervalDuration * 0.5).toLong() + } + enum class Type { AUDIO, VIDEO, diff --git a/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt b/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt index 873ca2c..e60be72 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt @@ -18,6 +18,7 @@ import androidx.annotation.RequiresApi import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile import app.myzel394.alibi.db.AppSettings +import app.myzel394.alibi.db.RecordingInformation import app.myzel394.alibi.ui.MEDIA_RECORDINGS_PREFIX import app.myzel394.alibi.ui.RECORDER_INTERNAL_SELECTED_VALUE import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE @@ -249,19 +250,23 @@ abstract class BatchesFolder( abstract fun cleanup() suspend fun concatenate( - recordingStart: LocalDateTime, - extension: String, + recording: RecordingInformation, + filenameFormat: AppSettings.FilenameFormat, disableCache: Boolean? = null, onNextParameterTry: (String) -> Unit = {}, - durationPerBatchInMilliseconds: Long = 0, onProgress: (Float?) -> Unit = {}, ): String { val disableCache = disableCache ?: (type != BatchType.INTERNAL) + val date = recording.getStartDateForFilename(filenameFormat) - if (!disableCache && checkIfOutputAlreadyExists(recordingStart, extension)) { + if (!disableCache && checkIfOutputAlreadyExists( + recording.recordingStart, + recording.fileExtension + ) + ) { return getOutputFileForFFmpeg( - date = recordingStart, - extension = extension, + date = recording.recordingStart, + extension = recording.fileExtension, ) } @@ -271,34 +276,12 @@ abstract class BatchesFolder( onProgress(null) try { + val fullTime = recording.getFullDuration().toFloat(); val filePaths = getBatchesForFFmpeg() - // Casting here to float so it doesn't need to redo it on every progress update - var fullTime: Float? = null - - runCatching { - // `fullTime` is not accurate as the last batch might be shorter, - // but it's good enough for the progress bar - - // Using the code below results in a nasty bug: - // since we use ffmpeg to extract the duration, the saf parameter is already - // "used up" and we can't use it again for the actual concatenation - // Since an accurate progress bar is less important than speed, - // we currently don't use this code - /* - val lastBatchTime = (FFprobeKit.execute( - "-i ${filePaths.last()} -show_entries format=duration -v quiet -of csv=\"p=0\"", - ).output.toFloat() * 1000).toLong() - fullTime = - ((durationPerBatchInMilliseconds * (filePaths.size - 1)) + lastBatchTime).toFloat() - */ - // We use an approximation for the duration of the batches - fullTime = (durationPerBatchInMilliseconds * filePaths.size).toFloat() - } - val outputFile = getOutputFileForFFmpeg( - date = recordingStart, - extension = extension, + date = date, + extension = recording.fileExtension, ) concatenationFunction( @@ -308,11 +291,7 @@ abstract class BatchesFolder( ) { time -> // The progressbar for the conversion is calculated based on the // current time of the conversion and the total time of the batches. - if (fullTime != null) { - onProgress(time / fullTime!!) - } else { - onProgress(null) - } + onProgress(time / fullTime) }.await() return outputFile } catch (e: MediaConverter.FFmpegException) { @@ -607,19 +586,23 @@ abstract class BatchesFolder( } fun canAccessFolder(context: Context, uri: Uri): Boolean { + // This always returns false for some reason, let's just assume it's true + return true + /* return try { // Create temp file - val tempFile = DocumentFile.fromSingleUri(context, uri)!!.createFile( - "application/octet-stream", - "temp" - )!! - tempFile.delete() + val docFile = DocumentFile.fromSingleUri(context, uri)!! - true + return docFile.canWrite().also { + println("Can write? ${it}") + } && docFile.canRead().also { + println("Can read? ${it}") + } } catch (error: RuntimeException) { error.printStackTrace() false } + */ } } } diff --git a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt index ee39570..ced8302 100644 --- a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt @@ -16,9 +16,7 @@ import app.myzel394.alibi.db.RecordingInformation import app.myzel394.alibi.enums.RecorderState import app.myzel394.alibi.helpers.AudioBatchesFolder import app.myzel394.alibi.helpers.BatchesFolder -import app.myzel394.alibi.ui.SUPPORTS_SCOPED_STORAGE import app.myzel394.alibi.ui.utils.MicrophoneInfo -import java.lang.IllegalStateException class AudioRecorderService : IntervalRecorderService() { @@ -304,12 +302,14 @@ class AudioRecorderService : } // ==== Settings ==== - override fun getRecordingInformation() = RecordingInformation( - folderPath = batchesFolder.exportFolderForSettings(), - recordingStart = recordingStart, - maxDuration = settings.maxDuration, - fileExtension = settings.audioRecorderSettings.fileExtension, - intervalDuration = settings.intervalDuration, - type = RecordingInformation.Type.AUDIO, - ) + override fun getRecordingInformation() = + RecordingInformation( + folderPath = batchesFolder.exportFolderForSettings(), + recordingStart = recordingStart, + maxDuration = settings.maxDuration, + batchesAmount = batchesFolder.getBatchesForFFmpeg().size, + fileExtension = settings.audioRecorderSettings.fileExtension, + intervalDuration = settings.intervalDuration, + type = RecordingInformation.Type.AUDIO, + ) } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt index 4cfc641..4889333 100644 --- a/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt @@ -304,14 +304,16 @@ class VideoRecorderService : this } - override fun getRecordingInformation(): RecordingInformation = RecordingInformation( - folderPath = batchesFolder.exportFolderForSettings(), - recordingStart = recordingStart, - maxDuration = settings.maxDuration, - fileExtension = settings.videoRecorderSettings.fileExtension, - intervalDuration = settings.intervalDuration, - type = RecordingInformation.Type.VIDEO, - ) + override fun getRecordingInformation() = + RecordingInformation( + folderPath = batchesFolder.exportFolderForSettings(), + recordingStart = recordingStart, + maxDuration = settings.maxDuration, + batchesAmount = batchesFolder.getBatchesForFFmpeg().size, + fileExtension = settings.videoRecorderSettings.fileExtension, + intervalDuration = settings.intervalDuration, + type = RecordingInformation.Type.VIDEO, + ) companion object { const val CAMERA_CLOSE_TIMEOUT = 20000L diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/RecorderEventsHandler.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/RecorderEventsHandler.kt index ce137e9..5040e45 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/RecorderEventsHandler.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/RecorderEventsHandler.kt @@ -162,6 +162,7 @@ fun RecorderEventsHandler( // When recording is loaded from lastRecording ?: settings.lastRecording ?: throw Exception("No recording information available") + val batchesFolder = when (recorder.javaClass) { AudioRecorderModel::class.java -> AudioBatchesFolder.importFromFolder( recording.folderPath, @@ -177,9 +178,8 @@ fun RecorderEventsHandler( } batchesFolder.concatenate( - recording.recordingStart, - recording.fileExtension, - durationPerBatchInMilliseconds = settings.intervalDuration, + recording, + filenameFormat = settings.filenameFormat, onProgress = { percentage -> processingProgress = percentage } diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/FilenameFormatTile.kt b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/FilenameFormatTile.kt new file mode 100644 index 0000000..56ff9f6 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/FilenameFormatTile.kt @@ -0,0 +1,235 @@ +package app.myzel394.alibi.ui.components.SettingsScreen.Tiles + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.TextSnippet +import androidx.compose.material.icons.filled.AccessTime +import androidx.compose.material.icons.filled.Circle +import androidx.compose.material.icons.filled.Timelapse +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +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.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import app.myzel394.alibi.R +import app.myzel394.alibi.dataStore +import app.myzel394.alibi.db.AppSettings +import app.myzel394.alibi.ui.SHEET_BOTTOM_OFFSET +import app.myzel394.alibi.ui.components.atoms.SettingsTile +import kotlinx.coroutines.launch + +val FORMAT_RESOURCE_MAP: Map = mapOf( + AppSettings.FilenameFormat.DATETIME_RELATIVE_START to R.string.ui_settings_option_filenameFormat_action_relativeStart_label, + AppSettings.FilenameFormat.DATETIME_ABSOLUTE_START to R.string.ui_settings_option_filenameFormat_action_absoluteStart_label, +) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FilenameFormatTile( + settings: AppSettings, + snackbarHostState: SnackbarHostState, +) { + val scope = rememberCoroutineScope() + val context = LocalContext.current + val dataStore = context.dataStore + + val successMessage = stringResource(R.string.ui_settings_option_filenameFormat_success) + fun updateValue(format: AppSettings.FilenameFormat) { + scope.launch { + dataStore.updateData { + it.setFilenameFormat(format) + } + } + } + + var selectionVisible by remember { mutableStateOf(false) } + val selectionSheetState = rememberModalBottomSheetState(true) + + fun hideSheet() { + scope.launch { + selectionSheetState.hide() + selectionVisible = false + } + } + + if (selectionVisible) { + SelectionSheet( + sheetState = selectionSheetState, + updateValue = { format -> + hideSheet() + + if (format != null) { + updateValue(format) + + scope.launch { + snackbarHostState.showSnackbar( + message = successMessage, + duration = SnackbarDuration.Short, + ) + } + } + }, + onDismiss = ::hideSheet, + ) + } + + SettingsTile( + title = stringResource(R.string.ui_settings_option_filenameFormat_title), + description = stringResource(R.string.ui_settings_option_filenameFormat_explanation), + leading = { + Icon( + Icons.AutoMirrored.Filled.TextSnippet, + contentDescription = null, + ) + }, + trailing = { + Button( + onClick = { + scope.launch { + selectionVisible = true + } + }, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + ), + shape = MaterialTheme.shapes.medium, + ) { + Text( + text = stringResource(FORMAT_RESOURCE_MAP[settings.filenameFormat]!!), + ) + } + }, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SelectionSheet( + sheetState: SheetState, + updateValue: (AppSettings.FilenameFormat?) -> Unit, + onDismiss: () -> Unit, +) { + ModalBottomSheet( + sheetState = sheetState, + onDismissRequest = onDismiss, + ) { + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(bottom = SHEET_BOTTOM_OFFSET) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(24.dp), + ) { + Text( + stringResource(R.string.ui_settings_option_filenameFormat_title), + style = MaterialTheme.typography.headlineMedium, + textAlign = TextAlign.Center, + ) + + SelectionButton( + label = stringResource(R.string.ui_settings_option_filenameFormat_action_absoluteStart_label), + explanation = stringResource(R.string.ui_settings_option_filenameFormat_action_absoluteStart_explanation), + icon = Icons.Default.AccessTime, + onClick = { + updateValue(AppSettings.FilenameFormat.DATETIME_ABSOLUTE_START) + }, + ) + + HorizontalDivider() + + SelectionButton( + label = stringResource(R.string.ui_settings_option_filenameFormat_action_relativeStart_label), + explanation = stringResource(R.string.ui_settings_option_filenameFormat_action_relativeStart_explanation), + icon = Icons.Default.Timelapse, + onClick = { + updateValue(AppSettings.FilenameFormat.DATETIME_RELATIVE_START) + }, + ) + + HorizontalDivider() + + SelectionButton( + label = stringResource(R.string.ui_settings_option_filenameFormat_action_now_label), + explanation = stringResource(R.string.ui_settings_option_filenameFormat_action_now_explanation), + icon = Icons.Default.Circle, + onClick = { + updateValue(AppSettings.FilenameFormat.DATETIME_RELATIVE_START) + }, + ) + } + } +} + +@Composable +private fun SelectionButton( + label: String, + explanation: String, + icon: ImageVector, + onClick: () -> Unit, +) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .clip(MaterialTheme.shapes.medium) + .semantics { + contentDescription = label + } + .clickable { + onClick() + } + .padding(horizontal = 16.dp) + ) { + Icon( + icon, + contentDescription = null, + modifier = Modifier + .size(ButtonDefaults.IconSize) + .fillMaxWidth(0.1f), + ) + Column( + modifier = Modifier.fillMaxWidth(0.9f), + ) { + Text(label) + Text( + explanation, + style = MaterialTheme.typography.bodySmall, + ) + } + } +} diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/SaveFolderTile.kt b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/SaveFolderTile.kt index 4d40c27..126753f 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/SaveFolderTile.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/SaveFolderTile.kt @@ -489,7 +489,7 @@ fun InternalFolderExplanationDialog( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun SelectionSheet( +private fun SelectionSheet( sheetState: SheetState, updateValue: (String?) -> Unit, onDismiss: () -> Unit, @@ -596,7 +596,7 @@ fun SelectionSheet( } @Composable -fun SelectionButton( +private fun SelectionButton( label: String, icon: ImageVector, onClick: () -> Unit, diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/SaveFolderPage.kt b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/SaveFolderPage.kt index 8e1dec5..4edf1d1 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/SaveFolderPage.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/SaveFolderPage.kt @@ -1,6 +1,7 @@ package app.myzel394.alibi.ui.components.WelcomeScreen.pages import android.Manifest +import android.content.Intent import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -19,6 +20,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.InsertDriveFile import androidx.compose.material.icons.filled.ChevronLeft import androidx.compose.material.icons.filled.ChevronRight +import androidx.compose.material.icons.filled.Error import androidx.compose.material.icons.filled.Folder import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button @@ -156,6 +158,17 @@ fun SaveFolderPage( contentDescription = null, ) } + + var showError by rememberSaveable { mutableStateOf(false) } + + if (showError) { + _FolderInaccessibleDialog( + onClose = { + showError = false + } + ) + } + PermissionRequester( permission = Manifest.permission.WRITE_EXTERNAL_STORAGE, icon = Icons.AutoMirrored.Filled.InsertDriveFile, @@ -166,7 +179,23 @@ fun SaveFolderPage( return@rememberFolderSelectorDialog } - onContinue(saveFolder) + context.contentResolver.takePersistableUriPermission( + folder, + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + + if (BatchesFolder.canAccessFolder(context, folder)) { + onContinue(folder.toString()) + } else { + showError = true + + runCatching { + context.contentResolver.releasePersistableUriPermission( + folder, + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + } + } } var showCustomFolderHint by rememberSaveable { mutableStateOf(false) } @@ -216,6 +245,44 @@ fun SaveFolderPage( } } +@Composable +fun _FolderInaccessibleDialog( + onClose: () -> Unit, +) { + AlertDialog( + onDismissRequest = onClose, + icon = { + Icon( + Icons.Default.Error, + contentDescription = null, + ) + }, + title = { + Text(stringResource(R.string.ui_error_occurred_title)) + }, + confirmButton = { + Button(onClick = onClose) { + Text(stringResource(R.string.dialog_close_neutral_label)) + } + }, + text = { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(32.dp), + ) { + Text( + stringResource(R.string.ui_settings_option_saveFolder_batchesFolderInaccessible_error), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurface, + ) + } + } + ) +} + @Composable fun _CustomFolderDialog( onAbort: () -> Unit, diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt index 8881c1e..1381b7b 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt @@ -45,6 +45,7 @@ import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.CustomNotificationT import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.DeleteRecordingsImmediatelyTile import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.DividerTitle import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.EnableAppLockTile +import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.FilenameFormatTile import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.ImportExport import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.IntervalDurationTile import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.MaxDurationTile @@ -145,6 +146,7 @@ fun SettingsScreen( DeleteRecordingsImmediatelyTile(settings = settings) CustomNotificationTile(onNavigateToCustomRecordingNotifications, settings = settings) EnableAppLockTile(settings = settings) + FilenameFormatTile(settings = settings, snackbarHostState = snackbarHostState) SaveFolderTile( settings = settings, snackbarHostState = snackbarHostState, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 94ec34c..25dbb43 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -224,4 +224,13 @@ 1 Minute There was an error Alibi can\'t access this folder. Please select a different one + Filename Format + How should the file be named? + Absolute start + Use the time you started the recording + Recording start + Use the time the actual recording starts at + The new format will be used for future recordings + Save time + Use the time you saved the recording \ No newline at end of file