mirror of
https://github.com/Myzel394/Alibi.git
synced 2025-06-18 14:55:26 +02:00
Merge pull request #108 from Myzel394/fix-105-custom-filename
Add custom filename option
This commit is contained in:
commit
a88507a905
@ -41,4 +41,4 @@ jobs:
|
||||
track: production
|
||||
status: inProgress
|
||||
inAppUpdatePriority: 2
|
||||
userFraction: 0.33
|
||||
userFraction: 0.2
|
||||
|
@ -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'
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<RecordingInformation, AudioBatchesFolder>() {
|
||||
@ -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,
|
||||
)
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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<AppSettings.FilenameFormat, Int> = 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -224,4 +224,13 @@
|
||||
<string name="ui_welcome_timeSettings_values_1min">1 Minute</string>
|
||||
<string name="ui_error_occurred_title">There was an error</string>
|
||||
<string name="ui_settings_option_saveFolder_batchesFolderInaccessible_error">Alibi can\'t access this folder. Please select a different one</string>
|
||||
<string name="ui_settings_option_filenameFormat_title">Filename Format</string>
|
||||
<string name="ui_settings_option_filenameFormat_explanation">How should the file be named?</string>
|
||||
<string name="ui_settings_option_filenameFormat_action_absoluteStart_label">Absolute start</string>
|
||||
<string name="ui_settings_option_filenameFormat_action_absoluteStart_explanation">Use the time you started the recording</string>
|
||||
<string name="ui_settings_option_filenameFormat_action_relativeStart_label">Recording start</string>
|
||||
<string name="ui_settings_option_filenameFormat_action_relativeStart_explanation">Use the time the actual recording starts at</string>
|
||||
<string name="ui_settings_option_filenameFormat_success">The new format will be used for future recordings</string>
|
||||
<string name="ui_settings_option_filenameFormat_action_now_label">Save time</string>
|
||||
<string name="ui_settings_option_filenameFormat_action_now_explanation">Use the time you saved the recording</string>
|
||||
</resources>
|
Loading…
x
Reference in New Issue
Block a user