mirror of
https://github.com/Myzel394/Alibi.git
synced 2025-06-18 14:55:26 +02:00
Merge branch 'feat/0.5.0' into master
This commit is contained in:
commit
53701067ef
@ -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}"
|
||||
|
@ -19,6 +19,7 @@
|
||||
android:maxSdkVersion="30" />
|
||||
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.CAPTURE_AUDIO_OUTPUT" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
@ -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,
|
||||
|
@ -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<String>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -11,6 +11,9 @@ abstract class IntervalRecorderService<I, B : BatchesFolder> :
|
||||
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<I, B : BatchesFolder> :
|
||||
|
||||
abstract fun getRecordingInformation(): I
|
||||
|
||||
// When saving the recording, the files should be locked.
|
||||
// This prevents the service from deleting the currently available files, so that
|
||||
// they can be safely used to save the recording.
|
||||
// Once finished, make sure to unlock the files using `unlockFiles`.
|
||||
fun lockFiles() {
|
||||
lockedIndex = counter
|
||||
}
|
||||
|
||||
// Unlocks and deletes the files that were locked using `lockFiles`.
|
||||
fun unlockFiles(cleanupFiles: Boolean = false) {
|
||||
if (cleanupFiles) {
|
||||
batchesFolder.deleteRecordings(0..<lockedIndex!!)
|
||||
}
|
||||
|
||||
lockedIndex = null
|
||||
}
|
||||
|
||||
// Make overrideable
|
||||
open fun startNewCycle() {
|
||||
counter += 1
|
||||
@ -72,12 +92,12 @@ abstract class IntervalRecorderService<I, B : BatchesFolder> :
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
@ -50,7 +50,7 @@ class VideoRecorderService :
|
||||
|
||||
// Used to listen and check if the camera is available
|
||||
private var _cameraAvailableListener = CompletableDeferred<Unit>()
|
||||
private var _videoFinalizerListener = CompletableDeferred<Unit>()
|
||||
private lateinit var _videoFinalizerListener: CompletableDeferred<Unit>;
|
||||
|
||||
// Absolute last completer that can be awaited to ensure that the camera is closed
|
||||
private var _cameraCloserListener = CompletableDeferred<Unit>()
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<String, String>(
|
||||
"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"
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -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 = {}
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
},
|
||||
)
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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<Long, String>(
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@ -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<String?>(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))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
}
|
@ -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,
|
||||
}
|
@ -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<I, B : BatchesFolder, T : IntervalRecorderService<I, B>> :
|
||||
@ -31,6 +32,9 @@ abstract class BaseRecorderModel<I, B : BatchesFolder, T : IntervalRecorderServi
|
||||
open val isInRecording: Boolean
|
||||
get() = recorderService != null
|
||||
|
||||
open val isCurrentlyActivelyRecording
|
||||
get() = recorderState === RecorderState.RECORDING
|
||||
|
||||
val isPaused: Boolean
|
||||
get() = recorderState === RecorderState.PAUSED
|
||||
|
||||
@ -45,7 +49,9 @@ abstract class BaseRecorderModel<I, B : BatchesFolder, T : IntervalRecorderServi
|
||||
|
||||
// If `isSavingAsOldRecording` is true, the user is saving an old recording,
|
||||
// thus the service is not running and thus doesn't need to be stopped or destroyed
|
||||
var onRecordingSave: (isSavingAsOldRecording: Boolean) -> Unit = {}
|
||||
var onRecordingSave: (cleanupOldFiles: Boolean) -> Job = {
|
||||
throw NotImplementedError("onRecordingSave not implemented")
|
||||
}
|
||||
var onRecordingStart: () -> Unit = {}
|
||||
var onError: () -> Unit = {}
|
||||
var onBatchesFolderNotAccessible: () -> Unit = {}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,8 @@
|
||||
<string name="form_error_value_mustBeGreaterThan">Please enter a number greater than <xliff:g name="min">%s</xliff:g></string>
|
||||
<string name="form_value_selected">Selected: %s</string>
|
||||
|
||||
<string name="a11y_selectValue">Select %s</string>
|
||||
|
||||
<string name="notificationChannels_recorder_name">Recorder</string>
|
||||
<string name="notificationChannels_recorder_description">Shows the current recording status</string>
|
||||
|
||||
@ -37,7 +39,7 @@
|
||||
<string name="ui_recorder_action_start_description_3">\u0020at your request</string>
|
||||
|
||||
<string name="ui_recorder_action_save_processing_dialog_title">Processing</string>
|
||||
<string name="ui_recorder_action_save_processing_dialog_description">Processing your recording, do not close Alibi! You will automatically be prompted to save the file once it\'s ready</string>
|
||||
<string name="ui_recorder_action_save_processing_dialog_description">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.</string>
|
||||
|
||||
<string name="ui_recorder_state_recording_description">Alibi keeps recording in the background</string>
|
||||
|
||||
@ -78,7 +80,7 @@
|
||||
<string name="ui_recorder_state_paused_title">Recording paused</string>
|
||||
<string name="ui_recorder_state_paused_description">Alibi is paused</string>
|
||||
<string name="ui_recorder_error_recording_title">An error occurred</string>
|
||||
<string name="ui_recorder_error_recording_description">Alibi encountered an error during recording. Would you like to try saving the recording?</string>
|
||||
<string name="ui_recorder_error_recording_description">Alibi encountered an error during recording. Try using different settings or restart the app.</string>
|
||||
<string name="ui_settings_language_title">Language</string>
|
||||
<string name="ui_settings_language_update_label">Change</string>
|
||||
<string name="ui_audioRecorder_info_microphone_deviceMicrophone">Device Microphone</string>
|
||||
@ -160,7 +162,7 @@
|
||||
<string name="ui_recorder_action_changeMaxDuration_title">When stopped, save the last...</string>
|
||||
<string name="ui_recorder_info_startTime_short">Recording started at %s</string>
|
||||
<string name="ui_recorder_info_startTime_full">Recording started %s</string>
|
||||
<string name="ui_recorder_info_saveNowTime">Saving now will save until %s</string>
|
||||
<string name="ui_recorder_info_saveNowTime">Press to save from %s till now</string>
|
||||
<string name="ui_videoRecorder_info_starting">Video Recorder is starting...</string>
|
||||
<string name="ui_locked_title">Alibi is locked</string>
|
||||
<string name="ui_locked_unlocked">Unlock</string>
|
||||
@ -191,4 +193,33 @@
|
||||
<string name="ui_settings_option_saveFolder_explainInternalFolder_explanation">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.</string>
|
||||
<string name="ui_rotateDevice_portrait_label">Please rotate your device to portait mode</string>
|
||||
<string name="goBack">Back</string>
|
||||
<string name="ui_recorder_action_saveCurrent">Save now?</string>
|
||||
<string name="ui_recorder_action_saveCurrent_explanation">You can save the current ongoing recording by pressing and holding down on the save button. The recording will continue in the background.</string>
|
||||
<string name="ui_recorder_lowOnStorage_hint">You are low on storage. Alibi may not function properly. Please free up some space.</string>
|
||||
<string name="ui_recorder_lowOnStorage_hintANDswitchSaveFolder">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.</string>
|
||||
<string name="ui_welcome_timeSettings_title">How long should Alibi remember?</string>
|
||||
<string name="ui_welcome_timeSettings_message">Alibi will continuously record and delete old recordings to make space for new ones. You decide how long Alibi should remember the past.</string>
|
||||
<string name="ui_welcome_timeSettings_values_5min">5 Minutes</string>
|
||||
<string name="ui_welcome_timeSettings_values_15min">15 minutes</string>
|
||||
<string name="ui_welcome_timeSettings_values_30min">30 minutes</string>
|
||||
<string name="ui_welcome_timeSettings_values_1hour">1 hour</string>
|
||||
<string name="ui_welcome_timeSettings_changeableHint">You can change this anytime</string>
|
||||
<string name="ui_welcome_saveFolder_title">Where should Alibi store the batches?</string>
|
||||
<string name="ui_welcome_saveFolder_message">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.</string>
|
||||
<string name="ui_welcome_saveFolder_values_internal">Internal Storage</string>
|
||||
<string name="ui_welcome_saveFolder_values_custom">Custom Folder</string>
|
||||
<string name="ui_welcome_saveFolder_values_media">Media Folder</string>
|
||||
<string name="ui_welcome_saveFolder_externalRequired">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.</string>
|
||||
<string name="ui_welcome_saveFolder_customFolder_title">Select a Custom Folder</string>
|
||||
<string name="ui_welcome_saveFolder_customFolder_message">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.</string>
|
||||
<string name="ui_welcome_timeSettings_values_custom">Custom Duration</string>
|
||||
<string name="ui_welcome_timeSettings_values_customFormat_mm">%s minutes</string>
|
||||
<string name="ui_welcome_timeSettings_values_customFormat_hh_mm">%s hour, %s minutes</string>
|
||||
<string name="ui_welcome_timeSettings_values_customFormat_h_mm">1 hour, %s minutes</string>
|
||||
<string name="ui_welcome_ready_title">You are ready!</string>
|
||||
<string name="ui_welcome_ready_message">You are ready to start using Alibi! Go ahead and try it out!</string>
|
||||
<string name="ui_welcome_ready_start">Start Alibi</string>
|
||||
<string name="ui_about_support_title">Get Support</string>
|
||||
<string name="ui_about_support_message">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:</string>
|
||||
<string name="ui_welcome_timeSettings_values_1min">1 Minute</string>
|
||||
</resources>
|
@ -10,7 +10,7 @@
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
-->
|
||||
<exclude
|
||||
<include
|
||||
domain="file"
|
||||
path=".recordings" />
|
||||
path="datastore/." />
|
||||
</full-backup-content>
|
@ -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'
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user