Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst: New Crowdin translations by GitHub Action remove unused imports guard for NaN crash with short recordings fix re-record button change icon to stop icon change from "hold to record" to "click to start, click to stop"
This commit is contained in:
+35
-34
@@ -37,7 +37,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
|||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.components.ClickAndHoldBoxComposable
|
import com.vitorpamplona.amethyst.ui.components.ToggleableBox
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
@@ -52,15 +52,16 @@ fun RecordAudioBox(
|
|||||||
val mediaRecorder = remember { mutableStateOf<VoiceMessageRecorder?>(null) }
|
val mediaRecorder = remember { mutableStateOf<VoiceMessageRecorder?>(null) }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var elapsedSeconds by remember { mutableIntStateOf(0) }
|
var elapsedSeconds by remember { mutableIntStateOf(0) }
|
||||||
var wantsToRecord by remember { mutableStateOf(false) }
|
var pendingPermissionStart by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// Must be called at Composable scope, not in callback
|
// Must be called at Composable scope, not in callback
|
||||||
val recordPermissionState = rememberPermissionState(Manifest.permission.RECORD_AUDIO)
|
val recordPermissionState = rememberPermissionState(Manifest.permission.RECORD_AUDIO)
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val isRecording = mediaRecorder.value != null
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
onDispose {
|
onDispose {
|
||||||
wantsToRecord = false
|
|
||||||
mediaRecorder.value?.stop()
|
mediaRecorder.value?.stop()
|
||||||
mediaRecorder.value = null
|
mediaRecorder.value = null
|
||||||
}
|
}
|
||||||
@@ -74,8 +75,25 @@ fun RecordAudioBox(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(recordPermissionState.status.isGranted, wantsToRecord) {
|
fun stopRecording() {
|
||||||
if (recordPermissionState.status.isGranted && wantsToRecord) {
|
val result = mediaRecorder.value?.stop()
|
||||||
|
mediaRecorder.value = null
|
||||||
|
if (result != null) {
|
||||||
|
onRecordTaken(result)
|
||||||
|
} else {
|
||||||
|
Toast
|
||||||
|
.makeText(
|
||||||
|
context,
|
||||||
|
stringRes(context, R.string.record_a_message_description),
|
||||||
|
Toast.LENGTH_SHORT,
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start recording after permission is granted
|
||||||
|
LaunchedEffect(recordPermissionState.status.isGranted) {
|
||||||
|
if (recordPermissionState.status.isGranted && pendingPermissionStart) {
|
||||||
|
pendingPermissionStart = false
|
||||||
startRecording()
|
startRecording()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,38 +114,21 @@ fun RecordAudioBox(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ClickAndHoldBoxComposable(
|
ToggleableBox(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onPress = {
|
isActive = isRecording,
|
||||||
wantsToRecord = true
|
onClick = {
|
||||||
if (!recordPermissionState.status.isGranted) {
|
if (isRecording) {
|
||||||
recordPermissionState.launchPermissionRequest()
|
stopRecording()
|
||||||
} else {
|
} else {
|
||||||
// Start immediately for responsive UX when permission already granted
|
if (!recordPermissionState.status.isGranted) {
|
||||||
startRecording()
|
pendingPermissionStart = true
|
||||||
|
recordPermissionState.launchPermissionRequest()
|
||||||
|
} else {
|
||||||
|
startRecording()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRelease = {
|
content = { active -> content(active, elapsedSeconds) },
|
||||||
wantsToRecord = false
|
|
||||||
val result = mediaRecorder.value?.stop()
|
|
||||||
mediaRecorder.value = null
|
|
||||||
if (result != null) {
|
|
||||||
onRecordTaken(result)
|
|
||||||
} else {
|
|
||||||
// less disruptive than error messages
|
|
||||||
Toast
|
|
||||||
.makeText(
|
|
||||||
context,
|
|
||||||
stringRes(context, R.string.record_a_message_description),
|
|
||||||
Toast.LENGTH_SHORT,
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onCancel = {
|
|
||||||
wantsToRecord = false
|
|
||||||
mediaRecorder.value?.stop()
|
|
||||||
mediaRecorder.value = null
|
|
||||||
},
|
|
||||||
content = @Composable { isRecording -> content(isRecording, elapsedSeconds) },
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -35,7 +35,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.FiberManualRecord
|
import androidx.compose.material.icons.filled.Stop
|
||||||
import androidx.compose.material3.Icon
|
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
|
||||||
@@ -206,8 +206,8 @@ fun FloatingRecordingIndicator(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.padding(horizontal = innerPadding),
|
modifier = Modifier.padding(horizontal = innerPadding),
|
||||||
) {
|
) {
|
||||||
// Pulsing red dot
|
// Pulsing stop square
|
||||||
val infiniteTransition = rememberInfiniteTransition(label = "recording_dot")
|
val infiniteTransition = rememberInfiniteTransition(label = "recording_stop")
|
||||||
val dotAlpha by infiniteTransition.animateFloat(
|
val dotAlpha by infiniteTransition.animateFloat(
|
||||||
initialValue = 1f,
|
initialValue = 1f,
|
||||||
targetValue = 0.5f,
|
targetValue = 0.5f,
|
||||||
@@ -220,7 +220,7 @@ fun FloatingRecordingIndicator(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.FiberManualRecord,
|
imageVector = Icons.Default.Stop,
|
||||||
contentDescription = recordingLabel,
|
contentDescription = recordingLabel,
|
||||||
tint = Color.White,
|
tint = Color.White,
|
||||||
modifier =
|
modifier =
|
||||||
|
|||||||
+14
-4
@@ -26,7 +26,6 @@ import androidx.compose.animation.core.tween
|
|||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.requiredHeight
|
import androidx.compose.foundation.layout.requiredHeight
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
@@ -71,7 +70,6 @@ fun AudioWaveformReadOnly(
|
|||||||
progressBrush: Brush = SolidColor(Color.Blue),
|
progressBrush: Brush = SolidColor(Color.Blue),
|
||||||
waveformAlignment: WaveformAlignment = WaveformAlignment.Center,
|
waveformAlignment: WaveformAlignment = WaveformAlignment.Center,
|
||||||
amplitudeType: AmplitudeType = AmplitudeType.Avg,
|
amplitudeType: AmplitudeType = AmplitudeType.Avg,
|
||||||
onProgressChangeFinished: (() -> Unit)? = null,
|
|
||||||
spikeAnimationSpec: AnimationSpec<Float> = tween(500),
|
spikeAnimationSpec: AnimationSpec<Float> = tween(500),
|
||||||
spikeWidth: Dp = 3.dp,
|
spikeWidth: Dp = 3.dp,
|
||||||
spikeRadius: Dp = 2.dp,
|
spikeRadius: Dp = 2.dp,
|
||||||
@@ -80,7 +78,6 @@ fun AudioWaveformReadOnly(
|
|||||||
amplitudes: List<Float>,
|
amplitudes: List<Float>,
|
||||||
onProgressChange: (Float) -> Unit,
|
onProgressChange: (Float) -> Unit,
|
||||||
) {
|
) {
|
||||||
val backgroundColor = MaterialTheme.colorScheme.background
|
|
||||||
val progressState = remember(progress) { progress.coerceIn(MIN_PROGRESS, MAX_PROGRESS) }
|
val progressState = remember(progress) { progress.coerceIn(MIN_PROGRESS, MAX_PROGRESS) }
|
||||||
val spikeWidthState =
|
val spikeWidthState =
|
||||||
remember(spikeWidth) { spikeWidth.coerceIn(MinSpikeWidthDp, MaxSpikeWidthDp) }
|
remember(spikeWidth) { spikeWidth.coerceIn(MinSpikeWidthDp, MaxSpikeWidthDp) }
|
||||||
@@ -195,7 +192,20 @@ internal fun <T> Iterable<T>.chunkToSize(
|
|||||||
internal fun Iterable<Float>.normalize(
|
internal fun Iterable<Float>.normalize(
|
||||||
min: Float,
|
min: Float,
|
||||||
max: Float,
|
max: Float,
|
||||||
): List<Float> = map { (max - min) * ((it - min()) / (max() - min())) + min }
|
): List<Float> {
|
||||||
|
val values = toList()
|
||||||
|
if (values.isEmpty()) return emptyList()
|
||||||
|
|
||||||
|
val currentMin = values.minOrNull() ?: return emptyList()
|
||||||
|
val currentMax = values.maxOrNull() ?: return emptyList()
|
||||||
|
val range = currentMax - currentMin
|
||||||
|
if (!range.isFinite() || range == 0f) {
|
||||||
|
return List(values.size) { min }
|
||||||
|
}
|
||||||
|
|
||||||
|
val scale = max - min
|
||||||
|
return values.map { scale * ((it - currentMin) / range) + min }
|
||||||
|
}
|
||||||
|
|
||||||
private fun Int.safeDiv(value: Int): Float {
|
private fun Int.safeDiv(value: Int): Float {
|
||||||
return if (value == 0) return 0F else this / value.toFloat()
|
return if (value == 0) return 0F else this / value.toFloat()
|
||||||
|
|||||||
@@ -28,17 +28,12 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.interaction.PressInteraction
|
|
||||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
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.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
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.scale
|
import androidx.compose.ui.draw.scale
|
||||||
@@ -64,116 +59,6 @@ fun ClickableBox(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ClickAndHoldBox(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onPress: () -> Unit,
|
|
||||||
onRelease: () -> Unit,
|
|
||||||
content: @Composable (Boolean) -> Unit,
|
|
||||||
) {
|
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
|
||||||
val isPressed by interactionSource.collectIsPressedAsState()
|
|
||||||
|
|
||||||
LaunchedEffect(isPressed) {
|
|
||||||
if (isPressed) {
|
|
||||||
// Button is pressed
|
|
||||||
onPress()
|
|
||||||
} else {
|
|
||||||
// Button is released
|
|
||||||
onRelease()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animation for the button scale
|
|
||||||
val scale by animateFloatAsState(
|
|
||||||
targetValue = if (isPressed) 1.5f else 1.0f, // Scale up when recording
|
|
||||||
animationSpec = tween(durationMillis = 150), // Smooth animation
|
|
||||||
)
|
|
||||||
|
|
||||||
// Animation for the button color
|
|
||||||
val backgroundColor by animateColorAsState(
|
|
||||||
targetValue = if (isPressed) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.background,
|
|
||||||
animationSpec = tween(durationMillis = 150),
|
|
||||||
)
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier
|
|
||||||
.scale(scale)
|
|
||||||
.background(backgroundColor, CircleShape)
|
|
||||||
.clickable(
|
|
||||||
role = Role.Button,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
indication = ripple24dp,
|
|
||||||
onClick = { },
|
|
||||||
),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
content(isPressed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ClickAndHoldBoxComposable(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onPress: () -> Unit,
|
|
||||||
onRelease: suspend () -> Unit,
|
|
||||||
onCancel: suspend () -> Unit,
|
|
||||||
content: @Composable (Boolean) -> Unit,
|
|
||||||
) {
|
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
|
||||||
var isPressed by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
LaunchedEffect(interactionSource) {
|
|
||||||
val pressInteractions = mutableListOf<PressInteraction.Press>()
|
|
||||||
interactionSource.interactions.collect { interaction ->
|
|
||||||
when (interaction) {
|
|
||||||
is PressInteraction.Press -> {
|
|
||||||
if (pressInteractions.isEmpty()) {
|
|
||||||
onPress()
|
|
||||||
}
|
|
||||||
pressInteractions.add(interaction)
|
|
||||||
}
|
|
||||||
is PressInteraction.Release -> {
|
|
||||||
onRelease()
|
|
||||||
pressInteractions.remove(interaction.press)
|
|
||||||
}
|
|
||||||
is PressInteraction.Cancel -> {
|
|
||||||
onCancel()
|
|
||||||
pressInteractions.remove(interaction.press)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isPressed = pressInteractions.isNotEmpty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animation for the button scale
|
|
||||||
val scale by animateFloatAsState(
|
|
||||||
targetValue = if (isPressed) 1.5f else 1.0f, // Scale up when recording
|
|
||||||
animationSpec = tween(durationMillis = 150), // Smooth animation
|
|
||||||
)
|
|
||||||
|
|
||||||
// Animation for the button color
|
|
||||||
val backgroundColor by animateColorAsState(
|
|
||||||
targetValue = if (isPressed) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.background,
|
|
||||||
animationSpec = tween(durationMillis = 150),
|
|
||||||
)
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier
|
|
||||||
.scale(scale)
|
|
||||||
.background(backgroundColor, CircleShape)
|
|
||||||
.clickable(
|
|
||||||
role = Role.Button,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
indication = ripple24dp,
|
|
||||||
onClick = { },
|
|
||||||
),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
content(isPressed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ClickableBox(
|
fun ClickableBox(
|
||||||
@@ -195,3 +80,38 @@ fun ClickableBox(
|
|||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ToggleableBox(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
isActive: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
content: @Composable (Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
// Animation for the button scale
|
||||||
|
val scale by animateFloatAsState(
|
||||||
|
targetValue = if (isActive) 1.5f else 1.0f,
|
||||||
|
animationSpec = tween(durationMillis = 150),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Animation for the button color
|
||||||
|
val backgroundColor by animateColorAsState(
|
||||||
|
targetValue = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.background,
|
||||||
|
animationSpec = tween(durationMillis = 150),
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier
|
||||||
|
.scale(scale)
|
||||||
|
.background(backgroundColor, CircleShape)
|
||||||
|
.clickable(
|
||||||
|
role = Role.Button,
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = ripple24dp,
|
||||||
|
onClick = onClick,
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
content(isActive)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+33
-16
@@ -35,6 +35,7 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
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.Mic
|
||||||
|
import androidx.compose.material.icons.filled.Stop
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -53,6 +54,7 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow
|
|||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.RecordAudioBox
|
import com.vitorpamplona.amethyst.ui.actions.uploads.RecordAudioBox
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator
|
import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview
|
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.uploads.formatSecondsToTime
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
|
||||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||||
@@ -212,29 +214,44 @@ private fun ReRecordButton(viewModel: VoiceReplyViewModel) {
|
|||||||
onRecordTaken = { recording ->
|
onRecordTaken = { recording ->
|
||||||
viewModel.selectRecording(recording)
|
viewModel.selectRecording(recording)
|
||||||
},
|
},
|
||||||
) { isRecording, _ ->
|
) { isRecording, elapsedSeconds ->
|
||||||
|
val contentColor =
|
||||||
|
if (isRecording) {
|
||||||
|
MaterialTheme.colorScheme.onPrimary
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onBackground
|
||||||
|
}
|
||||||
|
val icon =
|
||||||
|
if (isRecording) {
|
||||||
|
Icons.Default.Stop
|
||||||
|
} else {
|
||||||
|
Icons.Default.Mic
|
||||||
|
}
|
||||||
|
val label =
|
||||||
|
if (isRecording) {
|
||||||
|
formatSecondsToTime(elapsedSeconds)
|
||||||
|
} else {
|
||||||
|
stringRes(id = R.string.re_record)
|
||||||
|
}
|
||||||
|
val iconDescription =
|
||||||
|
if (isRecording) {
|
||||||
|
stringRes(id = R.string.recording_indicator_description)
|
||||||
|
} else {
|
||||||
|
stringRes(id = R.string.record_a_message)
|
||||||
|
}
|
||||||
Row(
|
Row(
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Mic,
|
imageVector = icon,
|
||||||
contentDescription = stringRes(id = R.string.record_a_message),
|
contentDescription = iconDescription,
|
||||||
tint =
|
tint = contentColor,
|
||||||
if (isRecording) {
|
|
||||||
MaterialTheme.colorScheme.primary
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.onBackground
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = stringRes(id = R.string.re_record),
|
text = label,
|
||||||
color =
|
color = contentColor,
|
||||||
if (isRecording) {
|
|
||||||
MaterialTheme.colorScheme.primary
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.onBackground
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -673,6 +673,7 @@
|
|||||||
<string name="posting_policy">Politika příspěvků</string>
|
<string name="posting_policy">Politika příspěvků</string>
|
||||||
<string name="privacy_policy">Zásady ochrany soukromí</string>
|
<string name="privacy_policy">Zásady ochrany soukromí</string>
|
||||||
<string name="terms_and_conditions">Podmínky & ujednání</string>
|
<string name="terms_and_conditions">Podmínky & ujednání</string>
|
||||||
|
<string name="not_available_acronym">N/A</string>
|
||||||
<string name="relay_error_messages">Chyby a upozornění z tohoto relé</string>
|
<string name="relay_error_messages">Chyby a upozornění z tohoto relé</string>
|
||||||
<string name="message_length">Délka zprávy</string>
|
<string name="message_length">Délka zprávy</string>
|
||||||
<string name="subscriptions">Předplatné</string>
|
<string name="subscriptions">Předplatné</string>
|
||||||
|
|||||||
@@ -678,6 +678,7 @@ anz der Bedingungen ist erforderlich</string>
|
|||||||
<string name="posting_policy">Veröffentlichungsrichtlinie</string>
|
<string name="posting_policy">Veröffentlichungsrichtlinie</string>
|
||||||
<string name="privacy_policy">Datenschutzerklärung</string>
|
<string name="privacy_policy">Datenschutzerklärung</string>
|
||||||
<string name="terms_and_conditions">Allgemeine Geschäftsbedingungen</string>
|
<string name="terms_and_conditions">Allgemeine Geschäftsbedingungen</string>
|
||||||
|
<string name="not_available_acronym">N/A</string>
|
||||||
<string name="relay_error_messages">Fehler und Hinweise von diesem Relais</string>
|
<string name="relay_error_messages">Fehler und Hinweise von diesem Relais</string>
|
||||||
<string name="message_length">Nachrichtenlänge</string>
|
<string name="message_length">Nachrichtenlänge</string>
|
||||||
<string name="subscriptions">Abonnements</string>
|
<string name="subscriptions">Abonnements</string>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
<string name="zaps">Zapy</string>
|
<string name="zaps">Zapy</string>
|
||||||
<string name="view_count">Liczba wyświetleń</string>
|
<string name="view_count">Liczba wyświetleń</string>
|
||||||
<string name="boost">Powtórz</string>
|
<string name="boost">Powtórz</string>
|
||||||
<string name="boosted">powtórzony</string>
|
<string name="boosted">powtórzono</string>
|
||||||
<string name="edited">edytowano</string>
|
<string name="edited">edytowano</string>
|
||||||
<string name="edited_number">edytuj #%1$s</string>
|
<string name="edited_number">edytuj #%1$s</string>
|
||||||
<string name="original">oryginalny</string>
|
<string name="original">oryginalny</string>
|
||||||
@@ -135,6 +135,8 @@
|
|||||||
<string name="add_a_user">Dodaj użytkownika</string>
|
<string name="add_a_user">Dodaj użytkownika</string>
|
||||||
<string name="add_a_relay">Dodaj Transmiter</string>
|
<string name="add_a_relay">Dodaj Transmiter</string>
|
||||||
<string name="profile_name">Imię</string>
|
<string name="profile_name">Imię</string>
|
||||||
|
<string name="profile_name_with_explainer">Imię (dla @tagging)</string>
|
||||||
|
<string name="my_name">Moje imię @tag</string>
|
||||||
<string name="display_name">Nazwa użytkownika</string>
|
<string name="display_name">Nazwa użytkownika</string>
|
||||||
<string name="my_display_name">Mój nick</string>
|
<string name="my_display_name">Mój nick</string>
|
||||||
<string name="my_awesome_name">G Braun</string>
|
<string name="my_awesome_name">G Braun</string>
|
||||||
|
|||||||
@@ -673,6 +673,7 @@
|
|||||||
<string name="posting_policy">Política de postagem</string>
|
<string name="posting_policy">Política de postagem</string>
|
||||||
<string name="privacy_policy">Política de privacidade</string>
|
<string name="privacy_policy">Política de privacidade</string>
|
||||||
<string name="terms_and_conditions">Termos & condições</string>
|
<string name="terms_and_conditions">Termos & condições</string>
|
||||||
|
<string name="not_available_acronym">N/A</string>
|
||||||
<string name="relay_error_messages">Erros e Avisos deste Relé</string>
|
<string name="relay_error_messages">Erros e Avisos deste Relé</string>
|
||||||
<string name="message_length">Tamanho da mensagem</string>
|
<string name="message_length">Tamanho da mensagem</string>
|
||||||
<string name="subscriptions">Assinaturas</string>
|
<string name="subscriptions">Assinaturas</string>
|
||||||
@@ -686,6 +687,8 @@
|
|||||||
<string name="accepts_up_to">Aceita até</string>
|
<string name="accepts_up_to">Aceita até</string>
|
||||||
<string name="time_in_the_future">%1$s no futuro</string>
|
<string name="time_in_the_future">%1$s no futuro</string>
|
||||||
<string name="time_in_the_past">há %1$s</string>
|
<string name="time_in_the_past">há %1$s</string>
|
||||||
|
<string name="amount_in_zeros">%1$s zeros</string>
|
||||||
|
<string name="amount_in_bits">%1$s bits</string>
|
||||||
<string name="event_retention">Retenção de eventos</string>
|
<string name="event_retention">Retenção de eventos</string>
|
||||||
<string name="content_size">Tamanho do conteúdo</string>
|
<string name="content_size">Tamanho do conteúdo</string>
|
||||||
<string name="connectivity">Conectividade</string>
|
<string name="connectivity">Conectividade</string>
|
||||||
|
|||||||
@@ -672,6 +672,7 @@
|
|||||||
<string name="posting_policy">Publiceringspolicy</string>
|
<string name="posting_policy">Publiceringspolicy</string>
|
||||||
<string name="privacy_policy">Integritetspolicy</string>
|
<string name="privacy_policy">Integritetspolicy</string>
|
||||||
<string name="terms_and_conditions">Villkor & bestämmelser</string>
|
<string name="terms_and_conditions">Villkor & bestämmelser</string>
|
||||||
|
<string name="not_available_acronym">N/A</string>
|
||||||
<string name="relay_error_messages">Fel och meddelanden från detta relä</string>
|
<string name="relay_error_messages">Fel och meddelanden från detta relä</string>
|
||||||
<string name="message_length">Meddelandelängd</string>
|
<string name="message_length">Meddelandelängd</string>
|
||||||
<string name="subscriptions">Prenumerationer</string>
|
<string name="subscriptions">Prenumerationer</string>
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
|
|||||||
-1
@@ -20,7 +20,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.commons.util
|
package com.vitorpamplona.amethyst.commons.util
|
||||||
|
|
||||||
import com.sun.org.apache.xalan.internal.lib.ExsltDatetime.time
|
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|||||||
Reference in New Issue
Block a user