mirror of
https://github.com/Myzel394/Alibi.git
synced 2025-06-18 23:05:26 +02:00
commit
df9443eb9b
11
.github/workflows/build-testing.yaml
vendored
11
.github/workflows/build-testing.yaml
vendored
@ -7,15 +7,15 @@ jobs:
|
|||||||
debug-builds:
|
debug-builds:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: gradle/wrapper-validation-action@v1
|
- uses: gradle/wrapper-validation-action@v2
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: "adopt"
|
distribution: "adopt"
|
||||||
java-version: 19
|
java-version: 21
|
||||||
cache: "gradle"
|
cache: "gradle"
|
||||||
|
|
||||||
- name: Compile
|
- name: Compile
|
||||||
@ -23,6 +23,7 @@ jobs:
|
|||||||
./gradlew assembleDebug
|
./gradlew assembleDebug
|
||||||
|
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
name: alibi-app-debug-apks
|
||||||
path: app/build/outputs/apk/debug/app-*-debug.apk
|
path: app/build/outputs/apk/debug/app-*-debug.apk
|
||||||
|
8
.github/workflows/release-app-github.yaml
vendored
8
.github/workflows/release-app-github.yaml
vendored
@ -10,7 +10,9 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: gradle/wrapper-validation-action@v2
|
||||||
|
|
||||||
- name: Write KeyStore 🗝️
|
- name: Write KeyStore 🗝️
|
||||||
uses: ./.github/actions/prepare-keystore
|
uses: ./.github/actions/prepare-keystore
|
||||||
@ -21,10 +23,10 @@ jobs:
|
|||||||
keyStoreBase64: ${{ secrets.KEYSTORE }}
|
keyStoreBase64: ${{ secrets.KEYSTORE }}
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
java-version: "17.x"
|
java-version: 21
|
||||||
cache: 'gradle'
|
cache: 'gradle'
|
||||||
|
|
||||||
- name: Build APKs 📱
|
- name: Build APKs 📱
|
||||||
|
@ -10,7 +10,9 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: gradle/wrapper-validation-action@v2
|
||||||
|
|
||||||
- name: Write KeyStore 🗝️
|
- name: Write KeyStore 🗝️
|
||||||
uses: ./.github/actions/prepare-keystore
|
uses: ./.github/actions/prepare-keystore
|
||||||
@ -21,10 +23,10 @@ jobs:
|
|||||||
keyStoreBase64: ${{ secrets.KEYSTORE }}
|
keyStoreBase64: ${{ secrets.KEYSTORE }}
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
java-version: "17.x"
|
java-version: 21
|
||||||
cache: 'gradle'
|
cache: 'gradle'
|
||||||
|
|
||||||
- name: Build APKs 📱
|
- name: Build APKs 📱
|
||||||
|
@ -36,7 +36,7 @@ android {
|
|||||||
minSdk 24
|
minSdk 24
|
||||||
targetSdk 34
|
targetSdk 34
|
||||||
versionCode 13
|
versionCode 13
|
||||||
versionName "0.4.1"
|
versionName "0.5.0"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
@ -97,12 +97,12 @@ dependencies {
|
|||||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
|
||||||
implementation 'androidx.activity:activity-compose:1.8.2'
|
implementation 'androidx.activity:activity-compose:1.8.2'
|
||||||
implementation 'androidx.activity:activity-ktx:1.8.2'
|
implementation 'androidx.activity:activity-ktx:1.8.2'
|
||||||
implementation platform('androidx.compose:compose-bom:2024.02.02')
|
implementation platform('androidx.compose:compose-bom:2024.03.00')
|
||||||
implementation 'androidx.compose.ui:ui'
|
implementation 'androidx.compose.ui:ui'
|
||||||
implementation 'androidx.compose.ui:ui-graphics'
|
implementation 'androidx.compose.ui:ui-graphics'
|
||||||
implementation 'androidx.compose.ui:ui-tooling-preview'
|
implementation 'androidx.compose.ui:ui-tooling-preview'
|
||||||
implementation 'androidx.compose.material3:material3:1.2.1'
|
implementation 'androidx.compose.material3:material3:1.2.1'
|
||||||
implementation "androidx.compose.material:material-icons-extended:1.6.3"
|
implementation "androidx.compose.material:material-icons-extended:1.6.4"
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-service:2.7.0'
|
implementation 'androidx.lifecycle:lifecycle-service:2.7.0'
|
||||||
@ -110,7 +110,7 @@ dependencies {
|
|||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||||
androidTestImplementation platform('androidx.compose:compose-bom:2024.02.02')
|
androidTestImplementation platform('androidx.compose:compose-bom:2024.03.00')
|
||||||
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
|
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
|
||||||
debugImplementation 'androidx.compose.ui:ui-tooling'
|
debugImplementation 'androidx.compose.ui:ui-tooling'
|
||||||
debugImplementation 'androidx.compose.ui:ui-test-manifest'
|
debugImplementation 'androidx.compose.ui:ui-test-manifest'
|
||||||
|
@ -31,7 +31,7 @@ data class AppSettings(
|
|||||||
|
|
||||||
/// Recording information
|
/// Recording information
|
||||||
// 30 minutes
|
// 30 minutes
|
||||||
val maxDuration: Long = 30 * 60 * 1000L,
|
val maxDuration: Long = 15 * 60 * 1000L,
|
||||||
// 60 seconds
|
// 60 seconds
|
||||||
val intervalDuration: Long = 60 * 1000L,
|
val intervalDuration: Long = 60 * 1000L,
|
||||||
|
|
||||||
@ -308,14 +308,15 @@ data class AudioRecorderSettings(
|
|||||||
companion object {
|
companion object {
|
||||||
fun getDefaultInstance(): AudioRecorderSettings = AudioRecorderSettings()
|
fun getDefaultInstance(): AudioRecorderSettings = AudioRecorderSettings()
|
||||||
val EXAMPLE_MAX_DURATIONS = listOf(
|
val EXAMPLE_MAX_DURATIONS = listOf(
|
||||||
|
1 * 60 * 1000L,
|
||||||
|
5 * 60 * 1000L,
|
||||||
15 * 60 * 1000L,
|
15 * 60 * 1000L,
|
||||||
30 * 60 * 1000L,
|
30 * 60 * 1000L,
|
||||||
60 * 60 * 1000L,
|
60 * 60 * 1000L,
|
||||||
2 * 60 * 60 * 1000L,
|
|
||||||
3 * 60 * 60 * 1000L,
|
|
||||||
)
|
)
|
||||||
val EXAMPLE_DURATION_TIMES = listOf(
|
val EXAMPLE_DURATION_TIMES = listOf(
|
||||||
60 * 1000L,
|
60 * 1000L,
|
||||||
|
60 * 2 * 1000L,
|
||||||
60 * 5 * 1000L,
|
60 * 5 * 1000L,
|
||||||
60 * 10 * 1000L,
|
60 * 10 * 1000L,
|
||||||
60 * 15 * 1000L,
|
60 * 15 * 1000L,
|
||||||
|
@ -7,12 +7,14 @@ import android.content.Context
|
|||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.os.ParcelFileDescriptor
|
||||||
import android.os.storage.StorageManager
|
import android.os.storage.StorageManager
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.provider.MediaStore.Video.Media
|
import android.provider.MediaStore.Video.Media
|
||||||
|
import android.system.Os
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.net.toFile
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import app.myzel394.alibi.db.AppSettings
|
import app.myzel394.alibi.db.AppSettings
|
||||||
@ -28,6 +30,7 @@ import java.time.LocalDateTime
|
|||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import kotlin.reflect.KFunction4
|
import kotlin.reflect.KFunction4
|
||||||
|
|
||||||
|
|
||||||
abstract class BatchesFolder(
|
abstract class BatchesFolder(
|
||||||
open val context: Context,
|
open val context: Context,
|
||||||
open val type: BatchType,
|
open val type: BatchType,
|
||||||
@ -523,11 +526,46 @@ abstract class BatchesFolder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getAvailableBytes(): Long? {
|
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 storageManager = context.getSystemService(StorageManager::class.java) ?: return null
|
||||||
val file = when (type) {
|
val file = when (type) {
|
||||||
BatchType.INTERNAL -> context.filesDir
|
BatchType.INTERNAL -> context.filesDir
|
||||||
BatchType.CUSTOM -> customFolder!!.uri.toFile()
|
BatchType.MEDIA ->
|
||||||
BatchType.MEDIA -> scopedMediaContentUri.toFile()
|
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) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
@ -545,8 +583,8 @@ abstract class BatchesFolder(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun requiredBytesForOneMinuteOfRecording(appSettings: AppSettings): Long {
|
fun requiredBytesForOneMinuteOfRecording(appSettings: AppSettings): Long {
|
||||||
// 250 MiB sounds like a good default
|
// 350 MiB sounds like a good default
|
||||||
return 250 * 1024 * 1024
|
return 350 * 1024 * 1024
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,11 @@
|
|||||||
package app.myzel394.alibi.helpers
|
package app.myzel394.alibi.helpers
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import com.arthenica.ffmpegkit.FFmpegKit
|
import com.arthenica.ffmpegkit.FFmpegKit
|
||||||
import com.arthenica.ffmpegkit.FFmpegKitConfig
|
|
||||||
import com.arthenica.ffmpegkit.ReturnCode
|
import com.arthenica.ffmpegkit.ReturnCode
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.lang.Compiler.command
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.math.log
|
|
||||||
|
|
||||||
// Abstract class for concatenating audio and video files
|
// Abstract class for concatenating audio and video files
|
||||||
// The concatenator runs in its own thread to avoid unresponsiveness.
|
// The concatenator runs in its own thread to avoid unresponsiveness.
|
||||||
@ -56,7 +50,7 @@ data class AudioConcatenator(
|
|||||||
command
|
command
|
||||||
) { session ->
|
) { session ->
|
||||||
if (!ReturnCode.isSuccess(session!!.returnCode)) {
|
if (!ReturnCode.isSuccess(session!!.returnCode)) {
|
||||||
Log.d(
|
Log.i(
|
||||||
"Audio Concatenation",
|
"Audio Concatenation",
|
||||||
String.format(
|
String.format(
|
||||||
"Command failed with state %s and rc %s.%s",
|
"Command failed with state %s and rc %s.%s",
|
||||||
@ -100,7 +94,7 @@ class MediaConverter {
|
|||||||
command,
|
command,
|
||||||
{ session ->
|
{ session ->
|
||||||
if (!ReturnCode.isSuccess(session!!.returnCode)) {
|
if (!ReturnCode.isSuccess(session!!.returnCode)) {
|
||||||
Log.d(
|
Log.i(
|
||||||
"Audio Concatenation",
|
"Audio Concatenation",
|
||||||
String.format(
|
String.format(
|
||||||
"Command failed with state %s and rc %s.%s",
|
"Command failed with state %s and rc %s.%s",
|
||||||
@ -162,7 +156,7 @@ class MediaConverter {
|
|||||||
if (ReturnCode.isSuccess(session!!.returnCode)) {
|
if (ReturnCode.isSuccess(session!!.returnCode)) {
|
||||||
completer.complete(Unit)
|
completer.complete(Unit)
|
||||||
} else {
|
} else {
|
||||||
Log.d(
|
Log.i(
|
||||||
"Video Concatenation",
|
"Video Concatenation",
|
||||||
String.format(
|
String.format(
|
||||||
"Command failed with state %s and rc %s.%s",
|
"Command failed with state %s and rc %s.%s",
|
||||||
|
@ -132,7 +132,7 @@ class VideoRecorderService :
|
|||||||
_videoFinalizerListener = CompletableDeferred()
|
_videoFinalizerListener = CompletableDeferred()
|
||||||
|
|
||||||
activeRecording = newRecording.start(ContextCompat.getMainExecutor(this)) { event ->
|
activeRecording = newRecording.start(ContextCompat.getMainExecutor(this)) { event ->
|
||||||
if (event is VideoRecordEvent.Finalize && this@VideoRecorderService.state == RecorderState.STOPPED || this@VideoRecorderService.state == RecorderState.PAUSED) {
|
if (event is VideoRecordEvent.Finalize && (this@VideoRecorderService.state == RecorderState.STOPPED || this@VideoRecorderService.state == RecorderState.PAUSED)) {
|
||||||
_videoFinalizerListener.complete(Unit)
|
_videoFinalizerListener.complete(Unit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -191,17 +191,22 @@ class VideoRecorderService :
|
|||||||
videoCapture = buildVideoCapture(recorder)
|
videoCapture = buildVideoCapture(recorder)
|
||||||
|
|
||||||
runOnMain {
|
runOnMain {
|
||||||
camera = cameraProvider!!.bindToLifecycle(
|
try {
|
||||||
this,
|
camera = cameraProvider!!.bindToLifecycle(
|
||||||
selectedCamera,
|
this,
|
||||||
videoCapture
|
selectedCamera,
|
||||||
)
|
videoCapture
|
||||||
cameraControl = CameraControl(camera!!).also {
|
)
|
||||||
it.init()
|
|
||||||
}
|
|
||||||
onCameraControlAvailable()
|
|
||||||
|
|
||||||
_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 android.os.Build
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import java.util.Base64
|
||||||
|
|
||||||
val BIG_PRIMARY_BUTTON_SIZE = 64.dp
|
val BIG_PRIMARY_BUTTON_SIZE = 64.dp
|
||||||
val BIG_PRIMARY_BUTTON_MAX_WIDTH = 450.dp
|
val BIG_PRIMARY_BUTTON_MAX_WIDTH = 450.dp
|
||||||
@ -63,3 +64,17 @@ val CRYPTO_DONATIONS = mapOf(
|
|||||||
"Litecoin" to "LZayhTosZ9ToRvcbeR1gEDgb76Z7ZA2drN",
|
"Litecoin" to "LZayhTosZ9ToRvcbeR1gEDgb76Z7ZA2drN",
|
||||||
"Filecoin" to "f1j6pm3chzhgadpf6iwmtux33jb5gccj5arkg4dsq",
|
"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.runtime.collectAsState
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
@ -29,6 +30,7 @@ import app.myzel394.alibi.ui.screens.SettingsScreen
|
|||||||
import app.myzel394.alibi.ui.screens.WelcomeScreen
|
import app.myzel394.alibi.ui.screens.WelcomeScreen
|
||||||
|
|
||||||
const val SCALE_IN = 1.25f
|
const val SCALE_IN = 1.25f
|
||||||
|
const val DEBUG_SKIP_WELCOME = false;
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Navigation(
|
fun Navigation(
|
||||||
@ -57,10 +59,18 @@ fun Navigation(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(MaterialTheme.colorScheme.background),
|
.background(MaterialTheme.colorScheme.background),
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = if (settings.hasSeenOnboarding) Screen.AudioRecorder.route else Screen.Welcome.route,
|
startDestination = if (settings.hasSeenOnboarding || DEBUG_SKIP_WELCOME) Screen.AudioRecorder.route else Screen.Welcome.route,
|
||||||
) {
|
) {
|
||||||
composable(Screen.Welcome.route) {
|
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(
|
composable(
|
||||||
Screen.AudioRecorder.route,
|
Screen.AudioRecorder.route,
|
||||||
|
@ -35,15 +35,17 @@ fun BigButton(
|
|||||||
description: String? = null,
|
description: String? = null,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: () -> Unit = {},
|
onLongClick: () -> Unit = {},
|
||||||
|
isBig: Boolean? = null,
|
||||||
) {
|
) {
|
||||||
val orientation = LocalConfiguration.current.orientation
|
val orientation = LocalConfiguration.current.orientation
|
||||||
|
|
||||||
BoxWithConstraints {
|
BoxWithConstraints {
|
||||||
val isLarge = maxWidth > 500.dp && orientation == Configuration.ORIENTATION_PORTRAIT
|
val isLarge = if (isBig == null)
|
||||||
|
maxWidth > 250.dp && maxHeight > 600.dp && orientation == Configuration.ORIENTATION_PORTRAIT else isBig
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(if (isLarge) 250.dp else 200.dp)
|
.size(if (isLarge) 250.dp else 190.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.semantics {
|
.semantics {
|
||||||
contentDescription = label
|
contentDescription = label
|
||||||
|
@ -1,56 +1,56 @@
|
|||||||
package app.myzel394.alibi.ui.components.RecorderScreen.atoms
|
package app.myzel394.alibi.ui.components.RecorderScreen.atoms
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.camera.core.CameraSelector
|
import androidx.camera.core.CameraSelector
|
||||||
import androidx.camera.core.Preview
|
import androidx.camera.core.Preview
|
||||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
|
||||||
import androidx.camera.view.PreviewView
|
import androidx.camera.view.PreviewView
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import app.myzel394.alibi.ui.utils.getCameraProvider
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CameraPreview(
|
fun CameraPreview(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier,
|
||||||
scaleType: PreviewView.ScaleType = PreviewView.ScaleType.FILL_CENTER,
|
|
||||||
cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
AndroidView(
|
|
||||||
modifier = modifier,
|
|
||||||
factory = { context ->
|
|
||||||
val previewView = PreviewView(context).apply {
|
|
||||||
this.scaleType = scaleType
|
|
||||||
layoutParams = ViewGroup.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CameraX Preview UseCase
|
Box(modifier = modifier) {
|
||||||
val previewUseCase = Preview.Builder()
|
// Video preview
|
||||||
.build()
|
AndroidView(
|
||||||
.also {
|
factory = { context ->
|
||||||
it.setSurfaceProvider(previewView.surfaceProvider)
|
val previewView = PreviewView(context).apply {
|
||||||
}
|
layoutParams = ViewGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
coroutineScope.launch {
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
val cameraProvider = ProcessCameraProvider.getInstance(context).get()
|
|
||||||
try {
|
|
||||||
// Must unbind the use-cases before rebinding them.
|
|
||||||
cameraProvider.unbindAll()
|
|
||||||
cameraProvider.bindToLifecycle(
|
|
||||||
lifecycleOwner, cameraSelector, previewUseCase
|
|
||||||
)
|
)
|
||||||
} catch (ex: Exception) {
|
|
||||||
}
|
}
|
||||||
}
|
val previewUseCase = Preview.Builder()
|
||||||
|
.build()
|
||||||
|
.also { it.setSurfaceProvider(previewView.surfaceProvider) }
|
||||||
|
|
||||||
previewView
|
coroutineScope.launch {
|
||||||
}
|
val cameraProvider = context.getCameraProvider()
|
||||||
)
|
try {
|
||||||
}
|
cameraProvider.unbindAll()
|
||||||
|
cameraProvider.bindToLifecycle(
|
||||||
|
lifecycleOwner,
|
||||||
|
cameraSelector,
|
||||||
|
previewUseCase
|
||||||
|
)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Log.e("CameraPreview", "Use case binding failed", ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previewView
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package app.myzel394.alibi.ui.components.RecorderScreen.atoms
|
package app.myzel394.alibi.ui.components.RecorderScreen.atoms
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@ -13,9 +14,11 @@ import app.myzel394.alibi.helpers.BatchesFolder
|
|||||||
import app.myzel394.alibi.helpers.VideoBatchesFolder
|
import app.myzel394.alibi.helpers.VideoBatchesFolder
|
||||||
import app.myzel394.alibi.ui.components.atoms.MessageBox
|
import app.myzel394.alibi.ui.components.atoms.MessageBox
|
||||||
import app.myzel394.alibi.ui.components.atoms.MessageType
|
import app.myzel394.alibi.ui.components.atoms.MessageType
|
||||||
|
import app.myzel394.alibi.ui.components.atoms.VisualDensity
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LowStorageInfo(
|
fun LowStorageInfo(
|
||||||
|
modifier: Modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||||
appSettings: AppSettings,
|
appSettings: AppSettings,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@ -34,14 +37,17 @@ fun LowStorageInfo(
|
|||||||
println("LowStorageInfo: availableBytes: $availableBytes, requiredBytes: $requiredBytes, isLowOnStorage: $isLowOnStorage")
|
println("LowStorageInfo: availableBytes: $availableBytes, requiredBytes: $requiredBytes, isLowOnStorage: $isLowOnStorage")
|
||||||
|
|
||||||
if (isLowOnStorage)
|
if (isLowOnStorage)
|
||||||
Box(
|
Box(modifier = modifier) {
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
BoxWithConstraints {
|
||||||
) {
|
val isLarge = maxHeight > 600.dp;
|
||||||
MessageBox(
|
|
||||||
type = MessageType.WARNING,
|
MessageBox(
|
||||||
message = if (appSettings.saveFolder == null)
|
type = MessageType.WARNING,
|
||||||
stringResource(R.string.ui_recorder_lowOnStorage_hintANDswitchSaveFolder)
|
message = if (appSettings.saveFolder == null)
|
||||||
else stringResource(R.string.ui_recorder_lowOnStorage_hint)
|
stringResource(R.string.ui_recorder_lowOnStorage_hintANDswitchSaveFolder)
|
||||||
)
|
else stringResource(R.string.ui_recorder_lowOnStorage_hint),
|
||||||
|
density = if (isLarge) VisualDensity.COMFORTABLE else VisualDensity.COMPACT
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,16 @@ import androidx.compose.animation.core.Animatable
|
|||||||
import androidx.compose.animation.core.LinearEasing
|
import androidx.compose.animation.core.LinearEasing
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.gestures.rememberTransformableState
|
||||||
|
import androidx.compose.foundation.gestures.transformable
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.CornerRadius
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
@ -75,7 +80,15 @@ fun RealtimeAudioVisualizer(
|
|||||||
audioRecorder.setMaxAmplitudesAmount(ceil(availableSpace.toInt() / BOX_DIFF).toInt() + 1)
|
audioRecorder.setMaxAmplitudesAmount(ceil(availableSpace.toInt() / BOX_DIFF).toInt() + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
Canvas(modifier = modifier) {
|
var scale by remember { mutableFloatStateOf(1f) }
|
||||||
|
val transformState = rememberTransformableState { zoomChange, _, _ ->
|
||||||
|
scale *= zoomChange
|
||||||
|
}
|
||||||
|
val amplitudePercentageModifier = MAX_AMPLITUDE * (1 / scale)
|
||||||
|
|
||||||
|
Canvas(
|
||||||
|
modifier = modifier.transformable(transformState),
|
||||||
|
) {
|
||||||
val height = this.size.height / 2f
|
val height = this.size.height / 2f
|
||||||
val width = this.size.width
|
val width = this.size.width
|
||||||
|
|
||||||
@ -88,7 +101,8 @@ fun RealtimeAudioVisualizer(
|
|||||||
val horizontalProgress = (
|
val horizontalProgress = (
|
||||||
clamp(horizontalValue, GROW_START, GROW_END)
|
clamp(horizontalValue, GROW_START, GROW_END)
|
||||||
- GROW_START) / (GROW_END - GROW_START)
|
- GROW_START) / (GROW_END - GROW_START)
|
||||||
val amplitudePercentage = (amplitude.toFloat() / MAX_AMPLITUDE).coerceAtMost(1f)
|
val amplitudePercentage =
|
||||||
|
(amplitude.toFloat() / amplitudePercentageModifier).coerceAtMost(1f)
|
||||||
val boxHeight =
|
val boxHeight =
|
||||||
(height * amplitudePercentage * horizontalProgress).coerceAtLeast(15f)
|
(height * amplitudePercentage * horizontalProgress).coerceAtLeast(15f)
|
||||||
|
|
||||||
|
@ -3,8 +3,6 @@ package app.myzel394.alibi.ui.components.RecorderScreen.atoms
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Warning
|
import androidx.compose.material.icons.filled.Warning
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.ButtonDefaults
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
@ -15,7 +13,6 @@ import app.myzel394.alibi.R
|
|||||||
@Composable
|
@Composable
|
||||||
fun RecorderErrorDialog(
|
fun RecorderErrorDialog(
|
||||||
onClose: () -> Unit,
|
onClose: () -> Unit,
|
||||||
onSave: () -> Unit,
|
|
||||||
) {
|
) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onClose,
|
onDismissRequest = onClose,
|
||||||
@ -31,14 +28,9 @@ fun RecorderErrorDialog(
|
|||||||
text = {
|
text = {
|
||||||
Text(stringResource(R.string.ui_recorder_error_recording_description))
|
Text(stringResource(R.string.ui_recorder_error_recording_description))
|
||||||
},
|
},
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = onClose) {
|
|
||||||
Text(stringResource(R.string.dialog_close_cancel_label))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = onSave) {
|
TextButton(onClick = onClose) {
|
||||||
Text(stringResource(R.string.ui_recorder_action_save_label))
|
Text(stringResource(R.string.dialog_close_neutral_label))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
package app.myzel394.alibi.ui.components.RecorderScreen.atoms
|
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.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Memory
|
import androidx.compose.material.icons.filled.Memory
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import app.myzel394.alibi.R
|
import app.myzel394.alibi.R
|
||||||
@ -38,15 +37,18 @@ fun RecorderProcessingDialog(
|
|||||||
text = {
|
text = {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(32.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.ui_recorder_action_save_processing_dialog_description),
|
stringResource(R.string.ui_recorder_action_save_processing_dialog_description),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
CircularProgressIndicator()
|
||||||
if (progress == null)
|
if (progress == null)
|
||||||
LinearProgressIndicator()
|
LinearProgressIndicator()
|
||||||
else
|
else
|
||||||
LinearProgressIndicator(progress = progress)
|
LinearProgressIndicator(
|
||||||
|
progress = { progress },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {}
|
confirmButton = {}
|
||||||
|
@ -22,6 +22,7 @@ import app.myzel394.alibi.ui.models.AudioRecorderModel
|
|||||||
fun AudioRecordingStart(
|
fun AudioRecordingStart(
|
||||||
audioRecorder: AudioRecorderModel,
|
audioRecorder: AudioRecorderModel,
|
||||||
appSettings: AppSettings,
|
appSettings: AppSettings,
|
||||||
|
useLargeButtons: Boolean? = null,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
@ -59,6 +60,7 @@ fun AudioRecordingStart(
|
|||||||
label = stringResource(R.string.ui_audioRecorder_action_start_label),
|
label = stringResource(R.string.ui_audioRecorder_action_start_label),
|
||||||
icon = Icons.Default.Mic,
|
icon = Icons.Default.Mic,
|
||||||
onClick = triggerRecordAudio,
|
onClick = triggerRecordAudio,
|
||||||
|
isBig = useLargeButtons,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ fun VideoRecordingStart(
|
|||||||
onHideAudioRecording: () -> Unit,
|
onHideAudioRecording: () -> Unit,
|
||||||
onShowAudioRecording: () -> Unit,
|
onShowAudioRecording: () -> Unit,
|
||||||
showPreview: Boolean,
|
showPreview: Boolean,
|
||||||
|
useLargeButtons: Boolean? = null,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
@ -87,6 +88,7 @@ fun VideoRecordingStart(
|
|||||||
showSheet = true
|
showSheet = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isBig = useLargeButtons,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -38,7 +38,6 @@ import java.time.LocalDateTime
|
|||||||
fun AudioRecordingStatus(
|
fun AudioRecordingStatus(
|
||||||
audioRecorder: AudioRecorderModel,
|
audioRecorder: AudioRecorderModel,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
val configuration = LocalConfiguration.current.orientation
|
val configuration = LocalConfiguration.current.orientation
|
||||||
|
|
||||||
var now by remember { mutableStateOf(LocalDateTime.now()) }
|
var now by remember { mutableStateOf(LocalDateTime.now()) }
|
||||||
|
@ -30,9 +30,11 @@ import app.myzel394.alibi.ui.models.AudioRecorderModel
|
|||||||
import app.myzel394.alibi.ui.models.BaseRecorderModel
|
import app.myzel394.alibi.ui.models.BaseRecorderModel
|
||||||
import app.myzel394.alibi.ui.models.VideoRecorderModel
|
import app.myzel394.alibi.ui.models.VideoRecorderModel
|
||||||
import app.myzel394.alibi.ui.utils.rememberFileSaverDialog
|
import app.myzel394.alibi.ui.utils.rememberFileSaverDialog
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import java.util.Timer
|
||||||
|
import kotlin.concurrent.schedule
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
typealias RecorderModel = BaseRecorderModel<
|
typealias RecorderModel = BaseRecorderModel<
|
||||||
@ -93,9 +95,16 @@ fun RecorderEventsHandler(
|
|||||||
recorder: RecorderModel
|
recorder: RecorderModel
|
||||||
) {
|
) {
|
||||||
if (!settings.deleteRecordingsImmediately) {
|
if (!settings.deleteRecordingsImmediately) {
|
||||||
|
val information = recorder.recorderService?.getRecordingInformation()
|
||||||
|
|
||||||
|
if (information == null) {
|
||||||
|
Log.e("RecorderEventsHandler", "Recording information is null")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
dataStore.updateData {
|
dataStore.updateData {
|
||||||
it.setLastRecording(
|
it.setLastRecording(
|
||||||
recorder.recorderService!!.getRecordingInformation()
|
information
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -129,13 +138,18 @@ fun RecorderEventsHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun saveRecording(recorder: RecorderModel, cleanupOldFiles: Boolean = false): Thread {
|
fun saveRecording(
|
||||||
isProcessing = true
|
recorder: RecorderModel,
|
||||||
|
cleanupOldFiles: Boolean = false
|
||||||
|
): CompletableDeferred<Unit> {
|
||||||
|
val completer = CompletableDeferred<Unit>()
|
||||||
|
|
||||||
// Give the user some time to see the processing dialog
|
// If processing takes this short, don't show the processing dialog
|
||||||
delay(100)
|
val timer = Timer().schedule(250L) {
|
||||||
|
isProcessing = true
|
||||||
|
}
|
||||||
|
|
||||||
return thread {
|
thread {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
try {
|
try {
|
||||||
if (recorder.isCurrentlyActivelyRecording) {
|
if (recorder.isCurrentlyActivelyRecording) {
|
||||||
@ -222,95 +236,118 @@ fun RecorderEventsHandler(
|
|||||||
if (recorder.isCurrentlyActivelyRecording) {
|
if (recorder.isCurrentlyActivelyRecording) {
|
||||||
recorder.recorderService?.unlockFiles(cleanupOldFiles)
|
recorder.recorderService?.unlockFiles(cleanupOldFiles)
|
||||||
}
|
}
|
||||||
|
timer.cancel()
|
||||||
isProcessing = false
|
isProcessing = false
|
||||||
|
processingProgress = null
|
||||||
|
completer.complete(Unit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return completer
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register audio recorder events
|
// Register audio recorder events
|
||||||
DisposableEffect(key1 = audioRecorder, key2 = settings) {
|
// Absolutely no idea, but somehow on some devices the `DisposableEffect`
|
||||||
audioRecorder.onRecordingSave = { cleanupOldFiles ->
|
// is registered twice, and THEN disposed once (AFTER being called twice),
|
||||||
// We create our own coroutine because we show our own dialog and we want to
|
// which then causes the `onRecordingSave` to be in a weird state.
|
||||||
// keep saving until it's finished.
|
// This variable is a workaround to prevent this from happening.
|
||||||
// So it's smarter to take things into our own hands and use our local coroutine,
|
var previousAudioSettings: AppSettings? = null
|
||||||
// instead of hoping that the coroutine from where this will be called will be alive
|
DisposableEffect(settings) {
|
||||||
// until the end of the saving process
|
if (previousAudioSettings == settings) {
|
||||||
scope.launch {
|
onDispose { }
|
||||||
saveRecording(audioRecorder as RecorderModel, cleanupOldFiles).join()
|
} else {
|
||||||
|
previousAudioSettings = settings
|
||||||
|
audioRecorder.onRecordingSave = { cleanupOldFiles ->
|
||||||
|
saveRecording(audioRecorder as RecorderModel, cleanupOldFiles)
|
||||||
}
|
}
|
||||||
}
|
audioRecorder.onRecordingStart = {
|
||||||
audioRecorder.onRecordingStart = {
|
snackbarHostState.currentSnackbarData?.dismiss()
|
||||||
snackbarHostState.currentSnackbarData?.dismiss()
|
|
||||||
}
|
|
||||||
audioRecorder.onError = {
|
|
||||||
scope.launch {
|
|
||||||
saveAsLastRecording(audioRecorder as RecorderModel)
|
|
||||||
|
|
||||||
showRecorderError = true
|
|
||||||
}
|
}
|
||||||
}
|
audioRecorder.onError = {
|
||||||
audioRecorder.onBatchesFolderNotAccessible = {
|
scope.launch {
|
||||||
scope.launch {
|
saveAsLastRecording(audioRecorder as RecorderModel)
|
||||||
showBatchesInaccessibleError = true
|
|
||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
audioRecorder.stopRecording(context)
|
audioRecorder.stopRecording(context)
|
||||||
}
|
}
|
||||||
runCatching {
|
runCatching {
|
||||||
audioRecorder.destroyService(context)
|
audioRecorder.destroyService(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
showRecorderError = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
audioRecorder.onBatchesFolderNotAccessible = {
|
||||||
|
scope.launch {
|
||||||
|
showBatchesInaccessibleError = true
|
||||||
|
|
||||||
onDispose {
|
runCatching {
|
||||||
audioRecorder.onRecordingSave = {
|
audioRecorder.stopRecording(context)
|
||||||
throw NotImplementedError("onRecordingSave should not be called now")
|
}
|
||||||
|
runCatching {
|
||||||
|
audioRecorder.destroyService(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDispose {
|
||||||
|
audioRecorder.onRecordingSave = {
|
||||||
|
throw NotImplementedError("onRecordingSave should not be called now")
|
||||||
|
}
|
||||||
|
audioRecorder.onError = {}
|
||||||
}
|
}
|
||||||
audioRecorder.onError = {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register video recorder events
|
// Register video recorder events
|
||||||
DisposableEffect(key1 = videoRecorder, key2 = settings) {
|
var previousVideoSettings: AppSettings? = null
|
||||||
videoRecorder.onRecordingSave = { cleanupOldFiles ->
|
DisposableEffect(settings) {
|
||||||
// We create our own coroutine because we show our own dialog and we want to
|
if (previousVideoSettings == settings) {
|
||||||
// keep saving until it's finished.
|
onDispose { }
|
||||||
// So it's smarter to take things into our own hands and use our local coroutine,
|
} else {
|
||||||
// instead of hoping that the coroutine from where this will be called will be alive
|
previousVideoSettings = settings
|
||||||
// until the end of the saving process
|
Log.i("Alibi", "===== Registering videoRecorder events $videoRecorder")
|
||||||
scope.launch {
|
videoRecorder.onRecordingSave = { cleanupOldFiles ->
|
||||||
saveRecording(videoRecorder as RecorderModel, cleanupOldFiles).join()
|
saveRecording(videoRecorder as RecorderModel, cleanupOldFiles)
|
||||||
}
|
}
|
||||||
}
|
videoRecorder.onRecordingStart = {
|
||||||
videoRecorder.onRecordingStart = {
|
snackbarHostState.currentSnackbarData?.dismiss()
|
||||||
snackbarHostState.currentSnackbarData?.dismiss()
|
|
||||||
}
|
|
||||||
videoRecorder.onError = {
|
|
||||||
scope.launch {
|
|
||||||
saveAsLastRecording(videoRecorder as RecorderModel)
|
|
||||||
|
|
||||||
showRecorderError = true
|
|
||||||
}
|
}
|
||||||
}
|
videoRecorder.onError = {
|
||||||
videoRecorder.onBatchesFolderNotAccessible = {
|
scope.launch {
|
||||||
scope.launch {
|
saveAsLastRecording(videoRecorder as RecorderModel)
|
||||||
showBatchesInaccessibleError = true
|
|
||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
videoRecorder.stopRecording(context)
|
videoRecorder.stopRecording(context)
|
||||||
}
|
}
|
||||||
runCatching {
|
runCatching {
|
||||||
videoRecorder.destroyService(context)
|
videoRecorder.destroyService(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
showRecorderError = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
videoRecorder.onBatchesFolderNotAccessible = {
|
||||||
|
scope.launch {
|
||||||
|
showBatchesInaccessibleError = true
|
||||||
|
|
||||||
onDispose {
|
runCatching {
|
||||||
videoRecorder.onRecordingSave = {
|
videoRecorder.stopRecording(context)
|
||||||
throw NotImplementedError("onRecordingSave should not be called now")
|
}
|
||||||
|
runCatching {
|
||||||
|
videoRecorder.destroyService(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDispose {
|
||||||
|
Log.i("Alibi", "===== Disposing videoRecorder events")
|
||||||
|
videoRecorder.onRecordingSave = {
|
||||||
|
throw NotImplementedError("onRecordingSave should not be called now")
|
||||||
|
}
|
||||||
|
videoRecorder.onError = {}
|
||||||
}
|
}
|
||||||
videoRecorder.onError = {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,8 +361,6 @@ fun RecorderEventsHandler(
|
|||||||
onClose = {
|
onClose = {
|
||||||
showRecorderError = false
|
showRecorderError = false
|
||||||
},
|
},
|
||||||
onSave = {
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (showBatchesInaccessibleError)
|
if (showBatchesInaccessibleError)
|
||||||
|
@ -3,6 +3,7 @@ package app.myzel394.alibi.ui.components.RecorderScreen.organisms
|
|||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@ -100,24 +101,47 @@ fun StartRecording(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
BoxWithConstraints {
|
||||||
modifier = Modifier
|
val isLargeDisplay =
|
||||||
.fillMaxSize()
|
maxWidth > 250.dp && maxHeight > 600.dp && orientation == Configuration.ORIENTATION_PORTRAIT
|
||||||
.padding(bottom = if (orientation == Configuration.ORIENTATION_PORTRAIT) 32.dp else 16.dp),
|
|
||||||
verticalArrangement = Arrangement.SpaceBetween,
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
modifier = Modifier
|
||||||
) {
|
.fillMaxSize()
|
||||||
when (orientation) {
|
.padding(bottom = if (orientation == Configuration.ORIENTATION_PORTRAIT) 0.dp else 16.dp),
|
||||||
Configuration.ORIENTATION_LANDSCAPE -> {
|
verticalArrangement = Arrangement.SpaceBetween,
|
||||||
Row(
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
) {
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
when (orientation) {
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
Configuration.ORIENTATION_LANDSCAPE -> {
|
||||||
) {
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
if (showAudioRecorder)
|
||||||
|
AudioRecordingStart(
|
||||||
|
audioRecorder = audioRecorder,
|
||||||
|
appSettings = appSettings,
|
||||||
|
)
|
||||||
|
VideoRecordingStart(
|
||||||
|
videoRecorder = videoRecorder,
|
||||||
|
appSettings = appSettings,
|
||||||
|
onHideAudioRecording = onHideTopBar,
|
||||||
|
onShowAudioRecording = onShowTopBar,
|
||||||
|
showPreview = !showAudioRecorder,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
if (showAudioRecorder)
|
if (showAudioRecorder)
|
||||||
AudioRecordingStart(
|
AudioRecordingStart(
|
||||||
audioRecorder = audioRecorder,
|
audioRecorder = audioRecorder,
|
||||||
appSettings = appSettings,
|
appSettings = appSettings,
|
||||||
|
useLargeButtons = isLargeDisplay,
|
||||||
)
|
)
|
||||||
VideoRecordingStart(
|
VideoRecordingStart(
|
||||||
videoRecorder = videoRecorder,
|
videoRecorder = videoRecorder,
|
||||||
@ -125,95 +149,86 @@ fun StartRecording(
|
|||||||
onHideAudioRecording = onHideTopBar,
|
onHideAudioRecording = onHideTopBar,
|
||||||
onShowAudioRecording = onShowTopBar,
|
onShowAudioRecording = onShowTopBar,
|
||||||
showPreview = !showAudioRecorder,
|
showPreview = !showAudioRecorder,
|
||||||
|
useLargeButtons = isLargeDisplay,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
|
|
||||||
if (showAudioRecorder)
|
val forceUpdate = rememberForceUpdateOnLifeCycleChange()
|
||||||
AudioRecordingStart(
|
Column(
|
||||||
audioRecorder = audioRecorder,
|
modifier = Modifier
|
||||||
appSettings = appSettings,
|
.weight(1f)
|
||||||
|
.then(forceUpdate),
|
||||||
|
verticalArrangement = Arrangement.Bottom,
|
||||||
|
) {
|
||||||
|
if (appSettings.lastRecording?.hasRecordingsAvailable(context) == true) {
|
||||||
|
val label = stringResource(
|
||||||
|
R.string.ui_recorder_action_saveOldRecording_label,
|
||||||
|
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)
|
||||||
|
.format(appSettings.lastRecording.recordingStart),
|
||||||
)
|
)
|
||||||
VideoRecordingStart(
|
TextButton(
|
||||||
videoRecorder = videoRecorder,
|
|
||||||
appSettings = appSettings,
|
|
||||||
onHideAudioRecording = onHideTopBar,
|
|
||||||
onShowAudioRecording = onShowTopBar,
|
|
||||||
showPreview = !showAudioRecorder,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val forceUpdate = rememberForceUpdateOnLifeCycleChange()
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.then(forceUpdate),
|
|
||||||
verticalArrangement = Arrangement.Bottom,
|
|
||||||
) {
|
|
||||||
if (appSettings.lastRecording?.hasRecordingsAvailable(context) == true) {
|
|
||||||
val label = stringResource(
|
|
||||||
R.string.ui_recorder_action_saveOldRecording_label,
|
|
||||||
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)
|
|
||||||
.format(appSettings.lastRecording.recordingStart),
|
|
||||||
)
|
|
||||||
TextButton(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.requiredWidthIn(max = BIG_PRIMARY_BUTTON_MAX_WIDTH)
|
|
||||||
.height(BIG_PRIMARY_BUTTON_SIZE)
|
|
||||||
.semantics {
|
|
||||||
contentDescription = label
|
|
||||||
},
|
|
||||||
onClick = onSaveLastRecording,
|
|
||||||
contentPadding = ButtonDefaults.TextButtonWithIconContentPadding,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Save,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(ButtonDefaults.IconSize),
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
|
|
||||||
Text(label)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.launcher_monochrome_noopacity),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant),
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(ButtonDefaults.IconSize)
|
.fillMaxWidth()
|
||||||
)
|
.requiredWidthIn(max = BIG_PRIMARY_BUTTON_MAX_WIDTH)
|
||||||
|
.height(BIG_PRIMARY_BUTTON_SIZE)
|
||||||
|
.semantics {
|
||||||
|
contentDescription = label
|
||||||
|
},
|
||||||
|
onClick = onSaveLastRecording,
|
||||||
|
contentPadding = ButtonDefaults.TextButtonWithIconContentPadding,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Save,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(ButtonDefaults.IconSize),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
|
||||||
|
Text(label)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.launcher_monochrome_noopacity),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(ButtonDefaults.IconSize)
|
||||||
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
|
Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
|
||||||
|
|
||||||
ClickableText(
|
ClickableText(
|
||||||
text = annotatedDescription,
|
text = annotatedDescription,
|
||||||
onClick = { textIndex ->
|
onClick = { textIndex ->
|
||||||
if (annotatedDescription.getStringAnnotations(textIndex, textIndex)
|
if (annotatedDescription.getStringAnnotations(textIndex, textIndex)
|
||||||
.firstOrNull()?.tag == "minutes"
|
.firstOrNull()?.tag == "minutes"
|
||||||
) {
|
) {
|
||||||
showQuickMaxDurationSelector = true
|
showQuickMaxDurationSelector = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.widthIn(max = 300.dp)
|
.widthIn(max = 300.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
style = MaterialTheme.typography.bodySmall.copy(
|
style = MaterialTheme.typography.bodySmall.copy(
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
LowStorageInfo(appSettings = appSettings)
|
LowStorageInfo(
|
||||||
|
modifier = if (isLargeDisplay) Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.widthIn(max = 400.dp) else Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(4.dp),
|
||||||
|
appSettings = appSettings
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package app.myzel394.alibi.ui.components.RecorderScreen.organisms
|
package app.myzel394.alibi.ui.components.RecorderScreen.organisms
|
||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@ -241,18 +242,27 @@ fun _PrimitiveControls(videoRecorder: VideoRecorderModel) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSaveAndStop = {
|
onSaveAndStop = {
|
||||||
|
println("User initiated video recording save and stop")
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
Log.i("Alibi", "====== Asking to stop recording...")
|
||||||
videoRecorder.stopRecording(context)
|
videoRecorder.stopRecording(context)
|
||||||
|
Log.i("Alibi", "====== Asking to stop recording... done")
|
||||||
|
|
||||||
|
Log.i("Alibi", "====== Updating data store...")
|
||||||
dataStore.updateData {
|
dataStore.updateData {
|
||||||
it.saveLastRecording(videoRecorder as RecorderModel)
|
it.saveLastRecording(videoRecorder as RecorderModel)
|
||||||
}
|
}
|
||||||
|
Log.i("Alibi", "====== Updating data store... done")
|
||||||
|
|
||||||
|
Log.i("Alibi", "===== Asking to save recording...")
|
||||||
videoRecorder.onRecordingSave(false).join()
|
videoRecorder.onRecordingSave(false).join()
|
||||||
|
Log.i("Alibi", "===== Asking to save recording... done")
|
||||||
|
|
||||||
|
Log.i("Alibi", "===== Destroying service...")
|
||||||
runCatching {
|
runCatching {
|
||||||
videoRecorder.destroyService(context)
|
videoRecorder.destroyService(context)
|
||||||
}
|
}
|
||||||
|
Log.i("Alibi", "===== Destroying service... done")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSaveCurrent = {
|
onSaveCurrent = {
|
||||||
|
@ -68,7 +68,7 @@ fun MaxDurationTile(
|
|||||||
timeFormat = DurationFormat.HH_MM,
|
timeFormat = DurationFormat.HH_MM,
|
||||||
currentTime = settings.maxDuration / 1000,
|
currentTime = settings.maxDuration / 1000,
|
||||||
minTime = 60,
|
minTime = 60,
|
||||||
maxTime = 10 * 24 * 60 * 60,
|
maxTime = 23 * 60 * 60 + 59 * 60,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
SettingsTile(
|
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.Lock
|
||||||
import androidx.compose.material.icons.filled.Mic
|
import androidx.compose.material.icons.filled.Mic
|
||||||
import androidx.compose.material.icons.filled.PermMedia
|
import androidx.compose.material.icons.filled.PermMedia
|
||||||
import androidx.compose.material.icons.filled.Warning
|
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
@ -435,8 +434,6 @@ fun SelectionSheet(
|
|||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
var showCustomFolderWarning by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val selectFolder = rememberFolderSelectorDialog { folder ->
|
val selectFolder = rememberFolderSelectorDialog { folder ->
|
||||||
if (folder == null) {
|
if (folder == null) {
|
||||||
return@rememberFolderSelectorDialog
|
return@rememberFolderSelectorDialog
|
||||||
@ -445,18 +442,6 @@ fun SelectionSheet(
|
|||||||
updateValue(folder.toString())
|
updateValue(folder.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showCustomFolderWarning) {
|
|
||||||
CustomFolderWarningDialog(
|
|
||||||
onDismiss = {
|
|
||||||
showCustomFolderWarning = false
|
|
||||||
},
|
|
||||||
onConfirm = {
|
|
||||||
showCustomFolderWarning = false
|
|
||||||
selectFolder()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var showExternalPermissionRequired by remember { mutableStateOf(false) }
|
var showExternalPermissionRequired by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
if (showExternalPermissionRequired) {
|
if (showExternalPermissionRequired) {
|
||||||
@ -523,9 +508,7 @@ fun SelectionSheet(
|
|||||||
SelectionButton(
|
SelectionButton(
|
||||||
label = stringResource(R.string.ui_settings_option_saveFolder_action_custom_label),
|
label = stringResource(R.string.ui_settings_option_saveFolder_action_custom_label),
|
||||||
icon = Icons.Default.Folder,
|
icon = Icons.Default.Folder,
|
||||||
onClick = {
|
onClick = selectFolder,
|
||||||
showCustomFolderWarning = true
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
if (!SUPPORTS_SAVING_VIDEOS_IN_CUSTOM_FOLDERS) {
|
if (!SUPPORTS_SAVING_VIDEOS_IN_CUSTOM_FOLDERS) {
|
||||||
Column(
|
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
|
@Composable
|
||||||
fun ExternalPermissionRequiredDialog(
|
fun ExternalPermissionRequiredDialog(
|
||||||
onDismiss: () -> Unit,
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
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.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material.icons.Icons
|
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.material.icons.filled.Warning
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
@ -59,7 +59,7 @@ fun ResponsibilityPage(
|
|||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
Button(
|
Button(
|
||||||
onClick = { onContinue() },
|
onClick = onContinue,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@ -67,12 +67,12 @@ fun ResponsibilityPage(
|
|||||||
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
|
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Check,
|
Icons.Default.ChevronRight,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(ButtonDefaults.IconSize)
|
modifier = Modifier.size(ButtonDefaults.IconSize)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
|
Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
|
||||||
Text(stringResource(R.string.ui_welcome_start_label))
|
Text(stringResource(R.string.continue_label))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,256 @@
|
|||||||
|
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.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
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SaveFolderPage(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onContinue: (saveFolder: String?) -> Unit,
|
||||||
|
appSettings: AppSettings,
|
||||||
|
) {
|
||||||
|
var saveFolder by rememberSaveable { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
var isLowOnStorage by rememberSaveable {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
// Fetching this synchronously results in the UI being blocked.
|
||||||
|
// Instead, we fetch this in a different thread and update the state when we have the result.
|
||||||
|
LaunchedEffect(appSettings, context) {
|
||||||
|
thread {
|
||||||
|
val availableBytes = VideoBatchesFolder.viaInternalFolder(context).getAvailableBytes()
|
||||||
|
|
||||||
|
if (availableBytes == null) {
|
||||||
|
isLowOnStorage = false
|
||||||
|
return@thread
|
||||||
|
}
|
||||||
|
|
||||||
|
val bytesPerMinute = BatchesFolder.requiredBytesForOneMinuteOfRecording(appSettings)
|
||||||
|
val requiredBytes = appSettings.maxDuration / 1000 / 60 * bytesPerMinute
|
||||||
|
|
||||||
|
// Allow for a 10% margin of error
|
||||||
|
isLowOnStorage = 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)
|
.clip(MaterialTheme.shapes.medium)
|
||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
.let {
|
.let {
|
||||||
if (density == VisualDensity.COMFORTABLE) {
|
when (density) {
|
||||||
it.padding(horizontal = 8.dp, vertical = 16.dp)
|
VisualDensity.COMFORTABLE -> it.padding(horizontal = 8.dp, vertical = 16.dp)
|
||||||
} else {
|
VisualDensity.DENSE -> it.padding(8.dp)
|
||||||
it.padding(8.dp)
|
VisualDensity.COMPACT -> it.padding(8.dp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.then(modifier)
|
.then(modifier)
|
||||||
) {
|
) {
|
||||||
if (density == VisualDensity.COMFORTABLE) {
|
if (density == VisualDensity.COMFORTABLE || density == VisualDensity.DENSE) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = when (type) {
|
imageVector = when (type) {
|
||||||
MessageType.ERROR -> Icons.Default.Error
|
MessageType.ERROR -> Icons.Default.Error
|
||||||
@ -121,4 +121,5 @@ enum class MessageType {
|
|||||||
enum class VisualDensity {
|
enum class VisualDensity {
|
||||||
COMPACT,
|
COMPACT,
|
||||||
COMFORTABLE,
|
COMFORTABLE,
|
||||||
|
DENSE,
|
||||||
}
|
}
|
@ -17,7 +17,7 @@ import app.myzel394.alibi.helpers.BatchesFolder
|
|||||||
import app.myzel394.alibi.services.IntervalRecorderService
|
import app.myzel394.alibi.services.IntervalRecorderService
|
||||||
import app.myzel394.alibi.services.RecorderNotificationHelper
|
import app.myzel394.alibi.services.RecorderNotificationHelper
|
||||||
import app.myzel394.alibi.services.RecorderService
|
import app.myzel394.alibi.services.RecorderService
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
abstract class BaseRecorderModel<I, B : BatchesFolder, T : IntervalRecorderService<I, B>> :
|
abstract class BaseRecorderModel<I, B : BatchesFolder, T : IntervalRecorderService<I, B>> :
|
||||||
@ -49,7 +49,7 @@ abstract class BaseRecorderModel<I, B : BatchesFolder, T : IntervalRecorderServi
|
|||||||
|
|
||||||
// If `isSavingAsOldRecording` is true, the user is saving an old recording,
|
// 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
|
// thus the service is not running and thus doesn't need to be stopped or destroyed
|
||||||
var onRecordingSave: (cleanupOldFiles: Boolean) -> Job = {
|
var onRecordingSave: (cleanupOldFiles: Boolean) -> CompletableDeferred<Unit> = {
|
||||||
throw NotImplementedError("onRecordingSave not implemented")
|
throw NotImplementedError("onRecordingSave not implemented")
|
||||||
}
|
}
|
||||||
var onRecordingStart: () -> Unit = {}
|
var onRecordingStart: () -> Unit = {}
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
package app.myzel394.alibi.ui.screens
|
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.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
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.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.automirrored.filled.OpenInNew
|
import androidx.compose.material.icons.automirrored.filled.OpenInNew
|
||||||
|
import androidx.compose.material.icons.filled.ContentCopy
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@ -34,6 +39,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@ -43,6 +49,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import app.myzel394.alibi.BuildConfig
|
import app.myzel394.alibi.BuildConfig
|
||||||
import app.myzel394.alibi.R
|
import app.myzel394.alibi.R
|
||||||
|
import app.myzel394.alibi.ui.CONTACT_METHODS
|
||||||
import app.myzel394.alibi.ui.REPO_URL
|
import app.myzel394.alibi.ui.REPO_URL
|
||||||
import app.myzel394.alibi.ui.TRANSLATION_HELP_URL
|
import app.myzel394.alibi.ui.TRANSLATION_HELP_URL
|
||||||
import app.myzel394.alibi.ui.components.AboutScreen.atoms.DonationsTile
|
import app.myzel394.alibi.ui.components.AboutScreen.atoms.DonationsTile
|
||||||
@ -82,8 +89,8 @@ fun AboutScreen(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.padding(horizontal = 32.dp)
|
.verticalScroll(rememberScrollState())
|
||||||
.verticalScroll(rememberScrollState()),
|
.padding(horizontal = 32.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(48.dp),
|
verticalArrangement = Arrangement.spacedBy(48.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
@ -125,7 +132,7 @@ fun AboutScreen(
|
|||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.ui_about_contribute_message),
|
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)
|
val githubLabel = stringResource(R.string.accessibility_open_in_browser, REPO_URL)
|
||||||
@ -203,6 +210,54 @@ fun AboutScreen(
|
|||||||
|
|
||||||
DonationsTile()
|
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()
|
GPGKeyOverview()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,17 +16,14 @@ import androidx.compose.material3.SnackbarHostState
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import app.myzel394.alibi.R
|
import app.myzel394.alibi.R
|
||||||
import app.myzel394.alibi.dataStore
|
|
||||||
import app.myzel394.alibi.db.AppSettings
|
import app.myzel394.alibi.db.AppSettings
|
||||||
import app.myzel394.alibi.db.RecordingInformation
|
import app.myzel394.alibi.db.RecordingInformation
|
||||||
import app.myzel394.alibi.ui.components.RecorderScreen.organisms.AudioRecordingStatus
|
import app.myzel394.alibi.ui.components.RecorderScreen.organisms.AudioRecordingStatus
|
||||||
@ -46,7 +43,6 @@ fun RecorderScreen(
|
|||||||
settings: AppSettings,
|
settings: AppSettings,
|
||||||
) {
|
) {
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val context = LocalContext.current
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
RecorderEventsHandler(
|
RecorderEventsHandler(
|
||||||
@ -104,9 +100,6 @@ fun RecorderScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding),
|
.padding(padding),
|
||||||
) {
|
) {
|
||||||
val appSettings =
|
|
||||||
context.dataStore.data.collectAsState(AppSettings.getDefaultInstance()).value
|
|
||||||
|
|
||||||
if (audioRecorder.isInRecording)
|
if (audioRecorder.isInRecording)
|
||||||
AudioRecordingStatus(audioRecorder = audioRecorder)
|
AudioRecordingStatus(audioRecorder = audioRecorder)
|
||||||
else if (videoRecorder.isInRecording)
|
else if (videoRecorder.isInRecording)
|
||||||
@ -115,7 +108,7 @@ fun RecorderScreen(
|
|||||||
StartRecording(
|
StartRecording(
|
||||||
audioRecorder = audioRecorder,
|
audioRecorder = audioRecorder,
|
||||||
videoRecorder = videoRecorder,
|
videoRecorder = videoRecorder,
|
||||||
appSettings = appSettings,
|
appSettings = settings,
|
||||||
onSaveLastRecording = {
|
onSaveLastRecording = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
when (settings.lastRecording!!.type) {
|
when (settings.lastRecording!!.type) {
|
||||||
|
@ -8,16 +8,17 @@ import androidx.compose.foundation.pager.HorizontalPager
|
|||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.navigation.NavController
|
|
||||||
import app.myzel394.alibi.dataStore
|
import app.myzel394.alibi.dataStore
|
||||||
import app.myzel394.alibi.ui.components.WelcomeScreen.atoms.ExplanationPage
|
import app.myzel394.alibi.ui.components.WelcomeScreen.pages.ExplanationPage
|
||||||
import app.myzel394.alibi.ui.components.WelcomeScreen.atoms.ResponsibilityPage
|
import app.myzel394.alibi.ui.components.WelcomeScreen.pages.MaxDurationSettingsPage
|
||||||
import app.myzel394.alibi.ui.enums.Screen
|
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
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@ -27,41 +28,76 @@ fun WelcomeScreen(
|
|||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val dataStore = context.dataStore
|
val dataStore = context.dataStore
|
||||||
val settings = dataStore
|
val settings = rememberSettings()
|
||||||
.data
|
|
||||||
.collectAsState(initial = null)
|
|
||||||
.value ?: return
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val pagerState = rememberPagerState(
|
val pagerState = rememberPagerState(
|
||||||
initialPage = 0,
|
initialPage = 0,
|
||||||
initialPageOffsetFraction = 0f,
|
initialPageOffsetFraction = 0f,
|
||||||
pageCount = {2}
|
pageCount = { 5 }
|
||||||
)
|
)
|
||||||
|
|
||||||
Scaffold() {padding ->
|
fun finishTutorial() {
|
||||||
|
scope.launch {
|
||||||
|
dataStore.updateData {
|
||||||
|
settings.setHasSeenOnboarding(true)
|
||||||
|
}
|
||||||
|
onNavigateToAudioRecorderScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold() { padding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding),
|
.padding(padding),
|
||||||
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
HorizontalPager(state = pagerState) {position ->
|
HorizontalPager(
|
||||||
|
state = pagerState,
|
||||||
|
) { position ->
|
||||||
when (position) {
|
when (position) {
|
||||||
0 -> ExplanationPage(
|
0 -> ExplanationPage(
|
||||||
onContinue = {
|
onContinue = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
pagerState.animateScrollToPage(2)
|
pagerState.animateScrollToPage(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
1 -> ResponsibilityPage {
|
1 -> ResponsibilityPage {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
dataStore.updateData {
|
pagerState.animateScrollToPage(2)
|
||||||
settings.setHasSeenOnboarding(true)
|
|
||||||
}
|
|
||||||
onNavigateToAudioRecorderScreen()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
16
app/src/main/java/app/myzel394/alibi/ui/utils/Context.kt
Normal file
16
app/src/main/java/app/myzel394/alibi/ui/utils/Context.kt
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package app.myzel394.alibi.ui.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
suspend fun Context.getCameraProvider(): ProcessCameraProvider = suspendCoroutine { continuation ->
|
||||||
|
ProcessCameraProvider.getInstance(this).also { future ->
|
||||||
|
future.addListener({
|
||||||
|
continuation.resume(future.get())
|
||||||
|
}, ContextCompat.getMainExecutor(this))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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_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="form_value_selected">Selected: %s</string>
|
||||||
|
|
||||||
|
<string name="a11y_selectValue">Select %s</string>
|
||||||
|
|
||||||
<string name="notificationChannels_recorder_name">Recorder</string>
|
<string name="notificationChannels_recorder_name">Recorder</string>
|
||||||
<string name="notificationChannels_recorder_description">Shows the current recording status</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_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_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>
|
<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_title">Recording paused</string>
|
||||||
<string name="ui_recorder_state_paused_description">Alibi is 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_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_title">Language</string>
|
||||||
<string name="ui_settings_language_update_label">Change</string>
|
<string name="ui_settings_language_update_label">Change</string>
|
||||||
<string name="ui_audioRecorder_info_microphone_deviceMicrophone">Device Microphone</string>
|
<string name="ui_audioRecorder_info_microphone_deviceMicrophone">Device Microphone</string>
|
||||||
@ -195,4 +197,29 @@
|
|||||||
<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_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_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_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>
|
</resources>
|
Loading…
x
Reference in New Issue
Block a user