From 6f8b68c7b22096bf7fca7d3a7e883a5fe27c9119 Mon Sep 17 00:00:00 2001 From: Roy Matero Date: Sat, 24 Feb 2024 14:24:49 -0800 Subject: [PATCH 01/41] feature: adds camera preview --- .../organisms/VideoRecordingStatus.kt | 61 ++++++++++++++++++- .../app/myzel394/alibi/ui/utils/Context.kt | 16 +++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/utils/Context.kt 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 47479d9b..a269bb6b 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 @@ -1,17 +1,23 @@ package app.myzel394.alibi.ui.components.RecorderScreen.organisms import android.content.res.Configuration +import android.util.Log +import android.view.ViewGroup +import androidx.camera.core.Preview +import androidx.camera.view.PreviewView import androidx.compose.foundation.background 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.aspectRatio 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.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CameraAlt import androidx.compose.material3.HorizontalDivider @@ -26,11 +32,14 @@ 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.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView import app.myzel394.alibi.R import app.myzel394.alibi.ui.components.RecorderScreen.atoms.TorchStatus import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingControl @@ -38,6 +47,7 @@ import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingStatus import app.myzel394.alibi.ui.models.VideoRecorderModel import app.myzel394.alibi.ui.utils.CameraInfo import app.myzel394.alibi.ui.utils.KeepScreenOn +import app.myzel394.alibi.ui.utils.getCameraProvider import com.valentinilk.shimmer.shimmer import kotlinx.coroutines.launch @@ -79,6 +89,12 @@ fun VideoRecordingStatus( horizontalAlignment = Alignment.CenterHorizontally, ) { _VideoControls(videoRecorder) + CameraPreview( + videoRecorder, + modifier = Modifier + .aspectRatio(5 / 2F) + .padding(horizontal = 12.dp) + ) HorizontalDivider() _PrimitiveControls(videoRecorder) } @@ -94,7 +110,11 @@ fun VideoRecordingStatus( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceBetween, ) { - Box {} + CameraPreview( + videoRecorder, modifier = Modifier + .padding(24.dp) + .aspectRatio(3 / 2F) + ) Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -120,6 +140,45 @@ fun VideoRecordingStatus( } +@Composable +fun CameraPreview(videoRecorder: VideoRecorderModel, modifier: Modifier) { + val coroutineScope = rememberCoroutineScope() + val lifecycleOwner = LocalLifecycleOwner.current + + Box(modifier = modifier.clip(RoundedCornerShape(12.dp))) { + + // Video preview + AndroidView( + factory = { context -> + val previewView = PreviewView(context).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + } + val previewUseCase = Preview.Builder() + .build() + .also { it.setSurfaceProvider(previewView.surfaceProvider) } + + coroutineScope.launch { + val cameraProvider = context.getCameraProvider() + try { + cameraProvider.unbindAll() + cameraProvider.bindToLifecycle( + lifecycleOwner, + videoRecorder.cameraSelector, + previewUseCase + ) + } catch (ex: Exception) { + Log.e("CameraPreview", "Use case binding failed", ex) + } + } + previewView + }, + ) + } +} + @Composable fun _VideoGeneralInfo(videoRecorder: VideoRecorderModel) { val context = LocalContext.current diff --git a/app/src/main/java/app/myzel394/alibi/ui/utils/Context.kt b/app/src/main/java/app/myzel394/alibi/ui/utils/Context.kt new file mode 100644 index 00000000..058f7dd1 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/utils/Context.kt @@ -0,0 +1,16 @@ +package app.myzel394.alibi.ui.utils + +import android.content.Context +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.core.content.ContextCompat +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +suspend fun Context.getCameraProvider(): ProcessCameraProvider = suspendCoroutine { continuation -> + ProcessCameraProvider.getInstance(this).also { future -> + future.addListener({ + continuation.resume(future.get()) + }, ContextCompat.getMainExecutor(this)) + } +} + From 386614800ba50332683556126d9ba8d8eb7fe962 Mon Sep 17 00:00:00 2001 From: Roy Matero Date: Mon, 26 Feb 2024 18:41:21 -0800 Subject: [PATCH 02/41] fix: change camera preview to full screen size --- .../RecorderScreen/atoms/CameraPreview.kt | 66 ++++---- .../organisms/VideoRecordingStatus.kt | 159 ++++++------------ 2 files changed, 89 insertions(+), 136 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/CameraPreview.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/CameraPreview.kt index d0530ac9..3046973a 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/CameraPreview.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/CameraPreview.kt @@ -1,56 +1,56 @@ package app.myzel394.alibi.ui.components.RecorderScreen.atoms +import android.util.Log import android.view.ViewGroup import androidx.camera.core.CameraSelector import androidx.camera.core.Preview -import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.view.PreviewView +import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.viewinterop.AndroidView +import app.myzel394.alibi.ui.utils.getCameraProvider import kotlinx.coroutines.launch @Composable fun CameraPreview( - modifier: Modifier = Modifier, - scaleType: PreviewView.ScaleType = PreviewView.ScaleType.FILL_CENTER, + modifier: Modifier, cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA ) { val coroutineScope = rememberCoroutineScope() val lifecycleOwner = LocalLifecycleOwner.current - AndroidView( - modifier = modifier, - factory = { context -> - val previewView = PreviewView(context).apply { - this.scaleType = scaleType - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) - } - // CameraX Preview UseCase - val previewUseCase = Preview.Builder() - .build() - .also { - it.setSurfaceProvider(previewView.surfaceProvider) - } - - coroutineScope.launch { - val cameraProvider = ProcessCameraProvider.getInstance(context).get() - try { - // Must unbind the use-cases before rebinding them. - cameraProvider.unbindAll() - cameraProvider.bindToLifecycle( - lifecycleOwner, cameraSelector, previewUseCase + Box(modifier = modifier) { + // Video preview + AndroidView( + factory = { context -> + val previewView = PreviewView(context).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, ) - } catch (ex: Exception) { } - } + val previewUseCase = Preview.Builder() + .build() + .also { it.setSurfaceProvider(previewView.surfaceProvider) } - previewView - } - ) -} \ No newline at end of file + coroutineScope.launch { + val cameraProvider = context.getCameraProvider() + try { + cameraProvider.unbindAll() + cameraProvider.bindToLifecycle( + lifecycleOwner, + cameraSelector, + previewUseCase + ) + } catch (ex: Exception) { + Log.e("CameraPreview", "Use case binding failed", ex) + } + } + previewView + }, + ) + } +} 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 a269bb6b..74f068f1 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 @@ -1,23 +1,17 @@ package app.myzel394.alibi.ui.components.RecorderScreen.organisms import android.content.res.Configuration -import android.util.Log -import android.view.ViewGroup -import androidx.camera.core.Preview -import androidx.camera.view.PreviewView import androidx.compose.foundation.background 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.aspectRatio 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.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CameraAlt import androidx.compose.material3.HorizontalDivider @@ -32,22 +26,19 @@ 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.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView import app.myzel394.alibi.R +import app.myzel394.alibi.ui.components.RecorderScreen.atoms.CameraPreview 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 import app.myzel394.alibi.ui.models.VideoRecorderModel import app.myzel394.alibi.ui.utils.CameraInfo import app.myzel394.alibi.ui.utils.KeepScreenOn -import app.myzel394.alibi.ui.utils.getCameraProvider import com.valentinilk.shimmer.shimmer import kotlinx.coroutines.launch @@ -61,78 +52,79 @@ fun VideoRecordingStatus( when (orientation) { Configuration.ORIENTATION_LANDSCAPE -> { - Row( - modifier = Modifier.fillMaxSize(), - horizontalArrangement = Arrangement.SpaceEvenly, - verticalAlignment = Alignment.CenterVertically, - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement - .spacedBy(32.dp), - modifier = Modifier - .weight(1f) - .fillMaxWidth(0.9f) - .align(Alignment.CenterVertically), - ) { - _VideoGeneralInfo(videoRecorder) - _VideoRecordingStatus(videoRecorder) - } - Box( - modifier = Modifier - .weight(1f) - .fillMaxWidth(0.9f) + Box { + CameraPreview( + modifier = Modifier, + cameraSelector = videoRecorder.cameraSelector + ) + Row( + modifier = Modifier.fillMaxSize(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, ) { Column( + horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement .spacedBy(32.dp), - horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .weight(1f) + .fillMaxWidth(0.9f) + .align(Alignment.CenterVertically), ) { - _VideoControls(videoRecorder) - CameraPreview( - videoRecorder, - modifier = Modifier - .aspectRatio(5 / 2F) - .padding(horizontal = 12.dp) - ) - HorizontalDivider() - _PrimitiveControls(videoRecorder) + _VideoGeneralInfo(videoRecorder) + _VideoRecordingStatus(videoRecorder) + } + Box( + modifier = Modifier + .weight(1f) + .fillMaxWidth(0.9f) + ) { + Column( + verticalArrangement = Arrangement + .spacedBy(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + _VideoControls(videoRecorder) + HorizontalDivider() + _PrimitiveControls(videoRecorder) + } } } } } else -> { - Column( - modifier = Modifier - .fillMaxSize() - .padding(bottom = 32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween, - ) { + Box { CameraPreview( - videoRecorder, modifier = Modifier - .padding(24.dp) - .aspectRatio(3 / 2F) + modifier = Modifier, + cameraSelector = videoRecorder.cameraSelector ) Column( + modifier = Modifier + .fillMaxSize() + .padding(bottom = 32.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement - .spacedBy(16.dp), + verticalArrangement = Arrangement.SpaceBetween, ) { - _VideoGeneralInfo(videoRecorder) - _VideoRecordingStatus(videoRecorder) - } + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement + .spacedBy(16.dp), + ) { + _VideoGeneralInfo(videoRecorder) + _VideoRecordingStatus(videoRecorder) + } - Column( - verticalArrangement = Arrangement - .spacedBy(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - _VideoControls(videoRecorder) - HorizontalDivider() - _PrimitiveControls(videoRecorder) + Column( + verticalArrangement = Arrangement + .spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + _VideoControls(videoRecorder) + HorizontalDivider() + _PrimitiveControls(videoRecorder) + } } } } @@ -140,45 +132,6 @@ fun VideoRecordingStatus( } -@Composable -fun CameraPreview(videoRecorder: VideoRecorderModel, modifier: Modifier) { - val coroutineScope = rememberCoroutineScope() - val lifecycleOwner = LocalLifecycleOwner.current - - Box(modifier = modifier.clip(RoundedCornerShape(12.dp))) { - - // Video preview - AndroidView( - factory = { context -> - val previewView = PreviewView(context).apply { - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, - ) - } - val previewUseCase = Preview.Builder() - .build() - .also { it.setSurfaceProvider(previewView.surfaceProvider) } - - coroutineScope.launch { - val cameraProvider = context.getCameraProvider() - try { - cameraProvider.unbindAll() - cameraProvider.bindToLifecycle( - lifecycleOwner, - videoRecorder.cameraSelector, - previewUseCase - ) - } catch (ex: Exception) { - Log.e("CameraPreview", "Use case binding failed", ex) - } - } - previewView - }, - ) - } -} - @Composable fun _VideoGeneralInfo(videoRecorder: VideoRecorderModel) { val context = LocalContext.current 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 03/41] 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 3b00d065..9aabc530 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 00000000..ca3f1fb1 --- /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 70051fe9..33148309 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 3b2b526f..537d61b2 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 00000000..712f83c4 --- /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 ff15de07..084a8f2b 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 baefa03e..2436d62a 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 04/41] 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 d67ad647..2af9d332 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 05/41] 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 a2d5b369..30f119df 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 00000000..73ebbf51 --- /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 084a8f2b..eebd65c5 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 2436d62a..68add452 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 06/41] 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 00000000..c84fe601 --- /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 ca3f1fb1..182115dc 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 73ebbf51..d45ea15c 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 eebd65c5..3ab59bff 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 68add452..cc23b400 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 07/41] 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 | 133 +++++++++++++++--- app/src/main/res/values/strings.xml | 2 + 3 files changed, 229 insertions(+), 31 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 c84fe601..02b79e7a 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,53 @@ 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, + 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_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, @@ -93,13 +134,67 @@ 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_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 d45ea15c..19d499ce 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, ) } + 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 = onContinue, - enabled = !isLowOnStorage, - modifier = Modifier - .fillMaxWidth() - .height(BIG_PRIMARY_BUTTON_SIZE), - contentPadding = ButtonDefaults.ButtonWithIconContentPadding, + onClick = onOk, ) { - Icon( - Icons.Default.ChevronRight, - contentDescription = null, - modifier = Modifier.size(ButtonDefaults.IconSize) - ) - Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) - Text(stringResource(R.string.continue_label)) + 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 cc23b400..15abe38a 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 08/41] 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 02b79e7a..0c20b04b 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 19d499ce..6eab9858 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 712f83c4..454b90fa 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 3ab59bff..c8686e52 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 09/41] 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 182115dc..560bae5c 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 15abe38a..30641a99 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 10/41] 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 0c20b04b..2c1ca123 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 6eab9858..1f87891a 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 c8686e52..c1c461c8 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 30641a99..4c2a370b 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 11/41] 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 30f119df..52d498bd 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 00000000..cdee3cb7 --- /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 c1c461c8..adcd27f3 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 4c2a370b..37615a8b 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 12/41] 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 1f87891a..8fc9990e 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 13/41] 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 8fc9990e..65ec0c25 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 adcd27f3..ec2c014d 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 14/41] 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 3c3716c7..705ae488 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 15/41] 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 65ec0c25..6302632d 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 454b90fa..35aa0a97 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 16/41] 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 979ee51e..a8afa200 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 4df51fbc..89bb22ff 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 37615a8b..82446006 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 17/41] 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 38565504..74ea84fd 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 d5677042..bb47a831 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 f59ad8e2..3b8121b7 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 18/41] 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 89bb22ff..a5fb877f 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 19/41] 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 9aabc530..cfd47e94 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 705ae488..29f2940a 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 74ea84fd..dc44c87c 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 fe5297a4..14ea861f 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 abf067b9..28e995d7 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 560bae5c..15044d7d 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 35aa0a97..a7790335 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 ec2c014d..6ed8ef23 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 82446006..79cab07e 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 20/41] 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 aa748674..06bcac88 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 + ) + + cameraControl = CameraControl(camera!!).also { + it.init() + } + onCameraControlAvailable() - _cameraAvailableListener.complete(Unit) + _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 92da6195..a05306ce 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 6db673d4..688a33af 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 79cab07e..ec7cd340 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 21/41] 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 29f2940a..7ec85363 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 22/41] 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 ec9f11d6..2d815cab 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 23/41] 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 dc44c87c..1f66f27d 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 From 7d83bca1fe7e05b25423efbe81a1f351455867ae Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 23 Mar 2024 13:44:53 +0100 Subject: [PATCH 24/41] fix: Properly concatenate in own thread wait for end properly Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../alibi/services/VideoRecorderService.kt | 2 +- .../organisms/RecorderEventsHandler.kt | 34 +++++++------------ .../alibi/ui/models/BaseRecorderModel.kt | 4 +-- 3 files changed, 16 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 06bcac88..4cfc641b 100644 --- a/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt @@ -132,7 +132,7 @@ class VideoRecorderService : _videoFinalizerListener = CompletableDeferred() activeRecording = newRecording.start(ContextCompat.getMainExecutor(this)) { event -> - if (event is VideoRecordEvent.Finalize && this@VideoRecorderService.state == RecorderState.STOPPED || this@VideoRecorderService.state == RecorderState.PAUSED) { + if (event is VideoRecordEvent.Finalize && (this@VideoRecorderService.state == RecorderState.STOPPED || this@VideoRecorderService.state == RecorderState.PAUSED)) { _videoFinalizerListener.complete(Unit) } } 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 688a33af..17370238 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,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.delay +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlin.concurrent.thread @@ -136,13 +136,15 @@ fun RecorderEventsHandler( } } - suspend fun saveRecording(recorder: RecorderModel, cleanupOldFiles: Boolean = false): Thread { + fun saveRecording( + recorder: RecorderModel, + cleanupOldFiles: Boolean = false + ): CompletableDeferred { isProcessing = true - // Give the user some time to see the processing dialog - delay(100) + val completer = CompletableDeferred() - return thread { + thread { runBlocking { try { if (recorder.isCurrentlyActivelyRecording) { @@ -230,22 +232,19 @@ fun RecorderEventsHandler( recorder.recorderService?.unlockFiles(cleanupOldFiles) } isProcessing = false + processingProgress = null + completer.complete(Unit) } } } + + return completer } // Register audio recorder events DisposableEffect(key1 = audioRecorder, key2 = settings) { 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, cleanupOldFiles).join() - } + saveRecording(audioRecorder as RecorderModel, cleanupOldFiles) } audioRecorder.onRecordingStart = { snackbarHostState.currentSnackbarData?.dismiss() @@ -288,14 +287,7 @@ fun RecorderEventsHandler( // Register video recorder events DisposableEffect(key1 = videoRecorder, key2 = settings) { 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, cleanupOldFiles).join() - } + saveRecording(videoRecorder as RecorderModel, cleanupOldFiles) } videoRecorder.onRecordingStart = { snackbarHostState.currentSnackbarData?.dismiss() 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 77968492..d625743f 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.Job +import kotlinx.coroutines.CompletableDeferred import kotlinx.serialization.json.Json abstract class BaseRecorderModel> : @@ -49,7 +49,7 @@ abstract class BaseRecorderModel Job = { + var onRecordingSave: (cleanupOldFiles: Boolean) -> CompletableDeferred = { throw NotImplementedError("onRecordingSave not implemented") } var onRecordingStart: () -> Unit = {} From cfea5a91434964aabeced4efcfb27ed6dc30ab07 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 23 Mar 2024 14:01:53 +0100 Subject: [PATCH 25/41] feat: Only show processing dialog after some time Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../RecorderScreen/organisms/RecorderEventsHandler.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 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 17370238..462233b3 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 @@ -33,6 +33,8 @@ import app.myzel394.alibi.ui.utils.rememberFileSaverDialog import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import java.util.Timer +import kotlin.concurrent.schedule import kotlin.concurrent.thread typealias RecorderModel = BaseRecorderModel< @@ -140,10 +142,13 @@ fun RecorderEventsHandler( recorder: RecorderModel, cleanupOldFiles: Boolean = false ): CompletableDeferred { - isProcessing = true - val completer = CompletableDeferred() + // If processing takes this short, don't show the processing dialog + val timer = Timer().schedule(250L) { + isProcessing = true + } + thread { runBlocking { try { @@ -231,6 +236,7 @@ fun RecorderEventsHandler( if (recorder.isCurrentlyActivelyRecording) { recorder.recorderService?.unlockFiles(cleanupOldFiles) } + timer.cancel() isProcessing = false processingProgress = null completer.complete(Unit) From 3db93cc96a0e674452eba33003a1b89f584daf46 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 23 Mar 2024 14:02:07 +0100 Subject: [PATCH 26/41] chore: Do not shore CameraPreview for now Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../RecorderScreen/organisms/VideoRecordingStatus.kt | 10 ---------- 1 file changed, 10 deletions(-) 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 7927f8b0..9ddcad3a 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 @@ -33,7 +33,6 @@ 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.ui.components.RecorderScreen.atoms.CameraPreview import app.myzel394.alibi.dataStore import app.myzel394.alibi.ui.components.RecorderScreen.atoms.SaveCurrentNowModal import app.myzel394.alibi.ui.components.RecorderScreen.atoms.TorchStatus @@ -56,10 +55,6 @@ fun VideoRecordingStatus( when (orientation) { Configuration.ORIENTATION_LANDSCAPE -> { Box { - CameraPreview( - modifier = Modifier, - cameraSelector = videoRecorder.cameraSelector - ) Row( modifier = Modifier.fillMaxSize(), horizontalArrangement = Arrangement.SpaceEvenly, @@ -98,11 +93,6 @@ fun VideoRecordingStatus( else -> { Box { - CameraPreview( - modifier = Modifier, - cameraSelector = videoRecorder.cameraSelector - ) - Column( modifier = Modifier .fillMaxSize() From 39458bd76ce44c6cf13f275b459d205ebc4f914f Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 23 Mar 2024 14:06:51 +0100 Subject: [PATCH 27/41] fix: Improve design Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../organisms/VideoRecordingStatus.kt | 96 +++++++++---------- 1 file changed, 47 insertions(+), 49 deletions(-) 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 9ddcad3a..9009a994 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 @@ -54,70 +54,68 @@ fun VideoRecordingStatus( when (orientation) { Configuration.ORIENTATION_LANDSCAPE -> { - Box { - Row( - modifier = Modifier.fillMaxSize(), - horizontalArrangement = Arrangement.SpaceEvenly, - verticalAlignment = Alignment.CenterVertically, + Row( + modifier = Modifier.fillMaxSize(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement + .spacedBy(32.dp), + modifier = Modifier + .weight(1f) + .fillMaxWidth(0.9f) + .align(Alignment.CenterVertically), + ) { + _VideoGeneralInfo(videoRecorder) + _VideoRecordingStatus(videoRecorder) + } + Box( + modifier = Modifier + .weight(1f) + .fillMaxWidth(0.9f) ) { Column( - horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement .spacedBy(32.dp), - modifier = Modifier - .weight(1f) - .fillMaxWidth(0.9f) - .align(Alignment.CenterVertically), - ) { - _VideoGeneralInfo(videoRecorder) - _VideoRecordingStatus(videoRecorder) - } - Box( - modifier = Modifier - .weight(1f) - .fillMaxWidth(0.9f) + horizontalAlignment = Alignment.CenterHorizontally, ) { - Column( - verticalArrangement = Arrangement - .spacedBy(32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - _VideoControls(videoRecorder) - HorizontalDivider() - _PrimitiveControls(videoRecorder) - } + _VideoControls(videoRecorder) + HorizontalDivider() + _PrimitiveControls(videoRecorder) } } } } else -> { - Box { + Column( + modifier = Modifier + .fillMaxSize() + .padding(bottom = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween, + ) { + Box {} + Column( - modifier = Modifier - .fillMaxSize() - .padding(bottom = 32.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween, + verticalArrangement = Arrangement + .spacedBy(16.dp), ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement - .spacedBy(16.dp), - ) { - _VideoGeneralInfo(videoRecorder) - _VideoRecordingStatus(videoRecorder) - } + _VideoGeneralInfo(videoRecorder) + _VideoRecordingStatus(videoRecorder) + } - Column( - verticalArrangement = Arrangement - .spacedBy(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - _VideoControls(videoRecorder) - HorizontalDivider() - _PrimitiveControls(videoRecorder) - } + Column( + verticalArrangement = Arrangement + .spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + _VideoControls(videoRecorder) + HorizontalDivider() + _PrimitiveControls(videoRecorder) } } } From 03e80861e93a7822bcdf62e16fd94bc5a3999af9 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 23 Mar 2024 14:06:59 +0100 Subject: [PATCH 28/41] chore: Update version Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index d6ea019f..3893e758 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -36,7 +36,7 @@ android { minSdk 24 targetSdk 34 versionCode 13 - versionName "0.4.1" + versionName "0.5.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From fa88f5917032607231285eaaf1a5aee52e752082 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 29 Mar 2024 14:39:34 +0100 Subject: [PATCH 29/41] chore: Add logging; fixes Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- app/src/main/java/app/myzel394/alibi/ui/Navigation.kt | 3 ++- .../RecorderScreen/organisms/RecorderEventsHandler.kt | 6 ++++-- .../RecorderScreen/organisms/VideoRecordingStatus.kt | 10 ++++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) 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 52d498bd..069fad65 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt @@ -30,6 +30,7 @@ import app.myzel394.alibi.ui.screens.SettingsScreen import app.myzel394.alibi.ui.screens.WelcomeScreen const val SCALE_IN = 1.25f +const val DEBUG_SKIP_WELCOME = false; @Composable fun Navigation( @@ -58,7 +59,7 @@ fun Navigation( modifier = Modifier .background(MaterialTheme.colorScheme.background), navController = navController, - startDestination = if (settings.hasSeenOnboarding) Screen.AudioRecorder.route else Screen.Welcome.route, + startDestination = if (settings.hasSeenOnboarding || DEBUG_SKIP_WELCOME) Screen.AudioRecorder.route else Screen.Welcome.route, ) { composable(Screen.Welcome.route) { WelcomeScreen( 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 462233b3..cf65273f 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 @@ -248,7 +248,7 @@ fun RecorderEventsHandler( } // Register audio recorder events - DisposableEffect(key1 = audioRecorder, key2 = settings) { + DisposableEffect(Unit) { audioRecorder.onRecordingSave = { cleanupOldFiles -> saveRecording(audioRecorder as RecorderModel, cleanupOldFiles) } @@ -291,7 +291,8 @@ fun RecorderEventsHandler( } // Register video recorder events - DisposableEffect(key1 = videoRecorder, key2 = settings) { + DisposableEffect(Unit) { + Log.d("Alibi", "===== Registering videoRecorder events $videoRecorder") videoRecorder.onRecordingSave = { cleanupOldFiles -> saveRecording(videoRecorder as RecorderModel, cleanupOldFiles) } @@ -326,6 +327,7 @@ fun RecorderEventsHandler( } onDispose { + Log.d("Alibi", "===== Disposing videoRecorder events") videoRecorder.onRecordingSave = { throw NotImplementedError("onRecordingSave should not be called now") } 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 9009a994..1a9eea24 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 @@ -1,6 +1,7 @@ package app.myzel394.alibi.ui.components.RecorderScreen.organisms import android.content.res.Configuration +import android.util.Log import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -241,18 +242,27 @@ fun _PrimitiveControls(videoRecorder: VideoRecorderModel) { } }, onSaveAndStop = { + println("User initiated video recording save and stop") scope.launch { + Log.d("Alibi", "====== Asking to stop recording...") videoRecorder.stopRecording(context) + Log.d("Alibi", "====== Asking to stop recording... done") + Log.d("Alibi", "====== Updating data store...") dataStore.updateData { it.saveLastRecording(videoRecorder as RecorderModel) } + Log.d("Alibi", "====== Updating data store... done") + Log.d("Alibi", "===== Asking to save recording...") videoRecorder.onRecordingSave(false).join() + Log.d("Alibi", "===== Asking to save recording... done") + Log.d("Alibi", "===== Destroying service...") runCatching { videoRecorder.destroyService(context) } + Log.d("Alibi", "===== Destroying service... done") } }, onSaveCurrent = { From 14d6b3616272eacff09c15f21b6cacb66d504a68 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 29 Mar 2024 14:39:42 +0100 Subject: [PATCH 30/41] chore: Update dependencies Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- app/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 3893e758..1c8442ca 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:2024.02.02') + implementation platform('androidx.compose:compose-bom:2024.03.00') 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.1' - implementation "androidx.compose.material:material-icons-extended:1.6.3" + implementation "androidx.compose.material:material-icons-extended:1.6.4" 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:2024.02.02') + androidTestImplementation platform('androidx.compose:compose-bom:2024.03.00') androidTestImplementation 'androidx.compose.ui:ui-test-junit4' debugImplementation 'androidx.compose.ui:ui-tooling' debugImplementation 'androidx.compose.ui:ui-test-manifest' From 798b6f21199086536a221732402b916a9d71e279 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 30 Mar 2024 22:47:09 +0100 Subject: [PATCH 31/41] fix: Add workaround for DisposableEffect Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../organisms/RecorderEventsHandler.kt | 74 +++++++++++-------- 1 file changed, 42 insertions(+), 32 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 cf65273f..3631682b 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 @@ -291,47 +291,57 @@ fun RecorderEventsHandler( } // Register video recorder events + // Absolutely no idea, but somehow on some devices the `DisposableEffect` + // is registered twice, and THEN disposed once (AFTER being called twice), + // which then causes the `onRecordingSave` to be in a weird state. + // This variable is a workaround to prevent this from happening. + var alreadyRegistered = false DisposableEffect(Unit) { - Log.d("Alibi", "===== Registering videoRecorder events $videoRecorder") - videoRecorder.onRecordingSave = { cleanupOldFiles -> - saveRecording(videoRecorder as RecorderModel, cleanupOldFiles) - } - videoRecorder.onRecordingStart = { - snackbarHostState.currentSnackbarData?.dismiss() - } - videoRecorder.onError = { - scope.launch { - saveAsLastRecording(videoRecorder as RecorderModel) + if (alreadyRegistered) { + onDispose { } + } else { + alreadyRegistered = true + Log.i("Alibi", "===== Registering videoRecorder events $videoRecorder") + videoRecorder.onRecordingSave = { cleanupOldFiles -> + saveRecording(videoRecorder as RecorderModel, cleanupOldFiles) + } + videoRecorder.onRecordingStart = { + snackbarHostState.currentSnackbarData?.dismiss() + } + videoRecorder.onError = { + scope.launch { + saveAsLastRecording(videoRecorder as RecorderModel) - runCatching { - videoRecorder.stopRecording(context) - } - runCatching { - videoRecorder.destroyService(context) - } + runCatching { + videoRecorder.stopRecording(context) + } + runCatching { + videoRecorder.destroyService(context) + } - showRecorderError = true + showRecorderError = true + } } - } - videoRecorder.onBatchesFolderNotAccessible = { - scope.launch { - showBatchesInaccessibleError = true + videoRecorder.onBatchesFolderNotAccessible = { + scope.launch { + showBatchesInaccessibleError = true - runCatching { - videoRecorder.stopRecording(context) - } - runCatching { - videoRecorder.destroyService(context) + runCatching { + videoRecorder.stopRecording(context) + } + runCatching { + videoRecorder.destroyService(context) + } } } - } - onDispose { - Log.d("Alibi", "===== Disposing videoRecorder events") - videoRecorder.onRecordingSave = { - throw NotImplementedError("onRecordingSave should not be called now") + onDispose { + Log.i("Alibi", "===== Disposing videoRecorder events") + videoRecorder.onRecordingSave = { + throw NotImplementedError("onRecordingSave should not be called now") + } + videoRecorder.onError = {} } - videoRecorder.onError = {} } } From d1fe46804e5a95e23ef1887360c53574c8165f9e Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 30 Mar 2024 23:00:27 +0100 Subject: [PATCH 32/41] fix: Add workaround for doubly registered events Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../organisms/RecorderEventsHandler.kt | 80 ++++++++++--------- 1 file changed, 43 insertions(+), 37 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 3631682b..8f548471 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 @@ -248,59 +248,65 @@ fun RecorderEventsHandler( } // Register audio recorder events + // Absolutely no idea, but somehow on some devices the `DisposableEffect` + // is registered twice, and THEN disposed once (AFTER being called twice), + // which then causes the `onRecordingSave` to be in a weird state. + // This variable is a workaround to prevent this from happening. + var audioAlreadyRegistered = false DisposableEffect(Unit) { - audioRecorder.onRecordingSave = { cleanupOldFiles -> - saveRecording(audioRecorder as RecorderModel, cleanupOldFiles) - } - audioRecorder.onRecordingStart = { - snackbarHostState.currentSnackbarData?.dismiss() - } - audioRecorder.onError = { - scope.launch { - saveAsLastRecording(audioRecorder as RecorderModel) + if (audioAlreadyRegistered) { + onDispose { } + } else { + audioAlreadyRegistered = true + audioRecorder.onRecordingSave = { cleanupOldFiles -> + saveRecording(audioRecorder as RecorderModel, cleanupOldFiles) + } + audioRecorder.onRecordingStart = { + snackbarHostState.currentSnackbarData?.dismiss() + } + audioRecorder.onError = { + scope.launch { + saveAsLastRecording(audioRecorder as RecorderModel) - runCatching { - audioRecorder.stopRecording(context) - } - runCatching { - audioRecorder.destroyService(context) - } + runCatching { + audioRecorder.stopRecording(context) + } + runCatching { + audioRecorder.destroyService(context) + } - showRecorderError = true + showRecorderError = true + } } - } - audioRecorder.onBatchesFolderNotAccessible = { - scope.launch { - showBatchesInaccessibleError = true + audioRecorder.onBatchesFolderNotAccessible = { + scope.launch { + showBatchesInaccessibleError = true - runCatching { - audioRecorder.stopRecording(context) - } - runCatching { - audioRecorder.destroyService(context) + runCatching { + audioRecorder.stopRecording(context) + } + runCatching { + audioRecorder.destroyService(context) + } } } - } - onDispose { - audioRecorder.onRecordingSave = { - throw NotImplementedError("onRecordingSave should not be called now") + onDispose { + audioRecorder.onRecordingSave = { + throw NotImplementedError("onRecordingSave should not be called now") + } + audioRecorder.onError = {} } - audioRecorder.onError = {} } } // Register video recorder events - // Absolutely no idea, but somehow on some devices the `DisposableEffect` - // is registered twice, and THEN disposed once (AFTER being called twice), - // which then causes the `onRecordingSave` to be in a weird state. - // This variable is a workaround to prevent this from happening. - var alreadyRegistered = false + var videoAlreadyRegistered = false DisposableEffect(Unit) { - if (alreadyRegistered) { + if (videoAlreadyRegistered) { onDispose { } } else { - alreadyRegistered = true + videoAlreadyRegistered = true Log.i("Alibi", "===== Registering videoRecorder events $videoRecorder") videoRecorder.onRecordingSave = { cleanupOldFiles -> saveRecording(videoRecorder as RecorderModel, cleanupOldFiles) From 7166ba13983de655a1c2f116bad67a7c43105728 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 30 Mar 2024 23:06:05 +0100 Subject: [PATCH 33/41] chore: Update ci:cd Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .github/workflows/build-testing.yaml | 6 +++--- .../app/myzel394/alibi/helpers/MediaConverter.kt | 12 +++--------- .../organisms/VideoRecordingStatus.kt | 16 ++++++++-------- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build-testing.yaml b/.github/workflows/build-testing.yaml index 680ea901..bb0b005f 100644 --- a/.github/workflows/build-testing.yaml +++ b/.github/workflows/build-testing.yaml @@ -7,15 +7,15 @@ jobs: debug-builds: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: gradle/wrapper-validation-action@v1 - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: "adopt" - java-version: 19 + java-version: 21 cache: "gradle" - name: Compile diff --git a/app/src/main/java/app/myzel394/alibi/helpers/MediaConverter.kt b/app/src/main/java/app/myzel394/alibi/helpers/MediaConverter.kt index 98819310..fc29e64a 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/MediaConverter.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/MediaConverter.kt @@ -1,17 +1,11 @@ package app.myzel394.alibi.helpers -import android.content.Context -import android.net.Uri import android.util.Log -import androidx.documentfile.provider.DocumentFile import com.arthenica.ffmpegkit.FFmpegKit -import com.arthenica.ffmpegkit.FFmpegKitConfig import com.arthenica.ffmpegkit.ReturnCode import kotlinx.coroutines.CompletableDeferred import java.io.File -import java.lang.Compiler.command import java.util.UUID -import kotlin.math.log // Abstract class for concatenating audio and video files // The concatenator runs in its own thread to avoid unresponsiveness. @@ -56,7 +50,7 @@ data class AudioConcatenator( command ) { session -> if (!ReturnCode.isSuccess(session!!.returnCode)) { - Log.d( + Log.i( "Audio Concatenation", String.format( "Command failed with state %s and rc %s.%s", @@ -100,7 +94,7 @@ class MediaConverter { command, { session -> if (!ReturnCode.isSuccess(session!!.returnCode)) { - Log.d( + Log.i( "Audio Concatenation", String.format( "Command failed with state %s and rc %s.%s", @@ -162,7 +156,7 @@ class MediaConverter { if (ReturnCode.isSuccess(session!!.returnCode)) { completer.complete(Unit) } else { - Log.d( + Log.i( "Video Concatenation", String.format( "Command failed with state %s and rc %s.%s", 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 1a9eea24..bfae5585 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 @@ -244,25 +244,25 @@ fun _PrimitiveControls(videoRecorder: VideoRecorderModel) { onSaveAndStop = { println("User initiated video recording save and stop") scope.launch { - Log.d("Alibi", "====== Asking to stop recording...") + Log.i("Alibi", "====== Asking to stop recording...") videoRecorder.stopRecording(context) - Log.d("Alibi", "====== Asking to stop recording... done") + Log.i("Alibi", "====== Asking to stop recording... done") - Log.d("Alibi", "====== Updating data store...") + Log.i("Alibi", "====== Updating data store...") dataStore.updateData { it.saveLastRecording(videoRecorder as RecorderModel) } - Log.d("Alibi", "====== Updating data store... done") + Log.i("Alibi", "====== Updating data store... done") - Log.d("Alibi", "===== Asking to save recording...") + Log.i("Alibi", "===== Asking to save recording...") videoRecorder.onRecordingSave(false).join() - Log.d("Alibi", "===== Asking to save recording... done") + Log.i("Alibi", "===== Asking to save recording... done") - Log.d("Alibi", "===== Destroying service...") + Log.i("Alibi", "===== Destroying service...") runCatching { videoRecorder.destroyService(context) } - Log.d("Alibi", "===== Destroying service... done") + Log.i("Alibi", "===== Destroying service... done") } }, onSaveCurrent = { From 3734008326e8c84d85067753a57f753dc9e4bc7b Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 30 Mar 2024 23:16:23 +0100 Subject: [PATCH 34/41] chore: Update actions in ci:cd Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .github/workflows/build-testing.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-testing.yaml b/.github/workflows/build-testing.yaml index bb0b005f..98267c57 100644 --- a/.github/workflows/build-testing.yaml +++ b/.github/workflows/build-testing.yaml @@ -9,7 +9,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: gradle/wrapper-validation-action@v1 + - uses: gradle/wrapper-validation-action@v2 - name: Set up JDK uses: actions/setup-java@v4 @@ -23,6 +23,7 @@ jobs: ./gradlew assembleDebug - name: Upload APK - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: + name: alibi-app-debug-apks path: app/build/outputs/apk/debug/app-*-debug.apk From f839416ce19b19c6d01858df1ffa0869232eeac0 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 30 Mar 2024 23:21:10 +0100 Subject: [PATCH 35/41] chore: Update ci:cd dependencies Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .github/workflows/release-app-github.yaml | 8 +++++--- .github/workflows/release-app-google-play.yaml | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release-app-github.yaml b/.github/workflows/release-app-github.yaml index 93c2e415..14b9c920 100644 --- a/.github/workflows/release-app-github.yaml +++ b/.github/workflows/release-app-github.yaml @@ -10,7 +10,9 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 + + - uses: gradle/wrapper-validation-action@v2 - name: Write KeyStore 🗝️ uses: ./.github/actions/prepare-keystore @@ -21,10 +23,10 @@ jobs: keyStoreBase64: ${{ secrets.KEYSTORE }} - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'adopt' - java-version: "17.x" + java-version: 21 cache: 'gradle' - name: Build APKs 📱 diff --git a/.github/workflows/release-app-google-play.yaml b/.github/workflows/release-app-google-play.yaml index dc0ecd43..e61b53ff 100644 --- a/.github/workflows/release-app-google-play.yaml +++ b/.github/workflows/release-app-google-play.yaml @@ -10,7 +10,9 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 + + - uses: gradle/wrapper-validation-action@v2 - name: Write KeyStore 🗝️ uses: ./.github/actions/prepare-keystore @@ -21,10 +23,10 @@ jobs: keyStoreBase64: ${{ secrets.KEYSTORE }} - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'adopt' - java-version: "17.x" + java-version: 21 cache: 'gradle' - name: Build APKs 📱 From 8c9143f1ec748d555fb5c42e064007ff8dcbd8e1 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 1 Apr 2024 23:49:38 +0200 Subject: [PATCH 36/41] fix: Manually check settings changes Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../organisms/RecorderEventsHandler.kt | 16 ++++++++-------- .../myzel394/alibi/ui/screens/RecorderScreen.kt | 9 +-------- 2 files changed, 9 insertions(+), 16 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 8f548471..99a43d78 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 @@ -252,12 +252,12 @@ fun RecorderEventsHandler( // is registered twice, and THEN disposed once (AFTER being called twice), // which then causes the `onRecordingSave` to be in a weird state. // This variable is a workaround to prevent this from happening. - var audioAlreadyRegistered = false - DisposableEffect(Unit) { - if (audioAlreadyRegistered) { + var previousAudioSettings: AppSettings? = null + DisposableEffect(settings) { + if (previousAudioSettings == settings) { onDispose { } } else { - audioAlreadyRegistered = true + previousAudioSettings = settings audioRecorder.onRecordingSave = { cleanupOldFiles -> saveRecording(audioRecorder as RecorderModel, cleanupOldFiles) } @@ -301,12 +301,12 @@ fun RecorderEventsHandler( } // Register video recorder events - var videoAlreadyRegistered = false - DisposableEffect(Unit) { - if (videoAlreadyRegistered) { + var previousVideoSettings: AppSettings? = null + DisposableEffect(settings) { + if (previousVideoSettings == settings) { onDispose { } } else { - videoAlreadyRegistered = true + previousVideoSettings = settings Log.i("Alibi", "===== Registering videoRecorder events $videoRecorder") videoRecorder.onRecordingSave = { cleanupOldFiles -> saveRecording(videoRecorder as RecorderModel, cleanupOldFiles) 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 a086840c..f3936cc1 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,17 +16,14 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable -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 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 @@ -46,7 +43,6 @@ fun RecorderScreen( settings: AppSettings, ) { val snackbarHostState = remember { SnackbarHostState() } - val context = LocalContext.current val scope = rememberCoroutineScope() RecorderEventsHandler( @@ -104,9 +100,6 @@ fun RecorderScreen( .fillMaxSize() .padding(padding), ) { - val appSettings = - context.dataStore.data.collectAsState(AppSettings.getDefaultInstance()).value - if (audioRecorder.isInRecording) AudioRecordingStatus(audioRecorder = audioRecorder) else if (videoRecorder.isInRecording) @@ -115,7 +108,7 @@ fun RecorderScreen( StartRecording( audioRecorder = audioRecorder, videoRecorder = videoRecorder, - appSettings = appSettings, + appSettings = settings, onSaveLastRecording = { scope.launch { when (settings.lastRecording!!.type) { From c2264342042b88ec2e0c75a38691c3039afeaab8 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Tue, 2 Apr 2024 18:54:42 +0200 Subject: [PATCH 37/41] feat: Fetch isLowOnStorage on different thread to avoid UI being blocked Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../WelcomeScreen/pages/SaveFolderPage.kt | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 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 6302632d..4ab70c8c 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 @@ -32,7 +32,6 @@ 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 @@ -50,6 +49,7 @@ import app.myzel394.alibi.ui.SUPPORTS_SCOPED_STORAGE import app.myzel394.alibi.ui.components.WelcomeScreen.atoms.SaveFolderSelection import app.myzel394.alibi.ui.components.atoms.PermissionRequester import app.myzel394.alibi.ui.utils.rememberFolderSelectorDialog +import kotlin.concurrent.thread @Composable fun SaveFolderPage( @@ -61,18 +61,26 @@ fun SaveFolderPage( val context = LocalContext.current - val isLowOnStorage: Boolean = remember(appSettings.maxDuration) { - val availableBytes = VideoBatchesFolder.viaInternalFolder(context).getAvailableBytes() + var isLowOnStorage by rememberSaveable { + mutableStateOf(false) + } + // Fetching this synchronously results in the UI being blocked. + // Instead, we fetch this in a different thread and update the state when we have the result. + LaunchedEffect(appSettings, context) { + thread { + val availableBytes = VideoBatchesFolder.viaInternalFolder(context).getAvailableBytes() - if (availableBytes == null) { - return@remember false - } + if (availableBytes == null) { + isLowOnStorage = false + return@thread + } - val bytesPerMinute = BatchesFolder.requiredBytesForOneMinuteOfRecording(appSettings) - val requiredBytes = appSettings.maxDuration / 1000 / 60 * bytesPerMinute + val bytesPerMinute = BatchesFolder.requiredBytesForOneMinuteOfRecording(appSettings) + val requiredBytes = appSettings.maxDuration / 1000 / 60 * bytesPerMinute - // Allow for a 10% margin of error - availableBytes < requiredBytes + // Allow for a 10% margin of error + isLowOnStorage = availableBytes < requiredBytes + } } LaunchedEffect(isLowOnStorage, appSettings.maxDuration) { @@ -126,7 +134,7 @@ fun SaveFolderPage( ) { SaveFolderSelection( saveFolder = saveFolder, - isLowOnStorage = isLowOnStorage, + isLowOnStorage = false, onSaveFolderChange = { saveFolder = it }, ) } @@ -189,7 +197,7 @@ fun SaveFolderPage( } } }, - enabled = if (saveFolder == null) !isLowOnStorage else true, + enabled = if (saveFolder == null) !false else true, modifier = Modifier .fillMaxWidth() .height(BIG_PRIMARY_BUTTON_SIZE), From 6b19cb81ed10119731eb25ea400cea58d206ba8c Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Tue, 2 Apr 2024 18:56:29 +0200 Subject: [PATCH 38/41] fix: Undo unwanted changes Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../alibi/ui/components/WelcomeScreen/pages/SaveFolderPage.kt | 4 ++-- 1 file changed, 2 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 4ab70c8c..8e1dec5d 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 @@ -134,7 +134,7 @@ fun SaveFolderPage( ) { SaveFolderSelection( saveFolder = saveFolder, - isLowOnStorage = false, + isLowOnStorage = isLowOnStorage, onSaveFolderChange = { saveFolder = it }, ) } @@ -197,7 +197,7 @@ fun SaveFolderPage( } } }, - enabled = if (saveFolder == null) !false else true, + enabled = if (saveFolder == null) !isLowOnStorage else true, modifier = Modifier .fillMaxWidth() .height(BIG_PRIMARY_BUTTON_SIZE), From 0ec149ee02c6f0293819c4f50b008a27c000af2c Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:14:12 +0200 Subject: [PATCH 39/41] fix: small ui improvements Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../alibi/ui/components/RecorderScreen/atoms/BigButton.kt | 2 +- .../alibi/ui/components/RecorderScreen/atoms/LowStorageInfo.kt | 2 +- 2 files 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 1f66f27d..70aaaaae 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 > 200.dp && maxHeight > 350.dp && orientation == Configuration.ORIENTATION_PORTRAIT + maxWidth > 250.dp && maxHeight > 600.dp && orientation == Configuration.ORIENTATION_PORTRAIT Column( modifier = Modifier 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 bb47a831..a9b30a73 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 @@ -37,7 +37,7 @@ fun LowStorageInfo( if (isLowOnStorage) Box( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp) ) { BoxWithConstraints { val isLarge = maxHeight > 600.dp; From 08084d207eadb180229b77b5de095acdb1a0be3b Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:24:57 +0200 Subject: [PATCH 40/41] fix: Improve responsive design Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../RecorderScreen/atoms/BigButton.kt | 5 +- .../RecorderScreen/atoms/LowStorageInfo.kt | 5 +- .../molecules/AudioRecordingStart.kt | 2 + .../molecules/VideoRecordingStart.kt | 2 + .../organisms/StartRecording.kt | 201 ++++++++++-------- 5 files changed, 117 insertions(+), 98 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 70aaaaae..a7dc7f33 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 @@ -35,12 +35,13 @@ fun BigButton( description: String? = null, onClick: () -> Unit, onLongClick: () -> Unit = {}, + isBig: Boolean? = null, ) { val orientation = LocalConfiguration.current.orientation BoxWithConstraints { - val isLarge = - maxWidth > 250.dp && maxHeight > 600.dp && orientation == Configuration.ORIENTATION_PORTRAIT + val isLarge = if (isBig == null) + maxWidth > 250.dp && maxHeight > 600.dp && orientation == Configuration.ORIENTATION_PORTRAIT else isBig Column( modifier = Modifier 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 a9b30a73..7642a78a 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 @@ -18,6 +18,7 @@ import app.myzel394.alibi.ui.components.atoms.VisualDensity @Composable fun LowStorageInfo( + modifier: Modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), appSettings: AppSettings, ) { val context = LocalContext.current @@ -36,9 +37,7 @@ fun LowStorageInfo( println("LowStorageInfo: availableBytes: $availableBytes, requiredBytes: $requiredBytes, isLowOnStorage: $isLowOnStorage") if (isLowOnStorage) - Box( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp) - ) { + Box(modifier = modifier) { BoxWithConstraints { val isLarge = maxHeight > 600.dp; diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/AudioRecordingStart.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/AudioRecordingStart.kt index 2eba2858..060ce390 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/AudioRecordingStart.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/AudioRecordingStart.kt @@ -22,6 +22,7 @@ import app.myzel394.alibi.ui.models.AudioRecorderModel fun AudioRecordingStart( audioRecorder: AudioRecorderModel, appSettings: AppSettings, + useLargeButtons: Boolean? = null, ) { val context = LocalContext.current @@ -59,6 +60,7 @@ fun AudioRecordingStart( label = stringResource(R.string.ui_audioRecorder_action_start_label), icon = Icons.Default.Mic, onClick = triggerRecordAudio, + isBig = useLargeButtons, ) } } diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/VideoRecordingStart.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/VideoRecordingStart.kt index 40fd2696..775fe634 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/VideoRecordingStart.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/VideoRecordingStart.kt @@ -27,6 +27,7 @@ fun VideoRecordingStart( onHideAudioRecording: () -> Unit, onShowAudioRecording: () -> Unit, showPreview: Boolean, + useLargeButtons: Boolean? = null, ) { val context = LocalContext.current @@ -87,6 +88,7 @@ fun VideoRecordingStart( showSheet = true } }, + isBig = useLargeButtons, ) } } \ No newline at end of file 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 3b8121b7..f93ce7cf 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 @@ -3,6 +3,7 @@ package app.myzel394.alibi.ui.components.RecorderScreen.organisms import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -100,24 +101,47 @@ fun StartRecording( ) } - Column( - modifier = Modifier - .fillMaxSize() - .padding(bottom = if (orientation == Configuration.ORIENTATION_PORTRAIT) 0.dp else 16.dp), - verticalArrangement = Arrangement.SpaceBetween, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - when (orientation) { - Configuration.ORIENTATION_LANDSCAPE -> { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly, - verticalAlignment = Alignment.CenterVertically, - ) { + BoxWithConstraints { + val isLargeDisplay = + maxWidth > 250.dp && maxHeight > 600.dp && orientation == Configuration.ORIENTATION_PORTRAIT + + Column( + modifier = Modifier + .fillMaxSize() + .padding(bottom = if (orientation == Configuration.ORIENTATION_PORTRAIT) 0.dp else 16.dp), + verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + when (orientation) { + Configuration.ORIENTATION_LANDSCAPE -> { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + ) { + if (showAudioRecorder) + AudioRecordingStart( + audioRecorder = audioRecorder, + appSettings = appSettings, + ) + VideoRecordingStart( + videoRecorder = videoRecorder, + appSettings = appSettings, + onHideAudioRecording = onHideTopBar, + onShowAudioRecording = onShowTopBar, + showPreview = !showAudioRecorder, + ) + } + } + + else -> { + Spacer(modifier = Modifier.weight(1f)) + if (showAudioRecorder) AudioRecordingStart( audioRecorder = audioRecorder, appSettings = appSettings, + useLargeButtons = isLargeDisplay, ) VideoRecordingStart( videoRecorder = videoRecorder, @@ -125,95 +149,86 @@ fun StartRecording( onHideAudioRecording = onHideTopBar, onShowAudioRecording = onShowTopBar, showPreview = !showAudioRecorder, + useLargeButtons = isLargeDisplay, ) } } - else -> { - Spacer(modifier = Modifier.weight(1f)) - if (showAudioRecorder) - AudioRecordingStart( - audioRecorder = audioRecorder, - appSettings = appSettings, + val forceUpdate = rememberForceUpdateOnLifeCycleChange() + Column( + modifier = Modifier + .weight(1f) + .then(forceUpdate), + verticalArrangement = Arrangement.Bottom, + ) { + if (appSettings.lastRecording?.hasRecordingsAvailable(context) == true) { + val label = stringResource( + R.string.ui_recorder_action_saveOldRecording_label, + DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL) + .format(appSettings.lastRecording.recordingStart), ) - VideoRecordingStart( - videoRecorder = videoRecorder, - appSettings = appSettings, - onHideAudioRecording = onHideTopBar, - onShowAudioRecording = onShowTopBar, - showPreview = !showAudioRecorder, - ) - } - } - - - val forceUpdate = rememberForceUpdateOnLifeCycleChange() - Column( - modifier = Modifier - .weight(1f) - .then(forceUpdate), - verticalArrangement = Arrangement.Bottom, - ) { - if (appSettings.lastRecording?.hasRecordingsAvailable(context) == true) { - val label = stringResource( - R.string.ui_recorder_action_saveOldRecording_label, - DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL) - .format(appSettings.lastRecording.recordingStart), - ) - TextButton( - modifier = Modifier - .fillMaxWidth() - .requiredWidthIn(max = BIG_PRIMARY_BUTTON_MAX_WIDTH) - .height(BIG_PRIMARY_BUTTON_SIZE) - .semantics { - contentDescription = label - }, - onClick = onSaveLastRecording, - contentPadding = ButtonDefaults.TextButtonWithIconContentPadding, - ) { - Icon( - Icons.Default.Save, - contentDescription = null, - modifier = Modifier.size(ButtonDefaults.IconSize), - ) - Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) - Text(label) - } - } else { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Image( - painter = painterResource(R.drawable.launcher_monochrome_noopacity), - contentDescription = null, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant), + TextButton( modifier = Modifier - .size(ButtonDefaults.IconSize) - ) + .fillMaxWidth() + .requiredWidthIn(max = BIG_PRIMARY_BUTTON_MAX_WIDTH) + .height(BIG_PRIMARY_BUTTON_SIZE) + .semantics { + contentDescription = label + }, + onClick = onSaveLastRecording, + contentPadding = ButtonDefaults.TextButtonWithIconContentPadding, + ) { + Icon( + Icons.Default.Save, + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) + Text(label) + } + } else { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(R.drawable.launcher_monochrome_noopacity), + contentDescription = null, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant), + modifier = Modifier + .size(ButtonDefaults.IconSize) + ) - Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) - - ClickableText( - text = annotatedDescription, - onClick = { textIndex -> - if (annotatedDescription.getStringAnnotations(textIndex, textIndex) - .firstOrNull()?.tag == "minutes" - ) { - showQuickMaxDurationSelector = true - } - }, - modifier = Modifier - .widthIn(max = 300.dp) - .fillMaxWidth(), - style = MaterialTheme.typography.bodySmall.copy( - color = MaterialTheme.colorScheme.onSurfaceVariant, - ), - ) + Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) + + ClickableText( + text = annotatedDescription, + onClick = { textIndex -> + if (annotatedDescription.getStringAnnotations(textIndex, textIndex) + .firstOrNull()?.tag == "minutes" + ) { + showQuickMaxDurationSelector = true + } + }, + modifier = Modifier + .widthIn(max = 300.dp) + .fillMaxWidth(), + style = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant, + ), + ) + } } } - } - LowStorageInfo(appSettings = appSettings) + LowStorageInfo( + modifier = if (isLargeDisplay) Modifier + .padding(16.dp) + .widthIn(max = 400.dp) else Modifier + .fillMaxWidth() + .padding(4.dp), + appSettings = appSettings + ) + } } } \ No newline at end of file From 6aff6c8683df29fa5a64f19e8bd0d40b53f86e89 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:41:30 +0200 Subject: [PATCH 41/41] feat: Make audio bar's sensitivity changeable by pinching (closes #99) Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../atoms/RealTimeAudioVisualizer.kt | 18 ++++++++++++++++-- .../organisms/AudioRecordingStatus.kt | 1 - 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/RealTimeAudioVisualizer.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/RealTimeAudioVisualizer.kt index b2d08087..cf4c8738 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/RealTimeAudioVisualizer.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/RealTimeAudioVisualizer.kt @@ -4,11 +4,16 @@ import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas +import androidx.compose.foundation.gestures.rememberTransformableState +import androidx.compose.foundation.gestures.transformable import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset @@ -75,7 +80,15 @@ fun RealtimeAudioVisualizer( audioRecorder.setMaxAmplitudesAmount(ceil(availableSpace.toInt() / BOX_DIFF).toInt() + 1) } - Canvas(modifier = modifier) { + var scale by remember { mutableFloatStateOf(1f) } + val transformState = rememberTransformableState { zoomChange, _, _ -> + scale *= zoomChange + } + val amplitudePercentageModifier = MAX_AMPLITUDE * (1 / scale) + + Canvas( + modifier = modifier.transformable(transformState), + ) { val height = this.size.height / 2f val width = this.size.width @@ -88,7 +101,8 @@ fun RealtimeAudioVisualizer( val horizontalProgress = ( clamp(horizontalValue, GROW_START, GROW_END) - GROW_START) / (GROW_END - GROW_START) - val amplitudePercentage = (amplitude.toFloat() / MAX_AMPLITUDE).coerceAtMost(1f) + val amplitudePercentage = + (amplitude.toFloat() / amplitudePercentageModifier).coerceAtMost(1f) val boxHeight = (height * amplitudePercentage * horizontalProgress).coerceAtLeast(15f) 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 673e573a..893b4501 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 @@ -38,7 +38,6 @@ import java.time.LocalDateTime fun AudioRecordingStatus( audioRecorder: AudioRecorderModel, ) { - val context = LocalContext.current val configuration = LocalConfiguration.current.orientation var now by remember { mutableStateOf(LocalDateTime.now()) }