From 2170f434dc502f1eb1ab202fc199e16a141f3235 Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Sun, 26 Jul 2026 02:19:30 +0200 Subject: [PATCH] Add TranslationChapter screen with side-by-side chunks Chapter cards in the translation detail now open a two-column chapter translation screen. - The screen loads the source MantraChapter's chunks paired with their MantraTranslationChunks (by chunkId). Column one is the original chunks, column two the translated chunks; the header row shows the original dialect name and the translation dialect name. When a chunk has no translation (missing or blank text), the original text is shown greyed out as a placeholder in the second column. - The ViewModel resolves the original dialect by walking chapter -> version -> artifact -> dialect and the translation dialect via the translation version. New DAO queries getArtifactVersionById and getTranslationChapterById plus repository accessors getArtifactVersion/getTranslationChapter/getDialect. - New TranslationChapterRoute + screen/ViewModel/UIState; TranslationDetail chapter cards navigate to it. Co-Authored-By: Claude Opus 4.8 --- .../database/dao/MantraArtifactVersionDao.kt | 3 + .../dao/MantraTranslationChapterDao.kt | 3 + .../repository/DatabaseMantraRepository.kt | 9 + .../compose/repository/MantraRepository.kt | 12 + .../ui/composable/TranslationChapterScreen.kt | 238 ++++++++++++++++++ .../ui/composable/TranslationDetailScreen.kt | 17 +- .../ui/composable/navigation/MantraNavHost.kt | 21 ++ .../routes/TranslationChapterRoute.kt | 11 + .../view/model/TranslationChapterViewModel.kt | 98 ++++++++ .../view/state/TranslationChapterUIState.kt | 28 +++ 10 files changed, 439 insertions(+), 1 deletion(-) create mode 100644 composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/TranslationChapterScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/routes/TranslationChapterRoute.kt create mode 100644 composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/TranslationChapterViewModel.kt create mode 100644 composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/state/TranslationChapterUIState.kt diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/MantraArtifactVersionDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/MantraArtifactVersionDao.kt index edfc7861..6e2d73c9 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/MantraArtifactVersionDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/MantraArtifactVersionDao.kt @@ -14,4 +14,7 @@ interface MantraArtifactVersionDao { @Query("SELECT * FROM MantraArtifactVersion WHERE artifactId = :artifactId ORDER BY createdAt DESC") suspend fun getArtifactVersionsByArtifactId(artifactId: String): List + + @Query("SELECT * FROM MantraArtifactVersion WHERE id = :id") + suspend fun getArtifactVersionById(id: String): MantraArtifactVersion? } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/MantraTranslationChapterDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/MantraTranslationChapterDao.kt index 0cdcf176..7812724b 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/MantraTranslationChapterDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/MantraTranslationChapterDao.kt @@ -14,4 +14,7 @@ interface MantraTranslationChapterDao { @Query("SELECT * FROM MantraTranslationChapter WHERE translationArtifactVersionId = :translationArtifactVersionId ORDER BY `index` ASC") suspend fun getTranslationChaptersByTranslationArtifactVersionId(translationArtifactVersionId: String): List + + @Query("SELECT * FROM MantraTranslationChapter WHERE id = :id") + suspend fun getTranslationChapterById(id: String): MantraTranslationChapter? } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/repository/DatabaseMantraRepository.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/repository/DatabaseMantraRepository.kt index 06b2984c..553ecea9 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/repository/DatabaseMantraRepository.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/repository/DatabaseMantraRepository.kt @@ -41,6 +41,9 @@ class DatabaseMantraRepository( override suspend fun getArtifactVersions(artifactId: String): List = database.mantraArtifactVersionDao().getArtifactVersionsByArtifactId(artifactId) + override suspend fun getArtifactVersion(id: String): MantraArtifactVersion? = + database.mantraArtifactVersionDao().getArtifactVersionById(id) + override suspend fun getChaptersForArtifact(artifactId: String): List = database.mantraChapterDao().getChaptersByArtifactId(artifactId) @@ -161,6 +164,9 @@ class DatabaseMantraRepository( override suspend fun getTranslation(id: String): MantraTranslationArtifactVersion? = database.mantraTranslationArtifactVersionDao().getTranslationById(id) + override suspend fun getTranslationChapter(id: String): MantraTranslationChapter? = + database.mantraTranslationChapterDao().getTranslationChapterById(id) + override suspend fun getTranslationChapters(translationArtifactVersionId: String): List = database.mantraTranslationChapterDao().getTranslationChaptersByTranslationArtifactVersionId(translationArtifactVersionId) @@ -251,6 +257,9 @@ class DatabaseMantraRepository( override suspend fun getDialects(chatRoomId: String): List = database.mantraDialectDao().getDialectsByChatRoomId(chatRoomId) + override suspend fun getDialect(id: String): MantraDialect? = + database.mantraDialectDao().getDialectById(id) + override suspend fun addDialect( name: String, country: String, diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/repository/MantraRepository.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/repository/MantraRepository.kt index 7e4fa5fb..8c129622 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/repository/MantraRepository.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/repository/MantraRepository.kt @@ -18,6 +18,8 @@ interface MantraRepository { suspend fun getArtifactVersions(artifactId: String): List + suspend fun getArtifactVersion(id: String): MantraArtifactVersion? + suspend fun getChaptersForArtifact(artifactId: String): List suspend fun getChapter(id: String): MantraChapter? @@ -28,6 +30,8 @@ interface MantraRepository { suspend fun getTranslation(id: String): MantraTranslationArtifactVersion? + suspend fun getTranslationChapter(id: String): MantraTranslationChapter? + suspend fun getTranslationChapters(translationArtifactVersionId: String): List suspend fun getTranslationChunks(translationChapterId: String): List @@ -62,6 +66,8 @@ interface MantraRepository { suspend fun getDialects(chatRoomId: String): List + suspend fun getDialect(id: String): MantraDialect? + suspend fun addDialect( name: String, country: String, @@ -99,6 +105,8 @@ interface MantraRepository { override suspend fun getArtifactVersions(artifactId: String): List = emptyList() + override suspend fun getArtifactVersion(id: String): MantraArtifactVersion? = null + override suspend fun getChaptersForArtifact(artifactId: String): List = emptyList() override suspend fun getChapter(id: String): MantraChapter? = null @@ -109,6 +117,8 @@ interface MantraRepository { override suspend fun getTranslation(id: String): MantraTranslationArtifactVersion? = null + override suspend fun getTranslationChapter(id: String): MantraTranslationChapter? = null + override suspend fun getTranslationChapters(translationArtifactVersionId: String): List = emptyList() override suspend fun getTranslationChunks(translationChapterId: String): List = emptyList() @@ -130,6 +140,8 @@ interface MantraRepository { override suspend fun getDialects(chatRoomId: String): List = emptyList() + override suspend fun getDialect(id: String): MantraDialect? = null + override suspend fun addDialect( name: String, country: String, diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/TranslationChapterScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/TranslationChapterScreen.kt new file mode 100644 index 00000000..c988b079 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/TranslationChapterScreen.kt @@ -0,0 +1,238 @@ +package press.mantra.compose.ui.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.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.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.VerticalDivider +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.font.FontWeight +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.widgets.LoadingDataIndicator +import press.mantra.compose.ui.theme.TorchTheme +import press.mantra.compose.ui.view.model.TranslationChapterViewModel +import press.mantra.compose.ui.view.state.ChunkTranslationPair +import press.mantra.compose.ui.view.state.TranslationChapterUIState + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TranslationChapterScreen( + activeUserPublicKey: HexKey, + translationChapterId: String, + chatRoomId: String, + relayHint: String?, + initialTranslationChapterUIState: TranslationChapterUIState = TranslationChapterUIState.Loading, + mantraRepository: MantraRepository, + onNavigateBack: () -> Unit, +) { + val translationChapterViewModel: TranslationChapterViewModel = viewModel( + factory = TranslationChapterViewModel.factory( + activeUserPublicKey = activeUserPublicKey, + translationChapterId = translationChapterId, + chatRoomId = chatRoomId, + relayHint = relayHint, + initialTranslationChapterUIState = initialTranslationChapterUIState, + mantraRepository = mantraRepository, + ) + ) + + when (val translationChapterUIState = translationChapterViewModel.translationChapterUIState) { + is TranslationChapterUIState.Error -> { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(50.dp)) + Text(text = translationChapterUIState.message) + } + } + + is TranslationChapterUIState.Loaded -> { + Scaffold( + topBar = { + TopAppBar( + title = { Text("Chapter translation") }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" + ) + } + }, + ) + } + ) { innerPadding -> + LazyColumn( + modifier = Modifier.padding(innerPadding).fillMaxSize() + ) { + // Header row: dialect names. + item { + TableRow( + left = { + Text( + text = translationChapterUIState.originalDialectName, + fontWeight = FontWeight.Bold + ) + }, + right = { + Text( + text = translationChapterUIState.translationDialectName, + fontWeight = FontWeight.Bold + ) + }, + ) + HorizontalDivider() + } + + if (translationChapterUIState.rows.isEmpty()) { + item { + Box( + modifier = Modifier.fillMaxWidth().padding(16.dp), + contentAlignment = Alignment.Center + ) { + Text("This chapter has no chunks.") + } + } + } else { + items( + items = translationChapterUIState.rows, + key = { pair -> pair.originalChunk.id } + ) { pair -> + ChunkTranslationRow(pair) + HorizontalDivider() + } + } + } + } + } + + TranslationChapterUIState.Loading -> { + Column( + modifier = Modifier.fillMaxWidth().padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + Spacer(modifier = Modifier.weight(1f)) + Text( + text = "Chapter Translation", + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center + ) + LoadingDataIndicator(fillScreen = false) + Spacer(modifier = Modifier.weight(2f)) + } + } + } + + LaunchedEffect(true) { + if (initialTranslationChapterUIState == TranslationChapterUIState.Loading) { + translationChapterViewModel.initiateTranslationChapter() + } + } +} + +@Composable +private fun ChunkTranslationRow(pair: ChunkTranslationPair) { + 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. + Text( + text = pair.originalChunk.text, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) + ) + } + }, + ) +} + +@Composable +private fun TableRow( + left: @Composable () -> Unit, + right: @Composable () -> Unit, +) { + Row( + modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min) + ) { + Box(modifier = Modifier.weight(1f).padding(12.dp)) { left() } + VerticalDivider(modifier = Modifier.fillMaxHeight()) + Box(modifier = Modifier.weight(1f).padding(12.dp)) { right() } + } +} + +@Preview +@Composable +private fun TranslationChapterScreenPreview() { + TorchTheme { + Surface(modifier = Modifier.fillMaxSize()) { + TranslationChapterScreen( + activeUserPublicKey = "", + translationChapterId = "translationChapterId", + chatRoomId = "chatRoomId", + relayHint = null, + initialTranslationChapterUIState = TranslationChapterUIState.Loaded( + originalDialectName = "English", + translationDialectName = "Sesotho", + rows = listOf( + ChunkTranslationPair( + originalChunk = sampleChunk("c1", "When he was nearly thirteen, my brother Jem got his arm badly broken."), + translationChunk = null + ), + ChunkTranslationPair( + originalChunk = sampleChunk("c2", "When enough years had gone by to enable us to look back on them..."), + translationChunk = null + ), + ) + ), + mantraRepository = MantraRepository.NO_OP_MANTRA_REPOSITORY, + onNavigateBack = {} + ) + } + } +} + +private fun sampleChunk(id: String, text: String) = MantraChunk( + id = id, + chapterId = "chapterId", + publicKey = "author", + text = text, + index = 0, + wordCount = 0, + characterCount = 0, + signature = "", + chatRoomId = "chatRoomId" +) diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/TranslationDetailScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/TranslationDetailScreen.kt index 48000503..61c2fa0c 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/TranslationDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/TranslationDetailScreen.kt @@ -34,6 +34,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.quartz.nip01Core.core.HexKey import press.mantra.compose.database.model.MantraTranslationArtifactVersion 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.DetailRow import press.mantra.compose.ui.composable.widgets.LoadingDataIndicator import press.mantra.compose.ui.theme.TorchTheme @@ -49,6 +51,7 @@ fun TranslationDetailScreen( relayHint: String?, initialTranslationDetailUIState: TranslationDetailUIState = TranslationDetailUIState.Loading, mantraRepository: MantraRepository, + onNavigateToRoute: (Route) -> Unit, onNavigateBack: () -> Unit, ) { val translationDetailViewModel: TranslationDetailViewModel = viewModel( @@ -129,7 +132,18 @@ fun TranslationDetailScreen( items = translationDetailUIState.chapters, key = { progress -> progress.chapter.id } ) { progress -> - Card { + Card( + onClick = { + onNavigateToRoute.invoke( + TranslationChapterRoute( + activeUserPublicKey = activeUserPublicKey, + translationChapterId = progress.chapter.id, + chatRoomId = chatRoomId, + relayHint = relayHint + ) + ) + } + ) { ListItem( leadingContent = { Icon( @@ -198,6 +212,7 @@ private fun TranslationDetailScreenPreview() { ) ), mantraRepository = MantraRepository.NO_OP_MANTRA_REPOSITORY, + onNavigateToRoute = {}, onNavigateBack = {} ) } diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/MantraNavHost.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/MantraNavHost.kt index 4d8e9c81..337aebfe 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/MantraNavHost.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/MantraNavHost.kt @@ -93,11 +93,13 @@ 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.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.TranslationChapterScreen import press.mantra.compose.ui.composable.TranslationDetailScreen @Composable @@ -727,6 +729,25 @@ fun MantraNavHost( chatRoomId = route.chatRoomId, relayHint = route.relayHint, mantraRepository = databaseMantraRepository, + onNavigateToRoute = { actionRoute -> + navController.navigate( + route = actionRoute + ) + }, + onNavigateBack = { + navController.popBackStack() + } + ) + } + composable { backStackEntry -> + val route = backStackEntry.toRoute() + + TranslationChapterScreen( + activeUserPublicKey = route.activeUserPublicKey, + translationChapterId = route.translationChapterId, + chatRoomId = route.chatRoomId, + relayHint = route.relayHint, + mantraRepository = databaseMantraRepository, onNavigateBack = { navController.popBackStack() } diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/routes/TranslationChapterRoute.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/routes/TranslationChapterRoute.kt new file mode 100644 index 00000000..c07cebd1 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/routes/TranslationChapterRoute.kt @@ -0,0 +1,11 @@ +package press.mantra.compose.ui.composable.navigation.routes + +import kotlinx.serialization.Serializable + +@Serializable +data class TranslationChapterRoute( + val activeUserPublicKey: String, + val translationChapterId: String, + val chatRoomId: String, + val relayHint: String? +): Route() diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/TranslationChapterViewModel.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/TranslationChapterViewModel.kt new file mode 100644 index 00000000..e98828f7 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/TranslationChapterViewModel.kt @@ -0,0 +1,98 @@ +package press.mantra.compose.ui.view.model + +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.ChunkTranslationPair +import press.mantra.compose.ui.view.state.TranslationChapterUIState + +class TranslationChapterViewModel( + val translationChapterId: String, + val chatRoomId: String, + val activeUserPublicKey: HexKey, + val relayHint: String?, + initialTranslationChapterUIState: TranslationChapterUIState, + val mantraRepository: MantraRepository, +): ViewModel() { + + var translationChapterUIState: TranslationChapterUIState by mutableStateOf(initialTranslationChapterUIState) + private set + + private val logger = Logger.withTag(TAG) + + fun initiateTranslationChapter() { + logger.d("initiateTranslationChapter: $translationChapterId") + viewModelScope.launch(Dispatchers.IO) { + val translationChapter = mantraRepository.getTranslationChapter(translationChapterId) + val sourceChapter = translationChapter?.let { mantraRepository.getChapter(it.chapterId) } + + translationChapterUIState = if (translationChapter == null || sourceChapter == null) { + TranslationChapterUIState.Error("Couldn't find the translation chapter") + } else { + // Original dialect: source chapter -> version -> artifact -> dialect. + val originalDialectName = mantraRepository.getArtifactVersion(sourceChapter.artifactVersionId) + ?.let { mantraRepository.getArtifact(it.artifactId) } + ?.let { mantraRepository.getDialect(it.dialectId) } + ?.name ?: "Original" + + // Translation dialect: translation version -> dialect. + val translationDialectName = mantraRepository.getTranslation(translationChapter.translationArtifactVersionId) + ?.let { mantraRepository.getDialect(it.dialectId) } + ?.name ?: "Translation" + + // Pair each original chunk with its translation chunk (by chunkId). + val translationChunksByChunkId = mantraRepository + .getTranslationChunks(translationChapter.id) + .associateBy { it.chunkId } + + val rows = mantraRepository.getChunksForChapter(sourceChapter.id).map { originalChunk -> + ChunkTranslationPair( + originalChunk = originalChunk, + translationChunk = translationChunksByChunkId[originalChunk.id], + ) + } + + TranslationChapterUIState.Loaded( + originalDialectName = originalDialectName, + translationDialectName = translationDialectName, + rows = rows, + ) + } + } + } + + companion object { + private const val TAG = "TranslationChapterViewModel" + + fun factory( + activeUserPublicKey: HexKey, + translationChapterId: String, + chatRoomId: String, + relayHint: String?, + initialTranslationChapterUIState: TranslationChapterUIState = TranslationChapterUIState.Loading, + mantraRepository: MantraRepository, + ): ViewModelProvider.Factory = viewModelFactory { + initializer { + TranslationChapterViewModel( + activeUserPublicKey = activeUserPublicKey, + translationChapterId = translationChapterId, + chatRoomId = chatRoomId, + relayHint = relayHint, + initialTranslationChapterUIState = initialTranslationChapterUIState, + mantraRepository = mantraRepository, + ) + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/state/TranslationChapterUIState.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/state/TranslationChapterUIState.kt new file mode 100644 index 00000000..57b98b74 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/state/TranslationChapterUIState.kt @@ -0,0 +1,28 @@ +package press.mantra.compose.ui.view.state + +import press.mantra.compose.database.model.MantraChunk +import press.mantra.compose.database.model.MantraTranslationChunk + +/** + * An original chunk paired with its translation chunk. [translationChunk] is + * null (or has blank text) when the chunk hasn't been translated yet, in which + * case the UI shows the original text as a greyed-out placeholder. + */ +data class ChunkTranslationPair( + val originalChunk: MantraChunk, + val translationChunk: MantraTranslationChunk?, +) + +sealed interface TranslationChapterUIState { + data class Loaded( + val originalDialectName: String, + val translationDialectName: String, + val rows: List = emptyList(), + ): TranslationChapterUIState + + data class Error( + val message: String + ): TranslationChapterUIState + + data object Loading: TranslationChapterUIState +}