Merge pull request #42 from Myzel394/delete-recordings-immediately-and-fix-dir

Delete recordings immediately and fix dir
This commit is contained in:
Myzel394 2023-10-26 20:11:41 +02:00 committed by GitHub
commit dd58ce23a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 293 additions and 162 deletions

View File

@ -2,7 +2,6 @@ package app.myzel394.alibi.db
import android.media.MediaRecorder
import android.os.Build
import android.util.Log
import app.myzel394.alibi.R
import com.arthenica.ffmpegkit.FFmpegKit
import com.arthenica.ffmpegkit.ReturnCode
@ -19,6 +18,7 @@ data class AppSettings(
val hasSeenOnboarding: Boolean = false,
val showAdvancedSettings: Boolean = false,
val theme: Theme = Theme.SYSTEM,
val lastRecording: RecordingInformation? = null,
) {
fun setShowAdvancedSettings(showAdvancedSettings: Boolean): AppSettings {
return copy(showAdvancedSettings = showAdvancedSettings)
@ -40,6 +40,10 @@ data class AppSettings(
return copy(theme = theme)
}
fun setLastRecording(lastRecording: RecordingInformation?): AppSettings {
return copy(lastRecording = lastRecording)
}
enum class Theme {
SYSTEM,
LIGHT,
@ -63,7 +67,7 @@ data class AppSettings(
}
@Serializable
data class LastRecording(
data class RecordingInformation(
val folderPath: String,
@Serializable(with = LocalDateTimeSerializer::class)
val recordingStart: LocalDateTime,
@ -72,91 +76,8 @@ data class LastRecording(
val fileExtension: String,
val forceExactMaxDuration: Boolean,
) {
val fileFolder: File
get() = File(folderPath)
val filePaths: List<File>
get() =
File(folderPath).listFiles()?.filter {
val name = it.nameWithoutExtension
name.toIntOrNull() != null
}?.toList() ?: emptyList()
val hasRecordingAvailable: Boolean
get() = filePaths.isNotEmpty()
private fun stripConcatenatedFileToExactDuration(
outputFile: File
) {
// Move the concatenated file to a temporary file
val rawFile = File("$folderPath/${outputFile.nameWithoutExtension}-raw.${fileExtension}")
outputFile.renameTo(rawFile)
val command = "-sseof ${maxDuration / -1000} -i $rawFile -y $outputFile"
val session = FFmpegKit.execute(command)
if (!ReturnCode.isSuccess(session.returnCode)) {
Log.d(
"Audio Concatenation",
String.format(
"Command failed with state %s and rc %s.%s",
session.getState(),
session.getReturnCode(),
session.getFailStackTrace()
)
)
throw Exception("Failed to strip concatenated audio")
}
}
suspend fun concatenateFiles(forceConcatenation: Boolean = false): File {
val paths = filePaths.joinToString("|")
val fileName = recordingStart
.format(ISO_DATE_TIME)
.toString()
.replace(":", "-")
.replace(".", "_")
val outputFile = File("$fileFolder/$fileName.${fileExtension}")
if (outputFile.exists() && !forceConcatenation) {
return outputFile
}
val command = "-i 'concat:$paths' -y" +
" -acodec copy" +
" -metadata title='$fileName' " +
" -metadata date='${recordingStart.format(ISO_DATE_TIME)}'" +
" -metadata batch_count='${filePaths.size}'" +
" -metadata batch_duration='${intervalDuration}'" +
" -metadata max_duration='${maxDuration}'" +
" $outputFile"
val session = FFmpegKit.execute(command)
if (!ReturnCode.isSuccess(session.returnCode)) {
Log.d(
"Audio Concatenation",
String.format(
"Command failed with state %s and rc %s.%s",
session.getState(),
session.getReturnCode(),
session.getFailStackTrace()
)
)
throw Exception("Failed to concatenate audios")
}
val minRequiredForPossibleInExactMaxDuration = maxDuration / intervalDuration
if (forceExactMaxDuration && filePaths.size > minRequiredForPossibleInExactMaxDuration) {
stripConcatenatedFileToExactDuration(outputFile)
}
return outputFile
}
val hasRecordingsAvailable
get() = File(folderPath).listFiles()?.isNotEmpty() ?: false
}
@Serializable
@ -172,6 +93,7 @@ data class AudioRecorderSettings(
val outputFormat: Int? = null,
val encoder: Int? = null,
val showAllMicrophones: Boolean = false,
val deleteRecordingsImmediately: Boolean = false,
) {
fun getOutputFormat(): Int {
if (outputFormat != null) {
@ -303,6 +225,10 @@ data class AudioRecorderSettings(
return copy(showAllMicrophones = showAllMicrophones)
}
fun setDeleteRecordingsImmediately(deleteRecordingsImmediately: Boolean): AudioRecorderSettings {
return copy(deleteRecordingsImmediately = deleteRecordingsImmediately)
}
fun isEncoderCompatible(encoder: Int): Boolean {
if (outputFormat == null || outputFormat == MediaRecorder.OutputFormat.DEFAULT) {
return true

View File

@ -0,0 +1,118 @@
package app.myzel394.alibi.helpers
import android.content.Context
import android.util.Log
import app.myzel394.alibi.db.RecordingInformation
import app.myzel394.alibi.ui.RECORDER_SUBFOLDER_NAME
import com.arthenica.ffmpegkit.FFmpegKit
import com.arthenica.ffmpegkit.ReturnCode
import java.io.File
import java.time.format.DateTimeFormatter
data class AudioRecorderExporter(
val recording: RecordingInformation,
) {
val filePaths: List<File>
get() =
File(recording.folderPath).listFiles()?.filter {
val name = it.nameWithoutExtension
name.toIntOrNull() != null
}?.toList() ?: emptyList()
val hasRecordingAvailable: Boolean
get() = filePaths.isNotEmpty()
private fun stripConcatenatedFileToExactDuration(
outputFile: File
) {
// Move the concatenated file to a temporary file
val rawFile =
File("${recording.folderPath}/${outputFile.nameWithoutExtension}-raw.${recording.fileExtension}")
outputFile.renameTo(rawFile)
val command = "-sseof ${recording.maxDuration / -1000} -i $rawFile -y $outputFile"
val session = FFmpegKit.execute(command)
if (!ReturnCode.isSuccess(session.returnCode)) {
Log.d(
"Audio Concatenation",
String.format(
"Command failed with state %s and rc %s.%s",
session.state,
session.returnCode,
session.failStackTrace,
)
)
throw Exception("Failed to strip concatenated audio")
}
}
suspend fun concatenateFiles(forceConcatenation: Boolean = false): File {
val paths = filePaths.joinToString("|")
val fileName = recording.recordingStart
.format(DateTimeFormatter.ISO_DATE_TIME)
.toString()
.replace(":", "-")
.replace(".", "_")
val outputFile = File("${recording.folderPath}/$fileName.${recording.fileExtension}")
if (outputFile.exists() && !forceConcatenation) {
return outputFile
}
val command = "-i 'concat:$paths' -y" +
" -acodec copy" +
" -metadata title='$fileName' " +
" -metadata date='${recording.recordingStart.format(DateTimeFormatter.ISO_DATE_TIME)}'" +
" -metadata batch_count='${filePaths.size}'" +
" -metadata batch_duration='${recording.intervalDuration}'" +
" -metadata max_duration='${recording.maxDuration}'" +
" $outputFile"
val session = FFmpegKit.execute(command)
if (!ReturnCode.isSuccess(session.returnCode)) {
Log.d(
"Audio Concatenation",
String.format(
"Command failed with state %s and rc %s.%s",
session.state,
session.returnCode,
session.failStackTrace,
)
)
throw Exception("Failed to concatenate audios")
}
val minRequiredForPossibleInExactMaxDuration =
recording.maxDuration / recording.intervalDuration
if (recording.forceExactMaxDuration && filePaths.size > minRequiredForPossibleInExactMaxDuration) {
stripConcatenatedFileToExactDuration(outputFile)
}
return outputFile
}
suspend fun cleanupFiles() {
filePaths.forEach {
runCatching {
it.delete()
}
}
}
companion object {
fun getFolder(context: Context) = File(context.filesDir, RECORDER_SUBFOLDER_NAME)
fun clearAllRecordings(context: Context) {
getFolder(context).deleteRecursively()
}
fun hasRecordingsAvailable(context: Context) =
getFolder(context).listFiles()?.isNotEmpty() ?: false
}
}

View File

@ -28,7 +28,7 @@ class AudioRecorderService : IntervalRecorderService() {
var onMicrophoneReconnected: () -> Unit = {}
val filePath: String
get() = "$folder/$counter.${settings!!.fileExtension}"
get() = "${outputFolder}/$counter.${settings!!.fileExtension}"
/// Tell Android to use the correct bluetooth microphone, if any selected
private fun startAudioDevice() {

View File

@ -1,39 +1,37 @@
package app.myzel394.alibi.services
import android.content.Context
import android.media.MediaRecorder
import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.AudioRecorderSettings
import app.myzel394.alibi.db.LastRecording
import app.myzel394.alibi.db.RecordingInformation
import app.myzel394.alibi.helpers.AudioRecorderExporter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.io.File
import java.time.LocalDateTime
import java.util.Timer
import java.util.TimerTask
import java.util.UUID
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
abstract class IntervalRecorderService: ExtraRecorderInformationService() {
abstract class IntervalRecorderService : ExtraRecorderInformationService() {
private var job = SupervisorJob()
private var scope = CoroutineScope(Dispatchers.IO + job)
protected var counter = 0
private set
protected lateinit var folder: File
var settings: Settings? = null
protected set
private lateinit var cycleTimer: ScheduledExecutorService
fun createLastRecording(): LastRecording = LastRecording(
folderPath = folder.absolutePath,
protected val outputFolder: File
get() = AudioRecorderExporter.getFolder(this)
fun getRecordingInformation(): RecordingInformation = RecordingInformation(
folderPath = outputFolder.absolutePath,
recordingStart = recordingStart,
maxDuration = settings!!.maxDuration,
fileExtension = settings!!.fileExtension,
@ -60,18 +58,10 @@ abstract class IntervalRecorderService: ExtraRecorderInformationService() {
}
}
private fun getRandomFileFolder(): String {
// uuid
val folder = UUID.randomUUID().toString()
return "${externalCacheDir!!.absolutePath}/$folder"
}
override fun start() {
super.start()
folder = File(getRandomFileFolder())
folder.mkdirs()
outputFolder.mkdirs()
scope.launch {
dataStore.data.collectLatest { preferenceSettings ->
@ -104,7 +94,7 @@ abstract class IntervalRecorderService: ExtraRecorderInformationService() {
val timeMultiplier = settings!!.maxDuration / settings!!.intervalDuration
val earliestCounter = counter - timeMultiplier
folder.listFiles()?.forEach { file ->
outputFolder.listFiles()?.forEach { file ->
val fileCounter = file.nameWithoutExtension.toIntOrNull() ?: return
if (fileCounter < earliestCounter) {
@ -123,7 +113,7 @@ abstract class IntervalRecorderService: ExtraRecorderInformationService() {
val encoder: Int,
) {
val fileExtension: String
get() = when(outputFormat) {
get() = when (outputFormat) {
MediaRecorder.OutputFormat.AAC_ADTS -> "aac"
MediaRecorder.OutputFormat.THREE_GPP -> "3gp"
MediaRecorder.OutputFormat.MPEG_4 -> "mp4"

View File

@ -2,10 +2,12 @@ package app.myzel394.alibi.ui
import android.os.Build
import androidx.compose.ui.unit.dp
import java.io.File
val BIG_PRIMARY_BUTTON_SIZE = 64.dp
val MAX_AMPLITUDE = 20000
val SUPPORTS_DARK_MODE_NATIVELY = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
val RECORDER_SUBFOLDER_NAME = ".recordings"
// You are not allowed to change the constants below.
// If you do so, you will be blocked on GitHub.

View File

@ -1,6 +1,5 @@
package app.myzel394.alibi.ui
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@ -13,10 +12,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
@ -24,11 +19,10 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.LastRecording
import app.myzel394.alibi.ui.enums.Screen
import app.myzel394.alibi.ui.models.AudioRecorderModel
import app.myzel394.alibi.ui.screens.AboutScreen
import app.myzel394.alibi.ui.screens.AudioRecorder
import app.myzel394.alibi.ui.screens.AudioRecorderScreen
import app.myzel394.alibi.ui.screens.CustomRecordingNotificationsScreen
import app.myzel394.alibi.ui.screens.SettingsScreen
import app.myzel394.alibi.ui.screens.WelcomeScreen
@ -76,7 +70,7 @@ fun Navigation(
scaleOut(targetScale = SCALE_IN) + fadeOut(tween(durationMillis = 150))
}
) {
AudioRecorder(
AudioRecorderScreen(
navController = navController,
audioRecorder = audioRecorder,
)

View File

@ -42,6 +42,7 @@ import app.myzel394.alibi.NotificationHelper
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.services.RecorderNotificationHelper
import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE
import app.myzel394.alibi.ui.components.atoms.PermissionRequester
@ -59,7 +60,8 @@ fun StartRecording(
// Loading this from parent, because if we load it ourselves
// and permissions have already been granted, initial
// settings will be used, instead of the actual settings.
appSettings: AppSettings
appSettings: AppSettings,
onSaveLastRecording: () -> Unit,
) {
val context = LocalContext.current
@ -79,6 +81,9 @@ fun StartRecording(
it
)
}
AudioRecorderExporter.clearAllRecordings(context)
audioRecorder.startRecording(context)
}
}
@ -144,7 +149,7 @@ fun StartRecording(
.fillMaxWidth(),
textAlign = TextAlign.Center,
)
if (audioRecorder.lastRecording != null && audioRecorder.lastRecording!!.hasRecordingAvailable) {
if (appSettings.lastRecording?.hasRecordingsAvailable == true) {
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,
@ -153,7 +158,7 @@ fun StartRecording(
val label = stringResource(
R.string.ui_audioRecorder_action_saveOldRecording_label,
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)
.format(audioRecorder.lastRecording!!.recordingStart),
.format(appSettings.lastRecording.recordingStart),
)
Button(
modifier = Modifier
@ -164,10 +169,7 @@ fun StartRecording(
contentDescription = label
},
colors = ButtonDefaults.textButtonColors(),
onClick = {
audioRecorder.stopRecording(context)
audioRecorder.onRecordingSave()
},
onClick = onSaveLastRecording,
) {
Icon(
Icons.Default.Save,

View File

@ -26,6 +26,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.helpers.AudioRecorderExporter
import app.myzel394.alibi.ui.components.AudioRecorder.atoms.DeleteButton
import app.myzel394.alibi.ui.components.AudioRecorder.atoms.MicrophoneDisconnectedDialog
import app.myzel394.alibi.ui.components.AudioRecorder.atoms.MicrophoneReconnectedDialog
@ -105,7 +106,9 @@ fun RecordingStatus(
) {
DeleteButton(
onDelete = {
audioRecorder.stopRecording(context, saveAsLastRecording = false)
audioRecorder.stopRecording(context)
AudioRecorderExporter.clearAllRecordings(context)
}
)
}

View File

@ -0,0 +1,52 @@
package app.myzel394.alibi.ui.components.SettingsScreen.atoms
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DeleteSweep
import androidx.compose.material3.Icon
import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import app.myzel394.alibi.R
import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.ui.components.atoms.SettingsTile
import kotlinx.coroutines.launch
@Composable
fun DeleteRecordingsImmediatelyTile() {
val scope = rememberCoroutineScope()
val dataStore = LocalContext.current.dataStore
val settings = dataStore
.data
.collectAsState(initial = AppSettings.getDefaultInstance())
.value
SettingsTile(
title = stringResource(R.string.ui_settings_option_deleteRecordingsImmediately_title),
description = stringResource(R.string.ui_settings_option_deleteRecordingsImmediately_description),
leading = {
Icon(
Icons.Default.DeleteSweep,
contentDescription = null,
)
},
trailing = {
Switch(
checked = settings.audioRecorderSettings.deleteRecordingsImmediately,
onCheckedChange = {
scope.launch {
dataStore.updateData {
it.setAudioRecorderSettings(
it.audioRecorderSettings.setDeleteRecordingsImmediately(it.audioRecorderSettings.deleteRecordingsImmediately.not())
)
}
}
}
)
}
)
}

View File

@ -10,13 +10,11 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel
import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.LastRecording
import app.myzel394.alibi.db.RecordingInformation
import app.myzel394.alibi.enums.RecorderState
import app.myzel394.alibi.services.AudioRecorderService
import app.myzel394.alibi.services.RecorderNotificationHelper
import app.myzel394.alibi.services.RecorderService
import kotlinx.coroutines.flow.last
import kotlinx.serialization.json.Json
import app.myzel394.alibi.ui.utils.MicrophoneInfo
@ -44,9 +42,6 @@ class AudioRecorderModel : ViewModel() {
var recorderService: AudioRecorderService? = null
private set
var lastRecording: LastRecording? by mutableStateOf<LastRecording?>(null)
private set
var onRecordingSave: () -> Unit = {}
var onError: () -> Unit = {}
var notificationDetails: RecorderNotificationHelper.NotificationDetails? = null
@ -75,7 +70,6 @@ class AudioRecorderModel : ViewModel() {
onAmplitudeChange()
}
recorder.onError = {
recorderService!!.createLastRecording()
onError()
}
recorder.onSelectedMicrophoneChange = { microphone ->
@ -134,11 +128,7 @@ class AudioRecorderModel : ViewModel() {
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
fun stopRecording(context: Context, saveAsLastRecording: Boolean = true) {
if (saveAsLastRecording) {
lastRecording = recorderService!!.createLastRecording()
}
fun stopRecording(context: Context) {
runCatching {
context.unbindService(connection)
}

View File

@ -36,8 +36,8 @@ import app.myzel394.alibi.ui.utils.rememberFileSaverDialog
import app.myzel394.alibi.R
import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.db.LastRecording
import app.myzel394.alibi.services.RecorderNotificationHelper
import app.myzel394.alibi.db.RecordingInformation
import app.myzel394.alibi.helpers.AudioRecorderExporter
import app.myzel394.alibi.ui.effects.rememberSettings
import app.myzel394.alibi.ui.models.AudioRecorderModel
import kotlinx.coroutines.delay
@ -45,40 +45,80 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AudioRecorder(
fun AudioRecorderScreen(
navController: NavController,
audioRecorder: AudioRecorderModel,
) {
val context = LocalContext.current
val dataStore = context.dataStore
val settings = rememberSettings()
val saveFile = rememberFileSaverDialog(settings.audioRecorderSettings.getMimeType())
val scope = rememberCoroutineScope()
val saveFile = rememberFileSaverDialog(
settings.audioRecorderSettings.getMimeType()
) {
if (settings.audioRecorderSettings.deleteRecordingsImmediately) {
AudioRecorderExporter.clearAllRecordings(context)
}
if (!AudioRecorderExporter.hasRecordingsAvailable(context)) {
scope.launch {
dataStore.updateData {
it.setLastRecording(null)
}
}
}
}
var isProcessingAudio by remember { mutableStateOf(false) }
var showRecorderError by remember { mutableStateOf(false) }
DisposableEffect(Unit) {
audioRecorder.onRecordingSave = {
fun saveAsLastRecording() {
if (!settings.audioRecorderSettings.deleteRecordingsImmediately) {
scope.launch {
isProcessingAudio = true
// Give the user some time to see the processing dialog
delay(100)
try {
val file = audioRecorder.lastRecording!!.concatenateFiles()
saveFile(file, file.name)
} catch (error: Exception) {
Log.getStackTraceString(error)
} finally {
isProcessingAudio = false
dataStore.updateData {
it.setLastRecording(
audioRecorder.recorderService!!.getRecordingInformation()
)
}
}
}
}
fun saveRecording() {
scope.launch {
isProcessingAudio = true
// Give the user some time to see the processing dialog
delay(100)
try {
val file = AudioRecorderExporter(
audioRecorder.recorderService?.getRecordingInformation()
?: settings.lastRecording
?: throw Exception("No recording information available"),
).concatenateFiles()
saveFile(file, file.name)
} catch (error: Exception) {
Log.getStackTraceString(error)
} finally {
isProcessingAudio = false
}
}
}
DisposableEffect(key1 = audioRecorder, key2 = settings) {
audioRecorder.onRecordingSave = onRecordingSave@{
saveAsLastRecording()
saveRecording()
}
audioRecorder.onError = {
// No need to save last recording as it's done automatically on error
audioRecorder.stopRecording(context, saveAsLastRecording = false)
saveAsLastRecording()
audioRecorder.stopRecording(context)
showRecorderError = true
}
@ -141,7 +181,9 @@ fun AudioRecorder(
confirmButton = {
Button(
onClick = {
audioRecorder.onRecordingSave()
showRecorderError = false
saveRecording()
},
colors = ButtonDefaults.textButtonColors(),
) {
@ -181,7 +223,10 @@ fun AudioRecorder(
if (audioRecorder.isInRecording)
RecordingStatus(audioRecorder = audioRecorder)
else
StartRecording(audioRecorder = audioRecorder, appSettings = appSettings)
StartRecording(
audioRecorder = audioRecorder, appSettings = appSettings,
onSaveLastRecording = ::saveRecording,
)
}
}
}

View File

@ -42,6 +42,7 @@ import app.myzel394.alibi.ui.SUPPORTS_DARK_MODE_NATIVELY
import app.myzel394.alibi.ui.components.SettingsScreen.atoms.AboutTile
import app.myzel394.alibi.ui.components.SettingsScreen.atoms.BitrateTile
import app.myzel394.alibi.ui.components.SettingsScreen.atoms.CustomNotificationTile
import app.myzel394.alibi.ui.components.SettingsScreen.atoms.DeleteRecordingsImmediatelyTile
import app.myzel394.alibi.ui.components.SettingsScreen.atoms.EncoderTile
import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ForceExactMaxDurationTile
import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ImportExport
@ -148,6 +149,7 @@ fun SettingsScreen(
IntervalDurationTile()
ForceExactMaxDurationTile()
InAppLanguagePicker()
DeleteRecordingsImmediatelyTile()
CustomNotificationTile(navController = navController)
AboutTile(navController = navController)
AnimatedVisibility(visible = settings.showAdvancedSettings) {

View File

@ -12,7 +12,10 @@ import androidx.compose.ui.platform.LocalContext
import java.io.File
@Composable
fun rememberFileSaverDialog(mimeType: String): ((File, String) -> Unit) {
fun rememberFileSaverDialog(
mimeType: String,
callback: (Uri?) -> Unit = {},
): ((File, String) -> Unit) {
val context = LocalContext.current
var file = remember { mutableStateOf<File?>(null) }
@ -28,6 +31,8 @@ fun rememberFileSaverDialog(mimeType: String): ((File, String) -> Unit) {
}
file.value = null
callback(it)
}
return { it, name ->

View File

@ -107,4 +107,6 @@
<string name="ui_about_gpg_key_hint">You can copy my GPG key here. This key only exists once and I can use it to prove to you that I\'m really who I am. Please save it now so that you can verify my signature later.</string>
<string name="ui_about_gpg_key_copy">Copy GPG Key</string>
<string name="ui_about_contribute_donation_githubSponsors">Become a GitHub Sponsor</string>
<string name="ui_settings_option_deleteRecordingsImmediately_title">Delete Recordings Immediately</string>
<string name="ui_settings_option_deleteRecordingsImmediately_description">If enabled, Alibi will immediately delete recordings after you have saved the file.</string>
</resources>