TOOD:
Move re-record higher up tp avoid google assistant on longpress Reply to a reply fails to upload Verify deletion of temp voice files Code review: - temp file cleanup on disposal - Made uploads cancellable - recording must exist; metadata-only no longer enables Send) - Removed duplication
This commit is contained in:
+67
@@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.actions.mediaServers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.TextSpinner
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.TitleExplainer
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FileServerSelectionRow(
|
||||||
|
fileServers: List<ServerName>,
|
||||||
|
selectedServer: ServerName?,
|
||||||
|
onSelect: (ServerName) -> Unit,
|
||||||
|
) {
|
||||||
|
val nip95description = stringRes(id = R.string.upload_server_relays_nip95)
|
||||||
|
|
||||||
|
val fileServerOptions =
|
||||||
|
remember(fileServers) {
|
||||||
|
fileServers
|
||||||
|
.map {
|
||||||
|
if (it.type == ServerType.NIP95) {
|
||||||
|
TitleExplainer(it.name, nip95description)
|
||||||
|
} else {
|
||||||
|
TitleExplainer(it.name, it.baseUrl)
|
||||||
|
}
|
||||||
|
}.toImmutableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val placeholder =
|
||||||
|
fileServers
|
||||||
|
.firstOrNull { it == selectedServer }
|
||||||
|
?.name
|
||||||
|
?: fileServers.firstOrNull()?.name
|
||||||
|
?: ""
|
||||||
|
|
||||||
|
SettingsRow(R.string.file_server, R.string.file_server_description) {
|
||||||
|
TextSpinner(
|
||||||
|
label = "",
|
||||||
|
placeholder = placeholder,
|
||||||
|
options = fileServerOptions,
|
||||||
|
onSelect = { index -> fileServers.getOrNull(index)?.let(onSelect) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.actions.uploads
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ProgressIndicatorDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UploadProgressIndicator(
|
||||||
|
orchestrator: UploadOrchestrator,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val progressValue = orchestrator.progress.collectAsState().value
|
||||||
|
val progressStatusValue = orchestrator.progressState.collectAsState().value
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 24.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.size(55.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
val animatedProgress =
|
||||||
|
animateFloatAsState(
|
||||||
|
targetValue = progressValue.toFloat(),
|
||||||
|
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
|
||||||
|
).value
|
||||||
|
|
||||||
|
CircularProgressIndicator(
|
||||||
|
progress = { animatedProgress },
|
||||||
|
modifier =
|
||||||
|
Size55Modifier
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.background),
|
||||||
|
strokeWidth = 5.dp,
|
||||||
|
)
|
||||||
|
|
||||||
|
val txt =
|
||||||
|
when (progressStatusValue) {
|
||||||
|
is com.vitorpamplona.amethyst.service.uploads.UploadingState.Ready -> stringRes(R.string.uploading_state_ready)
|
||||||
|
is com.vitorpamplona.amethyst.service.uploads.UploadingState.Compressing -> stringRes(R.string.uploading_state_compressing)
|
||||||
|
is com.vitorpamplona.amethyst.service.uploads.UploadingState.Uploading -> stringRes(R.string.uploading_state_uploading)
|
||||||
|
is com.vitorpamplona.amethyst.service.uploads.UploadingState.ServerProcessing -> stringRes(R.string.uploading_state_server_processing)
|
||||||
|
is com.vitorpamplona.amethyst.service.uploads.UploadingState.Downloading -> stringRes(R.string.uploading_state_downloading)
|
||||||
|
is com.vitorpamplona.amethyst.service.uploads.UploadingState.Hashing -> stringRes(R.string.uploading_state_hashing)
|
||||||
|
is com.vitorpamplona.amethyst.service.uploads.UploadingState.Finished -> stringRes(R.string.uploading_state_finished)
|
||||||
|
is com.vitorpamplona.amethyst.service.uploads.UploadingState.Error -> stringRes(R.string.uploading_state_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
txt,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-95
@@ -24,10 +24,7 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
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
|
||||||
@@ -38,47 +35,37 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ProgressIndicatorDefaults
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||||
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.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.core.util.Consumer
|
import androidx.core.util.Consumer
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow
|
||||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.RecordVoiceButton
|
import com.vitorpamplona.amethyst.ui.actions.uploads.RecordVoiceButton
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
|
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton
|
import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton
|
||||||
|
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.components.TextSpinner
|
|
||||||
import com.vitorpamplona.amethyst.ui.components.TitleExplainer
|
|
||||||
import com.vitorpamplona.amethyst.ui.components.getActivity
|
import com.vitorpamplona.amethyst.ui.components.getActivity
|
||||||
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
|
||||||
@@ -111,7 +98,6 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||||
@@ -365,24 +351,11 @@ private fun NewPostScreenBody(
|
|||||||
|
|
||||||
// Show preview for both uploaded messages (voiceMetadata) and pending recordings
|
// Show preview for both uploaded messages (voiceMetadata) and pending recordings
|
||||||
(postViewModel.voiceMetadata ?: postViewModel.getVoicePreviewMetadata())?.let { metadata ->
|
(postViewModel.voiceMetadata ?: postViewModel.getVoicePreviewMetadata())?.let { metadata ->
|
||||||
val nip95description = stringRes(id = R.string.upload_server_relays_nip95)
|
|
||||||
val fileServersState =
|
val fileServersState =
|
||||||
accountViewModel.account.serverLists.liveServerList
|
accountViewModel.account.serverLists.liveServerList
|
||||||
.collectAsState()
|
.collectAsState()
|
||||||
val fileServers = fileServersState.value
|
val fileServers = fileServersState.value
|
||||||
|
|
||||||
val fileServerOptions =
|
|
||||||
remember(fileServers) {
|
|
||||||
fileServers
|
|
||||||
.map {
|
|
||||||
if (it.type == ServerType.NIP95) {
|
|
||||||
TitleExplainer(it.name, nip95description)
|
|
||||||
} else {
|
|
||||||
TitleExplainer(it.name, it.baseUrl)
|
|
||||||
}
|
|
||||||
}.toImmutableList()
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
@@ -391,7 +364,7 @@ private fun NewPostScreenBody(
|
|||||||
) {
|
) {
|
||||||
// Display voice preview or uploading progress
|
// Display voice preview or uploading progress
|
||||||
postViewModel.voiceOrchestrator?.let { orchestrator ->
|
postViewModel.voiceOrchestrator?.let { orchestrator ->
|
||||||
VoiceUploadingProgress(orchestrator)
|
UploadProgressIndicator(orchestrator)
|
||||||
} ?: run {
|
} ?: run {
|
||||||
VoiceMessagePreview(
|
VoiceMessagePreview(
|
||||||
voiceMetadata = metadata,
|
voiceMetadata = metadata,
|
||||||
@@ -400,18 +373,11 @@ private fun NewPostScreenBody(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsRow(R.string.file_server, R.string.file_server_description) {
|
FileServerSelectionRow(
|
||||||
TextSpinner(
|
fileServers = fileServers,
|
||||||
label = "",
|
selectedServer = postViewModel.voiceSelectedServer ?: accountViewModel.account.settings.defaultFileServer,
|
||||||
placeholder =
|
onSelect = { postViewModel.voiceSelectedServer = it },
|
||||||
fileServers
|
)
|
||||||
.firstOrNull { it == (postViewModel.voiceSelectedServer ?: accountViewModel.account.settings.defaultFileServer) }
|
|
||||||
?.name
|
|
||||||
?: fileServers[0].name,
|
|
||||||
options = fileServerOptions,
|
|
||||||
onSelect = { postViewModel.voiceSelectedServer = fileServers[it] },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,56 +550,3 @@ private fun AddPollButton(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun VoiceUploadingProgress(orchestrator: UploadOrchestrator) {
|
|
||||||
val progressValue = orchestrator.progress.collectAsState().value
|
|
||||||
val progressStatusValue = orchestrator.progressState.collectAsState().value
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 24.dp),
|
|
||||||
contentAlignment = androidx.compose.ui.Alignment.Center,
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.size(55.dp),
|
|
||||||
contentAlignment = androidx.compose.ui.Alignment.Center,
|
|
||||||
) {
|
|
||||||
val animatedProgress =
|
|
||||||
animateFloatAsState(
|
|
||||||
targetValue = progressValue.toFloat(),
|
|
||||||
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
|
|
||||||
).value
|
|
||||||
|
|
||||||
CircularProgressIndicator(
|
|
||||||
progress = { animatedProgress },
|
|
||||||
modifier =
|
|
||||||
Size55Modifier
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(MaterialTheme.colorScheme.background),
|
|
||||||
strokeWidth = 5.dp,
|
|
||||||
)
|
|
||||||
|
|
||||||
val txt =
|
|
||||||
when (progressStatusValue) {
|
|
||||||
is com.vitorpamplona.amethyst.service.uploads.UploadingState.Ready -> stringRes(R.string.uploading_state_ready)
|
|
||||||
is com.vitorpamplona.amethyst.service.uploads.UploadingState.Compressing -> stringRes(R.string.uploading_state_compressing)
|
|
||||||
is com.vitorpamplona.amethyst.service.uploads.UploadingState.Uploading -> stringRes(R.string.uploading_state_uploading)
|
|
||||||
is com.vitorpamplona.amethyst.service.uploads.UploadingState.ServerProcessing -> stringRes(R.string.uploading_state_server_processing)
|
|
||||||
is com.vitorpamplona.amethyst.service.uploads.UploadingState.Downloading -> stringRes(R.string.uploading_state_downloading)
|
|
||||||
is com.vitorpamplona.amethyst.service.uploads.UploadingState.Hashing -> stringRes(R.string.uploading_state_hashing)
|
|
||||||
is com.vitorpamplona.amethyst.service.uploads.UploadingState.Finished -> stringRes(R.string.uploading_state_finished)
|
|
||||||
is com.vitorpamplona.amethyst.service.uploads.UploadingState.Error -> stringRes(R.string.uploading_state_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
|
||||||
txt,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
fontSize = 10.sp,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+49
-123
@@ -21,10 +21,7 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.home
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.home
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
|
||||||
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.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
|
||||||
@@ -33,49 +30,37 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
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.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
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.material3.CircularProgressIndicator
|
|
||||||
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
|
||||||
import androidx.compose.material3.ProgressIndicatorDefaults
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
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.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow
|
||||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
|
||||||
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.VoiceMessagePreview
|
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview
|
||||||
import com.vitorpamplona.amethyst.ui.components.TextSpinner
|
|
||||||
import com.vitorpamplona.amethyst.ui.components.TitleExplainer
|
|
||||||
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
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow
|
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -89,9 +74,9 @@ fun VoiceReplyScreen(
|
|||||||
nav: Nav,
|
nav: Nav,
|
||||||
) {
|
) {
|
||||||
val viewModel: VoiceReplyViewModel = viewModel()
|
val viewModel: VoiceReplyViewModel = viewModel()
|
||||||
viewModel.init(accountViewModel)
|
|
||||||
|
|
||||||
LaunchedEffect(replyToNoteId, recordingFilePath) {
|
LaunchedEffect(replyToNoteId, recordingFilePath, accountViewModel) {
|
||||||
|
viewModel.init(accountViewModel)
|
||||||
viewModel.load(replyToNoteId, recordingFilePath, mimeType, duration, amplitudesJson)
|
viewModel.load(replyToNoteId, recordingFilePath, mimeType, duration, amplitudesJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +85,12 @@ fun VoiceReplyScreen(
|
|||||||
nav.popBack()
|
nav.popBack()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
viewModel.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
PostingTopBar(
|
PostingTopBar(
|
||||||
@@ -115,6 +106,9 @@ fun VoiceReplyScreen(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
bottomBar = {
|
||||||
|
ReRecordButton(viewModel)
|
||||||
|
},
|
||||||
) { pad ->
|
) { pad ->
|
||||||
Surface(
|
Surface(
|
||||||
modifier =
|
modifier =
|
||||||
@@ -160,7 +154,7 @@ private fun VoiceReplyScreenBody(
|
|||||||
|
|
||||||
// Voice preview or upload progress
|
// Voice preview or upload progress
|
||||||
viewModel.voiceOrchestrator?.let { orchestrator ->
|
viewModel.voiceOrchestrator?.let { orchestrator ->
|
||||||
VoiceUploadingProgress(orchestrator)
|
UploadProgressIndicator(orchestrator)
|
||||||
} ?: run {
|
} ?: run {
|
||||||
viewModel.getVoicePreviewMetadata()?.let { metadata ->
|
viewModel.getVoicePreviewMetadata()?.let { metadata ->
|
||||||
VoiceMessagePreview(
|
VoiceMessagePreview(
|
||||||
@@ -177,62 +171,47 @@ private fun VoiceReplyScreenBody(
|
|||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// Server selection
|
// Server selection
|
||||||
ServerSelectionRow(viewModel, accountViewModel)
|
val fileServers =
|
||||||
|
accountViewModel.account.serverLists.liveServerList
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
.collectAsState()
|
||||||
|
.value
|
||||||
// Re-record button at bottom
|
FileServerSelectionRow(
|
||||||
ReRecordButton(viewModel)
|
fileServers = fileServers,
|
||||||
|
selectedServer = viewModel.voiceSelectedServer ?: accountViewModel.account.settings.defaultFileServer,
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
onSelect = { viewModel.voiceSelectedServer = it },
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ServerSelectionRow(
|
|
||||||
viewModel: VoiceReplyViewModel,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
) {
|
|
||||||
val nip95description = stringRes(id = R.string.upload_server_relays_nip95)
|
|
||||||
val fileServersState =
|
|
||||||
accountViewModel.account.serverLists.liveServerList
|
|
||||||
.collectAsState()
|
|
||||||
val fileServers = fileServersState.value
|
|
||||||
|
|
||||||
val fileServerOptions =
|
|
||||||
remember(fileServers) {
|
|
||||||
fileServers
|
|
||||||
.map {
|
|
||||||
if (it.type == ServerType.NIP95) {
|
|
||||||
TitleExplainer(it.name, nip95description)
|
|
||||||
} else {
|
|
||||||
TitleExplainer(it.name, it.baseUrl)
|
|
||||||
}
|
|
||||||
}.toImmutableList()
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsRow(R.string.file_server, R.string.file_server_description) {
|
|
||||||
TextSpinner(
|
|
||||||
label = "",
|
|
||||||
placeholder =
|
|
||||||
fileServers
|
|
||||||
.firstOrNull { it == (viewModel.voiceSelectedServer ?: accountViewModel.account.settings.defaultFileServer) }
|
|
||||||
?.name
|
|
||||||
?: fileServers.firstOrNull()?.name
|
|
||||||
?: "",
|
|
||||||
options = fileServerOptions,
|
|
||||||
onSelect = { viewModel.voiceSelectedServer = fileServers[it] },
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(80.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ReRecordButton(viewModel: VoiceReplyViewModel) {
|
private fun ReRecordButton(viewModel: VoiceReplyViewModel) {
|
||||||
Row(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier =
|
||||||
horizontalArrangement = Arrangement.Center,
|
Modifier
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 12.dp, horizontal = Size10dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
|
if (viewModel.isUploading) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Mic,
|
||||||
|
contentDescription = stringRes(id = R.string.record_a_message),
|
||||||
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringRes(id = R.string.re_record),
|
||||||
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
RecordAudioBox(
|
RecordAudioBox(
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
onRecordTaken = { recording ->
|
onRecordTaken = { recording ->
|
||||||
@@ -266,56 +245,3 @@ private fun ReRecordButton(viewModel: VoiceReplyViewModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun VoiceUploadingProgress(orchestrator: UploadOrchestrator) {
|
|
||||||
val progressValue = orchestrator.progress.collectAsState().value
|
|
||||||
val progressStatusValue = orchestrator.progressState.collectAsState().value
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 24.dp),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.size(55.dp),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
val animatedProgress =
|
|
||||||
animateFloatAsState(
|
|
||||||
targetValue = progressValue.toFloat(),
|
|
||||||
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
|
|
||||||
).value
|
|
||||||
|
|
||||||
CircularProgressIndicator(
|
|
||||||
progress = { animatedProgress },
|
|
||||||
modifier =
|
|
||||||
Size55Modifier
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(MaterialTheme.colorScheme.background),
|
|
||||||
strokeWidth = 5.dp,
|
|
||||||
)
|
|
||||||
|
|
||||||
val txt =
|
|
||||||
when (progressStatusValue) {
|
|
||||||
is com.vitorpamplona.amethyst.service.uploads.UploadingState.Ready -> stringRes(R.string.uploading_state_ready)
|
|
||||||
is com.vitorpamplona.amethyst.service.uploads.UploadingState.Compressing -> stringRes(R.string.uploading_state_compressing)
|
|
||||||
is com.vitorpamplona.amethyst.service.uploads.UploadingState.Uploading -> stringRes(R.string.uploading_state_uploading)
|
|
||||||
is com.vitorpamplona.amethyst.service.uploads.UploadingState.ServerProcessing -> stringRes(R.string.uploading_state_server_processing)
|
|
||||||
is com.vitorpamplona.amethyst.service.uploads.UploadingState.Downloading -> stringRes(R.string.uploading_state_downloading)
|
|
||||||
is com.vitorpamplona.amethyst.service.uploads.UploadingState.Hashing -> stringRes(R.string.uploading_state_hashing)
|
|
||||||
is com.vitorpamplona.amethyst.service.uploads.UploadingState.Finished -> stringRes(R.string.uploading_state_finished)
|
|
||||||
is com.vitorpamplona.amethyst.service.uploads.UploadingState.Error -> stringRes(R.string.uploading_state_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
|
||||||
txt,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
fontSize = 10.sp,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+97
-68
@@ -41,8 +41,11 @@ import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta
|
|||||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
|
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
|
||||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
|
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
|
||||||
import com.vitorpamplona.quartz.utils.Log
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -59,6 +62,8 @@ class VoiceReplyViewModel : ViewModel() {
|
|||||||
var voiceOrchestrator: UploadOrchestrator? by mutableStateOf(null)
|
var voiceOrchestrator: UploadOrchestrator? by mutableStateOf(null)
|
||||||
var isUploading: Boolean by mutableStateOf(false)
|
var isUploading: Boolean by mutableStateOf(false)
|
||||||
|
|
||||||
|
private var uploadJob: Job? = null
|
||||||
|
|
||||||
fun init(accountVM: AccountViewModel) {
|
fun init(accountVM: AccountViewModel) {
|
||||||
this.accountViewModel = accountVM
|
this.accountViewModel = accountVM
|
||||||
}
|
}
|
||||||
@@ -109,6 +114,7 @@ class VoiceReplyViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun removeVoiceMessage() {
|
fun removeVoiceMessage() {
|
||||||
|
cancelUpload()
|
||||||
deleteVoiceLocalFile()
|
deleteVoiceLocalFile()
|
||||||
voiceRecording = null
|
voiceRecording = null
|
||||||
voiceLocalFile = null
|
voiceLocalFile = null
|
||||||
@@ -118,6 +124,11 @@ class VoiceReplyViewModel : ViewModel() {
|
|||||||
voiceOrchestrator = null
|
voiceOrchestrator = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun cancelUpload() {
|
||||||
|
uploadJob?.cancel()
|
||||||
|
uploadJob = null
|
||||||
|
}
|
||||||
|
|
||||||
private fun deleteVoiceLocalFile() {
|
private fun deleteVoiceLocalFile() {
|
||||||
voiceLocalFile?.let { file ->
|
voiceLocalFile?.let { file ->
|
||||||
try {
|
try {
|
||||||
@@ -131,101 +142,118 @@ class VoiceReplyViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun canSend(): Boolean = (voiceRecording != null || voiceMetadata != null) && !isUploading
|
fun canSend(): Boolean = voiceRecording != null && !isUploading
|
||||||
|
|
||||||
fun sendVoiceReply(onSuccess: () -> Unit) {
|
fun sendVoiceReply(onSuccess: () -> Unit) {
|
||||||
val note = replyToNote ?: return
|
val note = replyToNote ?: return
|
||||||
val recording = voiceRecording ?: return
|
val recording = voiceRecording ?: return
|
||||||
val serverToUse = voiceSelectedServer ?: accountViewModel.account.settings.defaultFileServer
|
val serverToUse = voiceSelectedServer ?: accountViewModel.account.settings.defaultFileServer
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
cancelUpload()
|
||||||
uploadAndSend(note, recording, serverToUse, onSuccess)
|
uploadJob =
|
||||||
}
|
viewModelScope.launch {
|
||||||
|
isUploading = true
|
||||||
|
val orchestrator = UploadOrchestrator()
|
||||||
|
voiceOrchestrator = orchestrator
|
||||||
|
|
||||||
|
try {
|
||||||
|
val result =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val uri = android.net.Uri.fromFile(recording.file)
|
||||||
|
orchestrator.upload(
|
||||||
|
uri = uri,
|
||||||
|
mimeType = recording.mimeType,
|
||||||
|
alt = null,
|
||||||
|
contentWarningReason = null,
|
||||||
|
compressionQuality = CompressorQuality.UNCOMPRESSED,
|
||||||
|
server = serverToUse,
|
||||||
|
account = accountViewModel.account,
|
||||||
|
context = Amethyst.instance.appContext,
|
||||||
|
useH265 = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUploadResult(note, recording, serverToUse, result, onSuccess)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
// User canceled, or ViewModel cleared.
|
||||||
|
} catch (e: Exception) {
|
||||||
|
val appContext = Amethyst.instance.appContext
|
||||||
|
val uploadErrorTitle = stringRes(appContext, R.string.upload_error_title)
|
||||||
|
val uploadVoiceExceptionMessage: (String) -> String = { detail ->
|
||||||
|
stringRes(appContext, R.string.upload_error_voice_message_exception, detail)
|
||||||
|
}
|
||||||
|
accountViewModel.toastManager.toast(
|
||||||
|
uploadErrorTitle,
|
||||||
|
uploadVoiceExceptionMessage(e.message ?: e.javaClass.simpleName),
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
isUploading = false
|
||||||
|
voiceOrchestrator = null
|
||||||
|
uploadJob = null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun uploadAndSend(
|
private suspend fun handleUploadResult(
|
||||||
note: Note,
|
note: Note,
|
||||||
recording: RecordingResult,
|
recording: RecordingResult,
|
||||||
server: ServerName,
|
server: ServerName,
|
||||||
|
result: UploadingState,
|
||||||
onSuccess: () -> Unit,
|
onSuccess: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val appContext = Amethyst.instance.appContext
|
val appContext = Amethyst.instance.appContext
|
||||||
val uploadErrorTitle = stringRes(appContext, R.string.upload_error_title)
|
val uploadErrorTitle = stringRes(appContext, R.string.upload_error_title)
|
||||||
val uploadVoiceNip95NotSupported = stringRes(appContext, R.string.upload_error_voice_message_nip95_not_supported)
|
val uploadVoiceNip95NotSupported = stringRes(appContext, R.string.upload_error_voice_message_nip95_not_supported)
|
||||||
val uploadVoiceFailed = stringRes(appContext, R.string.upload_error_voice_message_failed)
|
val uploadVoiceFailed = stringRes(appContext, R.string.upload_error_voice_message_failed)
|
||||||
val uploadVoiceExceptionMessage: (String) -> String = { detail ->
|
|
||||||
stringRes(appContext, R.string.upload_error_voice_message_exception, detail)
|
|
||||||
}
|
|
||||||
|
|
||||||
isUploading = true
|
when (result) {
|
||||||
|
is UploadingState.Finished -> {
|
||||||
try {
|
when (val orchestratorResult = result.result) {
|
||||||
val uri = android.net.Uri.fromFile(recording.file)
|
is UploadOrchestrator.OrchestratorResult.ServerResult -> {
|
||||||
val orchestrator = UploadOrchestrator()
|
val hint = note.toEventHint<VoiceEvent>()
|
||||||
voiceOrchestrator = orchestrator
|
if (hint == null) {
|
||||||
|
accountViewModel.toastManager.toast(uploadErrorTitle, uploadVoiceFailed)
|
||||||
val result =
|
return
|
||||||
orchestrator.upload(
|
|
||||||
uri = uri,
|
|
||||||
mimeType = recording.mimeType,
|
|
||||||
alt = null,
|
|
||||||
contentWarningReason = null,
|
|
||||||
compressionQuality = CompressorQuality.UNCOMPRESSED,
|
|
||||||
server = server,
|
|
||||||
account = accountViewModel.account,
|
|
||||||
context = appContext,
|
|
||||||
useH265 = false,
|
|
||||||
)
|
|
||||||
|
|
||||||
when (result) {
|
|
||||||
is UploadingState.Finished -> {
|
|
||||||
when (val orchestratorResult = result.result) {
|
|
||||||
is UploadOrchestrator.OrchestratorResult.ServerResult -> {
|
|
||||||
val audioMeta =
|
|
||||||
AudioMeta(
|
|
||||||
url = orchestratorResult.url,
|
|
||||||
mimeType = recording.mimeType,
|
|
||||||
hash = orchestratorResult.fileHeader.hash,
|
|
||||||
duration = recording.duration,
|
|
||||||
waveform = recording.amplitudes,
|
|
||||||
)
|
|
||||||
|
|
||||||
val hint = note.toEventHint<VoiceEvent>()
|
|
||||||
if (hint != null) {
|
|
||||||
accountViewModel.account.signAndComputeBroadcast(
|
|
||||||
VoiceReplyEvent.build(audioMeta, hint),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (server.type != ServerType.NIP95) {
|
|
||||||
accountViewModel.account.settings.changeDefaultFileServer(server)
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteVoiceLocalFile()
|
|
||||||
voiceLocalFile = null
|
|
||||||
voiceRecording = null
|
|
||||||
voiceMetadata = audioMeta
|
|
||||||
|
|
||||||
onSuccess()
|
|
||||||
}
|
}
|
||||||
is UploadOrchestrator.OrchestratorResult.NIP95Result -> {
|
|
||||||
accountViewModel.toastManager.toast(uploadErrorTitle, uploadVoiceNip95NotSupported)
|
val audioMeta =
|
||||||
|
AudioMeta(
|
||||||
|
url = orchestratorResult.url,
|
||||||
|
mimeType = recording.mimeType,
|
||||||
|
hash = orchestratorResult.fileHeader.hash,
|
||||||
|
duration = recording.duration,
|
||||||
|
waveform = recording.amplitudes,
|
||||||
|
)
|
||||||
|
|
||||||
|
accountViewModel.account.signAndComputeBroadcast(VoiceReplyEvent.build(audioMeta, hint))
|
||||||
|
|
||||||
|
if (server.type != ServerType.NIP95) {
|
||||||
|
accountViewModel.account.settings.changeDefaultFileServer(server)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteVoiceLocalFile()
|
||||||
|
voiceLocalFile = null
|
||||||
|
voiceRecording = null
|
||||||
|
voiceMetadata = audioMeta
|
||||||
|
|
||||||
|
onSuccess()
|
||||||
|
}
|
||||||
|
is UploadOrchestrator.OrchestratorResult.NIP95Result -> {
|
||||||
|
accountViewModel.toastManager.toast(uploadErrorTitle, uploadVoiceNip95NotSupported)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is UploadingState.Error -> {
|
|
||||||
accountViewModel.toastManager.toast(uploadErrorTitle, uploadVoiceFailed)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
is UploadingState.Error -> {
|
||||||
accountViewModel.toastManager.toast(uploadErrorTitle, uploadVoiceExceptionMessage(e.message ?: e.javaClass.simpleName))
|
accountViewModel.toastManager.toast(uploadErrorTitle, uploadVoiceFailed)
|
||||||
} finally {
|
}
|
||||||
isUploading = false
|
else -> {
|
||||||
voiceOrchestrator = null
|
accountViewModel.toastManager.toast(uploadErrorTitle, uploadVoiceFailed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancel() {
|
fun cancel() {
|
||||||
|
cancelUpload()
|
||||||
deleteVoiceLocalFile()
|
deleteVoiceLocalFile()
|
||||||
voiceRecording = null
|
voiceRecording = null
|
||||||
voiceLocalFile = null
|
voiceLocalFile = null
|
||||||
@@ -236,6 +264,7 @@ class VoiceReplyViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
cancel()
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
|
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user