From b98a8a2103b63c0e4d2007a869862b07d5a3bc74 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 10 Mar 2024 19:52:19 +0100 Subject: [PATCH 01/39] chore: Update dependencies --- app/build.gradle | 10 +++++----- build.gradle | 4 ++-- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 7f147f9..3e02c26 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -97,12 +97,12 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' implementation 'androidx.activity:activity-compose:1.8.2' implementation 'androidx.activity:activity-ktx:1.8.2' - implementation platform('androidx.compose:compose-bom:2022.10.00') + implementation platform('androidx.compose:compose-bom:2024.02.02') implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.ui:ui-graphics' implementation 'androidx.compose.ui:ui-tooling-preview' - implementation 'androidx.compose.material3:material3:1.2.0' - implementation "androidx.compose.material:material-icons-extended:1.6.2" + implementation 'androidx.compose.material3:material3:1.2.1' + implementation "androidx.compose.material:material-icons-extended:1.6.3" implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.lifecycle:lifecycle-service:2.7.0' @@ -110,7 +110,7 @@ dependencies { testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00') + androidTestImplementation platform('androidx.compose:compose-bom:2024.02.02') androidTestImplementation 'androidx.compose.ui:ui-test-junit4' debugImplementation 'androidx.compose.ui:ui-tooling' debugImplementation 'androidx.compose.ui:ui-test-manifest' @@ -135,7 +135,7 @@ dependencies { implementation 'com.maxkeppeler.sheets-compose-dialogs:list:1.2.0' implementation 'com.maxkeppeler.sheets-compose-dialogs:input:1.2.0' - def camerax_version = "1.3.1" + def camerax_version = "1.3.2" implementation "androidx.camera:camera-core:${camerax_version}" implementation "androidx.camera:camera-camera2:${camerax_version}" implementation "androidx.camera:camera-lifecycle:${camerax_version}" diff --git a/build.gradle b/build.gradle index 8fdd478..a00ecda 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '8.2.2' apply false - id 'com.android.library' version '8.2.2' apply false + id 'com.android.application' version '8.3.0' apply false + id 'com.android.library' version '8.3.0' apply false id 'org.jetbrains.kotlin.android' version '1.9.22' apply false id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.21' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 17efaba..1c20144 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sun Jul 30 13:54:47 CEST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From c25decc777f22542fea1cd671de5d3f7d7c78796 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 16 Mar 2024 15:26:38 +0100 Subject: [PATCH 02/39] feat: Automatically change interval duration and max duration Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../SettingsScreen/Tiles/IntervalDurationTile.kt | 8 ++++++-- .../ui/components/SettingsScreen/Tiles/MaxDurationTile.kt | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/IntervalDurationTile.kt b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/IntervalDurationTile.kt index 09bc86e..edfe06a 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/IntervalDurationTile.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/IntervalDurationTile.kt @@ -2,7 +2,6 @@ package app.myzel394.alibi.ui.components.SettingsScreen.Tiles import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Mic -import androidx.compose.material.icons.filled.Timer import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api @@ -10,7 +9,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text 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 @@ -43,6 +41,12 @@ fun IntervalDurationTile( fun updateValue(intervalDuration: Long) { scope.launch { + if (intervalDuration > settings.maxDuration) { + dataStore.updateData { + it.setMaxDuration(intervalDuration) + } + } + dataStore.updateData { it.setIntervalDuration(intervalDuration) } diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/MaxDurationTile.kt b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/MaxDurationTile.kt index 6319cb6..abf067b 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/MaxDurationTile.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/MaxDurationTile.kt @@ -1,7 +1,6 @@ package app.myzel394.alibi.ui.components.SettingsScreen.Tiles import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Memory import androidx.compose.material.icons.filled.Timer import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -10,7 +9,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text 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 @@ -42,6 +40,12 @@ fun MaxDurationTile( fun updateValue(maxDuration: Long) { scope.launch { + if (maxDuration < settings.intervalDuration) { + dataStore.updateData { + it.setIntervalDuration(maxDuration) + } + } + dataStore.updateData { it.setMaxDuration(maxDuration) } From 0a626e5f66e1b6892a65b618a0554d83a5dc0ba8 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 16 Mar 2024 16:57:12 +0100 Subject: [PATCH 03/39] feat: Add completer to recording save functions Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../organisms/RecorderEventsHandler.kt | 24 +++++++++++++++---- .../alibi/ui/models/BaseRecorderModel.kt | 5 +++- 2 files changed, 23 insertions(+), 6 deletions(-) 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 d01c5ad..4db1b32 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 @@ -1,6 +1,5 @@ package app.myzel394.alibi.ui.components.RecorderScreen.organisms -import android.content.Intent import android.net.Uri import android.util.Log import androidx.compose.material3.SnackbarDuration @@ -9,8 +8,6 @@ import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableDoubleStateOf -import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -33,6 +30,7 @@ import app.myzel394.alibi.ui.models.AudioRecorderModel import app.myzel394.alibi.ui.models.BaseRecorderModel import app.myzel394.alibi.ui.models.VideoRecorderModel import app.myzel394.alibi.ui.utils.rememberFileSaverDialog +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -227,6 +225,8 @@ fun RecorderEventsHandler( // Register audio recorder events DisposableEffect(key1 = audioRecorder, key2 = settings) { audioRecorder.onRecordingSave = { justSave -> + val completer = CompletableDeferred() + scope.launch { if (justSave) { saveRecording(audioRecorder as RecorderModel) @@ -239,7 +239,11 @@ fun RecorderEventsHandler( audioRecorder.destroyService(context) } + + completer.complete(Unit) } + + completer } audioRecorder.onRecordingStart = { snackbarHostState.currentSnackbarData?.dismiss() @@ -265,7 +269,9 @@ fun RecorderEventsHandler( } onDispose { - audioRecorder.onRecordingSave = {} + audioRecorder.onRecordingSave = { + throw NotImplementedError("onRecordingSave should not be called now") + } audioRecorder.onError = {} } } @@ -273,6 +279,8 @@ fun RecorderEventsHandler( // Register video recorder events DisposableEffect(key1 = videoRecorder, key2 = settings) { videoRecorder.onRecordingSave = { justSave -> + val completer = CompletableDeferred() + scope.launch { if (justSave) { saveRecording(videoRecorder as RecorderModel) @@ -285,7 +293,11 @@ fun RecorderEventsHandler( videoRecorder.destroyService(context) } + + completer.complete(Unit) } + + completer } videoRecorder.onRecordingStart = { snackbarHostState.currentSnackbarData?.dismiss() @@ -311,7 +323,9 @@ fun RecorderEventsHandler( } onDispose { - videoRecorder.onRecordingSave = {} + videoRecorder.onRecordingSave = { + throw NotImplementedError("onRecordingSave should not be called now") + } videoRecorder.onError = {} } } diff --git a/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt b/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt index 452419b..ebc054e 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt @@ -17,6 +17,7 @@ import app.myzel394.alibi.helpers.BatchesFolder import app.myzel394.alibi.services.IntervalRecorderService import app.myzel394.alibi.services.RecorderNotificationHelper import app.myzel394.alibi.services.RecorderService +import kotlinx.coroutines.CompletableDeferred import kotlinx.serialization.json.Json abstract class BaseRecorderModel> : @@ -45,7 +46,9 @@ abstract class BaseRecorderModel Unit = {} + var onRecordingSave: (isSavingAsOldRecording: Boolean) -> CompletableDeferred = { + throw NotImplementedError("onRecordingSave not implemented") + } var onRecordingStart: () -> Unit = {} var onError: () -> Unit = {} var onBatchesFolderNotAccessible: () -> Unit = {} From 671c8da56ab43344f5f95c6fd9c1dc3fca866a79 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 16 Mar 2024 17:28:45 +0100 Subject: [PATCH 04/39] refactor: Move recording save stuff out to audio and video recording statuses component Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../java/app/myzel394/alibi/db/AppSettings.kt | 11 ++ .../organisms/AudioRecordingStatus.kt | 103 ++++++++---------- .../organisms/RecorderEventsHandler.kt | 51 +++------ .../organisms/VideoRecordingStatus.kt | 16 ++- .../alibi/ui/models/BaseRecorderModel.kt | 4 +- .../alibi/ui/screens/RecorderScreen.kt | 27 +++-- 6 files changed, 107 insertions(+), 105 deletions(-) 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 1f45583..3b00d06 100644 --- a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt +++ b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt @@ -11,6 +11,7 @@ import app.myzel394.alibi.helpers.AudioBatchesFolder import app.myzel394.alibi.helpers.VideoBatchesFolder import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE import app.myzel394.alibi.ui.SUPPORTS_SCOPED_STORAGE +import app.myzel394.alibi.ui.components.RecorderScreen.organisms.RecorderModel import app.myzel394.alibi.ui.utils.PermissionHelper import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json @@ -102,6 +103,16 @@ data class AppSettings( return copy(appLockSettings = appLockSettings) } + fun saveLastRecording(recorder: RecorderModel): AppSettings { + return if (deleteRecordingsImmediately) { + this + } else { + setLastRecording( + recorder.recorderService!!.getRecordingInformation() + ) + } + } + // If the object is present, biometric authentication is enabled. // To disable biometric authentication, set the instance to null. fun isAppLockEnabled() = appLockSettings != null diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt index 2a639a0..bc97706 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import app.myzel394.alibi.dataStore import app.myzel394.alibi.ui.components.RecorderScreen.atoms.RealtimeAudioVisualizer import app.myzel394.alibi.ui.components.RecorderScreen.molecules.MicrophoneStatus import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingControl @@ -39,8 +40,6 @@ fun AudioRecordingStatus( val context = LocalContext.current val configuration = LocalConfiguration.current.orientation - val scope = rememberCoroutineScope() - var now by remember { mutableStateOf(LocalDateTime.now()) } LaunchedEffect(Unit) { @@ -90,34 +89,7 @@ fun AudioRecordingStatus( MicrophoneStatus(audioRecorder) } - RecordingControl( - modifier = Modifier - .weight(1f) - .fillMaxWidth(), - isPaused = audioRecorder.isPaused, - recordingTime = audioRecorder.recordingTime, - onDelete = { - scope.launch { - runCatching { - audioRecorder.stopRecording(context) - } - runCatching { - audioRecorder.destroyService(context) - } - audioRecorder.batchesFolder!!.deleteRecordings() - } - }, - onPauseResume = { - if (audioRecorder.isPaused) { - audioRecorder.resumeRecording() - } else { - audioRecorder.pauseRecording() - } - }, - onSave = { - audioRecorder.onRecordingSave(false) - } - ) + _PrimitiveControls(audioRecorder) } } @@ -138,33 +110,54 @@ fun AudioRecordingStatus( HorizontalDivider() - RecordingControl( - isPaused = audioRecorder.isPaused, - recordingTime = audioRecorder.recordingTime, - onDelete = { - scope.launch { - runCatching { - audioRecorder.stopRecording(context) - } - runCatching { - audioRecorder.destroyService(context) - } - audioRecorder.batchesFolder!!.deleteRecordings() - } - }, - onPauseResume = { - if (audioRecorder.isPaused) { - audioRecorder.resumeRecording() - } else { - audioRecorder.pauseRecording() - } - }, - onSave = { - audioRecorder.onRecordingSave(false) - } - ) + _PrimitiveControls(audioRecorder) } } } } +} + +@Composable +fun _PrimitiveControls(audioRecorder: AudioRecorderModel) { + val context = LocalContext.current + val dataStore = context.dataStore + val scope = rememberCoroutineScope() + + RecordingControl( + isPaused = audioRecorder.isPaused, + recordingTime = audioRecorder.recordingTime, + onDelete = { + scope.launch { + runCatching { + audioRecorder.stopRecording(context) + } + runCatching { + audioRecorder.destroyService(context) + } + audioRecorder.batchesFolder!!.deleteRecordings() + } + }, + onPauseResume = { + if (audioRecorder.isPaused) { + audioRecorder.resumeRecording() + } else { + audioRecorder.pauseRecording() + } + }, + onSave = { + scope.launch { + audioRecorder.stopRecording(context) + + dataStore.updateData { + it.saveLastRecording(audioRecorder as RecorderModel) + } + + audioRecorder.onRecordingSave() + + runCatching { + audioRecorder.destroyService(context) + } + } + } + ) } \ No newline at end of file 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 4db1b32..bd2c241 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 @@ -30,7 +30,6 @@ import app.myzel394.alibi.ui.models.AudioRecorderModel import app.myzel394.alibi.ui.models.BaseRecorderModel import app.myzel394.alibi.ui.models.VideoRecorderModel import app.myzel394.alibi.ui.utils.rememberFileSaverDialog -import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -224,26 +223,15 @@ fun RecorderEventsHandler( // Register audio recorder events DisposableEffect(key1 = audioRecorder, key2 = settings) { - audioRecorder.onRecordingSave = { justSave -> - val completer = CompletableDeferred() - + audioRecorder.onRecordingSave = { + // We create our own coroutine because we show our own dialog and we want to + // keep saving until it's finished. + // So it's smarter to take things into our own hands and use our local coroutine, + // instead of hoping that the coroutine from where this will be called will be alive + // until the end of the saving process scope.launch { - if (justSave) { - saveRecording(audioRecorder as RecorderModel) - } else { - audioRecorder.stopRecording(context) - - saveAsLastRecording(audioRecorder as RecorderModel) - - saveRecording(audioRecorder) - - audioRecorder.destroyService(context) - } - - completer.complete(Unit) + saveRecording(audioRecorder as RecorderModel) } - - completer } audioRecorder.onRecordingStart = { snackbarHostState.currentSnackbarData?.dismiss() @@ -278,26 +266,15 @@ fun RecorderEventsHandler( // Register video recorder events DisposableEffect(key1 = videoRecorder, key2 = settings) { - videoRecorder.onRecordingSave = { justSave -> - val completer = CompletableDeferred() - + videoRecorder.onRecordingSave = { + // We create our own coroutine because we show our own dialog and we want to + // keep saving until it's finished. + // So it's smarter to take things into our own hands and use our local coroutine, + // instead of hoping that the coroutine from where this will be called will be alive + // until the end of the saving process scope.launch { - if (justSave) { - saveRecording(videoRecorder as RecorderModel) - } else { - videoRecorder.stopRecording(context) - - saveAsLastRecording(videoRecorder as RecorderModel) - - saveRecording(videoRecorder) - - videoRecorder.destroyService(context) - } - - completer.complete(Unit) + saveRecording(videoRecorder as RecorderModel) } - - completer } videoRecorder.onRecordingStart = { snackbarHostState.currentSnackbarData?.dismiss() diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt index 47479d9..e6f1b5a 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.myzel394.alibi.R +import app.myzel394.alibi.dataStore import app.myzel394.alibi.ui.components.RecorderScreen.atoms.TorchStatus import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingControl import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingStatus @@ -189,6 +190,7 @@ fun _VideoRecordingStatus(videoRecorder: VideoRecorderModel) { @Composable fun _PrimitiveControls(videoRecorder: VideoRecorderModel) { val context = LocalContext.current + val dataStore = context.dataStore val scope = rememberCoroutineScope() RecordingControl( @@ -218,7 +220,19 @@ fun _PrimitiveControls(videoRecorder: VideoRecorderModel) { } }, onSave = { - videoRecorder.onRecordingSave(false) + scope.launch { + videoRecorder.stopRecording(context) + + dataStore.updateData { + it.saveLastRecording(videoRecorder as RecorderModel) + } + + videoRecorder.onRecordingSave() + + runCatching { + videoRecorder.destroyService(context) + } + } } ) } diff --git a/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt b/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt index ebc054e..1b54229 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt @@ -17,7 +17,7 @@ import app.myzel394.alibi.helpers.BatchesFolder import app.myzel394.alibi.services.IntervalRecorderService import app.myzel394.alibi.services.RecorderNotificationHelper import app.myzel394.alibi.services.RecorderService -import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Job import kotlinx.serialization.json.Json abstract class BaseRecorderModel> : @@ -46,7 +46,7 @@ abstract class BaseRecorderModel CompletableDeferred = { + var onRecordingSave: () -> Job = { throw NotImplementedError("onRecordingSave not implemented") } var onRecordingStart: () -> Unit = {} diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt index 68752b3..ae5cb01 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt @@ -16,22 +16,26 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable -import androidx.compose.runtime.* +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.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.navigation.NavController -import app.myzel394.alibi.ui.components.RecorderScreen.organisms.AudioRecordingStatus -import app.myzel394.alibi.ui.components.RecorderScreen.organisms.StartRecording -import app.myzel394.alibi.ui.enums.Screen import app.myzel394.alibi.R import app.myzel394.alibi.dataStore import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.db.RecordingInformation +import app.myzel394.alibi.ui.components.RecorderScreen.organisms.AudioRecordingStatus import app.myzel394.alibi.ui.components.RecorderScreen.organisms.RecorderEventsHandler +import app.myzel394.alibi.ui.components.RecorderScreen.organisms.StartRecording import app.myzel394.alibi.ui.components.RecorderScreen.organisms.VideoRecordingStatus import app.myzel394.alibi.ui.models.AudioRecorderModel import app.myzel394.alibi.ui.models.VideoRecorderModel +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -43,6 +47,7 @@ fun RecorderScreen( ) { val snackbarHostState = remember { SnackbarHostState() } val context = LocalContext.current + val scope = rememberCoroutineScope() RecorderEventsHandler( settings = settings, @@ -112,12 +117,14 @@ fun RecorderScreen( videoRecorder = videoRecorder, appSettings = appSettings, onSaveLastRecording = { - when (settings.lastRecording!!.type) { - RecordingInformation.Type.AUDIO -> - audioRecorder.onRecordingSave(true) + scope.launch { + when (settings.lastRecording!!.type) { + RecordingInformation.Type.AUDIO -> + audioRecorder.onRecordingSave() - RecordingInformation.Type.VIDEO -> - videoRecorder.onRecordingSave(true) + RecordingInformation.Type.VIDEO -> + videoRecorder.onRecordingSave() + } } }, showAudioRecorder = topBarVisible, From 662728966676019509fa55c710f39486b9d354d2 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 16 Mar 2024 18:43:26 +0100 Subject: [PATCH 05/39] feat: Add lock / unlock method for interval files Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../myzel394/alibi/helpers/BatchesFolder.kt | 25 ++++++++----------- .../alibi/services/IntervalRecorderService.kt | 24 ++++++++++++++++-- .../alibi/services/VideoRecorderService.kt | 2 +- .../organisms/RecorderEventsHandler.kt | 11 +++++--- .../organisms/VideoRecordingStatus.kt | 2 +- 5 files changed, 42 insertions(+), 22 deletions(-) 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 70c12f5..ac48807 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt @@ -3,28 +3,26 @@ package app.myzel394.alibi.helpers import android.Manifest import android.content.ContentUris import android.content.ContentValues -import app.myzel394.alibi.ui.MEDIA_RECORDINGS_PREFIX - import android.content.Context import android.database.Cursor import android.net.Uri import android.os.Build import android.provider.MediaStore import android.provider.MediaStore.Video.Media -import androidx.documentfile.provider.DocumentFile -import java.io.File -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter -import com.arthenica.ffmpegkit.FFmpegKitConfig import android.util.Log import androidx.annotation.RequiresApi import androidx.core.net.toUri +import androidx.documentfile.provider.DocumentFile +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 import app.myzel394.alibi.ui.SUPPORTS_SCOPED_STORAGE import app.myzel394.alibi.ui.utils.PermissionHelper -import com.arthenica.ffmpegkit.FFprobeKit +import com.arthenica.ffmpegkit.FFmpegKitConfig import kotlinx.coroutines.CompletableDeferred +import java.io.File +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter import kotlin.reflect.KFunction4 abstract class BatchesFolder( @@ -197,7 +195,6 @@ abstract class BatchesFolder( createNewFile() } - fun checkIfOutputAlreadyExists( date: LocalDateTime, extension: String @@ -388,12 +385,12 @@ abstract class BatchesFolder( } } - fun deleteOldRecordings(earliestCounter: Long) { + fun deleteRecordings(range: LongRange) { when (type) { BatchType.INTERNAL -> getInternalFolder().listFiles()?.forEach { val fileCounter = it.nameWithoutExtension.toIntOrNull() ?: return@forEach - if (fileCounter < earliestCounter) { + if (fileCounter in range) { it.delete() } } @@ -401,7 +398,7 @@ abstract class BatchesFolder( BatchType.CUSTOM -> getCustomDefinedFolder().listFiles().forEach { val fileCounter = it.name?.substringBeforeLast(".")?.toIntOrNull() ?: return@forEach - if (fileCounter < earliestCounter) { + if (fileCounter in range) { it.delete() } } @@ -411,7 +408,7 @@ abstract class BatchesFolder( val deletableNames = mutableListOf() queryMediaContent { rawName, counter, _, _ -> - if (counter < earliestCounter) { + if (counter in range) { deletableNames.add(rawName) } } @@ -428,7 +425,7 @@ abstract class BatchesFolder( it.nameWithoutExtension.substring(mediaPrefix.length).toIntOrNull() ?: return@forEach - if (fileCounter < earliestCounter) { + if (fileCounter in range) { it.delete() } } diff --git a/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt index ed8f9c7..05a6a55 100644 --- a/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt @@ -11,6 +11,9 @@ abstract class IntervalRecorderService : protected var counter = 0L private set + // Tracks the index of the currently locked file + private var lockedIndex: Long? = null + lateinit var settings: AppSettings private lateinit var cycleTimer: ScheduledExecutorService @@ -21,6 +24,23 @@ abstract class IntervalRecorderService : abstract fun getRecordingInformation(): I + // When saving the recording, the files should be locked. + // This prevents the service from deleting the currently available files, so that + // they can be safely used to save the recording. + // Once finished, make sure to unlock the files using `unlockFiles`. + fun lockFiles() { + lockedIndex = counter + } + + // Unlocks and deletes the files that were locked using `lockFiles`. + fun unlockFiles(cleanupFiles: Boolean = false) { + if (cleanupFiles) { + batchesFolder.deleteRecordings(0..lockedIndex!!) + } + + lockedIndex = null + } + // Make overrideable open fun startNewCycle() { counter += 1 @@ -72,12 +92,12 @@ abstract class IntervalRecorderService : private fun deleteOldRecordings() { val timeMultiplier = settings.maxDuration / settings.intervalDuration - val earliestCounter = counter - timeMultiplier + val earliestCounter = Math.max(counter - timeMultiplier, lockedIndex ?: 0) if (earliestCounter <= 0) { return } - batchesFolder.deleteOldRecordings(earliestCounter) + batchesFolder.deleteRecordings(0..earliestCounter) } } \ 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 5aaf273..7f4ac2d 100644 --- a/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt @@ -139,7 +139,7 @@ class VideoRecorderService : if (_cameraAvailableListener.isCompleted) { action() } else { - // Race condition of `startNewCycle` being called before `invpkeOnCompletion` + // Race condition of `startNewCycle` being called before `invokeOnCompletion` // has been called can be ignored, as the camera usually opens within 5 seconds // and the interval can't be set shorter than 10 seconds. _cameraAvailableListener.invokeOnCompletion { 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 bd2c241..15eb477 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 @@ -129,15 +129,17 @@ fun RecorderEventsHandler( } } - suspend fun saveRecording(recorder: RecorderModel) { + suspend fun saveRecording(recorder: RecorderModel): Thread { isProcessing = true // Give the user some time to see the processing dialog delay(100) - thread { + return thread { runBlocking { try { + recorder.recorderService?.lockFiles() + val recording = // When new recording created recorder.recorderService?.getRecordingInformation() @@ -215,6 +217,7 @@ fun RecorderEventsHandler( } catch (error: Exception) { Log.getStackTraceString(error) } finally { + recorder.recorderService?.unlockFiles() isProcessing = false } } @@ -230,7 +233,7 @@ fun RecorderEventsHandler( // instead of hoping that the coroutine from where this will be called will be alive // until the end of the saving process scope.launch { - saveRecording(audioRecorder as RecorderModel) + saveRecording(audioRecorder as RecorderModel).join() } } audioRecorder.onRecordingStart = { @@ -273,7 +276,7 @@ fun RecorderEventsHandler( // instead of hoping that the coroutine from where this will be called will be alive // until the end of the saving process scope.launch { - saveRecording(videoRecorder as RecorderModel) + saveRecording(videoRecorder as RecorderModel).join() } } videoRecorder.onRecordingStart = { diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt index e6f1b5a..ec5471b 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt @@ -227,7 +227,7 @@ fun _PrimitiveControls(videoRecorder: VideoRecorderModel) { it.saveLastRecording(videoRecorder as RecorderModel) } - videoRecorder.onRecordingSave() + videoRecorder.onRecordingSave().join() runCatching { videoRecorder.destroyService(context) From fe24f3473f0a6aacb5464c462817e57ece2e97ea Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 16 Mar 2024 19:14:59 +0100 Subject: [PATCH 06/39] feat: Add onLongClick to SaveButton Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../RecorderScreen/atoms/SaveButton.kt | 28 +++++++++++++++---- .../molecules/RecordingControl.kt | 15 ++++++++-- .../organisms/AudioRecordingStatus.kt | 7 +++-- .../organisms/RecorderEventsHandler.kt | 12 ++++---- .../organisms/VideoRecordingStatus.kt | 7 +++-- .../alibi/ui/models/BaseRecorderModel.kt | 2 +- .../alibi/ui/screens/RecorderScreen.kt | 4 +-- 7 files changed, 52 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveButton.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveButton.kt index 28d960d..f8719c1 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveButton.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveButton.kt @@ -1,35 +1,51 @@ package app.myzel394.alibi.ui.components.RecorderScreen.atoms -import androidx.compose.material3.Button +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import app.myzel394.alibi.R +@OptIn(ExperimentalFoundationApi::class) @Composable fun SaveButton( modifier: Modifier = Modifier, onSave: () -> Unit, + onLongClick: () -> Unit = {}, ) { val label = stringResource(R.string.ui_recorder_action_save_label) - TextButton( + Box( modifier = Modifier + .clip(ButtonDefaults.textShape) .semantics { contentDescription = label } - .then(modifier), - onClick = onSave, + .combinedClickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(color = MaterialTheme.colorScheme.primary), + onClick = onSave, + onLongClick = onLongClick, + ) + .padding(ButtonDefaults.TextButtonContentPadding) + .then(modifier) ) { Text( label, - fontSize = MaterialTheme.typography.bodySmall.fontSize, + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.primary, ) } } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/RecordingControl.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/RecordingControl.kt index 8e1bdde..47764d5 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/RecordingControl.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/RecordingControl.kt @@ -35,7 +35,8 @@ fun RecordingControl( recordingTime: Long, onDelete: () -> Unit, onPauseResume: () -> Unit, - onSave: () -> Unit, + onSaveAndStop: () -> Unit, + onSaveCurrent: () -> Unit, ) { val animateIn = rememberInitialRecordingAnimation(recordingTime) @@ -106,7 +107,10 @@ fun RecordingControl( contentAlignment = Alignment.Center, ) { SaveButton( - onSave = onSave, + onSave = onSaveAndStop, + onLongClick = { + println("onLongClick") + }, modifier = Modifier.fillMaxWidth(), ) } @@ -170,7 +174,12 @@ fun RecordingControl( .alpha(saveButtonAlpha), contentAlignment = Alignment.Center, ) { - SaveButton(onSave = onSave) + SaveButton( + onSave = onSaveAndStop, + onLongClick = { + println("onLongClick") + }, + ) } } } diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt index bc97706..3fc4dba 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt @@ -144,7 +144,7 @@ fun _PrimitiveControls(audioRecorder: AudioRecorderModel) { audioRecorder.pauseRecording() } }, - onSave = { + onSaveAndStop = { scope.launch { audioRecorder.stopRecording(context) @@ -152,12 +152,13 @@ fun _PrimitiveControls(audioRecorder: AudioRecorderModel) { it.saveLastRecording(audioRecorder as RecorderModel) } - audioRecorder.onRecordingSave() + audioRecorder.onRecordingSave(false) runCatching { audioRecorder.destroyService(context) } } - } + }, + onSaveCurrent = {}, ) } \ No newline at end of file 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 15eb477..1af0f86 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 @@ -129,7 +129,7 @@ fun RecorderEventsHandler( } } - suspend fun saveRecording(recorder: RecorderModel): Thread { + suspend fun saveRecording(recorder: RecorderModel, cleanupOldFiles: Boolean = false): Thread { isProcessing = true // Give the user some time to see the processing dialog @@ -217,7 +217,7 @@ fun RecorderEventsHandler( } catch (error: Exception) { Log.getStackTraceString(error) } finally { - recorder.recorderService?.unlockFiles() + recorder.recorderService?.unlockFiles(cleanupOldFiles) isProcessing = false } } @@ -226,14 +226,14 @@ fun RecorderEventsHandler( // Register audio recorder events DisposableEffect(key1 = audioRecorder, key2 = settings) { - audioRecorder.onRecordingSave = { + audioRecorder.onRecordingSave = { cleanupOldFiles -> // We create our own coroutine because we show our own dialog and we want to // keep saving until it's finished. // So it's smarter to take things into our own hands and use our local coroutine, // instead of hoping that the coroutine from where this will be called will be alive // until the end of the saving process scope.launch { - saveRecording(audioRecorder as RecorderModel).join() + saveRecording(audioRecorder as RecorderModel, cleanupOldFiles).join() } } audioRecorder.onRecordingStart = { @@ -269,14 +269,14 @@ fun RecorderEventsHandler( // Register video recorder events DisposableEffect(key1 = videoRecorder, key2 = settings) { - videoRecorder.onRecordingSave = { + videoRecorder.onRecordingSave = { cleanupOldFiles -> // We create our own coroutine because we show our own dialog and we want to // keep saving until it's finished. // So it's smarter to take things into our own hands and use our local coroutine, // instead of hoping that the coroutine from where this will be called will be alive // until the end of the saving process scope.launch { - saveRecording(videoRecorder as RecorderModel).join() + saveRecording(videoRecorder as RecorderModel, cleanupOldFiles).join() } } videoRecorder.onRecordingStart = { diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt index ec5471b..4245f00 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt @@ -219,7 +219,7 @@ fun _PrimitiveControls(videoRecorder: VideoRecorderModel) { videoRecorder.pauseRecording() } }, - onSave = { + onSaveAndStop = { scope.launch { videoRecorder.stopRecording(context) @@ -227,12 +227,15 @@ fun _PrimitiveControls(videoRecorder: VideoRecorderModel) { it.saveLastRecording(videoRecorder as RecorderModel) } - videoRecorder.onRecordingSave().join() + videoRecorder.onRecordingSave(false).join() runCatching { videoRecorder.destroyService(context) } } + }, + onSaveCurrent = { + //TODO } ) } diff --git a/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt b/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt index 1b54229..e55ec83 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt @@ -46,7 +46,7 @@ abstract class BaseRecorderModel Job = { + var onRecordingSave: (cleanupOldFiles: Boolean) -> Job = { throw NotImplementedError("onRecordingSave not implemented") } var onRecordingStart: () -> Unit = {} diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt index ae5cb01..a086840 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt @@ -120,10 +120,10 @@ fun RecorderScreen( scope.launch { when (settings.lastRecording!!.type) { RecordingInformation.Type.AUDIO -> - audioRecorder.onRecordingSave() + audioRecorder.onRecordingSave(false) RecordingInformation.Type.VIDEO -> - videoRecorder.onRecordingSave() + videoRecorder.onRecordingSave(false) } } }, From dad8439d3da667de011567ba924350b4272a9331 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 17 Mar 2024 18:19:38 +0100 Subject: [PATCH 07/39] feat: Add save current audio recording without stopping it (wip) Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../myzel394/alibi/services/IntervalRecorderService.kt | 2 +- .../RecorderScreen/molecules/RecordingControl.kt | 8 ++------ .../RecorderScreen/organisms/AudioRecordingStatus.kt | 10 ++++++++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt index 05a6a55..6783ef2 100644 --- a/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt @@ -35,7 +35,7 @@ abstract class IntervalRecorderService : // Unlocks and deletes the files that were locked using `lockFiles`. fun unlockFiles(cleanupFiles: Boolean = false) { if (cleanupFiles) { - batchesFolder.deleteRecordings(0..lockedIndex!!) + batchesFolder.deleteRecordings(0.. Date: Sun, 17 Mar 2024 21:03:39 +0100 Subject: [PATCH 08/39] feat: Add save current on long press for audio recorder Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../RecorderScreen/organisms/AudioRecordingStatus.kt | 2 +- .../RecorderScreen/organisms/RecorderEventsHandler.kt | 8 ++++++-- .../app/myzel394/alibi/ui/models/BaseRecorderModel.kt | 3 +++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt index c302f24..664cc38 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt @@ -163,7 +163,7 @@ fun _PrimitiveControls(audioRecorder: AudioRecorderModel) { scope.launch { audioRecorder.recorderService!!.startNewCycle() - audioRecorder.onRecordingSave(true).join() + audioRecorder.onRecordingSave(false).join() } }, ) 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 1af0f86..6db673d 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 @@ -138,7 +138,9 @@ fun RecorderEventsHandler( return thread { runBlocking { try { - recorder.recorderService?.lockFiles() + if (recorder.isCurrentlyActivelyRecording) { + recorder.recorderService?.lockFiles() + } val recording = // When new recording created @@ -217,7 +219,9 @@ fun RecorderEventsHandler( } catch (error: Exception) { Log.getStackTraceString(error) } finally { - recorder.recorderService?.unlockFiles(cleanupOldFiles) + if (recorder.isCurrentlyActivelyRecording) { + recorder.recorderService?.unlockFiles(cleanupOldFiles) + } isProcessing = false } } diff --git a/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt b/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt index e55ec83..7796849 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt @@ -32,6 +32,9 @@ abstract class BaseRecorderModel Date: Sun, 17 Mar 2024 22:01:54 +0100 Subject: [PATCH 09/39] feat: Add confirmation modal for saving current recording Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../atoms/SaveCurrentNowModal.kt | 58 +++++++++++++++++++ .../organisms/AudioRecordingStatus.kt | 26 +++++++-- app/src/main/res/values/strings.xml | 2 + 3 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveCurrentNowModal.kt diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveCurrentNowModal.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveCurrentNowModal.kt new file mode 100644 index 0000000..8480f55 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveCurrentNowModal.kt @@ -0,0 +1,58 @@ +package app.myzel394.alibi.ui.components.RecorderScreen.atoms + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import app.myzel394.alibi.R +import app.myzel394.alibi.ui.SHEET_BOTTOM_OFFSET + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SaveCurrentNowModal( + onDismiss: () -> Unit, + onConfirm: () -> Unit, +) { + val sheetState = rememberModalBottomSheetState(true) + + // Auto save on specific events + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = SHEET_BOTTOM_OFFSET) + .padding(16.dp) + ) { + Text( + stringResource(R.string.ui_recorder_action_saveCurrent), + style = MaterialTheme.typography.headlineMedium, + textAlign = TextAlign.Center, + ) + + Text( + stringResource(R.string.ui_recorder_action_saveCurrent_explanation), + ) + + TextButton(onClick = onConfirm) { + Text(stringResource(R.string.ui_recorder_action_save_label)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt index 664cc38..0e6d983 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import app.myzel394.alibi.dataStore import app.myzel394.alibi.ui.components.RecorderScreen.atoms.RealtimeAudioVisualizer +import app.myzel394.alibi.ui.components.RecorderScreen.atoms.SaveCurrentNowModal import app.myzel394.alibi.ui.components.RecorderScreen.molecules.MicrophoneStatus import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingControl import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingStatus @@ -123,6 +124,25 @@ fun _PrimitiveControls(audioRecorder: AudioRecorderModel) { val dataStore = context.dataStore val scope = rememberCoroutineScope() + var showConfirmSaveNow by remember { mutableStateOf(false) } + + if (showConfirmSaveNow) { + SaveCurrentNowModal( + onDismiss = { + showConfirmSaveNow = false + }, + onConfirm = { + showConfirmSaveNow = false + + scope.launch { + audioRecorder.recorderService!!.startNewCycle() + + audioRecorder.onRecordingSave(false).join() + } + }, + ) + } + RecordingControl( isPaused = audioRecorder.isPaused, recordingTime = audioRecorder.recordingTime, @@ -160,11 +180,7 @@ fun _PrimitiveControls(audioRecorder: AudioRecorderModel) { } }, onSaveCurrent = { - scope.launch { - audioRecorder.recorderService!!.startNewCycle() - - audioRecorder.onRecordingSave(false).join() - } + showConfirmSaveNow = true }, ) } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 15b1d26..edeb3f2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -191,4 +191,6 @@ To protect your privacy, Alibi stores its batches into its own private, encrypted storage. This storage is only accessible by Alibi and can\'t be accessed by other apps or by a possible intruder. Once you save the recording, you will be asked where you want to save the recording to. Please rotate your device to portait mode Back + Save now? + You can save the current ongoing recording by pressing and holding down on the save button. The recording will continue in the background. \ No newline at end of file From 227e075c0baffddd150c2f320ffe3934ec17ec25 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 18 Mar 2024 16:38:57 +0100 Subject: [PATCH 10/39] feat: Add save current option to VideoRecordingStatus Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../organisms/VideoRecordingStatus.kt | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt index 4245f00..9009a99 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt @@ -21,6 +21,7 @@ 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.saveable.rememberSaveable import androidx.compose.runtime.setValue @@ -33,6 +34,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.myzel394.alibi.R import app.myzel394.alibi.dataStore +import app.myzel394.alibi.ui.components.RecorderScreen.atoms.SaveCurrentNowModal import app.myzel394.alibi.ui.components.RecorderScreen.atoms.TorchStatus import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingControl import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingStatus @@ -193,6 +195,25 @@ fun _PrimitiveControls(videoRecorder: VideoRecorderModel) { val dataStore = context.dataStore val scope = rememberCoroutineScope() + var showConfirmSaveNow by remember { mutableStateOf(false) } + + if (showConfirmSaveNow) { + SaveCurrentNowModal( + onDismiss = { + showConfirmSaveNow = false + }, + onConfirm = { + showConfirmSaveNow = false + + scope.launch { + videoRecorder.recorderService!!.startNewCycle() + + videoRecorder.onRecordingSave(false).join() + } + }, + ) + } + RecordingControl( orientation = Configuration.ORIENTATION_PORTRAIT, // There may be some edge cases where the app may crash if the @@ -235,7 +256,7 @@ fun _PrimitiveControls(videoRecorder: VideoRecorderModel) { } }, onSaveCurrent = { - //TODO + showConfirmSaveNow = true } ) } From 41f1aca833b1b29310dbd5354ff41b0de3476e5e Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 18 Mar 2024 17:02:38 +0100 Subject: [PATCH 11/39] fix: Always listen for video finalization event Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../app/myzel394/alibi/services/VideoRecorderService.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 7f4ac2d..aa74867 100644 --- a/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt @@ -50,7 +50,7 @@ class VideoRecorderService : // Used to listen and check if the camera is available private var _cameraAvailableListener = CompletableDeferred() - private var _videoFinalizerListener = CompletableDeferred() + private lateinit var _videoFinalizerListener: CompletableDeferred; // Absolute last completer that can be awaited to ensure that the camera is closed private var _cameraCloserListener = CompletableDeferred() @@ -129,8 +129,10 @@ class VideoRecorderService : stopActiveRecording() val newRecording = prepareVideoRecording() + _videoFinalizerListener = CompletableDeferred() + activeRecording = newRecording.start(ContextCompat.getMainExecutor(this)) { event -> - if (event is VideoRecordEvent.Finalize && this@VideoRecorderService.state == RecorderState.STOPPED) { + if (event is VideoRecordEvent.Finalize && this@VideoRecorderService.state == RecorderState.STOPPED || this@VideoRecorderService.state == RecorderState.PAUSED) { _videoFinalizerListener.complete(Unit) } } From f895700e34673187ce2976c7e91d35453530319e Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Tue, 19 Mar 2024 22:30:54 +0100 Subject: [PATCH 12/39] fix: Only backup datastore; Closes #51 Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- app/src/main/res/xml/backup_rules.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml index 251ec5e..82bd130 100644 --- a/app/src/main/res/xml/backup_rules.xml +++ b/app/src/main/res/xml/backup_rules.xml @@ -10,7 +10,7 @@ --> - + path="datastore/." /> \ No newline at end of file From 9c5cba2c91305850c0abb67d6f4aaed4d74ecdd6 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 21 Mar 2024 19:08:08 +0100 Subject: [PATCH 13/39] chore: Update version Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- app/build.gradle | 4 ++-- app/src/main/AndroidManifest.xml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 3e02c26..d6ea019 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,8 +35,8 @@ android { applicationId "app.myzel394.alibi" minSdk 24 targetSdk 34 - versionCode 12 - versionName "0.4.0" + versionCode 13 + versionName "0.4.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 87cbf80..b0e7f0b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,6 +19,7 @@ android:maxSdkVersion="30" /> + From bc609ae34b4e87f450cd00d0e02502b00194ed00 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 21 Mar 2024 19:11:25 +0100 Subject: [PATCH 14/39] fix: Improve wording; Closes #85 Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index edeb3f2..0e30de6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -160,7 +160,7 @@ When stopped, save the last... Recording started at %s Recording started %s - Saving now will save until %s + Press to save from %s till now Video Recorder is starting... Alibi is locked Unlock From ea1a7701bd9566583d3772bd63f1453b751babf3 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 21 Mar 2024 19:19:31 +0100 Subject: [PATCH 15/39] fix: Improve responsive layout; Closes #65 Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../alibi/ui/components/RecorderScreen/atoms/SaveButton.kt | 4 ++++ .../RecorderScreen/organisms/AudioRecordingStatus.kt | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveButton.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveButton.kt index f8719c1..e3351e3 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveButton.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveButton.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.ButtonDefaults @@ -16,6 +17,7 @@ import androidx.compose.ui.draw.clip 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 app.myzel394.alibi.R @OptIn(ExperimentalFoundationApi::class) @@ -46,6 +48,8 @@ fun SaveButton( label, style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.primary, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), ) } } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt index 0e6d983..673e573 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt @@ -90,7 +90,11 @@ fun AudioRecordingStatus( MicrophoneStatus(audioRecorder) } - _PrimitiveControls(audioRecorder) + Box( + modifier = Modifier.weight(1f) + ) { + _PrimitiveControls(audioRecorder) + } } } From 0ebbf864502b33c778cdb2637f1b3571c4bb8338 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 21 Mar 2024 19:30:50 +0100 Subject: [PATCH 16/39] fix: Improve SaveButton width --- .../ui/components/RecorderScreen/atoms/SaveButton.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveButton.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveButton.kt index e3351e3..fc259cb 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveButton.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveButton.kt @@ -3,8 +3,8 @@ package app.myzel394.alibi.ui.components.RecorderScreen.atoms import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.ButtonDefaults @@ -12,12 +12,12 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip 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 app.myzel394.alibi.R @OptIn(ExperimentalFoundationApi::class) @@ -29,7 +29,9 @@ fun SaveButton( ) { val label = stringResource(R.string.ui_recorder_action_save_label) - Box( + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, modifier = Modifier .clip(ButtonDefaults.textShape) .semantics { @@ -48,8 +50,6 @@ fun SaveButton( label, style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.primary, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth(), ) } } \ No newline at end of file From a6856476ce18f577b288fcdcb0a81c1690e0b632 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 21 Mar 2024 20:11:59 +0100 Subject: [PATCH 17/39] feat: Add low on storage info; Closes #56 Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../myzel394/alibi/helpers/BatchesFolder.kt | 25 +++++++++++ .../alibi/helpers/VideoBatchesFolder.kt | 3 +- .../RecorderScreen/atoms/LowStorageInfo.kt | 41 +++++++++++++++++++ .../organisms/StartRecording.kt | 3 ++ app/src/main/res/values/strings.xml | 1 + 5 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/LowStorageInfo.kt 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 ac48807..a025dca 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt @@ -7,12 +7,15 @@ import android.content.Context import android.database.Cursor import android.net.Uri import android.os.Build +import android.os.storage.StorageManager import android.provider.MediaStore import android.provider.MediaStore.Video.Media import android.util.Log import androidx.annotation.RequiresApi +import androidx.core.net.toFile import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile +import app.myzel394.alibi.db.AppSettings 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 @@ -519,10 +522,32 @@ abstract class BatchesFolder( return uri!! } + fun getAvailableBytes(): Long { + val storageManager = context.getSystemService(StorageManager::class.java) ?: return -1 + val file = when (type) { + BatchType.INTERNAL -> context.filesDir + BatchType.CUSTOM -> customFolder!!.uri.toFile() + BatchType.MEDIA -> scopedMediaContentUri.toFile() + } + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + storageManager.getAllocatableBytes(storageManager.getUuidForPath(file)) + } else { + file.usableSpace; + } + } + enum class BatchType { INTERNAL, CUSTOM, MEDIA, } + + companion object { + fun requiredBytesForOneMinuteOfRecording(appSettings: AppSettings): Long { + // 300 MiB sounds like a good default + return 300 * 1024 * 1024 + } + } } diff --git a/app/src/main/java/app/myzel394/alibi/helpers/VideoBatchesFolder.kt b/app/src/main/java/app/myzel394/alibi/helpers/VideoBatchesFolder.kt index 22f0201..e143681 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/VideoBatchesFolder.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/VideoBatchesFolder.kt @@ -135,7 +135,8 @@ class VideoBatchesFolder( fun viaMediaFolder(context: Context) = VideoBatchesFolder(context, BatchType.MEDIA) - fun importFromFolder(folder: String, context: Context) = when (folder) { + fun importFromFolder(folder: String?, context: Context) = when (folder) { + null -> viaInternalFolder(context) RECORDER_INTERNAL_SELECTED_VALUE -> viaInternalFolder(context) RECORDER_MEDIA_SELECTED_VALUE -> viaMediaFolder(context) else -> viaCustomFolder( diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/LowStorageInfo.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/LowStorageInfo.kt new file mode 100644 index 0000000..9ee6dac --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/LowStorageInfo.kt @@ -0,0 +1,41 @@ +package app.myzel394.alibi.ui.components.RecorderScreen.atoms + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import app.myzel394.alibi.R +import app.myzel394.alibi.db.AppSettings +import app.myzel394.alibi.helpers.BatchesFolder +import app.myzel394.alibi.helpers.VideoBatchesFolder +import app.myzel394.alibi.ui.components.atoms.MessageBox +import app.myzel394.alibi.ui.components.atoms.MessageType + +@Composable +fun LowStorageInfo( + appSettings: AppSettings, +) { + val context = LocalContext.current + val availableBytes = + VideoBatchesFolder.importFromFolder(appSettings.saveFolder, context).getAvailableBytes() + + val bytesPerMinute = BatchesFolder.requiredBytesForOneMinuteOfRecording(appSettings) + val requiredBytes = appSettings.maxDuration / 1000 / 60 * bytesPerMinute + + // Allow for a 10% margin of error + val isLowOnStorage = availableBytes < requiredBytes * 1.1 + println("LowStorageInfo: availableBytes: $availableBytes, requiredBytes: $requiredBytes, isLowOnStorage: $isLowOnStorage") + + if (isLowOnStorage) + Box( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) { + MessageBox( + type = MessageType.WARNING, + message = stringResource(R.string.ui_recorder_lowOnStorage_hint), + ) + } +} diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/StartRecording.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/StartRecording.kt index 0ea2ddc..f59ad8e 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/StartRecording.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/StartRecording.kt @@ -44,6 +44,7 @@ import app.myzel394.alibi.R import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_MAX_WIDTH import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE +import app.myzel394.alibi.ui.components.RecorderScreen.atoms.LowStorageInfo import app.myzel394.alibi.ui.components.RecorderScreen.molecules.AudioRecordingStart import app.myzel394.alibi.ui.components.RecorderScreen.molecules.QuickMaxDurationSelector import app.myzel394.alibi.ui.components.RecorderScreen.molecules.VideoRecordingStart @@ -212,5 +213,7 @@ fun StartRecording( } } } + + LowStorageInfo(appSettings = appSettings) } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0e30de6..e826fa3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -193,4 +193,5 @@ Back Save now? You can save the current ongoing recording by pressing and holding down on the save button. The recording will continue in the background. + You are low on storage. Alibi may not function properly. Please free up some space. \ No newline at end of file From 5cdbb605f2764e8651b0828ace097109c1b0bd19 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 21 Mar 2024 21:54:21 +0100 Subject: [PATCH 18/39] fix: Show low storage message dependent on whether user uses internal or external storage Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../main/java/app/myzel394/alibi/helpers/BatchesFolder.kt | 8 ++++---- .../ui/components/RecorderScreen/atoms/LowStorageInfo.kt | 8 +++++++- app/src/main/res/values/strings.xml | 1 + 3 files changed, 12 insertions(+), 5 deletions(-) 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 a025dca..3c3716c 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt @@ -522,8 +522,8 @@ abstract class BatchesFolder( return uri!! } - fun getAvailableBytes(): Long { - val storageManager = context.getSystemService(StorageManager::class.java) ?: return -1 + fun getAvailableBytes(): Long? { + val storageManager = context.getSystemService(StorageManager::class.java) ?: return null val file = when (type) { BatchType.INTERNAL -> context.filesDir BatchType.CUSTOM -> customFolder!!.uri.toFile() @@ -545,8 +545,8 @@ abstract class BatchesFolder( companion object { fun requiredBytesForOneMinuteOfRecording(appSettings: AppSettings): Long { - // 300 MiB sounds like a good default - return 300 * 1024 * 1024 + // 250 MiB sounds like a good default + return 250 * 1024 * 1024 } } } diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/LowStorageInfo.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/LowStorageInfo.kt index 9ee6dac..d567704 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/LowStorageInfo.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/LowStorageInfo.kt @@ -22,6 +22,10 @@ fun LowStorageInfo( val availableBytes = VideoBatchesFolder.importFromFolder(appSettings.saveFolder, context).getAvailableBytes() + if (availableBytes == null) { + return + } + val bytesPerMinute = BatchesFolder.requiredBytesForOneMinuteOfRecording(appSettings) val requiredBytes = appSettings.maxDuration / 1000 / 60 * bytesPerMinute @@ -35,7 +39,9 @@ fun LowStorageInfo( ) { MessageBox( type = MessageType.WARNING, - message = stringResource(R.string.ui_recorder_lowOnStorage_hint), + message = if (appSettings.saveFolder == null) + stringResource(R.string.ui_recorder_lowOnStorage_hintANDswitchSaveFolder) + else stringResource(R.string.ui_recorder_lowOnStorage_hint) ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e826fa3..baefa03 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -194,4 +194,5 @@ Save now? You can save the current ongoing recording by pressing and holding down on the save button. The recording will continue in the background. You are low on storage. Alibi may not function properly. Please free up some space. + You are low on storage. Alibi may not function properly. Please free up some space. Alternatively, change the batches folder to a different location in the settings. \ No newline at end of file From 6687b173a51f21531902859853e0adbc4f404418 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 21 Mar 2024 22:36:48 +0100 Subject: [PATCH 19/39] feat: Add TimeSettingsPage Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../java/app/myzel394/alibi/db/AppSettings.kt | 2 +- .../WelcomeScreen/atoms/TimeSelector.kt | 98 +++++++++++++++++ .../{atoms => pages}/ExplanationPage.kt | 2 +- .../{atoms => pages}/ResponsibilityPage.kt | 10 +- .../WelcomeScreen/pages/TimeSettingsPage.kt | 100 ++++++++++++++++++ .../alibi/ui/screens/WelcomeScreen.kt | 36 ++++--- app/src/main/res/values/strings.xml | 8 ++ 7 files changed, 237 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/TimeSelector.kt rename app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/{atoms => pages}/ExplanationPage.kt (97%) rename app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/{atoms => pages}/ResponsibilityPage.kt (91%) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/TimeSettingsPage.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 3b00d06..9aabc53 100644 --- a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt +++ b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt @@ -31,7 +31,7 @@ data class AppSettings( /// Recording information // 30 minutes - val maxDuration: Long = 30 * 60 * 1000L, + val maxDuration: Long = 15 * 60 * 1000L, // 60 seconds val intervalDuration: Long = 60 * 1000L, diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/TimeSelector.kt b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/TimeSelector.kt new file mode 100644 index 0000000..ca3f1fb --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/TimeSelector.kt @@ -0,0 +1,98 @@ +package app.myzel394.alibi.ui.components.WelcomeScreen.atoms + +import androidx.compose.foundation.background +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.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +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.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import app.myzel394.alibi.R +import app.myzel394.alibi.dataStore +import kotlinx.coroutines.launch + +const val MINUTES_5 = 1000 * 60 * 5L +const val MINUTES_15 = 1000 * 60 * 15L +const val MINUTES_30 = 1000 * 60 * 30L +const val HOURS_1 = 1000 * 60 * 60L + +@Composable +fun TimeSelector( + modifier: Modifier = Modifier, +) { + val OPTIONS = mapOf( + MINUTES_5 to stringResource(R.string.ui_welcome_timeSettings_values_5min), + MINUTES_15 to stringResource(R.string.ui_welcome_timeSettings_values_15min), + MINUTES_30 to stringResource(R.string.ui_welcome_timeSettings_values_30min), + HOURS_1 to stringResource(R.string.ui_welcome_timeSettings_values_1hour), + ) + + val scope = rememberCoroutineScope() + val dataStore = LocalContext.current.dataStore + + var selectedDuration by rememberSaveable { mutableLongStateOf(MINUTES_15) }; + + // Make sure appSettings is updated properly + LaunchedEffect(selectedDuration) { + scope.launch { + dataStore.updateData { + it.setMaxDuration(selectedDuration) + } + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + .background(MaterialTheme.colorScheme.surfaceContainer) + .then(modifier), + verticalArrangement = Arrangement.Center, + ) { + for ((duration, label) in OPTIONS) { + val a11yLabel = stringResource( + R.string.ui_welcome_timeSettings_selectTime, + label + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + .semantics { + contentDescription = a11yLabel + } + .clickable { + selectedDuration = duration + } + .padding(16.dp) + ) { + RadioButton( + selected = selectedDuration == duration, + onClick = { selectedDuration = duration }, + ) + Text(label) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/ExplanationPage.kt b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/ExplanationPage.kt similarity index 97% rename from app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/ExplanationPage.kt rename to app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/ExplanationPage.kt index 70051fe..3314830 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/ExplanationPage.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/ExplanationPage.kt @@ -1,4 +1,4 @@ -package app.myzel394.alibi.ui.components.WelcomeScreen.atoms +package app.myzel394.alibi.ui.components.WelcomeScreen.pages import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/ResponsibilityPage.kt b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/ResponsibilityPage.kt similarity index 91% rename from app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/ResponsibilityPage.kt rename to app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/ResponsibilityPage.kt index 3b2b526..537d61b 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/ResponsibilityPage.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/ResponsibilityPage.kt @@ -1,4 +1,4 @@ -package app.myzel394.alibi.ui.components.WelcomeScreen.atoms +package app.myzel394.alibi.ui.components.WelcomeScreen.pages import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -10,7 +10,7 @@ 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.Check +import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -59,7 +59,7 @@ fun ResponsibilityPage( } Spacer(modifier = Modifier.weight(1f)) Button( - onClick = { onContinue() }, + onClick = onContinue, modifier = Modifier .padding(16.dp) .fillMaxWidth() @@ -67,12 +67,12 @@ fun ResponsibilityPage( contentPadding = ButtonDefaults.ButtonWithIconContentPadding, ) { Icon( - Icons.Default.Check, + Icons.Default.ChevronRight, contentDescription = null, modifier = Modifier.size(ButtonDefaults.IconSize) ) Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) - Text(stringResource(R.string.ui_welcome_start_label)) + Text(stringResource(R.string.continue_label)) } } } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/TimeSettingsPage.kt b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/TimeSettingsPage.kt new file mode 100644 index 0000000..712f83c --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/TimeSettingsPage.kt @@ -0,0 +1,100 @@ +package app.myzel394.alibi.ui.components.WelcomeScreen.pages + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +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.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccessTime +import androidx.compose.material.icons.filled.ChevronRight +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import app.myzel394.alibi.R +import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE +import app.myzel394.alibi.ui.components.WelcomeScreen.atoms.TimeSelector +import app.myzel394.alibi.ui.components.atoms.MessageBox +import app.myzel394.alibi.ui.components.atoms.MessageType +import app.myzel394.alibi.ui.components.atoms.VisualDensity + +@Composable +fun TimeSettingsPage( + onContinue: () -> Unit, +) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.weight(1f)) + Column( + modifier = Modifier + .padding(horizontal = 32.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + Icons.Default.AccessTime, + contentDescription = null, + tint = MaterialTheme.colorScheme.tertiary, + modifier = Modifier.size(128.dp), + ) + Spacer(modifier = Modifier.height(32.dp)) + Text( + stringResource(R.string.ui_welcome_timeSettings_title), + style = MaterialTheme.typography.titleLarge, + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + stringResource(R.string.ui_welcome_timeSettings_message), + ) + } + Spacer(modifier = Modifier.weight(2f)) + Box( + modifier = Modifier.widthIn(max = 400.dp) + ) { + TimeSelector() + } + Spacer(modifier = Modifier.weight(1f)) + Box( + modifier = Modifier.widthIn(max = 400.dp) + ) { + MessageBox( + type = MessageType.INFO, + message = stringResource(R.string.ui_welcome_timeSettings_changeableHint), + density = VisualDensity.DENSE, + ) + } + Spacer(modifier = Modifier.weight(2f)) + Button( + onClick = { onContinue() }, + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + .height(BIG_PRIMARY_BUTTON_SIZE), + contentPadding = ButtonDefaults.ButtonWithIconContentPadding, + ) { + Icon( + Icons.Default.ChevronRight, + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize) + ) + Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) + Text(stringResource(R.string.continue_label)) + } + } +} diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt index ff15de0..084a8f2 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt @@ -13,11 +13,10 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.navigation.NavController import app.myzel394.alibi.dataStore -import app.myzel394.alibi.ui.components.WelcomeScreen.atoms.ExplanationPage -import app.myzel394.alibi.ui.components.WelcomeScreen.atoms.ResponsibilityPage -import app.myzel394.alibi.ui.enums.Screen +import app.myzel394.alibi.ui.components.WelcomeScreen.pages.ExplanationPage +import app.myzel394.alibi.ui.components.WelcomeScreen.pages.ResponsibilityPage +import app.myzel394.alibi.ui.components.WelcomeScreen.pages.TimeSettingsPage import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @@ -35,31 +34,44 @@ fun WelcomeScreen( val pagerState = rememberPagerState( initialPage = 0, initialPageOffsetFraction = 0f, - pageCount = {2} + pageCount = { 4 } ) - Scaffold() {padding -> + fun finishTutorial() { + scope.launch { + dataStore.updateData { + settings.setHasSeenOnboarding(true) + } + onNavigateToAudioRecorderScreen() + } + } + + Scaffold() { padding -> Column( modifier = Modifier .fillMaxSize() .padding(padding), horizontalAlignment = Alignment.CenterHorizontally, ) { - HorizontalPager(state = pagerState) {position -> + HorizontalPager(state = pagerState) { position -> when (position) { 0 -> ExplanationPage( onContinue = { scope.launch { - pagerState.animateScrollToPage(2) + pagerState.animateScrollToPage(1) } } ) + 1 -> ResponsibilityPage { scope.launch { - dataStore.updateData { - settings.setHasSeenOnboarding(true) - } - onNavigateToAudioRecorderScreen() + pagerState.animateScrollToPage(2) + } + } + + 2 -> TimeSettingsPage { + scope.launch { + pagerState.animateScrollToPage(3) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index baefa03..2436d62 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -195,4 +195,12 @@ You can save the current ongoing recording by pressing and holding down on the save button. The recording will continue in the background. You are low on storage. Alibi may not function properly. Please free up some space. You are low on storage. Alibi may not function properly. Please free up some space. Alternatively, change the batches folder to a different location in the settings. + How long should Alibi remember? + Alibi will continuously record and delete old recordings to make space for new ones. You decide how long Alibi should remember the past. + 5 Minutes + 15 minutes + 30 minutes + 1 hour + Select %s + You can change this anytime \ No newline at end of file From 552acdacf0395da5f469d28756c0a0666147f2a3 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 21 Mar 2024 22:37:02 +0100 Subject: [PATCH 20/39] feat: Add VisualDensity.DENSE to MessageBox Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../myzel394/alibi/ui/components/atoms/MessageBox.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/atoms/MessageBox.kt b/app/src/main/java/app/myzel394/alibi/ui/components/atoms/MessageBox.kt index d67ad64..2af9d33 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/atoms/MessageBox.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/atoms/MessageBox.kt @@ -68,15 +68,15 @@ fun MessageBox( .clip(MaterialTheme.shapes.medium) .background(backgroundColor) .let { - if (density == VisualDensity.COMFORTABLE) { - it.padding(horizontal = 8.dp, vertical = 16.dp) - } else { - it.padding(8.dp) + when (density) { + VisualDensity.COMFORTABLE -> it.padding(horizontal = 8.dp, vertical = 16.dp) + VisualDensity.DENSE -> it.padding(8.dp) + VisualDensity.COMPACT -> it.padding(8.dp) } } .then(modifier) ) { - if (density == VisualDensity.COMFORTABLE) { + if (density == VisualDensity.COMFORTABLE || density == VisualDensity.DENSE) { Icon( imageVector = when (type) { MessageType.ERROR -> Icons.Default.Error @@ -121,4 +121,5 @@ enum class MessageType { enum class VisualDensity { COMPACT, COMFORTABLE, + DENSE, } \ No newline at end of file From e968e7e589dd71f5f5a67496f11ed3c1b07f1770 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 21 Mar 2024 22:58:35 +0100 Subject: [PATCH 21/39] feat: Add SaveFolderPage Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../java/app/myzel394/alibi/ui/Navigation.kt | 2 +- .../WelcomeScreen/pages/SaveFolderPage.kt | 102 ++++++++++++++++++ .../alibi/ui/screens/WelcomeScreen.kt | 12 +++ app/src/main/res/values/strings.xml | 2 + 4 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/SaveFolderPage.kt diff --git a/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt b/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt index a2d5b36..30f119d 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt @@ -57,7 +57,7 @@ fun Navigation( modifier = Modifier .background(MaterialTheme.colorScheme.background), navController = navController, - startDestination = if (settings.hasSeenOnboarding) Screen.AudioRecorder.route else Screen.Welcome.route, + startDestination = Screen.Welcome.route, ) { composable(Screen.Welcome.route) { WelcomeScreen(onNavigateToAudioRecorderScreen = { navController.navigate(Screen.AudioRecorder.route) }) 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 new file mode 100644 index 0000000..73ebbf5 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/SaveFolderPage.kt @@ -0,0 +1,102 @@ +package app.myzel394.alibi.ui.components.WelcomeScreen.pages + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +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.layout.width +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.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import app.myzel394.alibi.R +import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE +import app.myzel394.alibi.ui.components.WelcomeScreen.atoms.SaveFolderSelection + +@Composable +fun SaveFolderPage( + onBack: () -> Unit, + onContinue: () -> Unit, +) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.weight(1f)) + Column( + modifier = Modifier + .padding(horizontal = 32.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + Icons.AutoMirrored.Filled.InsertDriveFile, + contentDescription = null, + tint = MaterialTheme.colorScheme.tertiary, + modifier = Modifier.size(128.dp), + ) + Spacer(modifier = Modifier.height(32.dp)) + Text( + stringResource(R.string.ui_welcome_saveFolder_title), + style = MaterialTheme.typography.titleLarge, + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + stringResource(R.string.ui_welcome_saveFolder_message), + ) + } + Spacer(modifier = Modifier.weight(1f)) + SaveFolderSelection() + Spacer(modifier = Modifier.weight(1f)) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + IconButton( + onClick = onBack, + modifier = Modifier + .size(BIG_PRIMARY_BUTTON_SIZE), + ) { + Icon( + Icons.Default.ChevronLeft, + contentDescription = null, + ) + } + Button( + onClick = onContinue, + modifier = Modifier + .fillMaxWidth() + .height(BIG_PRIMARY_BUTTON_SIZE), + contentPadding = ButtonDefaults.ButtonWithIconContentPadding, + ) { + Icon( + Icons.Default.ChevronRight, + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize) + ) + Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) + Text(stringResource(R.string.continue_label)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt index 084a8f2..eebd65c 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.platform.LocalContext import app.myzel394.alibi.dataStore import app.myzel394.alibi.ui.components.WelcomeScreen.pages.ExplanationPage import app.myzel394.alibi.ui.components.WelcomeScreen.pages.ResponsibilityPage +import app.myzel394.alibi.ui.components.WelcomeScreen.pages.SaveFolderPage import app.myzel394.alibi.ui.components.WelcomeScreen.pages.TimeSettingsPage import kotlinx.coroutines.launch @@ -74,6 +75,17 @@ fun WelcomeScreen( pagerState.animateScrollToPage(3) } } + + 3 -> SaveFolderPage( + onBack = { + scope.launch { + pagerState.animateScrollToPage(2) + } + }, + onContinue = { + finishTutorial() + } + ) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2436d62..68add45 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -203,4 +203,6 @@ 1 hour Select %s You can change this anytime + Where should Alibi store the batches? + By default, Alibi stores the batches into its own private, encrypted storage. You can change this and specify an external, unencrypted folder. If you want to let Alibi remember more than 15 minutes, you should choose an external folder, as the internal folder is very small. \ No newline at end of file From f06a79c1a895f83451841886e821f5515a4d977a Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 21 Mar 2024 23:23:12 +0100 Subject: [PATCH 22/39] feat: Adding SaveFolderSelection Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../atoms/SaveFolderSelection.kt | 110 ++++++++++++++++++ .../WelcomeScreen/atoms/TimeSelector.kt | 2 +- .../WelcomeScreen/pages/SaveFolderPage.kt | 44 ++++++- .../alibi/ui/screens/WelcomeScreen.kt | 14 +-- app/src/main/res/values/strings.xml | 7 +- 5 files changed, 167 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/SaveFolderSelection.kt diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/SaveFolderSelection.kt b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/SaveFolderSelection.kt new file mode 100644 index 0000000..c84fe60 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/SaveFolderSelection.kt @@ -0,0 +1,110 @@ +package app.myzel394.alibi.ui.components.WelcomeScreen.atoms + +import androidx.compose.foundation.background +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Folder +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.PermMedia +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import app.myzel394.alibi.R +import app.myzel394.alibi.db.AppSettings +import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE +import app.myzel394.alibi.ui.components.atoms.MessageBox +import app.myzel394.alibi.ui.components.atoms.MessageType + +const val CUSTOM_FOLDER = "custom" + +@Composable +fun SaveFolderSelection( + modifier: Modifier = Modifier, + appSettings: AppSettings, + saveFolder: String?, + isLowOnStorage: Boolean, + onSaveFolderChange: (String?) -> Unit, +) { + val OPTIONS = mapOf>( + null to (stringResource(R.string.ui_welcome_saveFolder_values_internal) to Icons.Default.Lock), + RECORDER_MEDIA_SELECTED_VALUE to (stringResource(R.string.ui_welcome_saveFolder_values_media) to Icons.Default.PermMedia), + CUSTOM_FOLDER to (stringResource(R.string.ui_welcome_saveFolder_values_custom) to Icons.Default.Folder), + ) + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + .background(MaterialTheme.colorScheme.surfaceContainer) + .then(modifier), + verticalArrangement = Arrangement.Center, + ) { + for ((folder, pair) in OPTIONS) { + val (label, icon) = pair + val a11yLabel = stringResource( + R.string.a11y_selectValue, + label + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + .semantics { + contentDescription = a11yLabel + } + .clickable { + onSaveFolderChange(folder) + } + .padding(16.dp) + .padding(end = 8.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + RadioButton( + selected = saveFolder == folder, + onClick = { onSaveFolderChange(folder) }, + ) + Text(label) + } + Icon( + icon, + contentDescription = null, + modifier = Modifier + .size(ButtonDefaults.IconSize) + ) + } + } + } + if (isLowOnStorage) + MessageBox( + type = MessageType.ERROR, + message = stringResource(R.string.ui_welcome_saveFolder_externalRequired) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/TimeSelector.kt b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/TimeSelector.kt index ca3f1fb..182115d 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/TimeSelector.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/TimeSelector.kt @@ -69,7 +69,7 @@ fun TimeSelector( ) { for ((duration, label) in OPTIONS) { val a11yLabel = stringResource( - R.string.ui_welcome_timeSettings_selectTime, + R.string.a11y_selectValue, label ) 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 73ebbf5..d45ea15 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 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 @@ -10,6 +11,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.InsertDriveFile import androidx.compose.material.icons.filled.ChevronLeft @@ -21,11 +23,19 @@ import androidx.compose.material3.IconButton 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.saveable.rememberSaveable +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.unit.dp import app.myzel394.alibi.R +import app.myzel394.alibi.db.AppSettings +import app.myzel394.alibi.helpers.BatchesFolder +import app.myzel394.alibi.helpers.VideoBatchesFolder import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE import app.myzel394.alibi.ui.components.WelcomeScreen.atoms.SaveFolderSelection @@ -33,7 +43,29 @@ import app.myzel394.alibi.ui.components.WelcomeScreen.atoms.SaveFolderSelection fun SaveFolderPage( onBack: () -> Unit, onContinue: () -> Unit, + appSettings: AppSettings, ) { + var saveFolder by rememberSaveable { mutableStateOf(null) } + + val context = LocalContext.current + + val isLowOnStorage = if (saveFolder != null) + false + else { + val availableBytes = VideoBatchesFolder.viaInternalFolder(context).getAvailableBytes() + + if (availableBytes == null) { + return + } + + val bytesPerMinute = BatchesFolder.requiredBytesForOneMinuteOfRecording(appSettings) + val requiredBytes = appSettings.maxDuration / 1000 / 60 * bytesPerMinute + + // Allow for a 10% margin of error + availableBytes < requiredBytes * 1.1 + } + + Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.SpaceBetween, @@ -63,7 +95,16 @@ fun SaveFolderPage( ) } Spacer(modifier = Modifier.weight(1f)) - SaveFolderSelection() + Box( + modifier = Modifier.widthIn(max = 400.dp) + ) { + SaveFolderSelection( + appSettings = appSettings, + saveFolder = saveFolder, + isLowOnStorage = isLowOnStorage, + onSaveFolderChange = { saveFolder = it }, + ) + } Spacer(modifier = Modifier.weight(1f)) Row( verticalAlignment = Alignment.CenterVertically, @@ -84,6 +125,7 @@ fun SaveFolderPage( } Button( onClick = onContinue, + enabled = !isLowOnStorage, modifier = Modifier .fillMaxWidth() .height(BIG_PRIMARY_BUTTON_SIZE), diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt index eebd65c..3ab59bf 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -18,6 +17,7 @@ import app.myzel394.alibi.ui.components.WelcomeScreen.pages.ExplanationPage import app.myzel394.alibi.ui.components.WelcomeScreen.pages.ResponsibilityPage import app.myzel394.alibi.ui.components.WelcomeScreen.pages.SaveFolderPage import app.myzel394.alibi.ui.components.WelcomeScreen.pages.TimeSettingsPage +import app.myzel394.alibi.ui.effects.rememberSettings import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @@ -27,10 +27,7 @@ fun WelcomeScreen( ) { val context = LocalContext.current val dataStore = context.dataStore - val settings = dataStore - .data - .collectAsState(initial = null) - .value ?: return + val settings = rememberSettings() val scope = rememberCoroutineScope() val pagerState = rememberPagerState( initialPage = 0, @@ -83,8 +80,11 @@ fun WelcomeScreen( } }, onContinue = { - finishTutorial() - } + scope.launch { + pagerState.animateScrollToPage(3) + } + }, + appSettings = settings ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 68add45..cc23b40 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,6 +11,8 @@ Please enter a number greater than %s Selected: %s + Select %s + Recorder Shows the current recording status @@ -201,8 +203,11 @@ 15 minutes 30 minutes 1 hour - Select %s You can change this anytime Where should Alibi store the batches? By default, Alibi stores the batches into its own private, encrypted storage. You can change this and specify an external, unencrypted folder. If you want to let Alibi remember more than 15 minutes, you should choose an external folder, as the internal folder is very small. + Internal Storage + Custom Folder + Media Folder + Please select either the Media Folder or a Custom Folder. Alibi has not enough space to store the batches in the internal storage. Alternatively, go back one step and select a shorter duration. \ No newline at end of file From 97acb6d977ac39800a7b6a32399f56df87f30a32 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 22 Mar 2024 00:10:42 +0100 Subject: [PATCH 23/39] feat: Improve SaveFolder; Request permission: Show warning if custom folder not supported Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../atoms/SaveFolderSelection.kt | 125 ++++++++++++++-- .../WelcomeScreen/pages/SaveFolderPage.kt | 135 +++++++++++++++--- app/src/main/res/values/strings.xml | 2 + 3 files changed, 230 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/SaveFolderSelection.kt b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/SaveFolderSelection.kt index c84fe60..02b79e7 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/SaveFolderSelection.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/SaveFolderSelection.kt @@ -8,6 +8,8 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth 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.filled.Folder import androidx.compose.material.icons.filled.Lock @@ -29,6 +31,7 @@ import androidx.compose.ui.unit.dp import app.myzel394.alibi.R import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE +import app.myzel394.alibi.ui.SUPPORTS_SAVING_VIDEOS_IN_CUSTOM_FOLDERS import app.myzel394.alibi.ui.components.atoms.MessageBox import app.myzel394.alibi.ui.components.atoms.MessageType @@ -48,9 +51,21 @@ fun SaveFolderSelection( CUSTOM_FOLDER to (stringResource(R.string.ui_welcome_saveFolder_values_custom) to Icons.Default.Folder), ) + @Composable + fun createModifier(a11yLabel: String, onClick: () -> Unit) = + Modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + .semantics { + contentDescription = a11yLabel + } + .clickable(onClick = onClick) + .padding(16.dp) + .padding(end = 8.dp) + Column( verticalArrangement = Arrangement.spacedBy(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.verticalScroll(rememberScrollState()), ) { Column( modifier = Modifier @@ -60,27 +75,20 @@ fun SaveFolderSelection( .then(modifier), verticalArrangement = Arrangement.Center, ) { - for ((folder, pair) in OPTIONS) { - val (label, icon) = pair + let { + val label = stringResource(R.string.ui_welcome_saveFolder_values_internal) val a11yLabel = stringResource( R.string.a11y_selectValue, label ) + val folder = null Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .fillMaxWidth() - .clip(MaterialTheme.shapes.medium) - .semantics { - contentDescription = a11yLabel - } - .clickable { - onSaveFolderChange(folder) - } - .padding(16.dp) - .padding(end = 8.dp) + modifier = createModifier(a11yLabel) { + onSaveFolderChange(folder) + }, ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -93,13 +101,100 @@ fun SaveFolderSelection( Text(label) } Icon( - icon, + Icons.Default.Lock, contentDescription = null, modifier = Modifier .size(ButtonDefaults.IconSize) ) } } + let { + val label = stringResource(R.string.ui_welcome_saveFolder_values_media) + val a11yLabel = stringResource( + R.string.a11y_selectValue, + label + ) + val folder = RECORDER_MEDIA_SELECTED_VALUE + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = createModifier(a11yLabel) { + onSaveFolderChange(folder) + }, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + RadioButton( + selected = saveFolder == folder, + onClick = { onSaveFolderChange(folder) }, + ) + Text(label) + } + Icon( + Icons.Default.Lock, + contentDescription = null, + modifier = Modifier + .size(ButtonDefaults.IconSize) + ) + } + } + let { + val label = stringResource(R.string.ui_welcome_saveFolder_values_custom) + val a11yLabel = stringResource( + R.string.a11y_selectValue, + label + ) + val folder = CUSTOM_FOLDER + + Column( + horizontalAlignment = Alignment.Start, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = createModifier(a11yLabel) { + onSaveFolderChange(folder) + }, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + RadioButton( + selected = saveFolder == folder, + onClick = { onSaveFolderChange(folder) }, + ) + Text(label) + } + Icon( + Icons.Default.Lock, + contentDescription = null, + modifier = Modifier + .size(ButtonDefaults.IconSize) + ) + } + if (!SUPPORTS_SAVING_VIDEOS_IN_CUSTOM_FOLDERS) { + Column( + modifier = Modifier + .padding(horizontal = 32.dp, vertical = 12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + stringResource(R.string.ui_settings_option_saveFolder_videoUnsupported), + fontSize = MaterialTheme.typography.titleSmall.fontSize, + ) + Text( + stringResource(R.string.ui_minApiRequired, 8, 26), + fontSize = MaterialTheme.typography.bodySmall.fontSize, + ) + } + } + } + } } if (isLowOnStorage) MessageBox( 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 d45ea15..19d499c 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,5 +1,6 @@ package app.myzel394.alibi.ui.components.WelcomeScreen.pages +import android.Manifest import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -16,12 +17,15 @@ 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.Folder +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -37,7 +41,14 @@ import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.helpers.BatchesFolder import app.myzel394.alibi.helpers.VideoBatchesFolder import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE +import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE +import app.myzel394.alibi.ui.SUPPORTS_SCOPED_STORAGE import app.myzel394.alibi.ui.components.WelcomeScreen.atoms.SaveFolderSelection +import app.myzel394.alibi.ui.components.atoms.MessageBox +import app.myzel394.alibi.ui.components.atoms.MessageType +import app.myzel394.alibi.ui.components.atoms.PermissionRequester +import app.myzel394.alibi.ui.components.atoms.VisualDensity +import app.myzel394.alibi.ui.utils.rememberFolderSelectorDialog @Composable fun SaveFolderPage( @@ -67,7 +78,8 @@ fun SaveFolderPage( Column( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize(), verticalArrangement = Arrangement.SpaceBetween, horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -94,7 +106,7 @@ fun SaveFolderPage( stringResource(R.string.ui_welcome_saveFolder_message), ) } - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.weight(2f)) Box( modifier = Modifier.widthIn(max = 400.dp) ) { @@ -106,6 +118,16 @@ fun SaveFolderPage( ) } Spacer(modifier = Modifier.weight(1f)) + Box( + modifier = Modifier.widthIn(max = 400.dp) + ) { + MessageBox( + type = MessageType.INFO, + message = stringResource(R.string.ui_welcome_timeSettings_changeableHint), + density = VisualDensity.DENSE, + ) + } + Spacer(modifier = Modifier.weight(2f)) Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier @@ -123,22 +145,101 @@ fun SaveFolderPage( contentDescription = null, ) } - Button( - onClick = onContinue, - enabled = !isLowOnStorage, - modifier = Modifier - .fillMaxWidth() - .height(BIG_PRIMARY_BUTTON_SIZE), - contentPadding = ButtonDefaults.ButtonWithIconContentPadding, - ) { - Icon( - Icons.Default.ChevronRight, - contentDescription = null, - modifier = Modifier.size(ButtonDefaults.IconSize) - ) - Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) - Text(stringResource(R.string.continue_label)) + PermissionRequester( + permission = Manifest.permission.WRITE_EXTERNAL_STORAGE, + icon = Icons.AutoMirrored.Filled.InsertDriveFile, + onPermissionAvailable = onContinue, + ) { requestWritePermission -> + val selectFolder = rememberFolderSelectorDialog { folder -> + if (folder == null) { + return@rememberFolderSelectorDialog + } + + onContinue() + } + var showCustomFolderHint by rememberSaveable { mutableStateOf(false) } + + if (showCustomFolderHint) { + _CustomFolderDialog( + onAbort = { showCustomFolderHint = false }, + onOk = { + showCustomFolderHint = false + selectFolder() + }, + ) + } + + Button( + onClick = { + when (saveFolder) { + null -> onContinue() + RECORDER_MEDIA_SELECTED_VALUE -> { + if (SUPPORTS_SCOPED_STORAGE) { + onContinue() + } else { + requestWritePermission() + } + } + + else -> { + showCustomFolderHint = true + } + } + }, + enabled = !isLowOnStorage, + modifier = Modifier + .fillMaxWidth() + .height(BIG_PRIMARY_BUTTON_SIZE), + contentPadding = ButtonDefaults.ButtonWithIconContentPadding, + ) { + Icon( + Icons.Default.ChevronRight, + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize) + ) + Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) + Text(stringResource(R.string.continue_label)) + } } } } +} + +@Composable +fun _CustomFolderDialog( + onAbort: () -> Unit, + onOk: () -> Unit, +) { + AlertDialog( + onDismissRequest = onAbort, + icon = { + Icon( + Icons.Default.Folder, + contentDescription = null, + ) + }, + title = { + Text(stringResource(R.string.ui_welcome_saveFolder_customFolder_title)) + }, + text = { + Text(stringResource(R.string.ui_welcome_saveFolder_customFolder_message)) + }, + dismissButton = { + TextButton( + onClick = onAbort, + contentPadding = ButtonDefaults.TextButtonContentPadding, + colors = ButtonDefaults.textButtonColors(), + ) { + Text(stringResource(R.string.dialog_close_cancel_label)) + } + }, + confirmButton = { + Button( + onClick = onOk, + ) { + Text(stringResource(R.string.dialog_close_neutral_label)) + } + } + ) + } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cc23b40..15abe38 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -210,4 +210,6 @@ Custom Folder Media Folder Please select either the Media Folder or a Custom Folder. Alibi has not enough space to store the batches in the internal storage. Alternatively, go back one step and select a shorter duration. + Select a Custom Folder + You will now be asked to select a folder where Alibi should store the batches. Please select a folder where you have write access to. \ No newline at end of file From dac133e6b3c57084fa55f4d52d3dacab60437a27 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 22 Mar 2024 08:59:58 +0100 Subject: [PATCH 24/39] fix: Improve WelcomeScreen overflow Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../atoms/SaveFolderSelection.kt | 20 ++++++++++++++----- .../WelcomeScreen/pages/SaveFolderPage.kt | 20 +++++-------------- .../WelcomeScreen/pages/TimeSettingsPage.kt | 6 +++++- .../alibi/ui/screens/WelcomeScreen.kt | 4 +++- 4 files changed, 28 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/SaveFolderSelection.kt b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/SaveFolderSelection.kt index 02b79e7..0c20b04 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/SaveFolderSelection.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/SaveFolderSelection.kt @@ -3,13 +3,13 @@ package app.myzel394.alibi.ui.components.WelcomeScreen.atoms import androidx.compose.foundation.background import androidx.compose.foundation.clickable 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.fillMaxWidth 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.foundation.layout.widthIn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.filled.Lock @@ -34,6 +34,7 @@ import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE import app.myzel394.alibi.ui.SUPPORTS_SAVING_VIDEOS_IN_CUSTOM_FOLDERS import app.myzel394.alibi.ui.components.atoms.MessageBox import app.myzel394.alibi.ui.components.atoms.MessageType +import app.myzel394.alibi.ui.components.atoms.VisualDensity const val CUSTOM_FOLDER = "custom" @@ -65,7 +66,6 @@ fun SaveFolderSelection( Column( verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.verticalScroll(rememberScrollState()), ) { Column( modifier = Modifier @@ -134,7 +134,7 @@ fun SaveFolderSelection( Text(label) } Icon( - Icons.Default.Lock, + Icons.Default.PermMedia, contentDescription = null, modifier = Modifier .size(ButtonDefaults.IconSize) @@ -170,7 +170,7 @@ fun SaveFolderSelection( Text(label) } Icon( - Icons.Default.Lock, + Icons.Default.Folder, contentDescription = null, modifier = Modifier .size(ButtonDefaults.IconSize) @@ -201,5 +201,15 @@ fun SaveFolderSelection( type = MessageType.ERROR, message = stringResource(R.string.ui_welcome_saveFolder_externalRequired) ) + else + Box( + modifier = Modifier.widthIn(max = 400.dp) + ) { + MessageBox( + type = MessageType.INFO, + message = stringResource(R.string.ui_welcome_timeSettings_changeableHint), + density = VisualDensity.DENSE, + ) + } } } \ No newline at end of file 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 19d499c..6eab985 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 @@ -13,6 +13,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.InsertDriveFile import androidx.compose.material.icons.filled.ChevronLeft @@ -44,10 +46,7 @@ import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE import app.myzel394.alibi.ui.SUPPORTS_SCOPED_STORAGE import app.myzel394.alibi.ui.components.WelcomeScreen.atoms.SaveFolderSelection -import app.myzel394.alibi.ui.components.atoms.MessageBox -import app.myzel394.alibi.ui.components.atoms.MessageType import app.myzel394.alibi.ui.components.atoms.PermissionRequester -import app.myzel394.alibi.ui.components.atoms.VisualDensity import app.myzel394.alibi.ui.utils.rememberFolderSelectorDialog @Composable @@ -79,7 +78,8 @@ fun SaveFolderPage( Column( modifier = Modifier - .fillMaxSize(), + .fillMaxSize() + .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.SpaceBetween, horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -106,7 +106,7 @@ fun SaveFolderPage( stringResource(R.string.ui_welcome_saveFolder_message), ) } - Spacer(modifier = Modifier.weight(2f)) + Spacer(modifier = Modifier.weight(1f)) Box( modifier = Modifier.widthIn(max = 400.dp) ) { @@ -118,16 +118,6 @@ fun SaveFolderPage( ) } Spacer(modifier = Modifier.weight(1f)) - Box( - modifier = Modifier.widthIn(max = 400.dp) - ) { - MessageBox( - type = MessageType.INFO, - message = stringResource(R.string.ui_welcome_timeSettings_changeableHint), - density = VisualDensity.DENSE, - ) - } - Spacer(modifier = Modifier.weight(2f)) Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/TimeSettingsPage.kt b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/TimeSettingsPage.kt index 712f83c..454b90f 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/TimeSettingsPage.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/TimeSettingsPage.kt @@ -11,6 +11,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccessTime import androidx.compose.material.icons.filled.ChevronRight @@ -36,7 +38,9 @@ fun TimeSettingsPage( onContinue: () -> Unit, ) { Column( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.SpaceBetween, horizontalAlignment = Alignment.CenterHorizontally, ) { diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt index 3ab59bf..c8686e5 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt @@ -44,11 +44,13 @@ fun WelcomeScreen( } } - Scaffold() { padding -> + Scaffold( + ) { padding -> Column( modifier = Modifier .fillMaxSize() .padding(padding), + horizontalAlignment = Alignment.CenterHorizontally, ) { HorizontalPager(state = pagerState) { position -> From aaee0c5cb626bfd1707bc8ad8ff23c5d95f3d847 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 22 Mar 2024 19:05:13 +0100 Subject: [PATCH 25/39] feat: Add custom value to TimeSelector Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../WelcomeScreen/atoms/TimeSelector.kt | 98 +++++++++++++++++++ app/src/main/res/values/strings.xml | 4 + 2 files changed, 102 insertions(+) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/TimeSelector.kt b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/TimeSelector.kt index 182115d..560bae5 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/TimeSelector.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/TimeSelector.kt @@ -7,9 +7,16 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.Timer +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.RadioButton import androidx.compose.material3.Text +import androidx.compose.material3.contentColorFor +import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -27,6 +34,14 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import app.myzel394.alibi.R import app.myzel394.alibi.dataStore +import app.myzel394.alibi.ui.utils.IconResource +import com.maxkeppeker.sheets.core.models.base.Header +import com.maxkeppeker.sheets.core.models.base.IconSource +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 const val MINUTES_5 = 1000 * 60 * 5L @@ -34,6 +49,7 @@ const val MINUTES_15 = 1000 * 60 * 15L const val MINUTES_30 = 1000 * 60 * 30L const val HOURS_1 = 1000 * 60 * 60L +@OptIn(ExperimentalMaterial3Api::class) @Composable fun TimeSelector( modifier: Modifier = Modifier, @@ -94,5 +110,87 @@ fun TimeSelector( Text(label) } } + + let { + val showDialog = rememberUseCaseState() + val label = stringResource(R.string.ui_welcome_timeSettings_values_custom) + val selected = selectedDuration !in OPTIONS.keys + + DurationDialog( + state = showDialog, + header = Header.Default( + title = stringResource(R.string.ui_settings_option_maxDuration_title), + icon = IconSource( + painter = IconResource.fromImageVector(Icons.Default.Timer) + .asPainterResource(), + contentDescription = null, + ) + ), + selection = DurationSelection { newTimeInSeconds -> + selectedDuration = newTimeInSeconds * 1000L + }, + config = DurationConfig( + timeFormat = DurationFormat.HH_MM, + currentTime = selectedDuration / 1000, + minTime = 60, + maxTime = 23 * 60 * 60 + 60 * 59, + ) + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + .semantics { + contentDescription = label + } + .clickable { + showDialog.show() + } + .clip(MaterialTheme.shapes.medium) + .padding(16.dp) + ) { + Icon( + Icons.Default.Edit, + contentDescription = null, + modifier = Modifier + .minimumInteractiveComponentSize() + .padding(2.dp), + tint = if (selected) MaterialTheme.colorScheme.primary else contentColorFor( + MaterialTheme.colorScheme.surfaceContainer + ) + ) + if (selected) { + val totalMinutes = selectedDuration / 1000 / 60 + val minutes = totalMinutes % 60 + val hours = (totalMinutes / 60).toInt() + + Text( + text = when (hours) { + 0 -> stringResource( + R.string.ui_welcome_timeSettings_values_customFormat_mm, + minutes + ) + + 1 -> stringResource( + R.string.ui_welcome_timeSettings_values_customFormat_h_mm, + minutes + ) + + else -> stringResource( + R.string.ui_welcome_timeSettings_values_customFormat_hh_mm, + hours, + minutes + ) + }, + color = MaterialTheme.colorScheme.primary, + ) + } else { + Text( + text = stringResource(R.string.ui_welcome_timeSettings_values_custom), + ) + } + } + } } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 15abe38..30641a9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -212,4 +212,8 @@ Please select either the Media Folder or a Custom Folder. Alibi has not enough space to store the batches in the internal storage. Alternatively, go back one step and select a shorter duration. Select a Custom Folder You will now be asked to select a folder where Alibi should store the batches. Please select a folder where you have write access to. + Custom Duration + %s minutes + %s hour, %s minutes + 1 hour, %s minutes \ No newline at end of file From f9d20c67d7d2703845302a2a99202ba2e630972d Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 22 Mar 2024 19:31:31 +0100 Subject: [PATCH 26/39] feat: Automatically select media folder if storage is not enough Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../WelcomeScreen/atoms/SaveFolderSelection.kt | 11 +---------- .../WelcomeScreen/pages/SaveFolderPage.kt | 18 +++++++++++------- .../myzel394/alibi/ui/screens/WelcomeScreen.kt | 4 +++- app/src/main/res/values/strings.xml | 2 +- 4 files changed, 16 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/SaveFolderSelection.kt b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/SaveFolderSelection.kt index 0c20b04..2c1ca12 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/SaveFolderSelection.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/SaveFolderSelection.kt @@ -23,13 +23,11 @@ import androidx.compose.runtime.Composable 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.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import app.myzel394.alibi.R -import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE import app.myzel394.alibi.ui.SUPPORTS_SAVING_VIDEOS_IN_CUSTOM_FOLDERS import app.myzel394.alibi.ui.components.atoms.MessageBox @@ -41,17 +39,10 @@ const val CUSTOM_FOLDER = "custom" @Composable fun SaveFolderSelection( modifier: Modifier = Modifier, - appSettings: AppSettings, saveFolder: String?, isLowOnStorage: Boolean, onSaveFolderChange: (String?) -> Unit, ) { - val OPTIONS = mapOf>( - null to (stringResource(R.string.ui_welcome_saveFolder_values_internal) to Icons.Default.Lock), - RECORDER_MEDIA_SELECTED_VALUE to (stringResource(R.string.ui_welcome_saveFolder_values_media) to Icons.Default.PermMedia), - CUSTOM_FOLDER to (stringResource(R.string.ui_welcome_saveFolder_values_custom) to Icons.Default.Folder), - ) - @Composable fun createModifier(a11yLabel: String, onClick: () -> Unit) = Modifier @@ -196,7 +187,7 @@ fun SaveFolderSelection( } } } - if (isLowOnStorage) + if (isLowOnStorage && saveFolder == null) MessageBox( type = MessageType.ERROR, message = stringResource(R.string.ui_welcome_saveFolder_externalRequired) 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 6eab985..1f87891 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 @@ -29,8 +29,10 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -59,22 +61,25 @@ fun SaveFolderPage( val context = LocalContext.current - val isLowOnStorage = if (saveFolder != null) - false - else { + val isLowOnStorage: Boolean = remember(appSettings.maxDuration) { val availableBytes = VideoBatchesFolder.viaInternalFolder(context).getAvailableBytes() if (availableBytes == null) { - return + return@remember false } val bytesPerMinute = BatchesFolder.requiredBytesForOneMinuteOfRecording(appSettings) val requiredBytes = appSettings.maxDuration / 1000 / 60 * bytesPerMinute // Allow for a 10% margin of error - availableBytes < requiredBytes * 1.1 + availableBytes < requiredBytes } + LaunchedEffect(isLowOnStorage, appSettings.maxDuration) { + if (isLowOnStorage && saveFolder == null) { + saveFolder = RECORDER_MEDIA_SELECTED_VALUE + } + } Column( modifier = Modifier @@ -111,7 +116,6 @@ fun SaveFolderPage( modifier = Modifier.widthIn(max = 400.dp) ) { SaveFolderSelection( - appSettings = appSettings, saveFolder = saveFolder, isLowOnStorage = isLowOnStorage, onSaveFolderChange = { saveFolder = it }, @@ -176,7 +180,7 @@ fun SaveFolderPage( } } }, - enabled = !isLowOnStorage, + enabled = if (saveFolder == null) !isLowOnStorage else true, modifier = Modifier .fillMaxWidth() .height(BIG_PRIMARY_BUTTON_SIZE), diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt index c8686e5..c1c461c 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt @@ -53,7 +53,9 @@ fun WelcomeScreen( horizontalAlignment = Alignment.CenterHorizontally, ) { - HorizontalPager(state = pagerState) { position -> + HorizontalPager( + state = pagerState, + ) { position -> when (position) { 0 -> ExplanationPage( onContinue = { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 30641a9..4c2a370 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -205,7 +205,7 @@ 1 hour You can change this anytime Where should Alibi store the batches? - By default, Alibi stores the batches into its own private, encrypted storage. You can change this and specify an external, unencrypted folder. If you want to let Alibi remember more than 15 minutes, you should choose an external folder, as the internal folder is very small. + Select where you would like to let Alibi store the batches of the ongoing recordings. The internal folder is encrypted and only accessible by Alibi. This folder is recommended if you only want to record a small time frame. If you need a longer time frame, you will most likely need to select a different folder, as the internal storage is very limited. Internal Storage Custom Folder Media Folder From ac6cc7a5c0947353a00baef0f4ecc1e307a0059d Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 22 Mar 2024 19:40:36 +0100 Subject: [PATCH 27/39] feat: Add ReadyPage Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../java/app/myzel394/alibi/ui/Navigation.kt | 13 +++- .../WelcomeScreen/pages/ReadyPage.kt | 78 +++++++++++++++++++ .../alibi/ui/screens/WelcomeScreen.kt | 9 ++- app/src/main/res/values/strings.xml | 3 + 4 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/ReadyPage.kt diff --git a/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt b/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt index 30f119d..52d498b 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.core.content.ContextCompat import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -57,10 +58,18 @@ fun Navigation( modifier = Modifier .background(MaterialTheme.colorScheme.background), navController = navController, - startDestination = Screen.Welcome.route, + startDestination = if (settings.hasSeenOnboarding) Screen.AudioRecorder.route else Screen.Welcome.route, ) { composable(Screen.Welcome.route) { - WelcomeScreen(onNavigateToAudioRecorderScreen = { navController.navigate(Screen.AudioRecorder.route) }) + WelcomeScreen( + onNavigateToAudioRecorderScreen = { + val mainHandler = ContextCompat.getMainExecutor(context) + + mainHandler.execute { + navController.navigate(Screen.AudioRecorder.route) + } + }, + ) } composable( Screen.AudioRecorder.route, diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/ReadyPage.kt b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/ReadyPage.kt new file mode 100644 index 0000000..cdee3cb --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/ReadyPage.kt @@ -0,0 +1,78 @@ +package app.myzel394.alibi.ui.components.WelcomeScreen.pages + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +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.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Celebration +import androidx.compose.material.icons.filled.Check +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import app.myzel394.alibi.R +import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE + +@Composable +fun ReadyPage( + onContinue: () -> Unit, +) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.weight(1f)) + Column( + modifier = Modifier + .padding(horizontal = 32.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + Icons.Default.Celebration, + contentDescription = null, + tint = MaterialTheme.colorScheme.tertiary, + modifier = Modifier.size(128.dp), + ) + Spacer(modifier = Modifier.height(32.dp)) + Text( + stringResource(R.string.ui_welcome_ready_title), + style = MaterialTheme.typography.titleLarge, + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + stringResource(R.string.ui_welcome_ready_message), + ) + } + Spacer(modifier = Modifier.weight(1f)) + Button( + onClick = onContinue, + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + .height(BIG_PRIMARY_BUTTON_SIZE), + contentPadding = ButtonDefaults.ButtonWithIconContentPadding, + ) { + Icon( + Icons.Default.Check, + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize) + ) + Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) + Text(stringResource(R.string.ui_welcome_ready_start)) + } + } +} diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt index c1c461c..adcd27f 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import app.myzel394.alibi.dataStore import app.myzel394.alibi.ui.components.WelcomeScreen.pages.ExplanationPage +import app.myzel394.alibi.ui.components.WelcomeScreen.pages.ReadyPage import app.myzel394.alibi.ui.components.WelcomeScreen.pages.ResponsibilityPage import app.myzel394.alibi.ui.components.WelcomeScreen.pages.SaveFolderPage import app.myzel394.alibi.ui.components.WelcomeScreen.pages.TimeSettingsPage @@ -32,7 +33,7 @@ fun WelcomeScreen( val pagerState = rememberPagerState( initialPage = 0, initialPageOffsetFraction = 0f, - pageCount = { 4 } + pageCount = { 5 } ) fun finishTutorial() { @@ -85,11 +86,15 @@ fun WelcomeScreen( }, onContinue = { scope.launch { - pagerState.animateScrollToPage(3) + pagerState.animateScrollToPage(4) } }, appSettings = settings ) + + 4 -> ReadyPage { + finishTutorial() + } } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4c2a370..37615a8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -216,4 +216,7 @@ %s minutes %s hour, %s minutes 1 hour, %s minutes + You are ready! + You are ready to start using Alibi! Go ahead and try it out! + Start Alibi \ No newline at end of file From 04c6cd92a330c50d53d42ef5ecd24b553170b5f4 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 22 Mar 2024 19:40:47 +0100 Subject: [PATCH 28/39] fix: Improvements Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../ui/components/WelcomeScreen/pages/SaveFolderPage.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 1f87891..8fc9990 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 @@ -76,8 +76,12 @@ fun SaveFolderPage( } LaunchedEffect(isLowOnStorage, appSettings.maxDuration) { - if (isLowOnStorage && saveFolder == null) { - saveFolder = RECORDER_MEDIA_SELECTED_VALUE + if (isLowOnStorage) { + if (saveFolder == null) { + saveFolder = RECORDER_MEDIA_SELECTED_VALUE + } + } else { + saveFolder = null } } From e4e23abcea9d37c0e2c61b62bd0021117dd01d2e Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 22 Mar 2024 19:46:18 +0100 Subject: [PATCH 29/39] fix: Save saveFolder on change Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../components/WelcomeScreen/pages/SaveFolderPage.kt | 10 +++++----- .../app/myzel394/alibi/ui/screens/WelcomeScreen.kt | 9 ++++++--- 2 files changed, 11 insertions(+), 8 deletions(-) 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 8fc9990..65ec0c2 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 @@ -54,7 +54,7 @@ import app.myzel394.alibi.ui.utils.rememberFolderSelectorDialog @Composable fun SaveFolderPage( onBack: () -> Unit, - onContinue: () -> Unit, + onContinue: (saveFolder: String?) -> Unit, appSettings: AppSettings, ) { var saveFolder by rememberSaveable { mutableStateOf(null) } @@ -146,14 +146,14 @@ fun SaveFolderPage( PermissionRequester( permission = Manifest.permission.WRITE_EXTERNAL_STORAGE, icon = Icons.AutoMirrored.Filled.InsertDriveFile, - onPermissionAvailable = onContinue, + onPermissionAvailable = { onContinue(saveFolder) }, ) { requestWritePermission -> val selectFolder = rememberFolderSelectorDialog { folder -> if (folder == null) { return@rememberFolderSelectorDialog } - onContinue() + onContinue(saveFolder) } var showCustomFolderHint by rememberSaveable { mutableStateOf(false) } @@ -170,10 +170,10 @@ fun SaveFolderPage( Button( onClick = { when (saveFolder) { - null -> onContinue() + null -> onContinue(saveFolder) RECORDER_MEDIA_SELECTED_VALUE -> { if (SUPPORTS_SCOPED_STORAGE) { - onContinue() + onContinue(saveFolder) } else { requestWritePermission() } diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt index adcd27f..ec2c014 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt @@ -45,8 +45,7 @@ fun WelcomeScreen( } } - Scaffold( - ) { padding -> + Scaffold() { padding -> Column( modifier = Modifier .fillMaxSize() @@ -84,8 +83,12 @@ fun WelcomeScreen( pagerState.animateScrollToPage(2) } }, - onContinue = { + onContinue = { saveFolder -> scope.launch { + dataStore.updateData { + settings.setSaveFolder(saveFolder) + } + pagerState.animateScrollToPage(4) } }, From 0461fe9596f8683f1d22d0d8a89e366dd096e99d Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 22 Mar 2024 20:03:01 +0100 Subject: [PATCH 30/39] fix: Use proper media folder for low storage info Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../app/myzel394/alibi/helpers/BatchesFolder.kt | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) 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 3c3716c..705ae48 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt @@ -7,6 +7,7 @@ import android.content.Context import android.database.Cursor import android.net.Uri import android.os.Build +import android.os.Environment import android.os.storage.StorageManager import android.provider.MediaStore import android.provider.MediaStore.Video.Media @@ -527,7 +528,17 @@ abstract class BatchesFolder( val file = when (type) { BatchType.INTERNAL -> context.filesDir BatchType.CUSTOM -> customFolder!!.uri.toFile() - BatchType.MEDIA -> scopedMediaContentUri.toFile() + BatchType.MEDIA -> + if (SUPPORTS_SCOPED_STORAGE) + File( + Environment.getExternalStoragePublicDirectory(VideoBatchesFolder.BASE_SCOPED_STORAGE_RELATIVE_PATH), + MediaStore.Video.Media.EXTERNAL_CONTENT_URI.toString(), + ) + else + File( + Environment.getExternalStoragePublicDirectory(VideoBatchesFolder.BASE_LEGACY_STORAGE_FOLDER), + VideoBatchesFolder.MEDIA_RECORDINGS_SUBFOLDER, + ) } return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { From eb6baac5037165a0fdd9751275e788e1a341bec2 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 22 Mar 2024 20:09:09 +0100 Subject: [PATCH 31/39] fix: Improve styling Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../WelcomeScreen/pages/SaveFolderPage.kt | 13 +++++++++---- .../WelcomeScreen/pages/TimeSettingsPage.kt | 19 +++++++++++++------ 2 files changed, 22 insertions(+), 10 deletions(-) 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 65ec0c2..6302632 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 @@ -92,7 +92,7 @@ fun SaveFolderPage( verticalArrangement = Arrangement.SpaceBetween, horizontalAlignment = Alignment.CenterHorizontally, ) { - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(40.dp)) Column( modifier = Modifier .padding(horizontal = 32.dp), @@ -113,11 +113,16 @@ fun SaveFolderPage( Spacer(modifier = Modifier.height(16.dp)) Text( stringResource(R.string.ui_welcome_saveFolder_message), + fontStyle = MaterialTheme.typography.bodySmall.fontStyle, + fontSize = MaterialTheme.typography.bodySmall.fontSize, + color = MaterialTheme.typography.bodySmall.color, ) } - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(40.dp)) Box( - modifier = Modifier.widthIn(max = 400.dp) + modifier = Modifier + .widthIn(max = 400.dp) + .padding(horizontal = 16.dp) ) { SaveFolderSelection( saveFolder = saveFolder, @@ -125,7 +130,7 @@ fun SaveFolderPage( onSaveFolderChange = { saveFolder = it }, ) } - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(40.dp)) Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/TimeSettingsPage.kt b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/TimeSettingsPage.kt index 454b90f..35aa0a9 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/TimeSettingsPage.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/TimeSettingsPage.kt @@ -44,7 +44,7 @@ fun TimeSettingsPage( verticalArrangement = Arrangement.SpaceBetween, horizontalAlignment = Alignment.CenterHorizontally, ) { - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(40.dp)) Column( modifier = Modifier .padding(horizontal = 32.dp), @@ -65,17 +65,24 @@ fun TimeSettingsPage( Spacer(modifier = Modifier.height(16.dp)) Text( stringResource(R.string.ui_welcome_timeSettings_message), + fontStyle = MaterialTheme.typography.bodySmall.fontStyle, + fontSize = MaterialTheme.typography.bodySmall.fontSize, + color = MaterialTheme.typography.bodySmall.color, ) } - Spacer(modifier = Modifier.weight(2f)) + Spacer(modifier = Modifier.height(40.dp)) Box( - modifier = Modifier.widthIn(max = 400.dp) + modifier = Modifier + .widthIn(max = 400.dp) + .padding(horizontal = 16.dp) ) { TimeSelector() } - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(20.dp)) Box( - modifier = Modifier.widthIn(max = 400.dp) + modifier = Modifier + .widthIn(max = 400.dp) + .padding(horizontal = 16.dp) ) { MessageBox( type = MessageType.INFO, @@ -83,7 +90,7 @@ fun TimeSettingsPage( density = VisualDensity.DENSE, ) } - Spacer(modifier = Modifier.weight(2f)) + Spacer(modifier = Modifier.height(40.dp)) Button( onClick = { onContinue() }, modifier = Modifier From 4e93ff4bb2e08d92818dfca64ab40eb49e322975 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 22 Mar 2024 21:08:45 +0100 Subject: [PATCH 32/39] feat: Add contact methods Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../java/app/myzel394/alibi/ui/Constants.kt | 15 +++++ .../myzel394/alibi/ui/screens/AboutScreen.kt | 57 ++++++++++++++++++- app/src/main/res/values/strings.xml | 2 + 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/Constants.kt b/app/src/main/java/app/myzel394/alibi/ui/Constants.kt index 979ee51..a8afa20 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/Constants.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/Constants.kt @@ -2,6 +2,7 @@ package app.myzel394.alibi.ui import android.os.Build import androidx.compose.ui.unit.dp +import java.util.Base64 val BIG_PRIMARY_BUTTON_SIZE = 64.dp val BIG_PRIMARY_BUTTON_MAX_WIDTH = 450.dp @@ -63,3 +64,17 @@ val CRYPTO_DONATIONS = mapOf( "Litecoin" to "LZayhTosZ9ToRvcbeR1gEDgb76Z7ZA2drN", "Filecoin" to "f1j6pm3chzhgadpf6iwmtux33jb5gccj5arkg4dsq", ) + +// Base64encoding these values so that bots can't easily scrape them. +val b64d = Base64.getDecoder() +val CONTACT_METHODS = mapOf( + "E-Mail" to String(b64d.decode("Z2" + "9vZ2xlLXBsYX" + "k" + "uMjlrMWFAYWxlZWFzL" + "mNvbQo=")).trim(), + "GitHub" to String( + b64d.decode( + "aHR" + + "0cHM6Ly9n" + "a" + "XRodWIuY29t" + "L015emVsMzk0L2NvbnRhY3QtbWUK" + ) + ).trim(), + "Mastodon" to String(b64d.decode("T" + "X" + "l6Z" + "WwzOTRAbWFzdG9kb24uc29" + "jaWFsCg" + "==")).trim(), + "Reddit" to "https://reddit.com/u/Myzel394" +) diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/AboutScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/AboutScreen.kt index 4df51fb..89bb22f 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/AboutScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/AboutScreen.kt @@ -1,8 +1,12 @@ package app.myzel394.alibi.ui.screens +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -18,6 +22,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.OpenInNew +import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -34,6 +39,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -43,6 +49,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import app.myzel394.alibi.BuildConfig import app.myzel394.alibi.R +import app.myzel394.alibi.ui.CONTACT_METHODS import app.myzel394.alibi.ui.REPO_URL import app.myzel394.alibi.ui.TRANSLATION_HELP_URL import app.myzel394.alibi.ui.components.AboutScreen.atoms.DonationsTile @@ -125,7 +132,7 @@ fun AboutScreen( ) Text( stringResource(R.string.ui_about_contribute_message), - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.bodySmall, ) val githubLabel = stringResource(R.string.accessibility_open_in_browser, REPO_URL) @@ -203,6 +210,54 @@ fun AboutScreen( DonationsTile() + Text( + stringResource(R.string.ui_about_support_title), + style = MaterialTheme.typography.titleMedium, + ) + Text( + stringResource(R.string.ui_about_support_message), + style = MaterialTheme.typography.bodySmall, + ) + Column( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + val clipboardManager = + LocalContext.current.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + + for (contact in CONTACT_METHODS) { + val name = contact.key + val uri = contact.value + + Row( + modifier = Modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + .clickable { + val clip = ClipData.newPlainText("text", uri) + clipboardManager.setPrimaryClip(clip) + } + .padding(16.dp) + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + Icons.Default.ContentCopy, + contentDescription = null, + ) + Text( + name, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold, + ) + Text( + uri, + fontSize = MaterialTheme.typography.bodyMedium.fontSize.times(0.5), + ) + } + } + } + GPGKeyOverview() } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 37615a8..8244600 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -219,4 +219,6 @@ You are ready! You are ready to start using Alibi! Go ahead and try it out! Start Alibi + Get Support + If you have any questions, feedback or face any issues, please don\'t hesitate to contact me. I\'m happy to help you! Below is a list of ways to get in touch with me: \ No newline at end of file From a250ec17885f0f15bd14362e657932dc56fc9d7b Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 22 Mar 2024 21:09:16 +0100 Subject: [PATCH 33/39] fix: Improve design for little screens Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../RecorderScreen/atoms/BigButton.kt | 5 +++-- .../RecorderScreen/atoms/LowStorageInfo.kt | 19 +++++++++++++------ .../organisms/StartRecording.kt | 2 +- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/BigButton.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/BigButton.kt index 3856550..74ea84f 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/BigButton.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/BigButton.kt @@ -39,11 +39,12 @@ fun BigButton( val orientation = LocalConfiguration.current.orientation BoxWithConstraints { - val isLarge = maxWidth > 500.dp && orientation == Configuration.ORIENTATION_PORTRAIT + val isLarge = + maxWidth > 500.dp && maxHeight > 1200.dp && orientation == Configuration.ORIENTATION_PORTRAIT Column( modifier = Modifier - .size(if (isLarge) 250.dp else 200.dp) + .size(if (isLarge) 250.dp else 180.dp) .clip(CircleShape) .semantics { contentDescription = label diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/LowStorageInfo.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/LowStorageInfo.kt index d567704..bb47a83 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/LowStorageInfo.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/LowStorageInfo.kt @@ -1,6 +1,7 @@ package app.myzel394.alibi.ui.components.RecorderScreen.atoms import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -13,6 +14,7 @@ import app.myzel394.alibi.helpers.BatchesFolder import app.myzel394.alibi.helpers.VideoBatchesFolder import app.myzel394.alibi.ui.components.atoms.MessageBox import app.myzel394.alibi.ui.components.atoms.MessageType +import app.myzel394.alibi.ui.components.atoms.VisualDensity @Composable fun LowStorageInfo( @@ -37,11 +39,16 @@ fun LowStorageInfo( Box( modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) ) { - MessageBox( - type = MessageType.WARNING, - message = if (appSettings.saveFolder == null) - stringResource(R.string.ui_recorder_lowOnStorage_hintANDswitchSaveFolder) - else stringResource(R.string.ui_recorder_lowOnStorage_hint) - ) + BoxWithConstraints { + val isLarge = maxHeight > 600.dp; + + MessageBox( + type = MessageType.WARNING, + message = if (appSettings.saveFolder == null) + stringResource(R.string.ui_recorder_lowOnStorage_hintANDswitchSaveFolder) + else stringResource(R.string.ui_recorder_lowOnStorage_hint), + density = if (isLarge) VisualDensity.COMFORTABLE else VisualDensity.COMPACT + ) + } } } diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/StartRecording.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/StartRecording.kt index f59ad8e..3b8121b 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/StartRecording.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/StartRecording.kt @@ -103,7 +103,7 @@ fun StartRecording( Column( modifier = Modifier .fillMaxSize() - .padding(bottom = if (orientation == Configuration.ORIENTATION_PORTRAIT) 32.dp else 16.dp), + .padding(bottom = if (orientation == Configuration.ORIENTATION_PORTRAIT) 0.dp else 16.dp), verticalArrangement = Arrangement.SpaceBetween, horizontalAlignment = Alignment.CenterHorizontally, ) { From 29984c59a229062ed4862dad9c57fcf51d8353a6 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 22 Mar 2024 21:11:12 +0100 Subject: [PATCH 34/39] fix: Improve design Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../main/java/app/myzel394/alibi/ui/screens/AboutScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/AboutScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/AboutScreen.kt index 89bb22f..a5fb877 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/AboutScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/AboutScreen.kt @@ -89,8 +89,8 @@ fun AboutScreen( Column( modifier = Modifier .padding(padding) - .padding(horizontal = 32.dp) - .verticalScroll(rememberScrollState()), + .verticalScroll(rememberScrollState()) + .padding(horizontal = 32.dp), verticalArrangement = Arrangement.spacedBy(48.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { From bc1dbf01dbd49658bacc665fd57eec39736ce892 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 22 Mar 2024 21:40:20 +0100 Subject: [PATCH 35/39] fix: Overall improvements Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../main/java/app/myzel394/alibi/db/AppSettings.kt | 5 +++-- .../java/app/myzel394/alibi/helpers/BatchesFolder.kt | 4 ++-- .../ui/components/RecorderScreen/atoms/BigButton.kt | 2 +- .../RecorderScreen/atoms/RecorderProcessingDialog.kt | 12 +++++++----- .../SettingsScreen/Tiles/MaxDurationTile.kt | 2 +- .../{TimeSelector.kt => MaxDurationSelector.kt} | 6 +++--- ...imeSettingsPage.kt => MaxDurationSettingsPage.kt} | 6 +++--- .../app/myzel394/alibi/ui/screens/WelcomeScreen.kt | 4 ++-- app/src/main/res/values/strings.xml | 3 ++- 9 files changed, 24 insertions(+), 20 deletions(-) rename app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/{TimeSelector.kt => MaxDurationSelector.kt} (98%) rename app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/{TimeSettingsPage.kt => MaxDurationSettingsPage.kt} (96%) 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 9aabc53..cfd47e9 100644 --- a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt +++ b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt @@ -308,14 +308,15 @@ data class AudioRecorderSettings( companion object { fun getDefaultInstance(): AudioRecorderSettings = AudioRecorderSettings() val EXAMPLE_MAX_DURATIONS = listOf( + 1 * 60 * 1000L, + 5 * 60 * 1000L, 15 * 60 * 1000L, 30 * 60 * 1000L, 60 * 60 * 1000L, - 2 * 60 * 60 * 1000L, - 3 * 60 * 60 * 1000L, ) val EXAMPLE_DURATION_TIMES = listOf( 60 * 1000L, + 60 * 2 * 1000L, 60 * 5 * 1000L, 60 * 10 * 1000L, 60 * 15 * 1000L, 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 705ae48..29f2940 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt @@ -556,8 +556,8 @@ abstract class BatchesFolder( companion object { fun requiredBytesForOneMinuteOfRecording(appSettings: AppSettings): Long { - // 250 MiB sounds like a good default - return 250 * 1024 * 1024 + // 350 MiB sounds like a good default + return 350 * 1024 * 1024 } } } diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/BigButton.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/BigButton.kt index 74ea84f..dc44c87 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/BigButton.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/BigButton.kt @@ -40,7 +40,7 @@ fun BigButton( BoxWithConstraints { val isLarge = - maxWidth > 500.dp && maxHeight > 1200.dp && orientation == Configuration.ORIENTATION_PORTRAIT + maxWidth > 200.dp && maxHeight > 400.dp && orientation == Configuration.ORIENTATION_PORTRAIT Column( modifier = Modifier diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/RecorderProcessingDialog.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/RecorderProcessingDialog.kt index fe5297a..14ea861 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/RecorderProcessingDialog.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/RecorderProcessingDialog.kt @@ -1,17 +1,16 @@ package app.myzel394.alibi.ui.components.RecorderScreen.atoms +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Memory import androidx.compose.material3.AlertDialog +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.myzel394.alibi.R @@ -38,15 +37,18 @@ fun RecorderProcessingDialog( text = { Column( horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(32.dp), ) { Text( stringResource(R.string.ui_recorder_action_save_processing_dialog_description), ) - Spacer(modifier = Modifier.height(32.dp)) + CircularProgressIndicator() if (progress == null) LinearProgressIndicator() else - LinearProgressIndicator(progress = progress) + LinearProgressIndicator( + progress = { progress }, + ) } }, confirmButton = {} diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/MaxDurationTile.kt b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/MaxDurationTile.kt index abf067b..28e995d 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/MaxDurationTile.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/MaxDurationTile.kt @@ -68,7 +68,7 @@ fun MaxDurationTile( timeFormat = DurationFormat.HH_MM, currentTime = settings.maxDuration / 1000, minTime = 60, - maxTime = 10 * 24 * 60 * 60, + maxTime = 23 * 60 * 60 + 59 * 60, ) ) SettingsTile( diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/TimeSelector.kt b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/MaxDurationSelector.kt similarity index 98% rename from app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/TimeSelector.kt rename to app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/MaxDurationSelector.kt index 560bae5..15044d7 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/TimeSelector.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/MaxDurationSelector.kt @@ -44,21 +44,21 @@ import com.maxkeppeler.sheets.duration.models.DurationFormat import com.maxkeppeler.sheets.duration.models.DurationSelection import kotlinx.coroutines.launch +const val MINUTES_1 = 1000 * 60 * 1L const val MINUTES_5 = 1000 * 60 * 5L const val MINUTES_15 = 1000 * 60 * 15L const val MINUTES_30 = 1000 * 60 * 30L -const val HOURS_1 = 1000 * 60 * 60L @OptIn(ExperimentalMaterial3Api::class) @Composable -fun TimeSelector( +fun MaxDurationSelector( modifier: Modifier = Modifier, ) { val OPTIONS = mapOf( + MINUTES_1 to stringResource(R.string.ui_welcome_timeSettings_values_1min), MINUTES_5 to stringResource(R.string.ui_welcome_timeSettings_values_5min), MINUTES_15 to stringResource(R.string.ui_welcome_timeSettings_values_15min), MINUTES_30 to stringResource(R.string.ui_welcome_timeSettings_values_30min), - HOURS_1 to stringResource(R.string.ui_welcome_timeSettings_values_1hour), ) val scope = rememberCoroutineScope() diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/TimeSettingsPage.kt b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/MaxDurationSettingsPage.kt similarity index 96% rename from app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/TimeSettingsPage.kt rename to app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/MaxDurationSettingsPage.kt index 35aa0a9..a779033 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/TimeSettingsPage.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/MaxDurationSettingsPage.kt @@ -28,13 +28,13 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.myzel394.alibi.R import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE -import app.myzel394.alibi.ui.components.WelcomeScreen.atoms.TimeSelector +import app.myzel394.alibi.ui.components.WelcomeScreen.atoms.MaxDurationSelector import app.myzel394.alibi.ui.components.atoms.MessageBox import app.myzel394.alibi.ui.components.atoms.MessageType import app.myzel394.alibi.ui.components.atoms.VisualDensity @Composable -fun TimeSettingsPage( +fun MaxDurationSettingsPage( onContinue: () -> Unit, ) { Column( @@ -76,7 +76,7 @@ fun TimeSettingsPage( .widthIn(max = 400.dp) .padding(horizontal = 16.dp) ) { - TimeSelector() + MaxDurationSelector() } Spacer(modifier = Modifier.height(20.dp)) Box( diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt index ec2c014..6ed8ef2 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt @@ -14,10 +14,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import app.myzel394.alibi.dataStore import app.myzel394.alibi.ui.components.WelcomeScreen.pages.ExplanationPage +import app.myzel394.alibi.ui.components.WelcomeScreen.pages.MaxDurationSettingsPage import app.myzel394.alibi.ui.components.WelcomeScreen.pages.ReadyPage import app.myzel394.alibi.ui.components.WelcomeScreen.pages.ResponsibilityPage import app.myzel394.alibi.ui.components.WelcomeScreen.pages.SaveFolderPage -import app.myzel394.alibi.ui.components.WelcomeScreen.pages.TimeSettingsPage import app.myzel394.alibi.ui.effects.rememberSettings import kotlinx.coroutines.launch @@ -71,7 +71,7 @@ fun WelcomeScreen( } } - 2 -> TimeSettingsPage { + 2 -> MaxDurationSettingsPage { scope.launch { pagerState.animateScrollToPage(3) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8244600..79cab07 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -39,7 +39,7 @@ \u0020at your request Processing - Processing your recording, do not close Alibi! You will automatically be prompted to save the file once it\'s ready + Processing your recording, do not close Alibi! You will automatically be prompted to save the file once it\'s ready. This process may take a few minutes if your time frame is big. Once this is finished and you are asked to save the file, please do so and then wait until you see Alibi\'s main screen again. Alibi keeps recording in the background @@ -221,4 +221,5 @@ Start Alibi Get Support If you have any questions, feedback or face any issues, please don\'t hesitate to contact me. I\'m happy to help you! Below is a list of ways to get in touch with me: + 1 Minute \ No newline at end of file From 1b006ba45c97168ec04912384e0641d9bdb308b5 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 22 Mar 2024 22:06:25 +0100 Subject: [PATCH 36/39] fix: Improve error handling Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../alibi/services/VideoRecorderService.kt | 25 +++++++++++-------- .../atoms/RecorderErrorDialog.kt | 12 ++------- .../organisms/RecorderEventsHandler.kt | 25 ++++++++++++++++--- app/src/main/res/values/strings.xml | 2 +- 4 files changed, 40 insertions(+), 24 deletions(-) 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 aa74867..06bcac8 100644 --- a/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt @@ -191,17 +191,22 @@ class VideoRecorderService : videoCapture = buildVideoCapture(recorder) runOnMain { - camera = cameraProvider!!.bindToLifecycle( - this, - selectedCamera, - videoCapture - ) - cameraControl = CameraControl(camera!!).also { - it.init() - } - onCameraControlAvailable() + try { + camera = cameraProvider!!.bindToLifecycle( + this, + selectedCamera, + videoCapture + ) - _cameraAvailableListener.complete(Unit) + cameraControl = CameraControl(camera!!).also { + it.init() + } + onCameraControlAvailable() + + _cameraAvailableListener.complete(Unit) + } catch (error: IllegalArgumentException) { + onError() + } } } diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/RecorderErrorDialog.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/RecorderErrorDialog.kt index 92da619..a05306c 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/RecorderErrorDialog.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/RecorderErrorDialog.kt @@ -3,8 +3,6 @@ package app.myzel394.alibi.ui.components.RecorderScreen.atoms import androidx.compose.material.icons.Icons 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.Text import androidx.compose.material3.TextButton @@ -15,7 +13,6 @@ import app.myzel394.alibi.R @Composable fun RecorderErrorDialog( onClose: () -> Unit, - onSave: () -> Unit, ) { AlertDialog( onDismissRequest = onClose, @@ -31,14 +28,9 @@ fun RecorderErrorDialog( text = { Text(stringResource(R.string.ui_recorder_error_recording_description)) }, - dismissButton = { - TextButton(onClick = onClose) { - Text(stringResource(R.string.dialog_close_cancel_label)) - } - }, confirmButton = { - TextButton(onClick = onSave) { - Text(stringResource(R.string.ui_recorder_action_save_label)) + TextButton(onClick = onClose) { + Text(stringResource(R.string.dialog_close_neutral_label)) } } ) 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 6db673d..688a33a 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 @@ -93,9 +93,16 @@ fun RecorderEventsHandler( recorder: RecorderModel ) { if (!settings.deleteRecordingsImmediately) { + val information = recorder.recorderService?.getRecordingInformation() + + if (information == null) { + Log.e("RecorderEventsHandler", "Recording information is null") + return + } + dataStore.updateData { it.setLastRecording( - recorder.recorderService!!.getRecordingInformation() + information ) } } @@ -247,6 +254,13 @@ fun RecorderEventsHandler( scope.launch { saveAsLastRecording(audioRecorder as RecorderModel) + runCatching { + audioRecorder.stopRecording(context) + } + runCatching { + audioRecorder.destroyService(context) + } + showRecorderError = true } } @@ -290,6 +304,13 @@ fun RecorderEventsHandler( scope.launch { saveAsLastRecording(videoRecorder as RecorderModel) + runCatching { + videoRecorder.stopRecording(context) + } + runCatching { + videoRecorder.destroyService(context) + } + showRecorderError = true } } @@ -324,8 +345,6 @@ fun RecorderEventsHandler( onClose = { showRecorderError = false }, - onSave = { - }, ) if (showBatchesInaccessibleError) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 79cab07..ec7cd34 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -80,7 +80,7 @@ Recording paused Alibi is paused An error occurred - Alibi encountered an error during recording. Would you like to try saving the recording? + Alibi encountered an error during recording. Try using different settings or restart the app. Language Change Device Microphone From f8c1db495d53b6b7bfa2fe928eecf3b5a6856c6c Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 22 Mar 2024 22:24:00 +0100 Subject: [PATCH 37/39] fix: Fix available bytes calculation when using a custom folder Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../myzel394/alibi/helpers/BatchesFolder.kt | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) 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 29f2940..7ec8536 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt @@ -8,12 +8,13 @@ import android.database.Cursor import android.net.Uri import android.os.Build import android.os.Environment +import android.os.ParcelFileDescriptor import android.os.storage.StorageManager import android.provider.MediaStore import android.provider.MediaStore.Video.Media +import android.system.Os import android.util.Log import androidx.annotation.RequiresApi -import androidx.core.net.toFile import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile import app.myzel394.alibi.db.AppSettings @@ -29,6 +30,7 @@ import java.time.LocalDateTime import java.time.format.DateTimeFormatter import kotlin.reflect.KFunction4 + abstract class BatchesFolder( open val context: Context, open val type: BatchType, @@ -524,21 +526,46 @@ abstract class BatchesFolder( } fun getAvailableBytes(): Long? { + if (type == BatchType.CUSTOM) { + var fileDescriptor: ParcelFileDescriptor? = null + + try { + fileDescriptor = + context.contentResolver.openFileDescriptor(customFolder!!.uri, "r")!! + val stats = Os.fstatvfs(fileDescriptor.fileDescriptor) + + val available = stats.f_bavail * stats.f_bsize + + runCatching { + fileDescriptor.close() + } + + return available + } catch (e: Exception) { + runCatching { + fileDescriptor?.close(); + } + + return null + } + } + val storageManager = context.getSystemService(StorageManager::class.java) ?: return null val file = when (type) { BatchType.INTERNAL -> context.filesDir - BatchType.CUSTOM -> customFolder!!.uri.toFile() BatchType.MEDIA -> if (SUPPORTS_SCOPED_STORAGE) File( Environment.getExternalStoragePublicDirectory(VideoBatchesFolder.BASE_SCOPED_STORAGE_RELATIVE_PATH), - MediaStore.Video.Media.EXTERNAL_CONTENT_URI.toString(), + Media.EXTERNAL_CONTENT_URI.toString(), ) else File( Environment.getExternalStoragePublicDirectory(VideoBatchesFolder.BASE_LEGACY_STORAGE_FOLDER), VideoBatchesFolder.MEDIA_RECORDINGS_SUBFOLDER, ) + + BatchType.CUSTOM -> throw IllegalArgumentException("This code should not be reachable") } return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { From e0dce7d359d32187394edd56487e8e0186d03df4 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 22 Mar 2024 22:24:30 +0100 Subject: [PATCH 38/39] feat: Do not show a warning for custom folders anymore Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../SettingsScreen/Tiles/SaveFolderTile.kt | 65 +------------------ 1 file changed, 1 insertion(+), 64 deletions(-) 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 ec9f11d..2d815ca 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 @@ -26,7 +26,6 @@ import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.Mic import androidx.compose.material.icons.filled.PermMedia -import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -435,8 +434,6 @@ fun SelectionSheet( ) { val context = LocalContext.current - var showCustomFolderWarning by remember { mutableStateOf(false) } - val selectFolder = rememberFolderSelectorDialog { folder -> if (folder == null) { return@rememberFolderSelectorDialog @@ -445,18 +442,6 @@ fun SelectionSheet( updateValue(folder.toString()) } - if (showCustomFolderWarning) { - CustomFolderWarningDialog( - onDismiss = { - showCustomFolderWarning = false - }, - onConfirm = { - showCustomFolderWarning = false - selectFolder() - }, - ) - } - var showExternalPermissionRequired by remember { mutableStateOf(false) } if (showExternalPermissionRequired) { @@ -523,9 +508,7 @@ fun SelectionSheet( SelectionButton( label = stringResource(R.string.ui_settings_option_saveFolder_action_custom_label), icon = Icons.Default.Folder, - onClick = { - showCustomFolderWarning = true - }, + onClick = selectFolder, ) if (!SUPPORTS_SAVING_VIDEOS_IN_CUSTOM_FOLDERS) { Column( @@ -581,52 +564,6 @@ fun SelectionButton( } } -@Composable -fun CustomFolderWarningDialog( - onDismiss: () -> Unit, - onConfirm: () -> Unit, -) { - 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 = onDismiss, - title = { - Text(text = title) - }, - text = { - Text(text = text) - }, - confirmButton = { - Button(onClick = onConfirm) { - Text( - text = stringResource(R.string.ui_settings_option_saveFolder_warning_action_confirm), - ) - } - }, - dismissButton = { - TextButton( - onClick = onDismiss, - contentPadding = ButtonDefaults.TextButtonWithIconContentPadding, - ) { - 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)) - } - } - ) -} - @Composable fun ExternalPermissionRequiredDialog( onDismiss: () -> Unit, From a122cbea039ad8c5a1aa676824c26e5ffb60b13f Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 22 Mar 2024 22:24:47 +0100 Subject: [PATCH 39/39] fix: Improve BigButton responsive design Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../alibi/ui/components/RecorderScreen/atoms/BigButton.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/BigButton.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/BigButton.kt index dc44c87..1f66f27 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/BigButton.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/BigButton.kt @@ -40,11 +40,11 @@ fun BigButton( BoxWithConstraints { val isLarge = - maxWidth > 200.dp && maxHeight > 400.dp && orientation == Configuration.ORIENTATION_PORTRAIT + maxWidth > 200.dp && maxHeight > 350.dp && orientation == Configuration.ORIENTATION_PORTRAIT Column( modifier = Modifier - .size(if (isLarge) 250.dp else 180.dp) + .size(if (isLarge) 250.dp else 190.dp) .clip(CircleShape) .semantics { contentDescription = label