Merge branch 'claude/admiring-mahavira-a862d1' into artifacts
This commit is contained in:
@@ -14,4 +14,7 @@ interface MantraChunkDao {
|
||||
|
||||
@Query("SELECT * FROM MantraChunk WHERE chapterId = :chapterId ORDER BY `index` ASC")
|
||||
suspend fun getChunksByChapterId(chapterId: String): List<MantraChunk>
|
||||
|
||||
@Query("SELECT * FROM MantraChunk WHERE id = :id")
|
||||
suspend fun getChunkById(id: String): MantraChunk?
|
||||
}
|
||||
@@ -14,4 +14,7 @@ interface MantraTranslationChunkDao {
|
||||
|
||||
@Query("SELECT * FROM MantraTranslationChunk WHERE translationChapterId = :translationChapterId ORDER BY `index` ASC")
|
||||
suspend fun getTranslationChunksByTranslationChapterId(translationChapterId: String): List<MantraTranslationChunk>
|
||||
|
||||
@Query("DELETE FROM MantraTranslationChunk WHERE id = :id")
|
||||
suspend fun deleteById(id: String)
|
||||
}
|
||||
@@ -14,4 +14,7 @@ interface MarmotInnerEventDao {
|
||||
|
||||
@Upsert
|
||||
suspend fun upsert(marmotInnerEvent: MarmotInnerEvent)
|
||||
|
||||
@Query("DELETE FROM MarmotInnerEvent WHERE id = :id")
|
||||
suspend fun deleteById(id: String)
|
||||
}
|
||||
@@ -158,6 +158,63 @@ class DatabaseMantraRepository(
|
||||
override suspend fun getChunksForChapter(chapterId: String): List<MantraChunk> =
|
||||
database.mantraChunkDao().getChunksByChapterId(chapterId)
|
||||
|
||||
override suspend fun getChunk(id: String): MantraChunk? =
|
||||
database.mantraChunkDao().getChunkById(id)
|
||||
|
||||
override suspend fun saveTranslationChunk(
|
||||
translationChapterId: String,
|
||||
chunkId: String,
|
||||
text: String,
|
||||
chatRoomId: String,
|
||||
userPublicKey: HexKey,
|
||||
): MantraTranslationChunk? {
|
||||
// The translation chunk mirrors the source chunk's position.
|
||||
val sourceChunk = database.mantraChunkDao().getChunkById(chunkId) ?: return null
|
||||
|
||||
val translationChunkTemplate = TranslationChunkEvent.build(
|
||||
translationChapterId = translationChapterId,
|
||||
chunkId = chunkId,
|
||||
index = sourceChunk.index,
|
||||
text = text,
|
||||
)
|
||||
val translationChunk = MantraTranslationChunk.fromTranslationChunkEventTemplate(
|
||||
translationChunkEventTemplate = translationChunkTemplate,
|
||||
chatRoomId = chatRoomId,
|
||||
userPublicKey = userPublicKey,
|
||||
) ?: return null
|
||||
|
||||
return try {
|
||||
// Replace any existing translation chunk for this source chunk. Its id
|
||||
// is derived from the (now changed) content, so it becomes a new row —
|
||||
// drop the old one (and its rumor) to keep one per source chunk.
|
||||
database.mantraTranslationChunkDao()
|
||||
.getTranslationChunksByTranslationChapterId(translationChapterId)
|
||||
.filter { it.chunkId == chunkId && it.id != translationChunk.id }
|
||||
.forEach { stale ->
|
||||
database.mantraTranslationChunkDao().deleteById(stale.id)
|
||||
database.marmotInnerEventDao().deleteById(stale.id)
|
||||
}
|
||||
|
||||
database.mantraTranslationChunkDao().upsert(translationChunk)
|
||||
database.marmotInnerEventDao().upsert(
|
||||
MarmotInnerEvent(
|
||||
id = translationChunk.id,
|
||||
publicKey = translationChunk.publicKey,
|
||||
kind = TranslationChunkEvent.KIND,
|
||||
createdAt = translationChunk.createdAt,
|
||||
tags = translationChunkTemplate.tags,
|
||||
content = translationChunkTemplate.content,
|
||||
chatRoomId = translationChunk.chatRoomId,
|
||||
)
|
||||
)
|
||||
|
||||
translationChunk
|
||||
} catch (error: Throwable) {
|
||||
logger.e("Failed to save translation for chunk $chunkId", error)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getTranslationsForArtifact(artifactId: String): List<MantraTranslationArtifactVersion> =
|
||||
database.mantraTranslationArtifactVersionDao().getTranslationsByArtifactId(artifactId)
|
||||
|
||||
|
||||
@@ -26,6 +26,22 @@ interface MantraRepository {
|
||||
|
||||
suspend fun getChunksForChapter(chapterId: String): List<MantraChunk>
|
||||
|
||||
suspend fun getChunk(id: String): MantraChunk?
|
||||
|
||||
/**
|
||||
* Create or replace the translation of a source chunk within a translation
|
||||
* chapter. Any existing translation chunk for the same (translationChapterId,
|
||||
* chunkId) is replaced. Returns the saved chunk, or null if the source chunk
|
||||
* can't be found.
|
||||
*/
|
||||
suspend fun saveTranslationChunk(
|
||||
translationChapterId: String,
|
||||
chunkId: String,
|
||||
text: String,
|
||||
chatRoomId: String,
|
||||
userPublicKey: HexKey,
|
||||
): MantraTranslationChunk?
|
||||
|
||||
suspend fun getTranslationsForArtifact(artifactId: String): List<MantraTranslationArtifactVersion>
|
||||
|
||||
suspend fun getTranslation(id: String): MantraTranslationArtifactVersion?
|
||||
@@ -113,6 +129,16 @@ interface MantraRepository {
|
||||
|
||||
override suspend fun getChunksForChapter(chapterId: String): List<MantraChunk> = emptyList()
|
||||
|
||||
override suspend fun getChunk(id: String): MantraChunk? = null
|
||||
|
||||
override suspend fun saveTranslationChunk(
|
||||
translationChapterId: String,
|
||||
chunkId: String,
|
||||
text: String,
|
||||
chatRoomId: String,
|
||||
userPublicKey: HexKey,
|
||||
): MantraTranslationChunk? = null
|
||||
|
||||
override suspend fun getTranslationsForArtifact(artifactId: String): List<MantraTranslationArtifactVersion> = emptyList()
|
||||
|
||||
override suspend fun getTranslation(id: String): MantraTranslationArtifactVersion? = null
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
package press.mantra.compose.ui.composable
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Save
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import press.mantra.compose.database.model.MantraChunk
|
||||
import press.mantra.compose.repository.MantraRepository
|
||||
import press.mantra.compose.ui.composable.navigation.routes.Route
|
||||
import press.mantra.compose.ui.composable.navigation.routes.TranslationChapterRoute
|
||||
import press.mantra.compose.ui.composable.widgets.LoadingDataIndicator
|
||||
import press.mantra.compose.ui.theme.TorchTheme
|
||||
import press.mantra.compose.ui.view.model.TranslateChunkViewModel
|
||||
import press.mantra.compose.ui.view.state.TranslateChunkUIState
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TranslateChunkScreen(
|
||||
activeUserPublicKey: HexKey,
|
||||
translationChapterId: String,
|
||||
chunkId: String,
|
||||
chatRoomId: String,
|
||||
relayHint: String?,
|
||||
initialTranslateChunkUIState: TranslateChunkUIState = TranslateChunkUIState.Loading,
|
||||
mantraRepository: MantraRepository,
|
||||
onNavigateToRoute: (Route) -> Unit,
|
||||
onNavigateBack: () -> Unit,
|
||||
) {
|
||||
val translateChunkViewModel: TranslateChunkViewModel = viewModel(
|
||||
factory = TranslateChunkViewModel.factory(
|
||||
activeUserPublicKey = activeUserPublicKey,
|
||||
translationChapterId = translationChapterId,
|
||||
chunkId = chunkId,
|
||||
chatRoomId = chatRoomId,
|
||||
relayHint = relayHint,
|
||||
initialTranslateChunkUIState = initialTranslateChunkUIState,
|
||||
mantraRepository = mantraRepository,
|
||||
)
|
||||
)
|
||||
|
||||
when (val translateChunkUIState = translateChunkViewModel.translateChunkUIState) {
|
||||
is TranslateChunkUIState.Error -> {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(50.dp))
|
||||
Text(text = translateChunkUIState.message)
|
||||
}
|
||||
}
|
||||
|
||||
is TranslateChunkUIState.Loaded -> {
|
||||
val translationFieldState = rememberTextFieldState(translateChunkUIState.existingTranslationText)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Translate chunk") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back"
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
BottomAppBar(
|
||||
actions = {},
|
||||
floatingActionButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
translateChunkViewModel.saveTranslation(
|
||||
translationField = translationFieldState,
|
||||
onSuccess = {
|
||||
// Return to a freshly-loaded chapter table so the
|
||||
// saved translation is reflected.
|
||||
onNavigateToRoute.invoke(
|
||||
TranslationChapterRoute(
|
||||
activeUserPublicKey = activeUserPublicKey,
|
||||
translationChapterId = translationChapterId,
|
||||
chatRoomId = chatRoomId,
|
||||
relayHint = relayHint
|
||||
)
|
||||
)
|
||||
},
|
||||
onFailure = {}
|
||||
)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Save,
|
||||
contentDescription = "Save translation"
|
||||
)
|
||||
Text("Save")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Original",
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
Card {
|
||||
Text(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
text = translateChunkUIState.originalChunk.text,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Translation",
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = translationFieldState,
|
||||
label = { Text("Translated text") },
|
||||
placeholder = { Text("Enter the translation for this chunk") },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TranslateChunkUIState.Loading -> {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
text = "Translate Chunk",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
LoadingDataIndicator(fillScreen = false)
|
||||
Spacer(modifier = Modifier.weight(2f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(true) {
|
||||
if (initialTranslateChunkUIState == TranslateChunkUIState.Loading) {
|
||||
translateChunkViewModel.initiateTranslateChunk()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun TranslateChunkScreenPreview() {
|
||||
TorchTheme {
|
||||
Surface(modifier = Modifier.fillMaxSize()) {
|
||||
TranslateChunkScreen(
|
||||
activeUserPublicKey = "",
|
||||
translationChapterId = "translationChapterId",
|
||||
chunkId = "chunkId",
|
||||
chatRoomId = "chatRoomId",
|
||||
relayHint = null,
|
||||
initialTranslateChunkUIState = TranslateChunkUIState.Loaded(
|
||||
originalChunk = MantraChunk(
|
||||
id = "chunkId",
|
||||
chapterId = "chapterId",
|
||||
publicKey = "author",
|
||||
text = "When he was nearly thirteen, my brother Jem got his arm badly broken at the elbow.",
|
||||
index = 0,
|
||||
wordCount = 0,
|
||||
characterCount = 0,
|
||||
signature = "",
|
||||
chatRoomId = "chatRoomId"
|
||||
),
|
||||
existingTranslationText = ""
|
||||
),
|
||||
mantraRepository = MantraRepository.NO_OP_MANTRA_REPOSITORY,
|
||||
onNavigateToRoute = {},
|
||||
onNavigateBack = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,11 +10,13 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ChevronRight
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -23,6 +25,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.VerticalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -37,6 +40,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import press.mantra.compose.database.model.MantraChunk
|
||||
import press.mantra.compose.repository.MantraRepository
|
||||
import press.mantra.compose.ui.composable.navigation.routes.Route
|
||||
import press.mantra.compose.ui.composable.navigation.routes.TranslateChunkRoute
|
||||
import press.mantra.compose.ui.composable.widgets.LoadingDataIndicator
|
||||
import press.mantra.compose.ui.theme.TorchTheme
|
||||
import press.mantra.compose.ui.view.model.TranslationChapterViewModel
|
||||
@@ -52,6 +57,7 @@ fun TranslationChapterScreen(
|
||||
relayHint: String?,
|
||||
initialTranslationChapterUIState: TranslationChapterUIState = TranslationChapterUIState.Loading,
|
||||
mantraRepository: MantraRepository,
|
||||
onNavigateToRoute: (Route) -> Unit,
|
||||
onNavigateBack: () -> Unit,
|
||||
) {
|
||||
val translationChapterViewModel: TranslationChapterViewModel = viewModel(
|
||||
@@ -128,7 +134,20 @@ fun TranslationChapterScreen(
|
||||
items = translationChapterUIState.rows,
|
||||
key = { pair -> pair.originalChunk.id }
|
||||
) { pair ->
|
||||
ChunkTranslationRow(pair)
|
||||
ChunkTranslationRow(
|
||||
pair = pair,
|
||||
onClick = {
|
||||
onNavigateToRoute.invoke(
|
||||
TranslateChunkRoute(
|
||||
activeUserPublicKey = activeUserPublicKey,
|
||||
translationChapterId = translationChapterId,
|
||||
chunkId = pair.originalChunk.id,
|
||||
chatRoomId = chatRoomId,
|
||||
relayHint = relayHint
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
@@ -162,18 +181,35 @@ fun TranslationChapterScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChunkTranslationRow(pair: ChunkTranslationPair) {
|
||||
private fun ChunkTranslationRow(
|
||||
pair: ChunkTranslationPair,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val translated = pair.translationChunk?.text?.takeIf { it.isNotBlank() }
|
||||
TableRow(
|
||||
left = { Text(pair.originalChunk.text) },
|
||||
right = {
|
||||
if (translated != null) {
|
||||
Text(translated)
|
||||
} else {
|
||||
// No translation yet: show the original as a greyed-out placeholder.
|
||||
// The translation cell is a button that opens the chunk translation
|
||||
// editor. When there is no translation yet, the original text is
|
||||
// shown greyed out as a placeholder.
|
||||
TextButton(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
) {
|
||||
Text(
|
||||
text = pair.originalChunk.text,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||
modifier = Modifier.weight(1f),
|
||||
text = translated ?: pair.originalChunk.text,
|
||||
color = if (translated != null) {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||
}
|
||||
)
|
||||
Icon(
|
||||
Icons.Default.ChevronRight,
|
||||
contentDescription = "Translate this chunk",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -219,6 +255,7 @@ private fun TranslationChapterScreenPreview() {
|
||||
)
|
||||
),
|
||||
mantraRepository = MantraRepository.NO_OP_MANTRA_REPOSITORY,
|
||||
onNavigateToRoute = {},
|
||||
onNavigateBack = {}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -93,12 +93,14 @@ import press.mantra.compose.ui.composable.navigation.routes.AddChapterRoute
|
||||
import press.mantra.compose.ui.composable.navigation.routes.AddTranslationRoute
|
||||
import press.mantra.compose.ui.composable.navigation.routes.ArtifactDetailRoute
|
||||
import press.mantra.compose.ui.composable.navigation.routes.ChapterDetailRoute
|
||||
import press.mantra.compose.ui.composable.navigation.routes.TranslateChunkRoute
|
||||
import press.mantra.compose.ui.composable.navigation.routes.TranslationChapterRoute
|
||||
import press.mantra.compose.ui.composable.navigation.routes.TranslationDetailRoute
|
||||
import press.mantra.compose.ui.composable.ArtifactDetailScreen
|
||||
import press.mantra.compose.ui.composable.AddChapterScreen
|
||||
import press.mantra.compose.ui.composable.AddTranslationScreen
|
||||
import press.mantra.compose.ui.composable.ChapterDetailScreen
|
||||
import press.mantra.compose.ui.composable.TranslateChunkScreen
|
||||
import press.mantra.compose.ui.composable.TranslationChapterScreen
|
||||
import press.mantra.compose.ui.composable.TranslationDetailScreen
|
||||
|
||||
@@ -748,6 +750,35 @@ fun MantraNavHost(
|
||||
chatRoomId = route.chatRoomId,
|
||||
relayHint = route.relayHint,
|
||||
mantraRepository = databaseMantraRepository,
|
||||
onNavigateToRoute = { actionRoute ->
|
||||
navController.navigate(
|
||||
route = actionRoute
|
||||
)
|
||||
},
|
||||
onNavigateBack = {
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
composable<TranslateChunkRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<TranslateChunkRoute>()
|
||||
|
||||
TranslateChunkScreen(
|
||||
activeUserPublicKey = route.activeUserPublicKey,
|
||||
translationChapterId = route.translationChapterId,
|
||||
chunkId = route.chunkId,
|
||||
chatRoomId = route.chatRoomId,
|
||||
relayHint = route.relayHint,
|
||||
mantraRepository = databaseMantraRepository,
|
||||
onNavigateToRoute = { actionRoute ->
|
||||
// Replace this editor and the stale chapter table beneath it so
|
||||
// we land on a freshly-loaded table reflecting the saved translation.
|
||||
navController.navigate(route = actionRoute) {
|
||||
popUpTo<TranslationChapterRoute> {
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
},
|
||||
onNavigateBack = {
|
||||
navController.popBackStack()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package press.mantra.compose.ui.composable.navigation.routes
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class TranslateChunkRoute(
|
||||
val activeUserPublicKey: String,
|
||||
val translationChapterId: String,
|
||||
val chunkId: String,
|
||||
val chatRoomId: String,
|
||||
val relayHint: String?
|
||||
): Route()
|
||||
@@ -0,0 +1,116 @@
|
||||
package press.mantra.compose.ui.view.model
|
||||
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.launch
|
||||
import press.mantra.compose.repository.MantraRepository
|
||||
import press.mantra.compose.ui.view.state.TranslateChunkUIState
|
||||
|
||||
class TranslateChunkViewModel(
|
||||
val translationChapterId: String,
|
||||
val chunkId: String,
|
||||
val chatRoomId: String,
|
||||
val activeUserPublicKey: HexKey,
|
||||
val relayHint: String?,
|
||||
initialTranslateChunkUIState: TranslateChunkUIState,
|
||||
val mantraRepository: MantraRepository,
|
||||
): ViewModel() {
|
||||
|
||||
var translateChunkUIState: TranslateChunkUIState by mutableStateOf(initialTranslateChunkUIState)
|
||||
private set
|
||||
|
||||
private val logger = Logger.withTag(TAG)
|
||||
|
||||
val isActionPending: MutableState<Boolean> = mutableStateOf(false)
|
||||
|
||||
fun initiateTranslateChunk() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val originalChunk = mantraRepository.getChunk(chunkId)
|
||||
translateChunkUIState = if (originalChunk == null) {
|
||||
TranslateChunkUIState.Error("Couldn't find the chunk")
|
||||
} else {
|
||||
val existing = mantraRepository.getTranslationChunks(translationChapterId)
|
||||
.firstOrNull { it.chunkId == chunkId }
|
||||
TranslateChunkUIState.Loaded(
|
||||
originalChunk = originalChunk,
|
||||
existingTranslationText = existing?.text.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveTranslation(
|
||||
translationField: TextFieldState,
|
||||
onSuccess: () -> Unit,
|
||||
onFailure: () -> Unit
|
||||
) {
|
||||
val text = translationField.text.toString()
|
||||
if (text.isBlank()) {
|
||||
onFailure.invoke()
|
||||
return
|
||||
}
|
||||
|
||||
if (isActionPending.value) return
|
||||
isActionPending.value = true
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val saved = runCatching {
|
||||
mantraRepository.saveTranslationChunk(
|
||||
translationChapterId = translationChapterId,
|
||||
chunkId = chunkId,
|
||||
text = text,
|
||||
chatRoomId = chatRoomId,
|
||||
userPublicKey = activeUserPublicKey,
|
||||
)
|
||||
}.onFailure { error ->
|
||||
logger.e("Failed to save translation", error)
|
||||
}.getOrNull()
|
||||
|
||||
if (saved != null) {
|
||||
onSuccess.invoke()
|
||||
} else {
|
||||
onFailure.invoke()
|
||||
}
|
||||
|
||||
isActionPending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "TranslateChunkViewModel"
|
||||
|
||||
fun factory(
|
||||
activeUserPublicKey: HexKey,
|
||||
translationChapterId: String,
|
||||
chunkId: String,
|
||||
chatRoomId: String,
|
||||
relayHint: String?,
|
||||
initialTranslateChunkUIState: TranslateChunkUIState = TranslateChunkUIState.Loading,
|
||||
mantraRepository: MantraRepository,
|
||||
): ViewModelProvider.Factory = viewModelFactory {
|
||||
initializer {
|
||||
TranslateChunkViewModel(
|
||||
activeUserPublicKey = activeUserPublicKey,
|
||||
translationChapterId = translationChapterId,
|
||||
chunkId = chunkId,
|
||||
chatRoomId = chatRoomId,
|
||||
relayHint = relayHint,
|
||||
initialTranslateChunkUIState = initialTranslateChunkUIState,
|
||||
mantraRepository = mantraRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package press.mantra.compose.ui.view.state
|
||||
|
||||
import press.mantra.compose.database.model.MantraChunk
|
||||
|
||||
sealed interface TranslateChunkUIState {
|
||||
data class Loaded(
|
||||
val originalChunk: MantraChunk,
|
||||
val existingTranslationText: String = "",
|
||||
): TranslateChunkUIState
|
||||
|
||||
data class Error(
|
||||
val message: String
|
||||
): TranslateChunkUIState
|
||||
|
||||
data object Loading: TranslateChunkUIState
|
||||
}
|
||||
Reference in New Issue
Block a user