diff --git a/app/build.gradle b/app/build.gradle
index 7f147f9..d6ea019 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -35,8 +35,8 @@ android {
applicationId "app.myzel394.alibi"
minSdk 24
targetSdk 34
- versionCode 12
- versionName "0.4.0"
+ versionCode 13
+ versionName "0.4.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -97,12 +97,12 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
implementation 'androidx.activity:activity-compose:1.8.2'
implementation 'androidx.activity:activity-ktx:1.8.2'
- implementation platform('androidx.compose:compose-bom:2022.10.00')
+ implementation platform('androidx.compose:compose-bom:2024.02.02')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
- implementation 'androidx.compose.material3:material3:1.2.0'
- implementation "androidx.compose.material:material-icons-extended:1.6.2"
+ implementation 'androidx.compose.material3:material3:1.2.1'
+ implementation "androidx.compose.material:material-icons-extended:1.6.3"
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.lifecycle:lifecycle-service:2.7.0'
@@ -110,7 +110,7 @@ dependencies {
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
- androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00')
+ androidTestImplementation platform('androidx.compose:compose-bom:2024.02.02')
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
@@ -135,7 +135,7 @@ dependencies {
implementation 'com.maxkeppeler.sheets-compose-dialogs:list:1.2.0'
implementation 'com.maxkeppeler.sheets-compose-dialogs:input:1.2.0'
- def camerax_version = "1.3.1"
+ def camerax_version = "1.3.2"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 87cbf80..b0e7f0b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -19,6 +19,7 @@
android:maxSdkVersion="30" />
+
diff --git a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt
index 1f45583..cfd47e9 100644
--- a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt
+++ b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt
@@ -11,6 +11,7 @@ import app.myzel394.alibi.helpers.AudioBatchesFolder
import app.myzel394.alibi.helpers.VideoBatchesFolder
import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE
import app.myzel394.alibi.ui.SUPPORTS_SCOPED_STORAGE
+import app.myzel394.alibi.ui.components.RecorderScreen.organisms.RecorderModel
import app.myzel394.alibi.ui.utils.PermissionHelper
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@@ -30,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,
@@ -102,6 +103,16 @@ data class AppSettings(
return copy(appLockSettings = appLockSettings)
}
+ fun saveLastRecording(recorder: RecorderModel): AppSettings {
+ return if (deleteRecordingsImmediately) {
+ this
+ } else {
+ setLastRecording(
+ recorder.recorderService!!.getRecordingInformation()
+ )
+ }
+ }
+
// If the object is present, biometric authentication is enabled.
// To disable biometric authentication, set the instance to null.
fun isAppLockEnabled() = appLockSettings != null
@@ -297,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 70c12f5..7ec8536 100644
--- a/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt
+++ b/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt
@@ -3,30 +3,34 @@ package app.myzel394.alibi.helpers
import android.Manifest
import android.content.ContentUris
import android.content.ContentValues
-import app.myzel394.alibi.ui.MEDIA_RECORDINGS_PREFIX
-
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Build
+import android.os.Environment
+import android.os.ParcelFileDescriptor
+import android.os.storage.StorageManager
import android.provider.MediaStore
import android.provider.MediaStore.Video.Media
-import androidx.documentfile.provider.DocumentFile
-import java.io.File
-import java.time.LocalDateTime
-import java.time.format.DateTimeFormatter
-import com.arthenica.ffmpegkit.FFmpegKitConfig
+import android.system.Os
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.net.toUri
+import androidx.documentfile.provider.DocumentFile
+import app.myzel394.alibi.db.AppSettings
+import app.myzel394.alibi.ui.MEDIA_RECORDINGS_PREFIX
import app.myzel394.alibi.ui.RECORDER_INTERNAL_SELECTED_VALUE
import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE
import app.myzel394.alibi.ui.SUPPORTS_SCOPED_STORAGE
import app.myzel394.alibi.ui.utils.PermissionHelper
-import com.arthenica.ffmpegkit.FFprobeKit
+import com.arthenica.ffmpegkit.FFmpegKitConfig
import kotlinx.coroutines.CompletableDeferred
+import java.io.File
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
import kotlin.reflect.KFunction4
+
abstract class BatchesFolder(
open val context: Context,
open val type: BatchType,
@@ -197,7 +201,6 @@ abstract class BatchesFolder(
createNewFile()
}
-
fun checkIfOutputAlreadyExists(
date: LocalDateTime,
extension: String
@@ -388,12 +391,12 @@ abstract class BatchesFolder(
}
}
- fun deleteOldRecordings(earliestCounter: Long) {
+ fun deleteRecordings(range: LongRange) {
when (type) {
BatchType.INTERNAL -> getInternalFolder().listFiles()?.forEach {
val fileCounter = it.nameWithoutExtension.toIntOrNull() ?: return@forEach
- if (fileCounter < earliestCounter) {
+ if (fileCounter in range) {
it.delete()
}
}
@@ -401,7 +404,7 @@ abstract class BatchesFolder(
BatchType.CUSTOM -> getCustomDefinedFolder().listFiles().forEach {
val fileCounter = it.name?.substringBeforeLast(".")?.toIntOrNull() ?: return@forEach
- if (fileCounter < earliestCounter) {
+ if (fileCounter in range) {
it.delete()
}
}
@@ -411,7 +414,7 @@ abstract class BatchesFolder(
val deletableNames = mutableListOf()
queryMediaContent { rawName, counter, _, _ ->
- if (counter < earliestCounter) {
+ if (counter in range) {
deletableNames.add(rawName)
}
}
@@ -428,7 +431,7 @@ abstract class BatchesFolder(
it.nameWithoutExtension.substring(mediaPrefix.length).toIntOrNull()
?: return@forEach
- if (fileCounter < earliestCounter) {
+ if (fileCounter in range) {
it.delete()
}
}
@@ -522,10 +525,67 @@ abstract class BatchesFolder(
return uri!!
}
+ 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.MEDIA ->
+ if (SUPPORTS_SCOPED_STORAGE)
+ File(
+ Environment.getExternalStoragePublicDirectory(VideoBatchesFolder.BASE_SCOPED_STORAGE_RELATIVE_PATH),
+ 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) {
+ storageManager.getAllocatableBytes(storageManager.getUuidForPath(file))
+ } else {
+ file.usableSpace;
+ }
+ }
+
enum class BatchType {
INTERNAL,
CUSTOM,
MEDIA,
}
+
+ companion object {
+ fun requiredBytesForOneMinuteOfRecording(appSettings: AppSettings): Long {
+ // 350 MiB sounds like a good default
+ return 350 * 1024 * 1024
+ }
+ }
}
diff --git a/app/src/main/java/app/myzel394/alibi/helpers/VideoBatchesFolder.kt b/app/src/main/java/app/myzel394/alibi/helpers/VideoBatchesFolder.kt
index 22f0201..e143681 100644
--- a/app/src/main/java/app/myzel394/alibi/helpers/VideoBatchesFolder.kt
+++ b/app/src/main/java/app/myzel394/alibi/helpers/VideoBatchesFolder.kt
@@ -135,7 +135,8 @@ class VideoBatchesFolder(
fun viaMediaFolder(context: Context) = VideoBatchesFolder(context, BatchType.MEDIA)
- fun importFromFolder(folder: String, context: Context) = when (folder) {
+ fun importFromFolder(folder: String?, context: Context) = when (folder) {
+ null -> viaInternalFolder(context)
RECORDER_INTERNAL_SELECTED_VALUE -> viaInternalFolder(context)
RECORDER_MEDIA_SELECTED_VALUE -> viaMediaFolder(context)
else -> viaCustomFolder(
diff --git a/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt
index ed8f9c7..6783ef2 100644
--- a/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt
+++ b/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt
@@ -11,6 +11,9 @@ abstract class IntervalRecorderService :
protected var counter = 0L
private set
+ // Tracks the index of the currently locked file
+ private var lockedIndex: Long? = null
+
lateinit var settings: AppSettings
private lateinit var cycleTimer: ScheduledExecutorService
@@ -21,6 +24,23 @@ abstract class IntervalRecorderService :
abstract fun getRecordingInformation(): I
+ // When saving the recording, the files should be locked.
+ // This prevents the service from deleting the currently available files, so that
+ // they can be safely used to save the recording.
+ // Once finished, make sure to unlock the files using `unlockFiles`.
+ fun lockFiles() {
+ lockedIndex = counter
+ }
+
+ // Unlocks and deletes the files that were locked using `lockFiles`.
+ fun unlockFiles(cleanupFiles: Boolean = false) {
+ if (cleanupFiles) {
+ batchesFolder.deleteRecordings(0.. :
private fun deleteOldRecordings() {
val timeMultiplier = settings.maxDuration / settings.intervalDuration
- val earliestCounter = counter - timeMultiplier
+ val earliestCounter = Math.max(counter - timeMultiplier, lockedIndex ?: 0)
if (earliestCounter <= 0) {
return
}
- batchesFolder.deleteOldRecordings(earliestCounter)
+ batchesFolder.deleteRecordings(0..earliestCounter)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt
index 5aaf273..06bcac8 100644
--- a/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt
+++ b/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt
@@ -50,7 +50,7 @@ class VideoRecorderService :
// Used to listen and check if the camera is available
private var _cameraAvailableListener = CompletableDeferred()
- private var _videoFinalizerListener = CompletableDeferred()
+ private lateinit var _videoFinalizerListener: CompletableDeferred;
// Absolute last completer that can be awaited to ensure that the camera is closed
private var _cameraCloserListener = CompletableDeferred()
@@ -129,8 +129,10 @@ class VideoRecorderService :
stopActiveRecording()
val newRecording = prepareVideoRecording()
+ _videoFinalizerListener = CompletableDeferred()
+
activeRecording = newRecording.start(ContextCompat.getMainExecutor(this)) { event ->
- if (event is VideoRecordEvent.Finalize && this@VideoRecorderService.state == RecorderState.STOPPED) {
+ if (event is VideoRecordEvent.Finalize && this@VideoRecorderService.state == RecorderState.STOPPED || this@VideoRecorderService.state == RecorderState.PAUSED) {
_videoFinalizerListener.complete(Unit)
}
}
@@ -139,7 +141,7 @@ class VideoRecorderService :
if (_cameraAvailableListener.isCompleted) {
action()
} else {
- // Race condition of `startNewCycle` being called before `invpkeOnCompletion`
+ // Race condition of `startNewCycle` being called before `invokeOnCompletion`
// has been called can be ignored, as the camera usually opens within 5 seconds
// and the interval can't be set shorter than 10 seconds.
_cameraAvailableListener.invokeOnCompletion {
@@ -189,17 +191,22 @@ class VideoRecorderService :
videoCapture = buildVideoCapture(recorder)
runOnMain {
- camera = cameraProvider!!.bindToLifecycle(
- this,
- selectedCamera,
- videoCapture
- )
- cameraControl = CameraControl(camera!!).also {
- it.init()
- }
- onCameraControlAvailable()
+ try {
+ camera = cameraProvider!!.bindToLifecycle(
+ this,
+ selectedCamera,
+ videoCapture
+ )
- _cameraAvailableListener.complete(Unit)
+ cameraControl = CameraControl(camera!!).also {
+ it.init()
+ }
+ onCameraControlAvailable()
+
+ _cameraAvailableListener.complete(Unit)
+ } catch (error: IllegalArgumentException) {
+ onError()
+ }
}
}
diff --git a/app/src/main/java/app/myzel394/alibi/ui/Constants.kt b/app/src/main/java/app/myzel394/alibi/ui/Constants.kt
index 979ee51..a8afa20 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/Constants.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/Constants.kt
@@ -2,6 +2,7 @@ package app.myzel394.alibi.ui
import android.os.Build
import androidx.compose.ui.unit.dp
+import java.util.Base64
val BIG_PRIMARY_BUTTON_SIZE = 64.dp
val BIG_PRIMARY_BUTTON_MAX_WIDTH = 450.dp
@@ -63,3 +64,17 @@ val CRYPTO_DONATIONS = mapOf(
"Litecoin" to "LZayhTosZ9ToRvcbeR1gEDgb76Z7ZA2drN",
"Filecoin" to "f1j6pm3chzhgadpf6iwmtux33jb5gccj5arkg4dsq",
)
+
+// Base64encoding these values so that bots can't easily scrape them.
+val b64d = Base64.getDecoder()
+val CONTACT_METHODS = mapOf(
+ "E-Mail" to String(b64d.decode("Z2" + "9vZ2xlLXBsYX" + "k" + "uMjlrMWFAYWxlZWFzL" + "mNvbQo=")).trim(),
+ "GitHub" to String(
+ b64d.decode(
+ "aHR" +
+ "0cHM6Ly9n" + "a" + "XRodWIuY29t" + "L015emVsMzk0L2NvbnRhY3QtbWUK"
+ )
+ ).trim(),
+ "Mastodon" to String(b64d.decode("T" + "X" + "l6Z" + "WwzOTRAbWFzdG9kb24uc29" + "jaWFsCg" + "==")).trim(),
+ "Reddit" to "https://reddit.com/u/Myzel394"
+)
diff --git a/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt b/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt
index a2d5b36..52d498b 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt
@@ -14,6 +14,7 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
+import androidx.core.content.ContextCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
@@ -60,7 +61,15 @@ fun Navigation(
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/RecorderScreen/atoms/BigButton.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/BigButton.kt
index 3856550..1f66f27 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/BigButton.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/BigButton.kt
@@ -39,11 +39,12 @@ fun BigButton(
val orientation = LocalConfiguration.current.orientation
BoxWithConstraints {
- val isLarge = maxWidth > 500.dp && orientation == Configuration.ORIENTATION_PORTRAIT
+ val isLarge =
+ maxWidth > 200.dp && maxHeight > 350.dp && orientation == Configuration.ORIENTATION_PORTRAIT
Column(
modifier = Modifier
- .size(if (isLarge) 250.dp else 200.dp)
+ .size(if (isLarge) 250.dp else 190.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
new file mode 100644
index 0000000..bb47a83
--- /dev/null
+++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/LowStorageInfo.kt
@@ -0,0 +1,54 @@
+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
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import app.myzel394.alibi.R
+import app.myzel394.alibi.db.AppSettings
+import app.myzel394.alibi.helpers.BatchesFolder
+import app.myzel394.alibi.helpers.VideoBatchesFolder
+import app.myzel394.alibi.ui.components.atoms.MessageBox
+import app.myzel394.alibi.ui.components.atoms.MessageType
+import app.myzel394.alibi.ui.components.atoms.VisualDensity
+
+@Composable
+fun LowStorageInfo(
+ appSettings: AppSettings,
+) {
+ val context = LocalContext.current
+ val availableBytes =
+ VideoBatchesFolder.importFromFolder(appSettings.saveFolder, 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
+ val isLowOnStorage = availableBytes < requiredBytes * 1.1
+ println("LowStorageInfo: availableBytes: $availableBytes, requiredBytes: $requiredBytes, isLowOnStorage: $isLowOnStorage")
+
+ if (isLowOnStorage)
+ Box(
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
+ ) {
+ 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/atoms/RecorderErrorDialog.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/RecorderErrorDialog.kt
index 92da619..a05306c 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/RecorderErrorDialog.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/RecorderErrorDialog.kt
@@ -3,8 +3,6 @@ package app.myzel394.alibi.ui.components.RecorderScreen.atoms
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.Button
-import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -15,7 +13,6 @@ import app.myzel394.alibi.R
@Composable
fun RecorderErrorDialog(
onClose: () -> Unit,
- onSave: () -> Unit,
) {
AlertDialog(
onDismissRequest = onClose,
@@ -31,14 +28,9 @@ fun RecorderErrorDialog(
text = {
Text(stringResource(R.string.ui_recorder_error_recording_description))
},
- dismissButton = {
- TextButton(onClick = onClose) {
- Text(stringResource(R.string.dialog_close_cancel_label))
- }
- },
confirmButton = {
- TextButton(onClick = onSave) {
- Text(stringResource(R.string.ui_recorder_action_save_label))
+ TextButton(onClick = onClose) {
+ Text(stringResource(R.string.dialog_close_neutral_label))
}
}
)
diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/RecorderProcessingDialog.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/RecorderProcessingDialog.kt
index fe5297a..14ea861 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/RecorderProcessingDialog.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/RecorderProcessingDialog.kt
@@ -1,17 +1,16 @@
package app.myzel394.alibi.ui.components.RecorderScreen.atoms
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Memory
import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.myzel394.alibi.R
@@ -38,15 +37,18 @@ fun RecorderProcessingDialog(
text = {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(32.dp),
) {
Text(
stringResource(R.string.ui_recorder_action_save_processing_dialog_description),
)
- Spacer(modifier = Modifier.height(32.dp))
+ CircularProgressIndicator()
if (progress == null)
LinearProgressIndicator()
else
- LinearProgressIndicator(progress = progress)
+ LinearProgressIndicator(
+ progress = { progress },
+ )
}
},
confirmButton = {}
diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveButton.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveButton.kt
index 28d960d..fc259cb 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveButton.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveButton.kt
@@ -1,35 +1,55 @@
package app.myzel394.alibi.ui.components.RecorderScreen.atoms
-import androidx.compose.material3.Button
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import app.myzel394.alibi.R
+@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SaveButton(
modifier: Modifier = Modifier,
onSave: () -> Unit,
+ onLongClick: () -> Unit = {},
) {
val label = stringResource(R.string.ui_recorder_action_save_label)
- TextButton(
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center,
modifier = Modifier
+ .clip(ButtonDefaults.textShape)
.semantics {
contentDescription = label
}
- .then(modifier),
- onClick = onSave,
+ .combinedClickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = rememberRipple(color = MaterialTheme.colorScheme.primary),
+ onClick = onSave,
+ onLongClick = onLongClick,
+ )
+ .padding(ButtonDefaults.TextButtonContentPadding)
+ .then(modifier)
) {
Text(
label,
- fontSize = MaterialTheme.typography.bodySmall.fontSize,
+ style = MaterialTheme.typography.titleSmall,
+ color = MaterialTheme.colorScheme.primary,
)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveCurrentNowModal.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveCurrentNowModal.kt
new file mode 100644
index 0000000..8480f55
--- /dev/null
+++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/atoms/SaveCurrentNowModal.kt
@@ -0,0 +1,58 @@
+package app.myzel394.alibi.ui.components.RecorderScreen.atoms
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import app.myzel394.alibi.R
+import app.myzel394.alibi.ui.SHEET_BOTTOM_OFFSET
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SaveCurrentNowModal(
+ onDismiss: () -> Unit,
+ onConfirm: () -> Unit,
+) {
+ val sheetState = rememberModalBottomSheetState(true)
+
+ // Auto save on specific events
+ ModalBottomSheet(
+ onDismissRequest = onDismiss,
+ sheetState = sheetState,
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = SHEET_BOTTOM_OFFSET)
+ .padding(16.dp)
+ ) {
+ Text(
+ stringResource(R.string.ui_recorder_action_saveCurrent),
+ style = MaterialTheme.typography.headlineMedium,
+ textAlign = TextAlign.Center,
+ )
+
+ Text(
+ stringResource(R.string.ui_recorder_action_saveCurrent_explanation),
+ )
+
+ TextButton(onClick = onConfirm) {
+ Text(stringResource(R.string.ui_recorder_action_save_label))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/RecordingControl.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/RecordingControl.kt
index 8e1bdde..6e36a86 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/RecordingControl.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/molecules/RecordingControl.kt
@@ -35,7 +35,8 @@ fun RecordingControl(
recordingTime: Long,
onDelete: () -> Unit,
onPauseResume: () -> Unit,
- onSave: () -> Unit,
+ onSaveAndStop: () -> Unit,
+ onSaveCurrent: () -> Unit,
) {
val animateIn = rememberInitialRecordingAnimation(recordingTime)
@@ -106,7 +107,8 @@ fun RecordingControl(
contentAlignment = Alignment.Center,
) {
SaveButton(
- onSave = onSave,
+ onSave = onSaveAndStop,
+ onLongClick = onSaveCurrent,
modifier = Modifier.fillMaxWidth(),
)
}
@@ -170,7 +172,10 @@ fun RecordingControl(
.alpha(saveButtonAlpha),
contentAlignment = Alignment.Center,
) {
- SaveButton(onSave = onSave)
+ SaveButton(
+ onSave = onSaveAndStop,
+ onLongClick = onSaveCurrent,
+ )
}
}
}
diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt
index 2a639a0..673e573 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/AudioRecordingStatus.kt
@@ -22,7 +22,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
+import app.myzel394.alibi.dataStore
import app.myzel394.alibi.ui.components.RecorderScreen.atoms.RealtimeAudioVisualizer
+import app.myzel394.alibi.ui.components.RecorderScreen.atoms.SaveCurrentNowModal
import app.myzel394.alibi.ui.components.RecorderScreen.molecules.MicrophoneStatus
import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingControl
import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingStatus
@@ -39,8 +41,6 @@ fun AudioRecordingStatus(
val context = LocalContext.current
val configuration = LocalConfiguration.current.orientation
- val scope = rememberCoroutineScope()
-
var now by remember { mutableStateOf(LocalDateTime.now()) }
LaunchedEffect(Unit) {
@@ -90,34 +90,11 @@ fun AudioRecordingStatus(
MicrophoneStatus(audioRecorder)
}
- RecordingControl(
- modifier = Modifier
- .weight(1f)
- .fillMaxWidth(),
- isPaused = audioRecorder.isPaused,
- recordingTime = audioRecorder.recordingTime,
- onDelete = {
- scope.launch {
- runCatching {
- audioRecorder.stopRecording(context)
- }
- runCatching {
- audioRecorder.destroyService(context)
- }
- audioRecorder.batchesFolder!!.deleteRecordings()
- }
- },
- onPauseResume = {
- if (audioRecorder.isPaused) {
- audioRecorder.resumeRecording()
- } else {
- audioRecorder.pauseRecording()
- }
- },
- onSave = {
- audioRecorder.onRecordingSave(false)
- }
- )
+ Box(
+ modifier = Modifier.weight(1f)
+ ) {
+ _PrimitiveControls(audioRecorder)
+ }
}
}
@@ -138,33 +115,76 @@ fun AudioRecordingStatus(
HorizontalDivider()
- RecordingControl(
- isPaused = audioRecorder.isPaused,
- recordingTime = audioRecorder.recordingTime,
- onDelete = {
- scope.launch {
- runCatching {
- audioRecorder.stopRecording(context)
- }
- runCatching {
- audioRecorder.destroyService(context)
- }
- audioRecorder.batchesFolder!!.deleteRecordings()
- }
- },
- onPauseResume = {
- if (audioRecorder.isPaused) {
- audioRecorder.resumeRecording()
- } else {
- audioRecorder.pauseRecording()
- }
- },
- onSave = {
- audioRecorder.onRecordingSave(false)
- }
- )
+ _PrimitiveControls(audioRecorder)
}
}
}
}
+}
+
+@Composable
+fun _PrimitiveControls(audioRecorder: AudioRecorderModel) {
+ val context = LocalContext.current
+ val dataStore = context.dataStore
+ val scope = rememberCoroutineScope()
+
+ var showConfirmSaveNow by remember { mutableStateOf(false) }
+
+ if (showConfirmSaveNow) {
+ SaveCurrentNowModal(
+ onDismiss = {
+ showConfirmSaveNow = false
+ },
+ onConfirm = {
+ showConfirmSaveNow = false
+
+ scope.launch {
+ audioRecorder.recorderService!!.startNewCycle()
+
+ audioRecorder.onRecordingSave(false).join()
+ }
+ },
+ )
+ }
+
+ RecordingControl(
+ isPaused = audioRecorder.isPaused,
+ recordingTime = audioRecorder.recordingTime,
+ onDelete = {
+ scope.launch {
+ runCatching {
+ audioRecorder.stopRecording(context)
+ }
+ runCatching {
+ audioRecorder.destroyService(context)
+ }
+ audioRecorder.batchesFolder!!.deleteRecordings()
+ }
+ },
+ onPauseResume = {
+ if (audioRecorder.isPaused) {
+ audioRecorder.resumeRecording()
+ } else {
+ audioRecorder.pauseRecording()
+ }
+ },
+ onSaveAndStop = {
+ scope.launch {
+ audioRecorder.stopRecording(context)
+
+ dataStore.updateData {
+ it.saveLastRecording(audioRecorder as RecorderModel)
+ }
+
+ audioRecorder.onRecordingSave(false).join()
+
+ runCatching {
+ audioRecorder.destroyService(context)
+ }
+ }
+ },
+ onSaveCurrent = {
+ showConfirmSaveNow = true
+ },
+ )
}
\ No newline at end of file
diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/RecorderEventsHandler.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/RecorderEventsHandler.kt
index d01c5ad..688a33a 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/RecorderEventsHandler.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/RecorderEventsHandler.kt
@@ -1,6 +1,5 @@
package app.myzel394.alibi.ui.components.RecorderScreen.organisms
-import android.content.Intent
import android.net.Uri
import android.util.Log
import androidx.compose.material3.SnackbarDuration
@@ -9,8 +8,6 @@ import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableDoubleStateOf
-import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -96,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
)
}
}
@@ -132,15 +136,19 @@ fun RecorderEventsHandler(
}
}
- suspend fun saveRecording(recorder: RecorderModel) {
+ suspend fun saveRecording(recorder: RecorderModel, cleanupOldFiles: Boolean = false): Thread {
isProcessing = true
// Give the user some time to see the processing dialog
delay(100)
- thread {
+ return thread {
runBlocking {
try {
+ if (recorder.isCurrentlyActivelyRecording) {
+ recorder.recorderService?.lockFiles()
+ }
+
val recording =
// When new recording created
recorder.recorderService?.getRecordingInformation()
@@ -218,6 +226,9 @@ fun RecorderEventsHandler(
} catch (error: Exception) {
Log.getStackTraceString(error)
} finally {
+ if (recorder.isCurrentlyActivelyRecording) {
+ recorder.recorderService?.unlockFiles(cleanupOldFiles)
+ }
isProcessing = false
}
}
@@ -226,19 +237,14 @@ fun RecorderEventsHandler(
// Register audio recorder events
DisposableEffect(key1 = audioRecorder, key2 = settings) {
- audioRecorder.onRecordingSave = { justSave ->
+ 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 {
- if (justSave) {
- saveRecording(audioRecorder as RecorderModel)
- } else {
- audioRecorder.stopRecording(context)
-
- saveAsLastRecording(audioRecorder as RecorderModel)
-
- saveRecording(audioRecorder)
-
- audioRecorder.destroyService(context)
- }
+ saveRecording(audioRecorder as RecorderModel, cleanupOldFiles).join()
}
}
audioRecorder.onRecordingStart = {
@@ -248,6 +254,13 @@ fun RecorderEventsHandler(
scope.launch {
saveAsLastRecording(audioRecorder as RecorderModel)
+ runCatching {
+ audioRecorder.stopRecording(context)
+ }
+ runCatching {
+ audioRecorder.destroyService(context)
+ }
+
showRecorderError = true
}
}
@@ -265,26 +278,23 @@ fun RecorderEventsHandler(
}
onDispose {
- audioRecorder.onRecordingSave = {}
+ audioRecorder.onRecordingSave = {
+ throw NotImplementedError("onRecordingSave should not be called now")
+ }
audioRecorder.onError = {}
}
}
// Register video recorder events
DisposableEffect(key1 = videoRecorder, key2 = settings) {
- videoRecorder.onRecordingSave = { justSave ->
+ 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 {
- if (justSave) {
- saveRecording(videoRecorder as RecorderModel)
- } else {
- videoRecorder.stopRecording(context)
-
- saveAsLastRecording(videoRecorder as RecorderModel)
-
- saveRecording(videoRecorder)
-
- videoRecorder.destroyService(context)
- }
+ saveRecording(videoRecorder as RecorderModel, cleanupOldFiles).join()
}
}
videoRecorder.onRecordingStart = {
@@ -294,6 +304,13 @@ fun RecorderEventsHandler(
scope.launch {
saveAsLastRecording(videoRecorder as RecorderModel)
+ runCatching {
+ videoRecorder.stopRecording(context)
+ }
+ runCatching {
+ videoRecorder.destroyService(context)
+ }
+
showRecorderError = true
}
}
@@ -311,7 +328,9 @@ fun RecorderEventsHandler(
}
onDispose {
- videoRecorder.onRecordingSave = {}
+ videoRecorder.onRecordingSave = {
+ throw NotImplementedError("onRecordingSave should not be called now")
+ }
videoRecorder.onError = {}
}
}
@@ -326,8 +345,6 @@ fun RecorderEventsHandler(
onClose = {
showRecorderError = false
},
- onSave = {
- },
)
if (showBatchesInaccessibleError)
diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/StartRecording.kt b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/StartRecording.kt
index 0ea2ddc..3b8121b 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/StartRecording.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/StartRecording.kt
@@ -44,6 +44,7 @@ import app.myzel394.alibi.R
import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_MAX_WIDTH
import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE
+import app.myzel394.alibi.ui.components.RecorderScreen.atoms.LowStorageInfo
import app.myzel394.alibi.ui.components.RecorderScreen.molecules.AudioRecordingStart
import app.myzel394.alibi.ui.components.RecorderScreen.molecules.QuickMaxDurationSelector
import app.myzel394.alibi.ui.components.RecorderScreen.molecules.VideoRecordingStart
@@ -102,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,
) {
@@ -212,5 +213,7 @@ fun StartRecording(
}
}
}
+
+ LowStorageInfo(appSettings = appSettings)
}
}
\ No newline at end of file
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 74f068f..7927f8b 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/components/RecorderScreen/organisms/VideoRecordingStatus.kt
@@ -21,6 +21,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
@@ -33,6 +34,8 @@ 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
import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingControl
import app.myzel394.alibi.ui.components.RecorderScreen.molecules.RecordingStatus
@@ -201,8 +204,28 @@ fun _VideoRecordingStatus(videoRecorder: VideoRecorderModel) {
@Composable
fun _PrimitiveControls(videoRecorder: VideoRecorderModel) {
val context = LocalContext.current
+ val dataStore = context.dataStore
val scope = rememberCoroutineScope()
+ var showConfirmSaveNow by remember { mutableStateOf(false) }
+
+ if (showConfirmSaveNow) {
+ SaveCurrentNowModal(
+ onDismiss = {
+ showConfirmSaveNow = false
+ },
+ onConfirm = {
+ showConfirmSaveNow = false
+
+ scope.launch {
+ videoRecorder.recorderService!!.startNewCycle()
+
+ videoRecorder.onRecordingSave(false).join()
+ }
+ },
+ )
+ }
+
RecordingControl(
orientation = Configuration.ORIENTATION_PORTRAIT,
// There may be some edge cases where the app may crash if the
@@ -229,8 +252,23 @@ fun _PrimitiveControls(videoRecorder: VideoRecorderModel) {
videoRecorder.pauseRecording()
}
},
- onSave = {
- videoRecorder.onRecordingSave(false)
+ onSaveAndStop = {
+ scope.launch {
+ videoRecorder.stopRecording(context)
+
+ dataStore.updateData {
+ it.saveLastRecording(videoRecorder as RecorderModel)
+ }
+
+ videoRecorder.onRecordingSave(false).join()
+
+ runCatching {
+ videoRecorder.destroyService(context)
+ }
+ }
+ },
+ onSaveCurrent = {
+ showConfirmSaveNow = true
}
)
}
diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/IntervalDurationTile.kt b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/IntervalDurationTile.kt
index 09bc86e..edfe06a 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/IntervalDurationTile.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/IntervalDurationTile.kt
@@ -2,7 +2,6 @@ package app.myzel394.alibi.ui.components.SettingsScreen.Tiles
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Mic
-import androidx.compose.material.icons.filled.Timer
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -10,7 +9,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -43,6 +41,12 @@ fun IntervalDurationTile(
fun updateValue(intervalDuration: Long) {
scope.launch {
+ if (intervalDuration > settings.maxDuration) {
+ dataStore.updateData {
+ it.setMaxDuration(intervalDuration)
+ }
+ }
+
dataStore.updateData {
it.setIntervalDuration(intervalDuration)
}
diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/MaxDurationTile.kt b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/MaxDurationTile.kt
index 6319cb6..28e995d 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/MaxDurationTile.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/MaxDurationTile.kt
@@ -1,7 +1,6 @@
package app.myzel394.alibi.ui.components.SettingsScreen.Tiles
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Memory
import androidx.compose.material.icons.filled.Timer
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
@@ -10,7 +9,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -42,6 +40,12 @@ fun MaxDurationTile(
fun updateValue(maxDuration: Long) {
scope.launch {
+ if (maxDuration < settings.intervalDuration) {
+ dataStore.updateData {
+ it.setIntervalDuration(maxDuration)
+ }
+ }
+
dataStore.updateData {
it.setMaxDuration(maxDuration)
}
@@ -64,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/SettingsScreen/Tiles/SaveFolderTile.kt b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/SaveFolderTile.kt
index ec9f11d..2d815ca 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/SaveFolderTile.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/SaveFolderTile.kt
@@ -26,7 +26,6 @@ import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material.icons.filled.PermMedia
-import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
@@ -435,8 +434,6 @@ fun SelectionSheet(
) {
val context = LocalContext.current
- var showCustomFolderWarning by remember { mutableStateOf(false) }
-
val selectFolder = rememberFolderSelectorDialog { folder ->
if (folder == null) {
return@rememberFolderSelectorDialog
@@ -445,18 +442,6 @@ fun SelectionSheet(
updateValue(folder.toString())
}
- if (showCustomFolderWarning) {
- CustomFolderWarningDialog(
- onDismiss = {
- showCustomFolderWarning = false
- },
- onConfirm = {
- showCustomFolderWarning = false
- selectFolder()
- },
- )
- }
-
var showExternalPermissionRequired by remember { mutableStateOf(false) }
if (showExternalPermissionRequired) {
@@ -523,9 +508,7 @@ fun SelectionSheet(
SelectionButton(
label = stringResource(R.string.ui_settings_option_saveFolder_action_custom_label),
icon = Icons.Default.Folder,
- onClick = {
- showCustomFolderWarning = true
- },
+ onClick = selectFolder,
)
if (!SUPPORTS_SAVING_VIDEOS_IN_CUSTOM_FOLDERS) {
Column(
@@ -581,52 +564,6 @@ fun SelectionButton(
}
}
-@Composable
-fun CustomFolderWarningDialog(
- onDismiss: () -> Unit,
- onConfirm: () -> Unit,
-) {
- val title = stringResource(R.string.ui_settings_option_saveFolder_warning_title)
- val text = stringResource(R.string.ui_settings_option_saveFolder_warning_text)
-
- AlertDialog(
- icon = {
- Icon(
- Icons.Default.Warning,
- contentDescription = null,
- )
- },
- onDismissRequest = onDismiss,
- title = {
- Text(text = title)
- },
- text = {
- Text(text = text)
- },
- confirmButton = {
- Button(onClick = onConfirm) {
- Text(
- text = stringResource(R.string.ui_settings_option_saveFolder_warning_action_confirm),
- )
- }
- },
- dismissButton = {
- TextButton(
- onClick = onDismiss,
- contentPadding = ButtonDefaults.TextButtonWithIconContentPadding,
- ) {
- Icon(
- Icons.Default.Cancel,
- contentDescription = null,
- modifier = Modifier.size(ButtonDefaults.IconSize),
- )
- Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
- Text(stringResource(R.string.dialog_close_cancel_label))
- }
- }
- )
-}
-
@Composable
fun ExternalPermissionRequiredDialog(
onDismiss: () -> Unit,
diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/MaxDurationSelector.kt b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/MaxDurationSelector.kt
new file mode 100644
index 0000000..15044d7
--- /dev/null
+++ b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/MaxDurationSelector.kt
@@ -0,0 +1,196 @@
+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.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
+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 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_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
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+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),
+ )
+
+ 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.a11y_selectValue,
+ 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)
+ }
+ }
+
+ 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/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/SaveFolderSelection.kt b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/SaveFolderSelection.kt
new file mode 100644
index 0000000..2c1ca12
--- /dev/null
+++ b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/SaveFolderSelection.kt
@@ -0,0 +1,206 @@
+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.layout.widthIn
+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.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.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"
+
+@Composable
+fun SaveFolderSelection(
+ modifier: Modifier = Modifier,
+ saveFolder: String?,
+ isLowOnStorage: Boolean,
+ onSaveFolderChange: (String?) -> Unit,
+) {
+ @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),
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(MaterialTheme.shapes.medium)
+ .background(MaterialTheme.colorScheme.surfaceContainer)
+ .then(modifier),
+ verticalArrangement = Arrangement.Center,
+ ) {
+ 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 = 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,
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ RadioButton(
+ selected = saveFolder == folder,
+ onClick = { onSaveFolderChange(folder) },
+ )
+ Text(label)
+ }
+ Icon(
+ Icons.Default.PermMedia,
+ 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.Folder,
+ 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 && saveFolder == null)
+ MessageBox(
+ 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/atoms/ExplanationPage.kt b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/ExplanationPage.kt
similarity index 97%
rename from app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/ExplanationPage.kt
rename to app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/ExplanationPage.kt
index 70051fe..3314830 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/ExplanationPage.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/ExplanationPage.kt
@@ -1,4 +1,4 @@
-package app.myzel394.alibi.ui.components.WelcomeScreen.atoms
+package app.myzel394.alibi.ui.components.WelcomeScreen.pages
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/MaxDurationSettingsPage.kt b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/MaxDurationSettingsPage.kt
new file mode 100644
index 0000000..a779033
--- /dev/null
+++ b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/MaxDurationSettingsPage.kt
@@ -0,0 +1,111 @@
+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.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
+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.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 MaxDurationSettingsPage(
+ onContinue: () -> Unit,
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState()),
+ verticalArrangement = Arrangement.SpaceBetween,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Spacer(modifier = Modifier.height(40.dp))
+ 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),
+ fontStyle = MaterialTheme.typography.bodySmall.fontStyle,
+ fontSize = MaterialTheme.typography.bodySmall.fontSize,
+ color = MaterialTheme.typography.bodySmall.color,
+ )
+ }
+ Spacer(modifier = Modifier.height(40.dp))
+ Box(
+ modifier = Modifier
+ .widthIn(max = 400.dp)
+ .padding(horizontal = 16.dp)
+ ) {
+ MaxDurationSelector()
+ }
+ Spacer(modifier = Modifier.height(20.dp))
+ Box(
+ modifier = Modifier
+ .widthIn(max = 400.dp)
+ .padding(horizontal = 16.dp)
+ ) {
+ MessageBox(
+ type = MessageType.INFO,
+ message = stringResource(R.string.ui_welcome_timeSettings_changeableHint),
+ density = VisualDensity.DENSE,
+ )
+ }
+ Spacer(modifier = Modifier.height(40.dp))
+ 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/components/WelcomeScreen/pages/ReadyPage.kt b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/ReadyPage.kt
new file mode 100644
index 0000000..cdee3cb
--- /dev/null
+++ b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/ReadyPage.kt
@@ -0,0 +1,78 @@
+package app.myzel394.alibi.ui.components.WelcomeScreen.pages
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Celebration
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import app.myzel394.alibi.R
+import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE
+
+@Composable
+fun ReadyPage(
+ onContinue: () -> Unit,
+) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.SpaceBetween,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Spacer(modifier = Modifier.weight(1f))
+ Column(
+ modifier = Modifier
+ .padding(horizontal = 32.dp),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Icon(
+ Icons.Default.Celebration,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.tertiary,
+ modifier = Modifier.size(128.dp),
+ )
+ Spacer(modifier = Modifier.height(32.dp))
+ Text(
+ stringResource(R.string.ui_welcome_ready_title),
+ style = MaterialTheme.typography.titleLarge,
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ stringResource(R.string.ui_welcome_ready_message),
+ )
+ }
+ Spacer(modifier = Modifier.weight(1f))
+ Button(
+ onClick = onContinue,
+ modifier = Modifier
+ .padding(16.dp)
+ .fillMaxWidth()
+ .height(BIG_PRIMARY_BUTTON_SIZE),
+ contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
+ ) {
+ Icon(
+ Icons.Default.Check,
+ contentDescription = null,
+ modifier = Modifier.size(ButtonDefaults.IconSize)
+ )
+ Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
+ Text(stringResource(R.string.ui_welcome_ready_start))
+ }
+ }
+}
diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/ResponsibilityPage.kt b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/ResponsibilityPage.kt
similarity index 91%
rename from app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/ResponsibilityPage.kt
rename to app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/ResponsibilityPage.kt
index 3b2b526..537d61b 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/atoms/ResponsibilityPage.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/ResponsibilityPage.kt
@@ -1,4 +1,4 @@
-package app.myzel394.alibi.ui.components.WelcomeScreen.atoms
+package app.myzel394.alibi.ui.components.WelcomeScreen.pages
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -10,7 +10,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
@@ -59,7 +59,7 @@ fun ResponsibilityPage(
}
Spacer(modifier = Modifier.weight(1f))
Button(
- onClick = { onContinue() },
+ onClick = onContinue,
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
@@ -67,12 +67,12 @@ fun ResponsibilityPage(
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
) {
Icon(
- Icons.Default.Check,
+ Icons.Default.ChevronRight,
contentDescription = null,
modifier = Modifier.size(ButtonDefaults.IconSize)
)
Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
- Text(stringResource(R.string.ui_welcome_start_label))
+ Text(stringResource(R.string.continue_label))
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/SaveFolderPage.kt b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/SaveFolderPage.kt
new file mode 100644
index 0000000..6302632
--- /dev/null
+++ b/app/src/main/java/app/myzel394/alibi/ui/components/WelcomeScreen/pages/SaveFolderPage.kt
@@ -0,0 +1,248 @@
+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
+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.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
+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.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
+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.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.PermissionRequester
+import app.myzel394.alibi.ui.utils.rememberFolderSelectorDialog
+
+@Composable
+fun SaveFolderPage(
+ onBack: () -> Unit,
+ onContinue: (saveFolder: String?) -> Unit,
+ appSettings: AppSettings,
+) {
+ var saveFolder by rememberSaveable { mutableStateOf(null) }
+
+ val context = LocalContext.current
+
+ val isLowOnStorage: Boolean = remember(appSettings.maxDuration) {
+ val availableBytes = VideoBatchesFolder.viaInternalFolder(context).getAvailableBytes()
+
+ if (availableBytes == null) {
+ return@remember false
+ }
+
+ val bytesPerMinute = BatchesFolder.requiredBytesForOneMinuteOfRecording(appSettings)
+ val requiredBytes = appSettings.maxDuration / 1000 / 60 * bytesPerMinute
+
+ // Allow for a 10% margin of error
+ availableBytes < requiredBytes
+ }
+
+ LaunchedEffect(isLowOnStorage, appSettings.maxDuration) {
+ if (isLowOnStorage) {
+ if (saveFolder == null) {
+ saveFolder = RECORDER_MEDIA_SELECTED_VALUE
+ }
+ } else {
+ saveFolder = null
+ }
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState()),
+ verticalArrangement = Arrangement.SpaceBetween,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Spacer(modifier = Modifier.height(40.dp))
+ 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),
+ fontStyle = MaterialTheme.typography.bodySmall.fontStyle,
+ fontSize = MaterialTheme.typography.bodySmall.fontSize,
+ color = MaterialTheme.typography.bodySmall.color,
+ )
+ }
+ Spacer(modifier = Modifier.height(40.dp))
+ Box(
+ modifier = Modifier
+ .widthIn(max = 400.dp)
+ .padding(horizontal = 16.dp)
+ ) {
+ SaveFolderSelection(
+ saveFolder = saveFolder,
+ isLowOnStorage = isLowOnStorage,
+ onSaveFolderChange = { saveFolder = it },
+ )
+ }
+ Spacer(modifier = Modifier.height(40.dp))
+ 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,
+ )
+ }
+ PermissionRequester(
+ permission = Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ icon = Icons.AutoMirrored.Filled.InsertDriveFile,
+ onPermissionAvailable = { onContinue(saveFolder) },
+ ) { requestWritePermission ->
+ val selectFolder = rememberFolderSelectorDialog { folder ->
+ if (folder == null) {
+ return@rememberFolderSelectorDialog
+ }
+
+ onContinue(saveFolder)
+ }
+ var showCustomFolderHint by rememberSaveable { mutableStateOf(false) }
+
+ if (showCustomFolderHint) {
+ _CustomFolderDialog(
+ onAbort = { showCustomFolderHint = false },
+ onOk = {
+ showCustomFolderHint = false
+ selectFolder()
+ },
+ )
+ }
+
+ Button(
+ onClick = {
+ when (saveFolder) {
+ null -> onContinue(saveFolder)
+ RECORDER_MEDIA_SELECTED_VALUE -> {
+ if (SUPPORTS_SCOPED_STORAGE) {
+ onContinue(saveFolder)
+ } else {
+ requestWritePermission()
+ }
+ }
+
+ else -> {
+ showCustomFolderHint = true
+ }
+ }
+ },
+ enabled = if (saveFolder == null) !isLowOnStorage else true,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(BIG_PRIMARY_BUTTON_SIZE),
+ contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
+ ) {
+ Icon(
+ Icons.Default.ChevronRight,
+ contentDescription = null,
+ modifier = Modifier.size(ButtonDefaults.IconSize)
+ )
+ Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
+ Text(stringResource(R.string.continue_label))
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun _CustomFolderDialog(
+ onAbort: () -> Unit,
+ onOk: () -> Unit,
+) {
+ AlertDialog(
+ onDismissRequest = onAbort,
+ icon = {
+ Icon(
+ Icons.Default.Folder,
+ contentDescription = null,
+ )
+ },
+ title = {
+ Text(stringResource(R.string.ui_welcome_saveFolder_customFolder_title))
+ },
+ text = {
+ Text(stringResource(R.string.ui_welcome_saveFolder_customFolder_message))
+ },
+ dismissButton = {
+ TextButton(
+ onClick = onAbort,
+ contentPadding = ButtonDefaults.TextButtonContentPadding,
+ colors = ButtonDefaults.textButtonColors(),
+ ) {
+ Text(stringResource(R.string.dialog_close_cancel_label))
+ }
+ },
+ confirmButton = {
+ Button(
+ onClick = onOk,
+ ) {
+ Text(stringResource(R.string.dialog_close_neutral_label))
+ }
+ }
+ )
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/atoms/MessageBox.kt b/app/src/main/java/app/myzel394/alibi/ui/components/atoms/MessageBox.kt
index d67ad64..2af9d33 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/components/atoms/MessageBox.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/components/atoms/MessageBox.kt
@@ -68,15 +68,15 @@ fun MessageBox(
.clip(MaterialTheme.shapes.medium)
.background(backgroundColor)
.let {
- if (density == VisualDensity.COMFORTABLE) {
- it.padding(horizontal = 8.dp, vertical = 16.dp)
- } else {
- it.padding(8.dp)
+ when (density) {
+ VisualDensity.COMFORTABLE -> it.padding(horizontal = 8.dp, vertical = 16.dp)
+ VisualDensity.DENSE -> it.padding(8.dp)
+ VisualDensity.COMPACT -> it.padding(8.dp)
}
}
.then(modifier)
) {
- if (density == VisualDensity.COMFORTABLE) {
+ if (density == VisualDensity.COMFORTABLE || density == VisualDensity.DENSE) {
Icon(
imageVector = when (type) {
MessageType.ERROR -> Icons.Default.Error
@@ -121,4 +121,5 @@ enum class MessageType {
enum class VisualDensity {
COMPACT,
COMFORTABLE,
+ DENSE,
}
\ No newline at end of file
diff --git a/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt b/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt
index 452419b..7796849 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt
@@ -17,6 +17,7 @@ import app.myzel394.alibi.helpers.BatchesFolder
import app.myzel394.alibi.services.IntervalRecorderService
import app.myzel394.alibi.services.RecorderNotificationHelper
import app.myzel394.alibi.services.RecorderService
+import kotlinx.coroutines.Job
import kotlinx.serialization.json.Json
abstract class BaseRecorderModel> :
@@ -31,6 +32,9 @@ abstract class BaseRecorderModel Unit = {}
+ var onRecordingSave: (cleanupOldFiles: Boolean) -> Job = {
+ throw NotImplementedError("onRecordingSave not implemented")
+ }
var onRecordingStart: () -> Unit = {}
var onError: () -> Unit = {}
var onBatchesFolderNotAccessible: () -> Unit = {}
diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/AboutScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/AboutScreen.kt
index 4df51fb..a5fb877 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/screens/AboutScreen.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/screens/AboutScreen.kt
@@ -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
@@ -82,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,
) {
@@ -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/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt
index 68752b3..a086840 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt
@@ -16,22 +16,26 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.*
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
-import androidx.navigation.NavController
-import app.myzel394.alibi.ui.components.RecorderScreen.organisms.AudioRecordingStatus
-import app.myzel394.alibi.ui.components.RecorderScreen.organisms.StartRecording
-import app.myzel394.alibi.ui.enums.Screen
import app.myzel394.alibi.R
import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.db.RecordingInformation
+import app.myzel394.alibi.ui.components.RecorderScreen.organisms.AudioRecordingStatus
import app.myzel394.alibi.ui.components.RecorderScreen.organisms.RecorderEventsHandler
+import app.myzel394.alibi.ui.components.RecorderScreen.organisms.StartRecording
import app.myzel394.alibi.ui.components.RecorderScreen.organisms.VideoRecordingStatus
import app.myzel394.alibi.ui.models.AudioRecorderModel
import app.myzel394.alibi.ui.models.VideoRecorderModel
+import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -43,6 +47,7 @@ fun RecorderScreen(
) {
val snackbarHostState = remember { SnackbarHostState() }
val context = LocalContext.current
+ val scope = rememberCoroutineScope()
RecorderEventsHandler(
settings = settings,
@@ -112,12 +117,14 @@ fun RecorderScreen(
videoRecorder = videoRecorder,
appSettings = appSettings,
onSaveLastRecording = {
- when (settings.lastRecording!!.type) {
- RecordingInformation.Type.AUDIO ->
- audioRecorder.onRecordingSave(true)
+ scope.launch {
+ when (settings.lastRecording!!.type) {
+ RecordingInformation.Type.AUDIO ->
+ audioRecorder.onRecordingSave(false)
- RecordingInformation.Type.VIDEO ->
- videoRecorder.onRecordingSave(true)
+ RecordingInformation.Type.VIDEO ->
+ videoRecorder.onRecordingSave(false)
+ }
}
},
showAudioRecorder = topBarVisible,
diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt
index ff15de0..6ed8ef2 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/screens/WelcomeScreen.kt
@@ -8,16 +8,17 @@ 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
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.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.effects.rememberSettings
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@@ -27,41 +28,76 @@ 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,
initialPageOffsetFraction = 0f,
- pageCount = {2}
+ pageCount = { 5 }
)
- 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 -> MaxDurationSettingsPage {
+ scope.launch {
+ pagerState.animateScrollToPage(3)
+ }
+ }
+
+ 3 -> SaveFolderPage(
+ onBack = {
+ scope.launch {
+ pagerState.animateScrollToPage(2)
+ }
+ },
+ onContinue = { saveFolder ->
+ scope.launch {
+ dataStore.updateData {
+ settings.setSaveFolder(saveFolder)
+ }
+
+ 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 15b1d26..ec7cd34 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
@@ -37,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
@@ -78,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
@@ -160,7 +162,7 @@
When stopped, save the last...
Recording started at %s
Recording started %s
- Saving now will save until %s
+ Press to save from %s till now
Video Recorder is starting...
Alibi is locked
Unlock
@@ -191,4 +193,33 @@
To protect your privacy, Alibi stores its batches into its own private, encrypted storage. This storage is only accessible by Alibi and can\'t be accessed by other apps or by a possible intruder. Once you save the recording, you will be asked where you want to save the recording to.
Please rotate your device to portait mode
Back
+ Save now?
+ You can save the current ongoing recording by pressing and holding down on the save button. The recording will continue in the background.
+ 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
+ You can change this anytime
+ Where should Alibi store the batches?
+ 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
+ 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
+ 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:
+ 1 Minute
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
index 251ec5e..82bd130 100644
--- a/app/src/main/res/xml/backup_rules.xml
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -10,7 +10,7 @@
-->
-
+ path="datastore/." />
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 8fdd478..a00ecda 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
- id 'com.android.application' version '8.2.2' apply false
- id 'com.android.library' version '8.2.2' apply false
+ id 'com.android.application' version '8.3.0' apply false
+ id 'com.android.library' version '8.3.0' apply false
id 'org.jetbrains.kotlin.android' version '1.9.22' apply false
id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.21'
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 17efaba..1c20144 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Sun Jul 30 13:54:47 CEST 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists