From aa8fd2a37f49dd96aaf86f4b4501244c05f8fc9b Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 30 Oct 2023 14:02:02 +0100 Subject: [PATCH] feat: Add SaveFolderTile --- .../java/app/myzel394/alibi/db/AppSettings.kt | 4 - .../alibi/helpers/AudioRecorderExporter.kt | 4 - .../SettingsScreen/atoms/SaveFolderTile.kt | 196 ++++++++++++++++++ .../alibi/ui/screens/SettingsScreen.kt | 2 + .../java/app/myzel394/alibi/ui/utils/file.kt | 15 ++ app/src/main/res/values/strings.xml | 9 + 6 files changed, 222 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt 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 8c612e7..a86ea23 100644 --- a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt +++ b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt @@ -173,10 +173,6 @@ data class AudioRecorderSettings( runCatching { return File(saveFolder!!).apply { - if (!AudioRecorderExporter.canFolderBeUsed(this)) { - throw SecurityException("Can't write to folder") - } - if (!exists()) { mkdirs() } diff --git a/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt index c2f2fe8..5c1d54c 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt @@ -114,9 +114,5 @@ data class AudioRecorderExporter( fun hasRecordingsAvailable(context: Context) = getFolder(context).listFiles()?.isNotEmpty() ?: false - - // Write required for saving the audio files - // Read required for concatenating the audio files - fun canFolderBeUsed(file: File) = file.canRead() && file.canWrite() } } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt new file mode 100644 index 0000000..113b549 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/SaveFolderTile.kt @@ -0,0 +1,196 @@ +package app.myzel394.alibi.ui.components.SettingsScreen.atoms + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AudioFile +import androidx.compose.material.icons.filled.Cancel +import androidx.compose.material.icons.filled.Folder +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.core.net.toFile +import app.myzel394.alibi.R +import app.myzel394.alibi.dataStore +import app.myzel394.alibi.db.AppSettings +import app.myzel394.alibi.helpers.AudioRecorderExporter +import app.myzel394.alibi.ui.components.atoms.SettingsTile +import app.myzel394.alibi.ui.utils.rememberFolderSelectorDialog +import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState +import kotlinx.coroutines.launch +import java.io.File + +@Composable +fun SaveFolderTile( + settings: AppSettings, +) { + val scope = rememberCoroutineScope() + val context = LocalContext.current + val dataStore = context.dataStore + + fun updateValue(path: String?) { + scope.launch { + dataStore.updateData { + it.setAudioRecorderSettings( + it.audioRecorderSettings.setSaveFolder(path) + ) + } + } + } + + val selectFolder = rememberFolderSelectorDialog { folder -> + if (folder == null) { + return@rememberFolderSelectorDialog + } + + updateValue(folder.path) + } + + var showWarning by remember { mutableStateOf(false) } + + if (showWarning) { + val title = stringResource(R.string.ui_settings_option_saveFolder_warning_title) + val text = stringResource(R.string.ui_settings_option_saveFolder_warning_text) + + AlertDialog( + icon = { + Icon( + Icons.Default.Warning, + contentDescription = null, + ) + }, + onDismissRequest = { + showWarning = false + }, + title = { + Text(text = title) + }, + text = { + Text(text = text) + }, + confirmButton = { + Button( + onClick = { + showWarning = false + selectFolder() + }, + ) { + Text( + text = stringResource(R.string.ui_settings_option_saveFolder_warning_action_confirm), + ) + } + }, + dismissButton = { + Button( + onClick = { + showWarning = false + }, + colors = ButtonDefaults.textButtonColors(), + ) { + Icon( + Icons.Default.Cancel, + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) + Text(stringResource(R.string.dialog_close_cancel_label)) + } + } + ) + } + + SettingsTile( + title = stringResource(R.string.ui_settings_option_saveFolder_title), + description = stringResource(R.string.ui_settings_option_saveFolder_explanation), + leading = { + Icon( + Icons.Default.AudioFile, + contentDescription = null, + ) + }, + trailing = { + Button( + onClick = { + showWarning = true + }, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + ), + shape = MaterialTheme.shapes.medium, + ) { + Icon( + Icons.Default.Folder, + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + Spacer( + modifier = Modifier.size(ButtonDefaults.IconSpacing) + ) + Text( + text = stringResource(R.string.ui_settings_option_saveFolder_action_select_label), + ) + } + }, + extra = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (settings.audioRecorderSettings.saveFolder != null) { + Button( + colors = ButtonDefaults.filledTonalButtonColors(), + onClick = { + updateValue(null) + } + ) { + Icon( + Icons.Default.Lock, + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + Spacer( + modifier = Modifier.size(ButtonDefaults.IconSpacing) + ) + Text( + text = stringResource(R.string.ui_settings_option_saveFolder_action_default_label), + ) + } + } + Text( + text = stringResource( + R.string.form_value_selected, + settings.audioRecorderSettings.saveFolder + ?: stringResource(R.string.ui_settings_option_saveFolder_defaultValue) + ), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + } + } + ) +} 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 0a1b4f9..b4f5247 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 @@ -51,6 +51,7 @@ import app.myzel394.alibi.ui.components.SettingsScreen.atoms.IntervalDurationTil import app.myzel394.alibi.ui.components.SettingsScreen.atoms.MaxDurationTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.OutputFormatTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.SamplingRateTile +import app.myzel394.alibi.ui.components.SettingsScreen.atoms.SaveFolderTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ShowAllMicrophonesTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ThemeSelector import app.myzel394.alibi.ui.components.atoms.GlobalSwitch @@ -161,6 +162,7 @@ fun SettingsScreen( .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 32.dp) ) + SaveFolderTile(settings = settings) ShowAllMicrophonesTile(settings = settings) BitrateTile(settings = settings) SamplingRateTile(settings = settings) diff --git a/app/src/main/java/app/myzel394/alibi/ui/utils/file.kt b/app/src/main/java/app/myzel394/alibi/ui/utils/file.kt index f90d01b..1dc5580 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/utils/file.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/utils/file.kt @@ -56,3 +56,18 @@ fun rememberFileSelectorDialog( launcher.launch(arrayOf(mimeType)) } } + +@Composable +fun rememberFolderSelectorDialog( + callback: (Uri?) -> Unit +): (() -> Unit) { + val launcher = + rememberLauncherForActivityResult( + ActivityResultContracts.OpenDocumentTree(), + callback, + ) + + return { + launcher.launch(null) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a1328ef..52b5e83 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,6 +9,7 @@ Please enter a valid number Please enter a number between %s and %s Please enter a number greater than %s + Selected: %s Recorder Shows the current recording status @@ -109,4 +110,12 @@ Become a GitHub Sponsor Delete Recordings Immediately If enabled, Alibi will immediately delete recordings after you have saved the file. + Batches folder + Where Alibi should store the temporary batches of your recordings. + Select + Encrypted Internal Storage + Are you sure you want to change the folder? + By default, Alibi will save the recording batches into its private, encrypted file storage. You can change this and specify an external, unencrypted folder. This will allow you to access the batches manually. ONLY DO THIS IF YOU KNOW WHAT YOU ARE DOING! + Yes, change folder + Use private, encrypted storage \ No newline at end of file