diff --git a/app/src/main/java/be/scri/App.kt b/app/src/main/java/be/scri/App.kt index 223f0a357..da31cb552 100644 --- a/app/src/main/java/be/scri/App.kt +++ b/app/src/main/java/be/scri/App.kt @@ -34,6 +34,7 @@ import be.scri.ui.common.appcomponents.HintDialog import be.scri.ui.common.bottombar.BottomBarScreen import be.scri.ui.common.bottombar.ScribeBottomBar import be.scri.ui.screens.ConjugateScreen +import be.scri.ui.screens.ConjugationSelectionScreen import be.scri.ui.screens.DefaultCurrencySymbolScreen import be.scri.ui.screens.InstallationScreen import be.scri.ui.screens.LanguageSettingsScreen @@ -197,6 +198,11 @@ fun ScribeApp( onNavigateToDownloadData = { navController.navigate("conjugate_download_data") }, + onNavigateToConjugationSelection = { verb, languageAlias -> + navController.navigate( + "${Screen.ConjugationSelection.route}/$verb/$languageAlias", + ) + }, ) } HandleBackPress(pagerState, coroutineScope) @@ -293,6 +299,17 @@ fun ScribeApp( ) } + composable("${Screen.ConjugationSelection.route}/{verb}/{languageAlias}") { backStackEntry -> + val verb = backStackEntry.arguments?.getString("verb") ?: "" + val languageAlias = backStackEntry.arguments?.getString("languageAlias") ?: "" + ConjugationSelectionScreen( + verb = verb, + languageAlias = languageAlias, + onBackNavigation = { navController.popBackStack() }, + modifier = Modifier.padding(innerPadding), + ) + } + composable("${Screen.LanguageSettings.route}/{languageName}") { val language = it.arguments?.getString("languageName") if (language != null) { diff --git a/app/src/main/java/be/scri/navigation/Screen.kt b/app/src/main/java/be/scri/navigation/Screen.kt index d521e632e..10abc90a6 100644 --- a/app/src/main/java/be/scri/navigation/Screen.kt +++ b/app/src/main/java/be/scri/navigation/Screen.kt @@ -48,4 +48,9 @@ sealed class Screen( * Screen containing the third party codes used in the application. */ data object ThirdParty : Screen("third_party_screen") + + /** + * Screen displaying the conjugation selection details for a specific verb. + */ + data object ConjugationSelection : Screen("conjugation_selection_screen") } diff --git a/app/src/main/java/be/scri/ui/screens/ConjugateScreen.kt b/app/src/main/java/be/scri/ui/screens/ConjugateScreen.kt index 2232ebc8f..0498daf23 100644 --- a/app/src/main/java/be/scri/ui/screens/ConjugateScreen.kt +++ b/app/src/main/java/be/scri/ui/screens/ConjugateScreen.kt @@ -2,7 +2,6 @@ package be.scri.ui.screens -import android.widget.Toast import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -36,7 +35,6 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -53,14 +51,14 @@ import be.scri.ui.common.ScribeBaseScreen fun ConjugateScreen( onNavigateToDownloadData: () -> Unit, modifier: Modifier = Modifier, + onNavigateToConjugationSelection: (String, String) -> Unit = { _, _ -> }, viewModel: ConjugateViewModel = viewModel(), ) { - val context = LocalContext.current val localConfiguration = LocalConfiguration.current val scrollState = rememberScrollState() val searchQuery by viewModel.searchQuery.collectAsState() - val searchResults by viewModel.searchResults.collectAsState() + val displayResults by viewModel.displayResults.collectAsState() val recentlyConjugated by viewModel.recentlyConjugated.collectAsState() val dynamicSpacing = localConfiguration.screenHeightDp.dp * 0.1f @@ -182,8 +180,10 @@ fun ConjugateScreen( .width(21.dp) .height(18.dp) .clickable { - if (searchResults.isNotEmpty()) { - viewModel.onVerbSelected(searchResults.first()) + val first = displayResults.firstOrNull { !it.isDummy } + if (first != null) { + viewModel.onVerbSelected(first) + onNavigateToConjugationSelection(first.verb, first.languageAlias) } }, ) @@ -192,7 +192,7 @@ fun ConjugateScreen( Spacer(modifier = Modifier.height(Dimensions.PaddingMedium)) if (searchQuery.isNotEmpty()) { - // Search suggestion container + // Search suggestion container — dummy rows until DB is populated (#570) Card( modifier = Modifier @@ -211,48 +211,41 @@ fun ConjugateScreen( .fillMaxWidth() .padding(Dimensions.PaddingSmall), ) { - if (searchResults.isEmpty()) { - Text( - text = "No verbs found", - modifier = Modifier.padding(Dimensions.PaddingMedium), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = Alpha.MEDIUM), - style = MaterialTheme.typography.bodyMedium, - ) - } else { - searchResults.forEachIndexed { index, result -> - Row( + displayResults.forEachIndexed { index, result -> + Row( + modifier = + Modifier + .fillMaxWidth() + .clickable { + viewModel.onVerbSelected(result) + onNavigateToConjugationSelection(result.verb, result.languageAlias) + }.padding(Dimensions.PaddingMedium), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "${result.verb} (${getLanguageDisplayName(result.languageAlias)})", + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelMedium, + ) + Image( + painter = painterResource(id = R.drawable.right_arrow), + contentDescription = "Right Arrow", + modifier = + Modifier + .size(Dimensions.IconSize) + .alpha(Alpha.HIGH), + ) + } + if (index < displayResults.lastIndex) { + Spacer( modifier = Modifier .fillMaxWidth() - .clickable { viewModel.onVerbSelected(result) } - .padding(Dimensions.PaddingMedium), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = "${result.verb} (${getLanguageDisplayName(result.languageAlias)})", - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.labelMedium, - ) - Image( - painter = painterResource(id = R.drawable.right_arrow), - contentDescription = "Right Arrow", - modifier = - Modifier - .size(Dimensions.IconSize) - .alpha(Alpha.HIGH), - ) - } - if (index < searchResults.lastIndex) { - Spacer( - modifier = - Modifier - .fillMaxWidth() - .height(1.dp) - .background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)), - ) - } + .height(1.dp) + .background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)), + ) } } } @@ -322,7 +315,60 @@ fun ConjugateScreen( } } - // Header 3: Recently conjugated + // Direct link to ConjugationSelectionScreen (issue #567). + // Andrew requested a link on the Conjugate tab while search-based + // navigation (#570) is not yet wired up. + Text( + text = stringResource(R.string.i18n_app_conjugate_choose_conjugation_title), + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.headlineMedium, + modifier = + Modifier + .padding( + start = 4.dp, + top = Dimensions.PaddingLarge, + bottom = Dimensions.PaddingSmall, + ).align(Alignment.Start), + ) + Card( + modifier = + Modifier + .fillMaxWidth() + .padding(bottom = Dimensions.PaddingSmall) + .clickable { onNavigateToConjugationSelection("verb", "DE") }, + shape = RoundedCornerShape(dimensionResource(id = R.dimen.rounded_corner_radius_standard)), + colors = + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + ) { + Row( + modifier = + Modifier + .padding(Dimensions.PaddingMedium) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.i18n_app_conjugate_choose_conjugation_select_tense), + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelMedium, + ) + Image( + painter = painterResource(R.drawable.right_arrow), + contentDescription = "Right Arrow", + modifier = + Modifier + .size(Dimensions.IconSize) + .alpha(Alpha.HIGH), + ) + } + } + + // Header 3: Recently conjugated — only shown when real items exist. if (recentlyConjugated.isNotEmpty()) { Row( modifier = @@ -365,23 +411,16 @@ fun ConjugateScreen( containerColor = MaterialTheme.colorScheme.surface, ), ) { - Column( - modifier = Modifier.fillMaxWidth(), - ) { + Column(modifier = Modifier.fillMaxWidth()) { recentlyConjugated.forEachIndexed { index, item -> Row( modifier = Modifier .fillMaxWidth() - .padding(Dimensions.PaddingMedium) .clickable { - Toast - .makeText( - context, - "Conjugating ${item.verb} (${getLanguageDisplayName(item.languageAlias)})...", - Toast.LENGTH_SHORT, - ).show() - }, + viewModel.onVerbSelected(item) + onNavigateToConjugationSelection(item.verb, item.languageAlias) + }.padding(Dimensions.PaddingMedium), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { diff --git a/app/src/main/java/be/scri/ui/screens/ConjugateViewModel.kt b/app/src/main/java/be/scri/ui/screens/ConjugateViewModel.kt index b5f4be932..d7b351f07 100644 --- a/app/src/main/java/be/scri/ui/screens/ConjugateViewModel.kt +++ b/app/src/main/java/be/scri/ui/screens/ConjugateViewModel.kt @@ -12,7 +12,10 @@ import be.scri.helpers.data.columnExists import be.scri.helpers.data.tableExists import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -22,6 +25,8 @@ import kotlinx.coroutines.withContext data class ConjugateSearchResult( val verb: String, val languageAlias: String, + /** True when this entry is a static placeholder shown before DB data is available. */ + val isDummy: Boolean = false, ) /** @@ -41,6 +46,28 @@ class ConjugateViewModel( private val _recentlyConjugated = MutableStateFlow>(emptyList()) val recentlyConjugated = _recentlyConjugated.asStateFlow() + /** + * Results shown in the search dropdown. + * - Blank query → empty + * - Real DB results → show them + * - No DB yet → dummy rows using the typed query as label + */ + val displayResults = + combine(_searchQuery, _searchResults) { query, results -> + when { + query.isBlank() -> emptyList() + results.isNotEmpty() -> results + else -> + listOf("DE", "ES", "FR", "IT", "PT", "RU", "SV", "EN", "DE").map { alias -> + ConjugateSearchResult(verb = query, languageAlias = alias, isDummy = true) + } + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = emptyList(), + ) + init { loadRecentlyConjugated() } @@ -114,10 +141,11 @@ class ConjugateViewModel( } /** - * Triggers when a verb search result or recently conjugated item is clicked. + * Triggers when a verb is selected. Always persists to recently conjugated + * (stripping isDummy) so the section shows real user history. */ fun onVerbSelected(result: ConjugateSearchResult) { - addToRecentlyConjugated(result) + addToRecentlyConjugated(result.copy(isDummy = false)) clearSearchQuery() } diff --git a/app/src/main/java/be/scri/ui/screens/ConjugationSelectionScreen.kt b/app/src/main/java/be/scri/ui/screens/ConjugationSelectionScreen.kt new file mode 100644 index 000000000..459e04b81 --- /dev/null +++ b/app/src/main/java/be/scri/ui/screens/ConjugationSelectionScreen.kt @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package be.scri.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import be.scri.R +import be.scri.ui.common.ScribeBaseScreen + +// --------------------------------------------------------------------------- +// Dummy data — replaced by real DB data once #564 / #570 are finalised +// --------------------------------------------------------------------------- + +private data class DummyTenseRow( + val label: String, + val forms: List, +) + +private data class DummyTenseGroup( + val sectionTitle: String, + val rows: List, +) + +private val DUMMY_CONJUGATION_DATA: List = + listOf( + DummyTenseGroup( + sectionTitle = "Indicative", + rows = + listOf( + DummyTenseRow("Present", listOf("I verb", "you verb", "he/she verbs", "we verb", "you verb", "they verb")), + DummyTenseRow("Past", listOf("I verbed", "you verbed", "he/she verbed", "we verbed", "you verbed", "they verbed")), + DummyTenseRow("Future", listOf("I will verb", "you will verb", "he/she will verb", "we will verb", "you will verb", "they will verb")), + ), + ), + DummyTenseGroup( + sectionTitle = "Subjunctive", + rows = + listOf( + DummyTenseRow("Present", listOf("I verb", "you verb", "he/she verb", "we verb", "you verb", "they verb")), + DummyTenseRow("Past", listOf("I had verbed", "you had verbed", "he/she had verbed", "we had verbed", "you had verbed", "they had verbed")), + ), + ), + DummyTenseGroup( + sectionTitle = "Imperative", + rows = + listOf( + DummyTenseRow("Present", listOf("verb!", "let's verb!", "verb!")), + ), + ), + ) + +// --------------------------------------------------------------------------- +// Screen +// --------------------------------------------------------------------------- + +/** + * Displays dummy conjugation tables for a selected verb. + * + * The header shows "[verb] ([language])" as specified in the Figma designs for issue #567. + * Real DB data will replace [DUMMY_CONJUGATION_DATA] once #564 / #570 are finalised. + * + * @param verb The verb selected by the user. + * @param languageAlias The language code (e.g. "DE", "ES"). + * @param onBackNavigation Called when the user presses the back button. + * @param modifier Optional modifier. + */ +@Composable +fun ConjugationSelectionScreen( + verb: String, + languageAlias: String, + onBackNavigation: () -> Unit, + modifier: Modifier = Modifier, +) { + val pageTitle = "$verb (${getLanguageDisplayName(languageAlias)})" + val backLabel = stringResource(R.string.i18n_app_conjugate_title) + + ScribeBaseScreen( + pageTitle = pageTitle, + lastPage = backLabel, + onBackNavigation = onBackNavigation, + modifier = modifier, + ) { + Column( + modifier = + Modifier + .fillMaxSize() + .padding(horizontal = Dimensions.PaddingMedium) + .verticalScroll(rememberScrollState()), + ) { + DUMMY_CONJUGATION_DATA.forEach { group -> + TenseGroupSection(group = group) + } + Spacer(modifier = Modifier.height(Dimensions.PaddingLarge)) + } + } +} + +// --------------------------------------------------------------------------- +// Sub-composables +// --------------------------------------------------------------------------- + +@Composable +private fun TenseGroupSection( + group: DummyTenseGroup, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Spacer(modifier = Modifier.height(Dimensions.PaddingLarge)) + + Text( + text = group.sectionTitle, + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(start = 4.dp, bottom = Dimensions.PaddingSmall), + ) + + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(dimensionResource(R.dimen.rounded_corner_radius_standard)), + colors = + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + elevation = CardDefaults.cardElevation(defaultElevation = Dimensions.ElevationSmall), + ) { + Column(modifier = Modifier.fillMaxWidth()) { + group.rows.forEachIndexed { index, row -> + TenseRow(row = row) + if (index < group.rows.lastIndex) { + Spacer( + modifier = + Modifier + .fillMaxWidth() + .height(1.dp) + .background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)), + ) + } + } + } + } + } +} + +@Composable +private fun TenseRow( + row: DummyTenseRow, + modifier: Modifier = Modifier, +) { + Row( + modifier = + modifier + .fillMaxWidth() + .padding(horizontal = Dimensions.PaddingMedium, vertical = Dimensions.PaddingSmall), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Text( + text = row.label, + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + modifier = + Modifier + .weight(1f) + .padding(end = Dimensions.PaddingSmall), + ) + Column( + modifier = Modifier.weight(2f), + horizontalAlignment = Alignment.End, + ) { + row.forms.forEach { form -> + Text( + text = form, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.End, + ) + } + } + } +}