mirror of
https://github.com/Myzel394/Alibi.git
synced 2025-06-19 07:15:25 +02:00
feat: Add AudioRecordingStart and VideoRecordingStart
This commit is contained in:
parent
e7989e2eba
commit
261753ad75
@ -23,9 +23,8 @@ import app.myzel394.alibi.ui.enums.Screen
|
|||||||
import app.myzel394.alibi.ui.models.AudioRecorderModel
|
import app.myzel394.alibi.ui.models.AudioRecorderModel
|
||||||
import app.myzel394.alibi.ui.models.VideoRecorderModel
|
import app.myzel394.alibi.ui.models.VideoRecorderModel
|
||||||
import app.myzel394.alibi.ui.screens.AboutScreen
|
import app.myzel394.alibi.ui.screens.AboutScreen
|
||||||
import app.myzel394.alibi.ui.screens.AudioRecorderScreen
|
import app.myzel394.alibi.ui.screens.RecorderScreen
|
||||||
import app.myzel394.alibi.ui.screens.CustomRecordingNotificationsScreen
|
import app.myzel394.alibi.ui.screens.CustomRecordingNotificationsScreen
|
||||||
import app.myzel394.alibi.ui.screens.POCVideo
|
|
||||||
import app.myzel394.alibi.ui.screens.SettingsScreen
|
import app.myzel394.alibi.ui.screens.SettingsScreen
|
||||||
import app.myzel394.alibi.ui.screens.WelcomeScreen
|
import app.myzel394.alibi.ui.screens.WelcomeScreen
|
||||||
|
|
||||||
@ -73,9 +72,10 @@ fun Navigation(
|
|||||||
scaleOut(targetScale = SCALE_IN) + fadeOut(tween(durationMillis = 150))
|
scaleOut(targetScale = SCALE_IN) + fadeOut(tween(durationMillis = 150))
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
AudioRecorderScreen(
|
RecorderScreen(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
audioRecorder = audioRecorder,
|
audioRecorder = audioRecorder,
|
||||||
|
videoRecorder = videoRecorder,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(
|
composable(
|
||||||
|
@ -0,0 +1,90 @@
|
|||||||
|
package app.myzel394.alibi.ui.components.AudioRecorder.atoms
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Mic
|
||||||
|
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.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.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.db.AppSettings
|
||||||
|
import app.myzel394.alibi.ui.components.atoms.PermissionRequester
|
||||||
|
import app.myzel394.alibi.ui.models.AudioRecorderModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AudioRecordingStart(
|
||||||
|
audioRecorder: AudioRecorderModel,
|
||||||
|
appSettings: AppSettings,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
// We can't get the current `notificationDetails` inside the
|
||||||
|
// `onPermissionAvailable` function. We'll instead use this hack
|
||||||
|
// with `LaunchedEffect` to get the current value.
|
||||||
|
var startRecording by rememberSaveable { mutableStateOf(false) }
|
||||||
|
LaunchedEffect(startRecording) {
|
||||||
|
if (startRecording) {
|
||||||
|
startRecording = false
|
||||||
|
|
||||||
|
audioRecorder.startRecording(context, appSettings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PermissionRequester(
|
||||||
|
permission = Manifest.permission.RECORD_AUDIO,
|
||||||
|
icon = Icons.Default.Mic,
|
||||||
|
onPermissionAvailable = {
|
||||||
|
startRecording = true
|
||||||
|
}
|
||||||
|
) { trigger ->
|
||||||
|
val label = stringResource(R.string.ui_audioRecorder_action_start_label)
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = trigger,
|
||||||
|
modifier = Modifier
|
||||||
|
.semantics {
|
||||||
|
contentDescription = label
|
||||||
|
}
|
||||||
|
.size(200.dp)
|
||||||
|
.clip(shape = CircleShape),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Mic,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(80.dp),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(ButtonDefaults.IconSpacing))
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
package app.myzel394.alibi.ui.components.AudioRecorder.atoms
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.CameraAlt
|
||||||
|
import androidx.compose.material.icons.filled.Mic
|
||||||
|
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.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.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.db.AppSettings
|
||||||
|
import app.myzel394.alibi.ui.components.atoms.PermissionRequester
|
||||||
|
import app.myzel394.alibi.ui.models.AudioRecorderModel
|
||||||
|
import app.myzel394.alibi.ui.models.VideoRecorderModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VideoRecordingStart(
|
||||||
|
videoRecorder: VideoRecorderModel,
|
||||||
|
appSettings: AppSettings,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
// We can't get the current `notificationDetails` inside the
|
||||||
|
// `onPermissionAvailable` function. We'll instead use this hack
|
||||||
|
// with `LaunchedEffect` to get the current value.
|
||||||
|
var startRecording by rememberSaveable { mutableStateOf(false) }
|
||||||
|
LaunchedEffect(startRecording) {
|
||||||
|
if (startRecording) {
|
||||||
|
startRecording = false
|
||||||
|
|
||||||
|
videoRecorder.startRecording(context, appSettings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PermissionRequester(
|
||||||
|
permission = Manifest.permission.RECORD_AUDIO,
|
||||||
|
icon = Icons.Default.Mic,
|
||||||
|
onPermissionAvailable = {
|
||||||
|
startRecording = true
|
||||||
|
}
|
||||||
|
) { trigger ->
|
||||||
|
val label = stringResource(R.string.ui_videoRecorder_action_start_label)
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = trigger,
|
||||||
|
modifier = Modifier
|
||||||
|
.semantics {
|
||||||
|
contentDescription = label
|
||||||
|
}
|
||||||
|
.size(200.dp)
|
||||||
|
.clip(shape = CircleShape),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.CameraAlt,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(80.dp),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(ButtonDefaults.IconSpacing))
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
package app.myzel394.alibi.ui.components.AudioRecorder.molecules
|
package app.myzel394.alibi.ui.components.AudioRecorder.molecules
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
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.Spacer
|
||||||
@ -11,9 +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.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Mic
|
|
||||||
import androidx.compose.material.icons.filled.Save
|
import androidx.compose.material.icons.filled.Save
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
@ -21,15 +18,8 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
@ -37,18 +27,20 @@ import androidx.compose.ui.semantics.semantics
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
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.ui.BIG_PRIMARY_BUTTON_SIZE
|
import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE
|
||||||
import app.myzel394.alibi.ui.components.atoms.PermissionRequester
|
import app.myzel394.alibi.ui.components.AudioRecorder.atoms.AudioRecordingStart
|
||||||
|
import app.myzel394.alibi.ui.components.AudioRecorder.atoms.VideoRecordingStart
|
||||||
import app.myzel394.alibi.ui.effects.rememberForceUpdateOnLifeCycleChange
|
import app.myzel394.alibi.ui.effects.rememberForceUpdateOnLifeCycleChange
|
||||||
import app.myzel394.alibi.ui.models.AudioRecorderModel
|
import app.myzel394.alibi.ui.models.AudioRecorderModel
|
||||||
|
import app.myzel394.alibi.ui.models.VideoRecorderModel
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.time.format.FormatStyle
|
import java.time.format.FormatStyle
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StartRecording(
|
fun StartRecording(
|
||||||
audioRecorder: AudioRecorderModel,
|
audioRecorder: AudioRecorderModel,
|
||||||
|
videoRecorder: VideoRecorderModel,
|
||||||
// Loading this from parent, because if we load it ourselves
|
// Loading this from parent, because if we load it ourselves
|
||||||
// and permissions have already been granted, initial
|
// and permissions have already been granted, initial
|
||||||
// settings will be used, instead of the actual settings.
|
// settings will be used, instead of the actual settings.
|
||||||
@ -57,70 +49,26 @@ fun StartRecording(
|
|||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
// We can't get the current `notificationDetails` inside the
|
|
||||||
// `onPermissionAvailable` function. We'll instead use this hack
|
|
||||||
// with `LaunchedEffect` to get the current value.
|
|
||||||
var startRecording by rememberSaveable { mutableStateOf(false) }
|
|
||||||
LaunchedEffect(startRecording) {
|
|
||||||
if (startRecording) {
|
|
||||||
startRecording = false
|
|
||||||
|
|
||||||
audioRecorder.startRecording(context, appSettings)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
verticalArrangement = Arrangement.SpaceBetween,
|
verticalArrangement = Arrangement.SpaceBetween,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
PermissionRequester(
|
|
||||||
permission = Manifest.permission.RECORD_AUDIO,
|
AudioRecordingStart(
|
||||||
icon = Icons.Default.Mic,
|
audioRecorder = audioRecorder,
|
||||||
onPermissionAvailable = {
|
appSettings = appSettings,
|
||||||
startRecording = true
|
|
||||||
},
|
|
||||||
) { trigger ->
|
|
||||||
val label = stringResource(R.string.ui_audioRecorder_action_start_label)
|
|
||||||
Button(
|
|
||||||
onClick = trigger,
|
|
||||||
modifier = Modifier
|
|
||||||
.semantics {
|
|
||||||
contentDescription = label
|
|
||||||
}
|
|
||||||
.size(200.dp)
|
|
||||||
.clip(shape = CircleShape),
|
|
||||||
colors = ButtonDefaults.outlinedButtonColors(),
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Mic,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier
|
|
||||||
.size(80.dp),
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(ButtonDefaults.IconSpacing))
|
VideoRecordingStart(
|
||||||
Text(
|
videoRecorder = videoRecorder,
|
||||||
label,
|
appSettings = appSettings,
|
||||||
style = MaterialTheme.typography.titleSmall,
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val settings = LocalContext
|
|
||||||
.current
|
|
||||||
.dataStore
|
|
||||||
.data
|
|
||||||
.collectAsState(initial = AppSettings.getDefaultInstance())
|
|
||||||
.value
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
stringResource(
|
stringResource(
|
||||||
R.string.ui_audioRecorder_action_start_description,
|
R.string.ui_audioRecorder_action_start_description,
|
||||||
settings.maxDuration / 1000 / 60
|
appSettings.maxDuration / 1000 / 60
|
||||||
),
|
),
|
||||||
style = MaterialTheme.typography.bodySmall.copy(
|
style = MaterialTheme.typography.bodySmall.copy(
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
@ -48,14 +48,16 @@ import app.myzel394.alibi.helpers.AudioBatchesFolder
|
|||||||
import app.myzel394.alibi.helpers.BatchesFolder
|
import app.myzel394.alibi.helpers.BatchesFolder
|
||||||
import app.myzel394.alibi.ui.effects.rememberSettings
|
import app.myzel394.alibi.ui.effects.rememberSettings
|
||||||
import app.myzel394.alibi.ui.models.AudioRecorderModel
|
import app.myzel394.alibi.ui.models.AudioRecorderModel
|
||||||
|
import app.myzel394.alibi.ui.models.VideoRecorderModel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AudioRecorderScreen(
|
fun RecorderScreen(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
audioRecorder: AudioRecorderModel,
|
audioRecorder: AudioRecorderModel,
|
||||||
|
videoRecorder: VideoRecorderModel,
|
||||||
) {
|
) {
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@ -299,7 +301,9 @@ fun AudioRecorderScreen(
|
|||||||
RecordingStatus(audioRecorder = audioRecorder)
|
RecordingStatus(audioRecorder = audioRecorder)
|
||||||
else
|
else
|
||||||
StartRecording(
|
StartRecording(
|
||||||
audioRecorder = audioRecorder, appSettings = appSettings,
|
audioRecorder = audioRecorder,
|
||||||
|
videoRecorder = videoRecorder,
|
||||||
|
appSettings = appSettings,
|
||||||
onSaveLastRecording = ::saveRecording,
|
onSaveLastRecording = ::saveRecording,
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -18,7 +18,7 @@
|
|||||||
<string name="ui_permissions_request">Please grant the permission to continue</string>
|
<string name="ui_permissions_request">Please grant the permission to continue</string>
|
||||||
<string name="ui_permissions_permanentlyDenied_message">You will be redirected to the app settings to grant the permission there.</string>
|
<string name="ui_permissions_permanentlyDenied_message">You will be redirected to the app settings to grant the permission there.</string>
|
||||||
|
|
||||||
<string name="ui_audioRecorder_action_start_label">Start Recording</string>
|
<string name="ui_audioRecorder_action_start_label">Start Audio Recording</string>
|
||||||
<string name="ui_audioRecorder_action_saveOldRecording_label">Save Recording from <xliff:g name="date">%s</xliff:g></string>
|
<string name="ui_audioRecorder_action_saveOldRecording_label">Save Recording from <xliff:g name="date">%s</xliff:g></string>
|
||||||
<string name="ui_audioRecorder_action_delete_label">Delete</string>
|
<string name="ui_audioRecorder_action_delete_label">Delete</string>
|
||||||
<string name="ui_audioRecorder_action_delete_confirm_title">Delete Recording?</string>
|
<string name="ui_audioRecorder_action_delete_confirm_title">Delete Recording?</string>
|
||||||
@ -138,4 +138,5 @@
|
|||||||
<string name="ui_settings_value_videoQuality_values_hd">HD</string>
|
<string name="ui_settings_value_videoQuality_values_hd">HD</string>
|
||||||
<string name="ui_settings_value_videoQuality_values_sd">Standard</string>
|
<string name="ui_settings_value_videoQuality_values_sd">Standard</string>
|
||||||
<string name="ui_settings_value_videoQuality_values_lowest">Lowest</string>
|
<string name="ui_settings_value_videoQuality_values_lowest">Lowest</string>
|
||||||
|
<string name="ui_videoRecorder_action_start_label">Start Video Recording</string>
|
||||||
</resources>
|
</resources>
|
Loading…
x
Reference in New Issue
Block a user