migrate hints system to a localized markdown-based architecture and refactor related UI components

This commit is contained in:
jonasgaudian
2026-02-14 17:15:26 +01:00
parent 306d0c7432
commit d2e77083ad
35 changed files with 1035 additions and 1944 deletions

View File

@@ -30,6 +30,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.gaudian.translator.R
import eu.gaudian.translator.ui.theme.ThemePreviews
import eu.gaudian.translator.view.hints.Hint
import eu.gaudian.translator.view.hints.HintBottomSheet
import eu.gaudian.translator.view.hints.LocalShowHints
@@ -41,7 +42,7 @@ fun AppTopAppBar(
navigationIcon: @Composable (() -> Unit)? = null,
actions: @Composable RowScope.() -> Unit = {},
colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),
hintContent: @Composable (() -> Unit)? = null
hintContent: Hint? = null
) {
val sheetState = rememberModalBottomSheetState()
var showBottomSheet by remember { mutableStateOf(false) }
@@ -111,7 +112,9 @@ fun AppTopAppBar(
showBottomSheet = false
},
sheetState = sheetState,
content = hintContent
content = {
hintContent?.Render()
}
)
}
}

View File

@@ -44,7 +44,7 @@ import eu.gaudian.translator.view.composable.DialogButton
import eu.gaudian.translator.view.composable.InspiringSearchField
import eu.gaudian.translator.view.composable.SourceLanguageDropdown
import eu.gaudian.translator.view.composable.TargetLanguageDropdown
import eu.gaudian.translator.view.hints.getImportVocabularyHint
import eu.gaudian.translator.view.hints.ImportVocabularyHint
import eu.gaudian.translator.viewmodel.LanguageViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel
import kotlinx.coroutines.launch
@@ -109,7 +109,7 @@ fun ImportDialogContent(
AppDialog(
onDismissRequest = onDismiss,
title = { Text(descriptionText) },
hintContent = { getImportVocabularyHint() },
hintContent = { ImportVocabularyHint() },
content = {
Column(
modifier = Modifier

View File

@@ -69,7 +69,7 @@ fun VocabularyReviewScreen(
topBar = {
AppTopAppBar(
title = { Text(stringResource(R.string.found_items)) },
hintContent = { getVocabularyReviewHint() }
hintContent = getVocabularyReviewHint()
)
},
) { paddingValues ->

View File

@@ -1,145 +1,21 @@
package eu.gaudian.translator.view.hints
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.SuggestionChipDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import eu.gaudian.translator.R
import eu.gaudian.translator.view.composable.AppIcons
/**
* Transformed AddModelScanHint using the new uniform Hint structure.
* Migrated AddModelScanHint using markdown-based format.
* Content is loaded from localized assets/hints based on device locale.
*/
@Composable
fun getAddModelScanHint(): Hint {
return Hint(
titleRes = R.string.hint_scan_hint_title,
elements = listOf(
HintElement.Section(
title = stringResource(R.string.scan_hint_section_how_scan_works),
content = listOf(
HintElement.Text(stringResource(R.string.scan_hint_how_scan_works_paragraph)),
HintElement.UIElement { Spacer(Modifier.height(8.dp)) },
HintElement.UIElement { ReusedScanButtonPreview() },
HintElement.UIElement { Spacer(Modifier.height(8.dp)) },
HintElement.BulletList(
listOf(
stringResource(R.string.scan_hint_bullet_results_depend),
stringResource(R.string.scan_hint_bullet_public_private),
stringResource(R.string.scan_hint_bullet_try_again)
)
)
)
),
HintElement.Divider,
HintElement.Section(
title = stringResource(R.string.scan_hint_section_why_missing),
content = listOf(
HintElement.InfoBadge(
icon = AppIcons.Lock,
text = stringResource(R.string.scan_hint_badge_restricted)
),
HintElement.InfoBadge(
icon = AppIcons.Warning,
text = stringResource(R.string.scan_hint_badge_not_suitable)
),
HintElement.InfoBadge(
icon = AppIcons.CheckCircle,
text = stringResource(R.string.scan_hint_badge_only_text_models)
),
HintElement.Divider,
HintElement.Text(stringResource(R.string.scan_hint_focus_text_models)),
HintElement.Divider,
HintElement.Card {
PerformanceTierChips()
},
HintElement.Divider,
HintElement.InfoBadge(
icon = AppIcons.Info,
text = stringResource(R.string.scan_hint_most_tasks_small_models)
)
)
),
HintElement.Divider,
HintElement.Section(
title = stringResource(R.string.scan_hint_section_tips),
content = listOf(
HintElement.BulletList(
listOf(
stringResource(R.string.scan_hint_tip_verify_key),
stringResource(R.string.scan_hint_tip_select_org),
stringResource(R.string.scan_hint_tip_type_manually),
stringResource(R.string.scan_hint_tip_instruct_chat_text)
)
)
)
),
HintElement.Divider,
HintElement.Section(
title = stringResource(R.string.hint_scan_hint_section_visual_guide),
content = listOf(
HintElement.VisualStep(
step = stringResource(R.string.hint_scan_hint_step_1),
title = stringResource(R.string.hint_scan_hint_step1_title),
description = stringResource(R.string.hint_scan_hint_step1_desc),
trailing = {
Icon(AppIcons.Search, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
}
),
HintElement.VisualStep(
step = stringResource(R.string.hint_scan_hint_step_2),
title = stringResource(R.string.hint_scan_hint_step2_title),
description = stringResource(R.string.hint_scan_hint_step2_desc),
trailing = {
SuggestionChip(
onClick = {},
label = { Text(stringResource(R.string.hint_scan_hint_label_text_chat)) },
enabled = false,
icon = {},
colors = SuggestionChipDefaults.suggestionChipColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
labelColor = MaterialTheme.colorScheme.onPrimaryContainer
)
)
}
),
HintElement.VisualStep(
step = stringResource(R.string.hint_scan_hint_step_3),
title = stringResource(R.string.hint_scan_hint_step3_title),
description = stringResource(R.string.hint_scan_hint_step3_desc),
trailing = {
SmallPrimaryCard(text = stringResource(R.string.hint_scan_hint_add_validate))
}
)
)
),
HintElement.Divider,
HintElement.Section(
title = stringResource(R.string.hint_scan_hint_section_cant_find),
content = listOf(
HintElement.Text(stringResource(R.string.hint_scan_hint_manual_add_paragraph))
)
titleRes = R.string.hint_scan_hint_title,
elements = listOf(
HintElement.LocalizedMarkdown("example_hint")
)
)
)
}
@Preview
@Composable
fun ReusedScanButtonPreviewPreview() {
ReusedScanButtonPreview()
}
@Preview
@@ -151,4 +27,4 @@ fun AddModelScanHintPreview() {
@Composable
fun AddModelScanHint() {
getAddModelScanHint().Render()
}
}

View File

@@ -1,130 +1,33 @@
package eu.gaudian.translator.view.hints
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
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.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import eu.gaudian.translator.R
import eu.gaudian.translator.view.composable.AppIcons
/**
* TODO: Migrate to markdown-based hint format.
* Create a .md file in assets/hints/ and use MarkdownHint composable instead.
* See MarkdownHint.kt for implementation details.
* Migrated API Key hint using markdown-based format.
* Content is loaded from localized assets/hints based on device locale.
*/
object ApiKeyHint: LegacyHint() {
override val titleRes: Int = R.string.hint_how_to_connect_to_an_ai
@Composable
override fun getTitle(): String = stringResource(R.string.hint_how_to_connect_to_an_ai)
@Composable
override fun Content() {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
HeaderWithIcon(
title = stringResource(R.string.hint_how_to_connect_to_an_ai)
)
HintSection(title = stringResource(R.string.connecting_your_ai_model)) {
Text(
text = stringResource(R.string.api_hint_intro_1),
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.api_hint_intro_2),
style = MaterialTheme.typography.bodyMedium
)
}
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
HintSection(title = stringResource(R.string.key_status_indicators_title)) {
Text(
text = stringResource(R.string.key_status_explanation),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 8.dp)
)
KeyStatus(hasKey = true)
Text(
text = stringResource(R.string.key_saved_and_active),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 20.dp, bottom = 8.dp)
)
KeyStatus(hasKey = false)
Text(
text = stringResource(R.string.key_missing_or_cleared),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 20.dp)
)
}
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
HintSection(title = stringResource(R.string.troubleshooting_title)) {
Text(
text = stringResource(R.string.troubleshooting_intro),
style = MaterialTheme.typography.bodyMedium
)
Text(
text = stringResource(R.string.troubleshooting_bullets),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(top = 4.dp)
)
}
}
}
@Composable
fun getApiKeyHint() = Hint (
titleRes = R.string.hint_how_to_connect_to_an_ai,
elements = listOf(
HintElement.LocalizedMarkdown("api_key_hint")
)
)
@Composable
private fun KeyStatus(hasKey: Boolean) {
val (text, color, icon) = if (hasKey) {
Triple(
stringResource(R.string.text_key_active),
MaterialTheme.colorScheme.primary,
AppIcons.Check
)
} else {
Triple(
stringResource(R.string.text_no_key),
MaterialTheme.colorScheme.error,
AppIcons.Warning
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = icon,
contentDescription = text,
tint = color,
modifier = Modifier.size(16.dp)
)
Spacer(Modifier.width(4.dp))
Text(text, color = color, style = MaterialTheme.typography.labelLarge)
}
}
@Composable
fun ApiKeyHint() {
getApiKeyHint().Render()
}
@Preview(showBackground = true)
@Preview
@Composable
fun ApiKeyHintPreview() {
MaterialTheme {
ApiKeyHint.Content()
ApiKeyHint()
}
}
}

View File

@@ -1,121 +1,33 @@
package eu.gaudian.translator.view.hints
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
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.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import eu.gaudian.translator.R
import eu.gaudian.translator.view.composable.AppIcons
/**
* TODO: Migrate to markdown-based hint format.
* Create a .md file in assets/hints/ and use MarkdownHint composable instead.
* See MarkdownHint.kt for implementation details.
* Migrated Category hint using markdown-based format.
* Content is loaded from localized assets/hints based on device locale.
*/
object CategoryHint : LegacyHint() {
override val titleRes: Int = R.string.category_hint_intro
@Composable
override fun Content() {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
HeaderWithIcon(
title = stringResource(R.string.category_hint_intro)
)
HintSection(title = stringResource(R.string.category_hint_intro)) {
// Tag Category Explanation (formerly List)
CategoryHintItem(
icon = {
Icon(
AppIcons.FilterList,
contentDescription = stringResource(R.string.content_desc_tag_category),
modifier = Modifier.size(32.dp),
tint = MaterialTheme.colorScheme.secondary
)
},
title = stringResource(R.string.text_list),
description = stringResource(R.string.hint_list_category)
)
// Filter Category Explanation
CategoryHintItem(
icon = {
Icon(
AppIcons.FilterCategory,
contentDescription = stringResource(R.string.content_desc_filter_category),
modifier = Modifier.size(32.dp),
tint = MaterialTheme.colorScheme.secondary
)
},
title = stringResource(R.string.text_filter),
description = stringResource(R.string.hint_filter_category_description)
)
}
}
}
@Composable
fun getCategoryHint(): Hint {
return Hint(
titleRes = R.string.category_hint_intro,
elements = listOf(
HintElement.LocalizedMarkdown("category_hint")
)
)
}
@Composable
fun CategoryHint() {
CategoryHint.Content()
}
@Composable
private fun CategoryHintItem(
icon: @Composable () -> Unit,
title: String,
description: String
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
icon()
Column {
Text(text = title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(4.dp))
Text(text = description, style = MaterialTheme.typography.bodyMedium)
}
}
getCategoryHint().Render()
}
@Preview
@Composable
fun CategoryHintPreview() {
CategoryHint()
MaterialTheme {
CategoryHint()
}
}
@Preview
@Composable
fun CategoryHintItemPreview() {
CategoryHintItem(
icon = {
Icon(
AppIcons.Category,
contentDescription = stringResource(R.string.cd_tag_category),
modifier = Modifier.size(32.dp),
tint = MaterialTheme.colorScheme.secondary
)
},
title = stringResource(R.string.text_list),
description = stringResource(R.string.category_hint_item_preview_description)
)
}

View File

@@ -1,136 +1,33 @@
package eu.gaudian.translator.view.hints
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
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.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import eu.gaudian.translator.R
import eu.gaudian.translator.view.composable.AppIcons
/**
* TODO: Migrate to markdown-based hint format.
* Create a .md file in assets/hints/ and use MarkdownHint composable instead.
* See MarkdownHint.kt for implementation details.
* Migrated Category Hint Screen using markdown-based format.
* Content is loaded from localized assets/hints based on device locale.
*/
object CategoryHintScreen : LegacyHint() {
override val titleRes: Int = R.string.category_hint_intro
@Composable
override fun Content() {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
HeaderWithIcon(
title = stringResource(R.string.category_hint_intro)
)
HintSection(title = stringResource(R.string.category_list_title)) {
Text(
text = stringResource(R.string.category_list_description),
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(8.dp))
// Visual representation for List Category
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
WordItem(text = stringResource(R.string.example_word_apple))
Icon(AppIcons.Add, contentDescription = stringResource(R.string.action_add), tint = MaterialTheme.colorScheme.secondary)
CategoryBox(icon = AppIcons.FilterList, text = stringResource(R.string.example_category_my_fruit_list))
}
}
HorizontalDivider()
HintSection(title = stringResource(R.string.category_filter_title)) {
Text(
text = stringResource(R.string.category_filter_description),
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(8.dp))
// Visual representation for Filter Category
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Row {
WordItem(text = stringResource(R.string.example_word_dog), subtext = stringResource(R.string.stage_1))
Spacer(modifier = Modifier.padding(horizontal = 4.dp))
WordItem(text = stringResource(R.string.example_word_cat), subtext = stringResource(R.string.stage_1))
}
CategoryBox(icon = AppIcons.FilterCategory, text = stringResource(R.string.example_filter_stage_1))
}
}
}
}
@Composable
fun getCategoryHintScreen(): Hint {
return Hint(
titleRes = R.string.category_hint_intro,
elements = listOf(
HintElement.LocalizedMarkdown("category_hint")
)
)
}
@Composable
fun CategoryHintScreen() {
CategoryHintScreen.Content()
getCategoryHintScreen().Render()
}
/**
* A simple visual representation of a vocabulary word.
*/
@Composable
private fun WordItem(text: String, subtext: String? = null) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.background(MaterialTheme.colorScheme.surfaceContainer)
.padding(horizontal = 12.dp, vertical = 6.dp)
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = text, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.SemiBold)
subtext?.let {
Text(text = it, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
}
}
/**
* A simple visual representation of a category.
*/
@Composable
private fun CategoryBox(icon: ImageVector, text: String) {
Row(
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.background(MaterialTheme.colorScheme.secondaryContainer)
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(imageVector = icon, contentDescription = text, modifier = Modifier.size(24.dp))
Text(text = text, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold)
}
}
@Preview(showBackground = true)
@Preview
@Composable
fun CategoryHintScreenPreview() {
MaterialTheme {
CategoryHintScreen()
}
}
}

View File

@@ -1,79 +1,25 @@
package eu.gaudian.translator.view.hints
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
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.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import eu.gaudian.translator.R
import eu.gaudian.translator.view.composable.AppIcons
/**
* Migrated DictionaryOptionsHint using markdown-based format.
* Content is loaded from localized assets/hints based on device locale.
*/
@Composable
fun getDictionaryOptionsHint(): Hint {
return Hint(
titleRes = R.string.label_dictionary_options,
elements = listOf(
HintElement.Section(
title = stringResource(R.string.hint_dictionary_desc),
content = listOf(
// VisualStep 1:
HintElement.VisualStep(
step = "1",
title = stringResource(R.string.hint_dict_options_step1_title),
description = stringResource(R.string.hint_dict_options_step1_desc)
),
// VisualStep 2:
HintElement.VisualStep(
step = "2",
title = stringResource(R.string.hint_dict_options_step2_title),
description = stringResource(R.string.hint_dict_options_step2_desc),
trailing = {
// Custom trailing composable from the original hint
Row(
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.background(MaterialTheme.colorScheme.surfaceContainer)
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = stringResource(R.string.eg_synonyms),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Icon(
imageVector = AppIcons.SwitchOn,
contentDescription = stringResource(R.string.example_toggle),
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(32.dp)
)
}
}
)
)
)
HintElement.LocalizedMarkdown("dictionary_hint")
)
)
}
/**
* Preview for the migrated DictionaryOptionsHint.
* It now calls getDictionaryOptionsHint() and then Render().
*/
@Preview
@Composable
fun DictionaryOptionsHintPreview() {
fun DictionaryOptionsHint() {
getDictionaryOptionsHint().Render()
}
}

View File

@@ -1,162 +1,7 @@
@file:Suppress("HardCodedStringLiteral")
package eu.gaudian.translator.view.hints
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.SuggestionChipDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import eu.gaudian.translator.R
import eu.gaudian.translator.view.composable.AppCard
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppOutlinedButton
/**
* Example of a modern hint using the new uniform structure with Hint and HintElements.
* This demonstrates how to migrate from the old Content() based hints to the new element-based system.
* This file is kept for reference only.
* All hints are now migrated to markdown-based format.
* See individual hint files for implementations.
*/
val ExampleModernHint = Hint(
titleRes = R.string.hint_example_hint_scan_for_models_hint,
subtitle = "How to find and add AI models to your app",
elements = listOf(
HintElement.Section(
title = "How Scanning Works",
content = listOf(
HintElement.Text("The scan feature searches for available AI models on your device or network."),
HintElement.UIElement {
ReusedScanButtonPreview()
},
HintElement.BulletList(
listOf(
"Results depend on your API key permissions.",
"Only public models are shown by default.",
"Try again if no models are found."
)
)
)
),
HintElement.Divider,
HintElement.Section(
title = "Why Some Models Are Missing",
content = listOf(
HintElement.InfoBadge(
icon = AppIcons.Lock,
text = "Restricted access"
),
HintElement.InfoBadge(
icon = AppIcons.Warning,
text = "Not suitable for this app"
),
HintElement.InfoBadge(
icon = AppIcons.CheckCircle,
text = "Only text models are supported"
),
HintElement.Text("Focus on text-based models for best performance."),
HintElement.Card {
PerformanceTierChips()
},
HintElement.InfoBadge(
icon = AppIcons.Info,
text = "Most tasks work well with smaller models"
)
)
),
HintElement.Divider,
HintElement.Section(
title = "Tips",
content = listOf(
HintElement.BulletList(
listOf(
"Verify your API key is active.",
"Select the correct organization.",
"Type model names manually if needed.",
"Prefer instruct or chat models for text."
)
)
)
),
HintElement.Divider,
HintElement.Section(
title = "Visual Guide",
content = listOf(
HintElement.VisualStep(
step = "1",
title = "Initiate Scan",
description = "Click the scan button to search for models.",
trailing = {
Icon(AppIcons.Search, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
}
),
HintElement.VisualStep(
step = "2",
title = "Select Model Type",
description = "Choose between text, chat, or instruct models.",
trailing = {
SuggestionChip(
onClick = {},
label = { Text("Text Chat") },
enabled = false,
colors = SuggestionChipDefaults.suggestionChipColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
labelColor = MaterialTheme.colorScheme.onPrimaryContainer
)
)
}
),
HintElement.VisualStep(
step = "3",
title = "Add and Validate",
description = "Add the selected model and validate it.",
trailing = {
SmallPrimaryCard(text = "Add & Validate")
}
)
)
),
HintElement.Divider,
HintElement.Section(
title = "Can't Find Your Model?",
content = listOf(
HintElement.Text("You can manually add models by entering their details.")
)
)
)
)
/**
* Preview composable for the example modern hint.
*/
@Preview
@Composable
fun ExampleModernHintPreview() {
ExampleModernHint.Render()
}
/**
* Reused composable for scan button preview (from original hint).
*/
@Preview
@Composable
fun ReusedScanButtonPreview() {
AppCard {
AppOutlinedButton(onClick = {}, enabled = true, modifier = Modifier.fillMaxWidth()) {
Icon(
AppIcons.Search,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
Spacer(Modifier.width(8.dp))
Text(text = "Scan for Models")
}
}
}

View File

@@ -3,36 +3,20 @@
package eu.gaudian.translator.view.hints
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.SuggestionChipDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
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.compose.ui.unit.sp
import eu.gaudian.translator.R
import eu.gaudian.translator.view.composable.AppCard
import eu.gaudian.translator.view.composable.AppIcons
/**
@@ -61,20 +45,7 @@ data class Hint(
fun getTitle(): String = stringResource(titleRes)
}
/**
* Legacy abstract class for existing hints (to be migrated).
* New hints should use the data class above.
*/
@Deprecated("Use Hint data class instead")
abstract class LegacyHint {
abstract val titleRes: Int
@Composable
abstract fun Content()
@Composable
open fun getTitle(): String = stringResource(titleRes)
}
@Composable
fun HeaderWithIcon(title: String, subtitle: String? = null) {
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -99,226 +70,4 @@ fun HeaderWithIcon(title: String, subtitle: String? = null) {
)}
}
}
}
@Suppress("HardCodedStringLiteral")
@Preview
@Composable
fun HeaderWithIconPreview() {
HeaderWithIcon(title = "Sample Title", subtitle = "Sample Subtitle")
}
@Composable
fun HintSection(title: String, content: @Composable () -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
content()
}
}
@Suppress("HardCodedStringLiteral")
@Preview
@Composable
fun HintSectionPreview() {
AppCard(
modifier = Modifier.fillMaxWidth(),
) {
HintSection(title = "Sample Section Title") {
Text("Sample section content.")
}
}
}
@Composable
fun BulletPoints(items: List<String>) {
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
items.forEach { line ->
Row(verticalAlignment = Alignment.Top) {
Text("", modifier = Modifier.width(16.dp), textAlign = TextAlign.Center)
Text(line, style = MaterialTheme.typography.bodyMedium)
}
}
}
}
@Suppress("HardCodedStringLiteral")
@Preview
@Composable
fun BulletPointsPreview() {
BulletPoints(items = listOf("Point 1", "Point 2", "Point 3"))
}
@Composable
fun InfoBadgeRow(icon: ImageVector, text: String) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(16.dp))
Spacer(Modifier.width(6.dp))
Text(text, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
@Suppress("HardCodedStringLiteral")
@Preview
@Composable
fun InfoBadgeRowPreview() {
InfoBadgeRow(icon = AppIcons.Info, text = "Sample info badge text")
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun PerformanceTierChips() {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
SuggestionChip(
onClick = {},
label = { Text(stringResource(R.string.hint_scan_hint_chip_nano)) },
enabled = false,
colors = SuggestionChipDefaults.suggestionChipColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
labelColor = MaterialTheme.colorScheme.onSecondaryContainer
)
)
SuggestionChip(
onClick = {},
label = { Text(stringResource(R.string.hint_scan_hint_chip_mini)) },
enabled = false,
colors = SuggestionChipDefaults.suggestionChipColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
labelColor = MaterialTheme.colorScheme.onSecondaryContainer
)
)
SuggestionChip(
onClick = {},
label = { Text(stringResource(R.string.hint_scan_hint_chip_small)) },
enabled = false,
colors = SuggestionChipDefaults.suggestionChipColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
labelColor = MaterialTheme.colorScheme.onSecondaryContainer
)
)
SuggestionChip(
onClick = {},
label = { Text(stringResource(R.string.hint_scan_hint_chip_medium)) },
enabled = false,
colors = SuggestionChipDefaults.suggestionChipColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
labelColor = MaterialTheme.colorScheme.onSurfaceVariant
)
)
SuggestionChip(
onClick = {},
label = { Text(stringResource(R.string.hint_scan_hint_chip_large_paid)) },
enabled = false,
colors = SuggestionChipDefaults.suggestionChipColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
labelColor = MaterialTheme.colorScheme.onPrimaryContainer
)
)
}
}
@Preview
@Composable
fun PerformanceTierChipsPreview() {
PerformanceTierChips()
}
@Composable
fun VisualStep(
step: String,
title: String,
description: String,
trailing: @Composable (() -> Unit)? = null
) {
AppCard(
modifier = Modifier.fillMaxWidth(),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
StepBadge(step)
Spacer(Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(title, style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
Text(description, style = MaterialTheme.typography.bodySmall)
}
}
if (trailing != null) {
Spacer(Modifier.width(8.dp))
Row(
Modifier
.fillMaxWidth()
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
){
trailing()
}
}
}
}
@Suppress("HardCodedStringLiteral")
@Preview
@Composable
fun VisualStepPreview() {
VisualStep(
step = "1",
title = "Sample Step Title",
description = "Sample step description.",
trailing = { Icon(AppIcons.Search, contentDescription = null) }
)
}
@Composable
private fun StepBadge(step: String) {
Card(
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer),
shape = RoundedCornerShape(8.dp)
) {
Box(modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp), contentAlignment = Alignment.Center) {
Text(step, color = MaterialTheme.colorScheme.onPrimaryContainer, fontWeight = FontWeight.Bold, fontSize = 12.sp)
}
}
}
@Preview
@Composable
fun StepBadgePreview() {
StepBadge(step = "1")
}
@Composable
fun SmallPrimaryCard(text: String) {
Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primary)) {
Box(modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp)) {
Text(text, color = MaterialTheme.colorScheme.onPrimary, fontSize = 12.sp, fontWeight = FontWeight.SemiBold)
}
}
}
@Composable
fun TextSection(title: String, text: String) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Text(text, style = MaterialTheme.typography.bodySmall)
}
}
@Suppress("HardCodedStringLiteral")
@Preview
@Composable
fun SmallPrimaryCardPreview() {
SmallPrimaryCard(text = "Sample Text")
}

View File

@@ -1,13 +1,19 @@
package eu.gaudian.translator.view.hints
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import eu.gaudian.translator.view.composable.AppIcons
import androidx.compose.ui.unit.dp
import dev.jeziellago.compose.markdowntext.MarkdownText
private const val TAG = "MarkdownHint"
/**
* Sealed class representing different types of elements that can be used in a Hint.
@@ -15,67 +21,20 @@ import eu.gaudian.translator.view.composable.AppIcons
*/
sealed class HintElement {
/**
* A simple text element.
*/
data class Text(val text: String) : HintElement()
/**
* A header element with an icon, title, and optional subtitle.
*/
data class Header(val title: String, val subtitle: String? = null) : HintElement()
/**
* A section element that groups content with a title.
*/
data class Section(val title: String, val content: List<HintElement>) : HintElement()
data class TextSection(val title: String, val text: String) : HintElement()
/**
* A bullet list element.
*/
data class BulletList(val items: List<String>) : HintElement()
/**
* A visual step element with step number, title, description, and optional trailing composable.
*/
data class VisualStep(
val step: String,
val title: String,
val description: String,
val trailing: (@Composable () -> Unit)? = null
) : HintElement()
/**
* An info badge row with an icon and text.
*/
data class InfoBadge(val icon: ImageVector, val text: String) : HintElement()
/**
* A divider element.
*/
object Divider : HintElement()
/**
* A card element with content.
*/
data class Card(val content: @Composable () -> Unit) : HintElement()
/**
* A chips element for performance tiers or similar.
*/
data class Chips(val chips: List<String>) : HintElement()
/**
* A small primary card with text.
*/
data class SmallCard(val text: String) : HintElement()
/**
* A custom UI element with a composable.
*/
data class UIElement(val composable: @Composable () -> Unit) : HintElement()
/**
* A localized markdown file element.
* The file is loaded from assets based on the current device locale.
* Follows Android's locale-qualified resource pattern:
* - assets/hints/ - Default (English)
* - assets/hints-de-rDE/ - German
* - assets/hints-pt-rBR/ - Portuguese (Brazil)
*
* @param fileName The base filename without extension (e.g., "api_key_hint")
*/
data class LocalizedMarkdown(val fileName: String) : HintElement()
}
/**
@@ -84,170 +43,87 @@ sealed class HintElement {
@Composable
fun RenderHintElement(element: HintElement) {
when (element) {
is HintElement.Text -> {
Text(
text = element.text,
style = MaterialTheme.typography.bodyMedium
)
}
is HintElement.Header -> {
HeaderWithIcon(title = element.title, subtitle = element.subtitle)
}
is HintElement.Section -> {
HintSection(title = element.title) {
element.content.forEach { RenderHintElement(it) }
}
}
is HintElement.BulletList -> {
BulletPoints(items = element.items)
}
is HintElement.VisualStep -> {
VisualStep(
step = element.step,
title = element.title,
description = element.description,
trailing = element.trailing
)
}
is HintElement.InfoBadge -> {
InfoBadgeRow(icon = element.icon, text = element.text)
}
is HintElement.Divider -> {
HorizontalDivider()
}
is HintElement.Card -> {
element.content()
}
is HintElement.Chips -> {
PerformanceTierChips(chips = element.chips)
}
is HintElement.SmallCard -> {
SmallPrimaryCard(text = element.text)
}
is HintElement.UIElement -> {
element.composable()
}
is HintElement.TextSection -> {
TextSection(title = element.title, text = element.text)
is HintElement.LocalizedMarkdown -> {
LocalizedMarkdownContent(fileName = element.fileName)
// Debug: Show filename when experimental features are enabled
if (eu.gaudian.translator.view.LocalShowExperimentalFeatures.current) {
androidx.compose.foundation.layout.Row(
modifier = Modifier.padding(top = 4.dp)
) {
androidx.compose.material3.Text(
text = "[DEBUG: ${element.fileName}]",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.error
)
}
}
}
}
}
/**
* Helper composable for PerformanceTierChips based on list of strings.
* Composable to render localized markdown content.
* Automatically loads the correct locale version based on device settings.
* Falls back to English default if localized version is not available.
*/
@Composable
fun PerformanceTierChips(chips: List<String>) {
// Assuming chips are resource strings or direct strings
chips.forEach { chip ->
Text(text = chip, style = MaterialTheme.typography.bodySmall) // Simplified for template
fun LocalizedMarkdownContent(
fileName: String,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
val content = remember(fileName) {
val locale = MarkdownHintLoader.getCurrentLocale(context)
val suffix = MarkdownHintLoader.getLocaleSuffix(locale)
// Try localized version (folder has suffix, filename doesn't)
val localizedPath = "hints$suffix/$fileName.md"
android.util.Log.d(TAG, "Loading hint: $fileName")
android.util.Log.d(TAG, "Device locale: ${locale.language}_${locale.country}")
android.util.Log.d(TAG, "Localized path: $localizedPath")
val localized = MarkdownHintLoader.loadFromAssets(context, localizedPath)
if (localized != null) {
android.util.Log.d(TAG, "Found localized version at: $localizedPath")
localized
} else {
// Fall back to English default in hints folder
val defaultPath = "hints/$fileName.md"
android.util.Log.d(TAG, "Localized not found, trying default: $defaultPath")
val default = MarkdownHintLoader.loadFromAssets(context, defaultPath)
if (default != null) {
android.util.Log.d(TAG, "Found default version at: $defaultPath")
} else {
android.util.Log.e(TAG, "No hint found for: $fileName (tried: $localizedPath, $defaultPath)")
}
default
}
}
Column(
modifier = modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
) {
if (content != null) {
MarkdownText(
markdown = content,
style = MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurface
)
)
}
// If content is null, nothing is rendered (empty hint)
}
}
// Preview Composables for each HintElement type
@Suppress("HardCodedStringLiteral")
@Preview
@Composable
fun TextElementPreview() {
RenderHintElement(HintElement.Text("This is a sample text element."))
}
@Suppress("HardCodedStringLiteral")
@Preview
@Composable
fun HeaderElementPreview() {
RenderHintElement(HintElement.Header("Sample Header", "Optional subtitle"))
}
@Suppress("HardCodedStringLiteral")
@Preview
@Composable
fun SectionElementPreview() {
RenderHintElement(
HintElement.Section(
title = "Sample Section",
content = listOf(
HintElement.Text("Content 1"),
HintElement.Text("Content 2")
)
)
)
}
@Suppress("HardCodedStringLiteral")
@Preview
@Composable
fun BulletListElementPreview() {
RenderHintElement(
HintElement.BulletList(
listOf("Item 1", "Item 2", "Item 3")
)
)
}
@Suppress("HardCodedStringLiteral")
@Preview
@Composable
fun VisualStepElementPreview() {
RenderHintElement(
HintElement.VisualStep(
step = "1",
title = "Sample Step",
description = "This is a description of the step.",
trailing = {
Icon(AppIcons.Info, contentDescription = null)
}
)
)
}
@Suppress("HardCodedStringLiteral")
@Preview
@Composable
fun InfoBadgeElementPreview() {
RenderHintElement(
HintElement.InfoBadge(
icon = AppIcons.Info,
text = "Sample info badge"
)
)
}
@Preview
@Composable
fun DividerElementPreview() {
RenderHintElement(HintElement.Divider)
}
@Suppress("HardCodedStringLiteral")
@Preview
@Composable
fun CardElementPreview() {
RenderHintElement(
HintElement.Card {
Text("Content inside card")
}
)
}
@Suppress("HardCodedStringLiteral")
@Preview
@Composable
fun ChipsElementPreview() {
RenderHintElement(
HintElement.Chips(
listOf("Chip 1", "Chip 2", "Chip 3")
)
)
}
@Suppress("HardCodedStringLiteral")
@Preview
@Composable
fun SmallCardElementPreview() {
RenderHintElement(HintElement.SmallCard("Sample Card"))
}
@Suppress("HardCodedStringLiteral")
@Preview
@@ -258,4 +134,13 @@ fun UIElementPreview() {
Text("Custom UI Element")
}
)
}
}
@Suppress("HardCodedStringLiteral")
@Preview
@Composable
fun LocalizedMarkdownElementPreview() {
RenderHintElement(
HintElement.LocalizedMarkdown("example_hint")
)
}

View File

@@ -1,7 +1,9 @@
package eu.gaudian.translator.view.hints
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController
import eu.gaudian.translator.R
/**
* Wrapper for Category Hint Screen
@@ -10,9 +12,9 @@ import androidx.navigation.NavController
fun CategoryHintScreenWrapper(navController: NavController) {
HintScreen(
navController = navController,
title = CategoryHint.getTitle()
title = stringResource(R.string.category_hint_intro)
) {
CategoryHint.Content()
CategoryHint()
}
}
@@ -23,9 +25,9 @@ fun CategoryHintScreenWrapper(navController: NavController) {
fun DictionaryHintScreen(navController: NavController) {
HintScreen(
navController = navController,
title = getDictionaryOptionsHint().getTitle()
title = stringResource(R.string.label_dictionary_options)
) {
getDictionaryOptionsHint()
getDictionaryOptionsHint().Render()
}
}
@@ -36,7 +38,7 @@ fun DictionaryHintScreen(navController: NavController) {
fun ImportHintScreen(navController: NavController) {
HintScreen(
navController = navController,
title = getImportVocabularyHint().getTitle()
title = stringResource(R.string.hint_how_to_generate_vocabulary_with_ai)
) {
getImportVocabularyHint()
}
@@ -49,9 +51,9 @@ fun ImportHintScreen(navController: NavController) {
fun SortingHintScreen(navController: NavController) {
HintScreen(
navController = navController,
title = SortingScreenHint.getTitle()
title = stringResource(R.string.sorting_hint_title)
) {
SortingScreenHint.Content()
SortingScreenHint()
}
}
@@ -62,9 +64,9 @@ fun SortingHintScreen(navController: NavController) {
fun StagesHintScreen(navController: NavController) {
HintScreen(
navController = navController,
title = LearningStagesHint.getTitle()
title = stringResource(R.string.learning_stages_title)
) {
LearningStagesHint.Content()
LearningStagesHint()
}
}
@@ -75,9 +77,9 @@ fun StagesHintScreen(navController: NavController) {
fun TranslationHintScreen(navController: NavController) {
HintScreen(
navController = navController,
title = getTranslationScreenHint().getTitle()
title = stringResource(R.string.hint_translate_how_it_works)
) {
getTranslationScreenHint()
getTranslationScreenHint().Render()
}
}
@@ -88,7 +90,7 @@ fun TranslationHintScreen(navController: NavController) {
fun ScanHintScreen(navController: NavController) {
HintScreen(
navController = navController,
title = getAddModelScanHint().getTitle()
title = stringResource(R.string.hint_scan_hint_title)
) {
AddModelScanHint()
}
@@ -101,9 +103,9 @@ fun ScanHintScreen(navController: NavController) {
fun ApiHintScreen(navController: NavController) {
HintScreen(
navController = navController,
title = ApiKeyHint.getTitle()
title = stringResource(R.string.hint_how_to_connect_to_an_ai)
) {
ApiKeyHint.Content()
ApiKeyHint()
}
}
@@ -114,8 +116,8 @@ fun ApiHintScreen(navController: NavController) {
fun VocabularyProgressHintScreen(navController: NavController) {
HintScreen(
navController = navController,
title = VocabularyProgressHint.getTitle()
title = stringResource(R.string.hint_vocabulary_progress_hint_title)
) {
VocabularyProgressHint.Content()
VocabularyProgressHint()
}
}

View File

@@ -41,38 +41,37 @@ fun HintsOverviewScreen(
) {
val showExperimental = LocalShowExperimentalFeatures.current
// Get hints using the new function-based approach
val importHint = getImportVocabularyHint()
val addModelScanHint = getAddModelScanHint()
val dictionaryOptionsHint = getDictionaryOptionsHint()
val translationScreenHint = getTranslationScreenHint()
val categoryHint = getCategoryHint()
val learningStagesHint = getLearningStagesHint()
val sortingScreenHint = getSortingScreenHint()
val vocabularyProgressHint = getVocabularyProgressHint()
val apiKeyHint = getApiKeyHint()
val hintGroups = remember(showExperimental, importHint) {
val hintGroups = remember(showExperimental, importHint, addModelScanHint, dictionaryOptionsHint, translationScreenHint) {
val allGroups = listOf(
R.string.hint_hints_header_basics to listOf(
HintItem(CategoryHint.titleRes, AppIcons.Category, SettingsRoutes.HINTS_CATEGORIES),
HintItem(LearningStagesHint.titleRes, AppIcons.Stages, SettingsRoutes.HINTS_STAGES),
HintItem(categoryHint.titleRes, AppIcons.Category, SettingsRoutes.HINTS_CATEGORIES),
HintItem(learningStagesHint.titleRes, AppIcons.Stages, SettingsRoutes.HINTS_STAGES),
HintItem(translationScreenHint.titleRes, AppIcons.Translate, SettingsRoutes.HINTS_TRANSLATION)
),
R.string.hint_hints_header_vocabulary to listOf(
HintItem(importHint.titleRes, AppIcons.Vocabulary, SettingsRoutes.HINTS_IMPORT),
HintItem(SortingScreenHint.titleRes, AppIcons.Sort, SettingsRoutes.HINTS_SORTING),
HintItem(sortingScreenHint.titleRes, AppIcons.Sort, SettingsRoutes.HINTS_SORTING),
HintItem(dictionaryOptionsHint.titleRes, AppIcons.Dictionary, SettingsRoutes.HINTS_DICTIONARY),
HintItem(VocabularyProgressHint.titleRes, AppIcons.Stages, SettingsRoutes.HINTS_VOCABULARY_PROGRESS)
HintItem(vocabularyProgressHint.titleRes, AppIcons.Stages, SettingsRoutes.HINTS_VOCABULARY_PROGRESS)
),
R.string.hint_hints_header_advanced to listOf(
HintItem(addModelScanHint.titleRes, AppIcons.AI, SettingsRoutes.HINTS_SCAN),
HintItem(ApiKeyHint.titleRes, AppIcons.ApiKey, SettingsRoutes.HINTS_API)
HintItem(apiKeyHint.titleRes, AppIcons.ApiKey, SettingsRoutes.HINTS_API)
)
)
if (showExperimental) {
allGroups
} else {
allGroups
}
allGroups
}
AppOutlinedCard {
@@ -185,4 +184,4 @@ fun HintListItemPreview() {
icon = AppIcons.Category,
onClick = {}
)
}
}

View File

@@ -1,173 +1,45 @@
package eu.gaudian.translator.view.hints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
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.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import eu.gaudian.translator.R
import eu.gaudian.translator.view.composable.AppCheckbox
import eu.gaudian.translator.view.composable.AppOutlinedTextField
import eu.gaudian.translator.view.composable.AppSlider
/**
* Provides the migrated Hint for ImportVocabulary.
* This function is @Composable to access stringResource.
* Migrated ImportVocabularyHint using markdown-based format.
* Content is loaded from localized assets/hints based on device locale.
*/
@Composable
fun getImportVocabularyHint(): Hint {
return Hint(
titleRes = R.string.hint_how_to_generate_vocabulary_with_ai,
elements = listOf(
HintElement.Section(
title = stringResource(R.string.import_ai_intro),
content = listOf(
// VisualStep 1: Search Term
HintElement.VisualStep(
step = "1",
title = stringResource(R.string.import_step1_title),
description = stringResource(R.string.text_hint_you_can_search),
trailing = {
// The AppTextField is wrapped in the VisualStep's trailing composable
AppOutlinedTextField(
value = stringResource(R.string.search_term_placeholder),
onValueChange = {},
label = { Text(stringResource(R.string.text_search_term)) },
modifier = Modifier.fillMaxWidth(),
enabled = false
)
}
),
// VisualStep 2: Select Languages
HintElement.VisualStep(
step = "2",
title = stringResource(R.string.import_step2_title),
description = stringResource(R.string.import_step2_desc)
),
// VisualStep 3: Select Amount
HintElement.VisualStep(
step = "3",
title = stringResource(R.string.import_step3_title),
description = stringResource(R.string.import_step3_desc),
trailing = {
// The Column with Slider and Text is wrapped in the trailing composable
Column {
AppSlider(
value = 10f,
onValueChange = {},
valueRange = 1f..25f,
steps = 24,
modifier = Modifier.fillMaxWidth(),
enabled = false
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.import_after_generating),
style = MaterialTheme.typography.bodyMedium
)
}
}
)
)
)
elements = listOf(
HintElement.LocalizedMarkdown("import_hint")
)
)
}
/**
* Preview for the migrated ImportVocabularyHint.
* It now calls getImportVocabularyHint() and then Render().
*/
@Preview
@Composable
fun ImportVocabularyHintPreview() {
fun ImportVocabularyHint() {
getImportVocabularyHint().Render()
}
/**
* Provides the migrated Hint for VocabularyReview.
* This function is @Composable to access stringResource.
* Migrated VocabularyReviewHint using markdown-based format.
* Content is loaded from localized assets/hints based on device locale.
*/
@Composable
fun getVocabularyReviewHint(): Hint {
return Hint(
titleRes = R.string.review_intro,
elements = listOf(
// Section 1: Select Items
HintElement.Section(
title = stringResource(R.string.review_select_items_title),
content = listOf(
HintElement.Text(stringResource(R.string.review_select_items_desc)),
// Custom Row with Checkbox is wrapped in a UIElement
HintElement.UIElement {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
AppCheckbox(checked = true, onCheckedChange = {}, enabled = false)
Spacer(modifier = Modifier.width(8.dp))
Column {
Text(text = stringResource(R.string.example_word_der_apfel), style = MaterialTheme.typography.bodyLarge)
Text(text = stringResource(R.string.example_word_the_apple), style = MaterialTheme.typography.bodyMedium)
}
}
}
)
),
// Divider
HintElement.Divider,
// Section 2: Duplicate Handling
HintElement.Section(
title = stringResource(R.string.duplicate_handling_title),
content = listOf(
HintElement.Text(stringResource(R.string.duplicate_handling_desc)),
// Custom Row with Checkbox and Error Text is wrapped in a UIElement
HintElement.UIElement {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
AppCheckbox(checked = false, onCheckedChange = {}, enabled = false)
Spacer(modifier = Modifier.width(8.dp))
Column {
Text(text = stringResource(R.string.example_word_der_hund), style = MaterialTheme.typography.bodyLarge)
Text(text = stringResource(R.string.example_word_the_dog), style = MaterialTheme.typography.bodyMedium)
}
Spacer(modifier = Modifier.weight(1f))
Text(stringResource(R.string.duplicate), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.error)
}
}
)
),
// Divider
HintElement.Divider,
// Section 3: Add to List
HintElement.Section(
title = stringResource(R.string.add_to_list_optional),
content = listOf(
HintElement.Text(stringResource(R.string.add_to_list_optional_desc))
)
)
HintElement.LocalizedMarkdown("review_hint")
)
)
}
/**
* Preview for the migrated VocabularyReviewHint.
* It now calls getVocabularyReviewHint() and then Render().
*/
@Preview
@Composable
fun VocabularyReviewHintPreview() {
getVocabularyReviewHint().Render()
}
fun VocabularyReviewHint() {
getVocabularyReviewHint()
}

View File

@@ -1,205 +1,33 @@
package eu.gaudian.translator.view.hints
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
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.geometry.Offset
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import eu.gaudian.translator.R
import eu.gaudian.translator.view.composable.AppIcons
private data class LearningStage(
val icon: ImageVector,
val name: String,
val interval: String? = null
)
/**
* TODO: Migrate to markdown-based hint format.
* Create a .md file in assets/hints/ and use MarkdownHint composable instead.
* See MarkdownHint.kt for implementation details.
* Migrated Learning Stages hint using markdown-based format.
* Content is loaded from localized assets/hints based on device locale.
*/
object LearningStagesHint : LegacyHint() {
override val titleRes: Int = R.string.learning_stages_title
@Composable
override fun Content() {
val stages = listOf(
LearningStage(AppIcons.StageNew, stringResource(R.string.stage_new)),
LearningStage(AppIcons.Stage1, stringResource(R.string.stage_1), stringResource(R.string.interval_1_day)),
LearningStage(AppIcons.Stage2, stringResource(R.string.stage_2), stringResource(R.string.interval_3_days)),
LearningStage(AppIcons.Stage3, stringResource(R.string.stage_3), stringResource(R.string.interval_1_week)),
LearningStage(AppIcons.Stage4, stringResource(R.string.stage_4), stringResource(R.string.interval_2_weeks)),
LearningStage(AppIcons.Stage5, stringResource(R.string.stage_5), stringResource(R.string.interval_1_month)),
LearningStage(AppIcons.StageLearned, stringResource(R.string.stage_learned))
@Composable
fun getLearningStagesHint(): Hint {
return Hint(
titleRes = R.string.learning_stages_title,
elements = listOf(
HintElement.LocalizedMarkdown("learning_stages_hint")
)
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
HeaderWithIcon(
title = stringResource(R.string.learning_stages_title)
)
HintSection(title = stringResource(R.string.learning_stages_title)) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Group stages into chunks of 2 to create rows.
stages.chunked(2).forEach { rowItems ->
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
rowItems.forEach { stage ->
StageNode(stage)
// If the stage has an interval, show the connector arrow
stage.interval?.let { interval ->
StageConnector(interval)
}
}
}
}
}
}
HintSection(title = stringResource(R.string.hint_how_it_works)) {
RuleExplanation(
icon = AppIcons.Check,
title = stringResource(R.string.hint_answer_correctly),
description = stringResource(R.string.hint_the_word_moves),
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(8.dp))
RuleExplanation(
icon = AppIcons.Error,
title = stringResource(R.string.hint_answer_incorrectly),
description = stringResource(R.string.hint_the_word_moves_back_another_stage_this_helps_you_focus_on_),
tint = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.height(8.dp))
RuleExplanation(
icon = AppIcons.Info,
title = stringResource(R.string.hint_customizable),
description = stringResource(R.string.hint_you_can_costumize_all_intervals_and_rules_in_the_settings),
tint = MaterialTheme.colorScheme.primary
)
}
}
}
)
}
@Composable
fun LearningStagesHint() {
LearningStagesHint.Content()
getLearningStagesHint().Render()
}
/**
* A composable that displays a single stage icon and its name.
*/
@Composable
private fun StageNode(stage: LearningStage) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Icon(
imageVector = stage.icon,
contentDescription = stage.name,
modifier = Modifier.size(32.dp),
tint = MaterialTheme.colorScheme.primary
)
Text(
text = stage.name,
style = MaterialTheme.typography.labelMedium
)
}
}
/**
* A composable that draws a dashed arrow and displays the time interval between stages.
*/
@Composable
private fun StageConnector(interval: String) {
val arrowColor = MaterialTheme.colorScheme.onSurfaceVariant
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(horizontal = 8.dp)
) {
Text(
text = interval,
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.secondary
)
Spacer(modifier = Modifier.height(4.dp))
Canvas(modifier = Modifier.size(width = 50.dp, height = 10.dp)) {
val startY = center.y
val endX = size.width
// Draw the dashed line
drawLine(
color = arrowColor,
start = Offset(0f, startY),
end = Offset(endX - 5.dp.toPx(), startY),
strokeWidth = 1.5.dp.toPx(),
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
)
// Draw the arrowhead
drawLine(arrowColor, Offset(endX - 5.dp.toPx(), startY - 4.dp.toPx()), Offset(endX, startY), strokeWidth = 1.5.dp.toPx())
drawLine(arrowColor, Offset(endX - 5.dp.toPx(), startY + 4.dp.toPx()), Offset(endX, startY), strokeWidth = 1.5.dp.toPx())
}
}
}
/**
* A helper composable to explain the success/failure rules.
*/
@Composable
private fun RuleExplanation(icon: ImageVector, title: String, description: String, tint: androidx.compose.ui.graphics.Color) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Icon(
imageVector = icon,
contentDescription = title,
modifier = Modifier.size(28.dp),
tint = tint
)
Column {
Text(text = title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Text(text = description, style = MaterialTheme.typography.bodyMedium)
}
}
}
@Preview(showBackground = true)
@Preview
@Composable
fun LearningStagesHintPreview() {
MaterialTheme {
Box(modifier = Modifier.padding(8.dp)) {
LearningStagesHint()
}
LearningStagesHint()
}
}
}

View File

@@ -0,0 +1,225 @@
package eu.gaudian.translator.view.hints
import android.content.Context
import android.os.Build
import java.util.Locale
/**
* Internationalization system for markdown hints.
*
* This follows Android's locale-qualified resource pattern:
* - assets/hints/ - Default (English)
* - assets/hints-de-rDE/ - German
* - assets/hints-pt-rBR/ - Portuguese (Brazil)
* - etc.
*
* Usage:
* val content = MarkdownHintLoader.loadHint(context, "api_key_hint")
* MarkdownHintLoader.getHintFileName("api_key_hint") // Returns localized filename
*/
object MarkdownHintLoader {
/**
* Load markdown content with automatic locale detection.
*
* @param context The context for accessing assets
* @param hintFileName The base filename without locale suffix (e.g., "api_key_hint")
* @return The markdown content as a string, or null if not found
*/
fun loadHint(context: Context, hintFileName: String): String? {
val locale = getCurrentLocale(context)
val suffix = getLocaleSuffix(locale)
// Try localized version (folder has suffix, filename doesn't)
val localizedPath = "hints$suffix/$hintFileName.md"
val localizedContent = loadFromAssets(context, localizedPath)
if (localizedContent != null) {
return localizedContent
}
// Try with just language code (e.g., hints-pt/ instead of hints-pt-rBR/)
val languageSuffix = if (locale.country.isNotEmpty()) "-${locale.language}" else ""
val languageOnlyPath = "hints$languageSuffix/$hintFileName.md"
val languageContent = loadFromAssets(context, languageOnlyPath)
if (languageContent != null) {
return languageContent
}
// Fall back to default (English) in hints folder
val defaultPath = "hints/$hintFileName.md"
val defaultContent = loadFromAssets(context, defaultPath)
if (defaultContent != null) {
return defaultContent
}
return null
}
/**
* Get the localized file path for a hint.
*
* @param hintFileName The base filename (e.g., "api_key_hint")
* @return The full path including locale folder (e.g., "hints-de-rDE/api_key_hint.md")
*/
fun getHintPath(hintFileName: String): String {
val locale = Locale.getDefault()
val localeSuffix = getLocaleSuffix(locale)
return "hints$localeSuffix/$hintFileName.md"
}
/**
* Get the localized file name for a hint.
*
* @param hintFileName The base filename (e.g., "api_key_hint")
* @param locale The target locale
* @return The file name with locale suffix (e.g., "api_key_hint-de-rDE.md")
*/
fun getLocalizedFileName(hintFileName: String, locale: Locale): String {
val localeSuffix = getLocaleSuffix(locale)
return "$hintFileName$localeSuffix.md"
}
/**
* Get the file name with language-only suffix.
*
* @param hintFileName The base filename
* @param locale The target locale
* @return The file name with language suffix (e.g., "api_key_hint-de.md")
*/
private fun getLanguageOnlyFileName(hintFileName: String, locale: Locale): String {
val languageCode = locale.language
return "$hintFileName-$languageCode.md"
}
/**
* Get the current device locale.
*/
@Suppress("DEPRECATION")
fun getCurrentLocale(context: Context): Locale {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
context.resources.configuration.locales[0]
} else {
context.resources.configuration.locale
}
}
/**
* Get all supported locales for hints.
*/
fun getSupportedLocales(): List<Locale> {
return listOf(
Locale.ENGLISH, // Default
Locale.GERMAN, // de-rDE
Locale("pt", "BR"), // pt-rBR
Locale.FRENCH, // fr-rFR
Locale("es", "ES"), // es-rES
Locale.ITALIAN, // it-rIT
Locale("nl", "NL"), // nl-rNL
Locale("hr", "HR") // hr-rHR
)
}
/**
* Check if a localized version exists for the given locale.
*/
fun localizedVersionExists(context: Context, hintFileName: String, locale: Locale): Boolean {
val localizedFileName = getLocalizedFileName(hintFileName, locale)
val suffix = getLocaleSuffix(locale)
return assetExists(context, "hints$suffix/$localizedFileName")
}
/**
* Load content from assets.
*/
fun loadFromAssets(context: Context, fileName: String): String? {
return try {
context.assets.open(fileName).bufferedReader().use { it.readText() }
} catch (e: Exception) {
null
}
}
/**
* Check if an asset file exists.
*/
private fun assetExists(context: Context, path: String): Boolean {
return try {
context.assets.open(path).close()
true
} catch (e: Exception) {
false
}
}
/**
* Get the locale suffix string.
*/
fun getLocaleSuffix(locale: Locale): String {
val language = locale.language
val country = locale.country
return buildString {
if (language.isNotEmpty()) {
append("-")
append(language.lowercase())
}
if (country.isNotEmpty()) {
append("-r")
append(country.uppercase())
}
}
}
}
/**
* Extension function to get localized hint content.
*/
fun Context.loadLocalizedHint(hintFileName: String): String? {
return MarkdownHintLoader.loadHint(this, hintFileName)
}
/**
* Data class for localized hint information.
*/
data class LocalizedHint(
val fileName: String,
val locale: Locale,
val isDefault: Boolean = false
)
/**
* Hint localization manager that tracks available translations.
*/
object HintLocalizationManager {
/**
* Get all available translations for a hint.
*/
fun getAvailableTranslations(context: Context, baseFileName: String): List<LocalizedHint> {
val available = mutableListOf<LocalizedHint>()
val defaultLocale = MarkdownHintLoader.getCurrentLocale(context)
// Check each supported locale
MarkdownHintLoader.getSupportedLocales().forEach { locale ->
val fileName = MarkdownHintLoader.getLocalizedFileName(baseFileName, locale)
val path = "hints${MarkdownHintLoader.getLocaleSuffix(locale)}/$fileName"
if (MarkdownHintLoader.loadFromAssets(context, path) != null) {
available.add(LocalizedHint(
fileName = fileName,
locale = locale,
isDefault = locale.language == "en" && locale.country.isEmpty()
))
}
}
return available.sortedBy { it.locale.language }
}
/**
* Get the best available translation for current locale.
*/
fun getBestTranslation(context: Context, baseFileName: String): String? {
return MarkdownHintLoader.loadHint(context, baseFileName)
}
}

View File

@@ -1,244 +1,33 @@
package eu.gaudian.translator.view.hints
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.SuggestionChipDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.gaudian.translator.R
import eu.gaudian.translator.view.composable.AppButton
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppOutlinedTextField
/**
* TODO: Migrate to markdown-based hint format.
* Create a .md file in assets/hints/ and use MarkdownHint composable instead.
* See MarkdownHint.kt for implementation details.
*/
object SortingScreenHint : LegacyHint() {
override val titleRes: Int = R.string.sorting_hint_title
@Composable
override fun getTitle(): String = stringResource(R.string.sorting_hint_title)
@Composable
override fun Content() {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
HeaderWithIcon(
title = stringResource(R.string.sorting_hint_title)
)
HintSection(title = stringResource(R.string.sorting_hint_intro_text)) {
@Suppress("HardCodedStringLiteral")
AppOutlinedTextField(
value = "der Hund",
onValueChange = {},
label = { Text(stringResource(R.string.label_word)) },
modifier = Modifier.fillMaxWidth(),
enabled = false
)
@Suppress("HardCodedStringLiteral")
AppOutlinedTextField(
value = "the dog",
onValueChange = {},
label = { Text(stringResource(R.string.label_translation)) },
modifier = Modifier.fillMaxWidth(),
enabled = false
)
}
HorizontalDivider()
HintSection(title = stringResource(R.string.sorting_hint_helper_text)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
SuggestionChip(
onClick = {},
label = { Text(stringResource(R.string.sorting_hint_chip_duplicate)) },
icon = {
Icon(
imageVector = AppIcons.Warning,
contentDescription = stringResource(R.string.label_warning),
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
colors = SuggestionChipDefaults.suggestionChipColors(
containerColor = MaterialTheme.colorScheme.errorContainer,
labelColor = MaterialTheme.colorScheme.onErrorContainer,
iconContentColor = MaterialTheme.colorScheme.onErrorContainer
),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.error),
enabled = false
)
SuggestionChip(
onClick = {},
label = { Text(stringResource(R.string.label_remove_articles)) },
icon = {
Icon(
imageVector = AppIcons.Clean,
contentDescription = stringResource(R.string.label_remove_articles),
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
border = BorderStroke(1.dp, MaterialTheme.colorScheme.onSurface),
enabled = false
)
}
}
HorizontalDivider()
HintSection(title = stringResource(R.string.sorting_hint_decide_next_action)) {
LabeledSegmentedIconButtons(
onDeleteClick = {},
onLearnedClick = {},
onDoneClick = {}
)
}
}
}
}
@Preview
@Composable
private fun SortingScreenHintContentPreview() {
SortingScreenHint.Content()
}
/**
* A non-functional, recycled composable for visual demonstration in the hint.
* Migrated Sorting Screen hint using markdown-based format.
* Content is loaded from localized assets/hints based on device locale.
*/
@Composable
private fun LabeledSegmentedIconButtons(
onDeleteClick: () -> Unit,
onLearnedClick: () -> Unit,
onDoneClick: () -> Unit
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
val buttonHeight = 48.dp
val cornerRadius = 24.dp
val secondaryButtonColor = MaterialTheme.colorScheme.surfaceVariant
Row(
modifier = Modifier
.fillMaxWidth()
.height(buttonHeight)
.clip(RoundedCornerShape(cornerRadius)),
verticalAlignment = Alignment.CenterVertically
) {
AppButton(
onClick = onDeleteClick,
modifier = Modifier
.weight(1f)
.fillMaxHeight(),
shape = RoundedCornerShape(0.dp),
colors = ButtonDefaults.buttonColors(containerColor = secondaryButtonColor),
enabled = false
) {
Icon(AppIcons.Delete, contentDescription = stringResource(R.string.label_delete), tint = MaterialTheme.colorScheme.onSurfaceVariant)
}
VerticalDivider()
AppButton(
onClick = onLearnedClick,
modifier = Modifier
.weight(1f)
.fillMaxHeight(),
shape = RoundedCornerShape(0.dp),
colors = ButtonDefaults.buttonColors(containerColor = secondaryButtonColor),
enabled = false
) {
Icon(AppIcons.StageLearned, contentDescription = stringResource(R.string.label_learned), tint = MaterialTheme.colorScheme.onSurfaceVariant)
}
AppButton(
onClick = onDoneClick,
modifier = Modifier
.weight(2f)
.fillMaxHeight(),
shape = RoundedCornerShape(0.dp),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary),
enabled = false
) {
Icon(AppIcons.Stage1, contentDescription = stringResource(R.string.label_done), tint = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
Spacer(Modifier.height(4.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp)
) {
Text(stringResource(R.string.label_delete), Modifier.weight(1f), textAlign = TextAlign.Center, fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(stringResource(R.string.label_learned), Modifier.weight(1f), textAlign = TextAlign.Center, fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(stringResource(R.string.label_move_first_stage), Modifier.weight(2f), textAlign = TextAlign.Center, fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurface)
}
}
}
@Preview
@Composable
private fun LabeledSegmentedIconButtonsPreview() {
LabeledSegmentedIconButtons(onDeleteClick = {}, onLearnedClick = {}, onDoneClick = {})
}
@Composable
private fun VerticalDivider(
modifier: Modifier = Modifier,
thickness: Dp = 1.dp,
color: Color = MaterialTheme.colorScheme.background.copy(alpha = 0.5f)
) {
Box(
modifier
.fillMaxHeight()
.width(thickness)
.background(color = color)
fun getSortingScreenHint(): Hint {
return Hint(
titleRes = R.string.sorting_hint_title,
elements = listOf(
HintElement.LocalizedMarkdown("sorting_hint")
)
)
}
@Composable
fun SortingScreenHint() {
getSortingScreenHint()
}
@Preview
@Composable
private fun VerticalDividerPreview() {
VerticalDivider()
}
fun SortingScreenHintPreview() {
MaterialTheme {
SortingScreenHint()
}
}

View File

@@ -1,62 +1,21 @@
package eu.gaudian.translator.view.hints
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
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.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import eu.gaudian.translator.R
import eu.gaudian.translator.view.composable.AppIcons
/**
* Migrated TranslationScreenHint using markdown-based format.
* Content is loaded from localized assets/hints based on device locale.
*/
@Composable
fun getTranslationScreenHint() = Hint(
titleRes = R.string.hint_translate_how_it_works,
elements = listOf(
HintElement.TextSection(
title = stringResource(R.string.hint_translate_alternative_translations_title),
text = stringResource(R.string.hint_translate_alternative_translations_desc)
),
HintElement.TextSection(
title = stringResource(R.string.hint_translate_custom_prompts_title),
text = stringResource(R.string.hint_translate_custom_prompts_desc),
),
HintElement.TextSection(
title = stringResource(R.string.hint_translate_multiple_services_title),
text = stringResource(R.string.hint_translate_multiple_services_desc)
),
HintElement.TextSection(
title = stringResource(R.string.hint_translate_history_title),
text = stringResource(R.string.hint_translate_history_desc)
),
HintElement.TextSection(
title = stringResource(R.string.hint_translate_tts_title),
text = stringResource(R.string.hint_translate_tts_desc)
),
HintElement.TextSection(
title = stringResource(R.string.hint_translate_quick_actions_title),
text = stringResource(R.string.hint_translate_quick_actions_desc)
),
HintElement.TextSection(
title = stringResource(R.string.hint_translate_model_selection_title),
text = stringResource(R.string.hint_translate_model_selection_desc))
)
HintElement.LocalizedMarkdown("translation_hint")
)
)
@Composable
fun TranslationScreenHint() {
getTranslationScreenHint().Render()
@@ -67,38 +26,3 @@ fun TranslationScreenHint() {
fun TranslationScreenHintPreview() {
getTranslationScreenHint().Render()
}
@Composable
private fun TranslationHintItem(
icon: ImageVector,
title: String,
description: String
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Icon(
imageVector = icon,
contentDescription = title,
modifier = Modifier.size(32.dp),
tint = MaterialTheme.colorScheme.secondary
)
Column {
Text(text = title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(4.dp))
Text(text = description, style = MaterialTheme.typography.bodyMedium)
}
}
}
@Preview
@Composable
private fun TranslationHintItemPreview() {
TranslationHintItem(
icon = AppIcons.Info,
title = stringResource(R.string.hint_translation_context_aware_title),
description = stringResource(R.string.hint_translation_context_aware_desc)
)
}

View File

@@ -1,127 +1,33 @@
package eu.gaudian.translator.view.hints
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
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.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import eu.gaudian.translator.R
import eu.gaudian.translator.view.composable.AppIcons
/**
* TODO: Migrate to markdown-based hint format.
* Create a .md file in assets/hints/ and use MarkdownHint composable instead.
* See MarkdownHint.kt for implementation details.
* Migrated Vocabulary Progress hint using markdown-based format.
* Content is loaded from localized assets/hints based on device locale.
*/
object VocabularyProgressHint : LegacyHint() {
override val titleRes: Int = R.string.hint_vocabulary_progress_hint_title
@Composable
override fun Content() {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
HeaderWithIcon(
title = stringResource(R.string.hint_vocabulary_progress_hint_title)
)
HintSection(title = stringResource(R.string.hint_vocabulary_progress_tracking_title)) {
// Progress Tracking
VocabularyProgressHintItem(
icon = AppIcons.BarChart,
title = stringResource(R.string.hint_vocabulary_progress_tracking_title),
description = stringResource(R.string.hint_vocabulary_progress_tracking_desc)
)
// Learning Stages
VocabularyProgressHintItem(
icon = AppIcons.Stages,
title = stringResource(R.string.hint_vocabulary_learning_stages_title),
description = stringResource(R.string.hint_vocabulary_learning_stages_desc)
)
// Review System
VocabularyProgressHintItem(
icon = AppIcons.History,
title = stringResource(R.string.hint_vocabulary_review_system_title),
description = stringResource(R.string.hint_vocabulary_review_system_desc)
)
// Customization
VocabularyProgressHintItem(
icon = AppIcons.Tune,
title = stringResource(R.string.hint_vocabulary_customization_title),
description = stringResource(R.string.hint_vocabulary_customization_desc)
)
}
}
}
}
@Preview
@Composable
private fun ContentPreview() {
VocabularyProgressHint.Content()
fun getVocabularyProgressHint(): Hint {
return Hint(
titleRes = R.string.hint_vocabulary_progress_hint_title,
elements = listOf(
HintElement.LocalizedMarkdown("vocabulary_progress_hint")
)
)
}
@Composable
fun VocabularyProgressHint() {
VocabularyProgressHint.Content()
getVocabularyProgressHint().Render()
}
@Preview
@Composable
private fun VocabularyProgressHintPreview() {
VocabularyProgressHint()
}
@Composable
private fun VocabularyProgressHintItem(
icon: androidx.compose.ui.graphics.vector.ImageVector,
title: String,
description: String
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Icon(
imageVector = icon,
contentDescription = title,
modifier = Modifier.size(32.dp),
tint = MaterialTheme.colorScheme.secondary
)
Column {
Text(text = title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(4.dp))
Text(text = description, style = MaterialTheme.typography.bodyMedium)
}
fun VocabularyProgressHintPreview() {
MaterialTheme {
VocabularyProgressHint()
}
}
@Suppress("HardCodedStringLiteral")
@Preview
@Composable
private fun VocabularyProgressHintItemPreview() {
VocabularyProgressHintItem(
icon = AppIcons.BarChart,
title = "Progress Tracking",
description = "Track your learning progress with detailed statistics."
)
}

View File

@@ -59,6 +59,7 @@ import eu.gaudian.translator.view.composable.AppSwitch
import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.composable.ModelBadges
import eu.gaudian.translator.view.hints.AddModelScanHint
import eu.gaudian.translator.view.hints.getAddModelScanHint
import eu.gaudian.translator.viewmodel.ApiViewModel
@Composable
@@ -140,7 +141,7 @@ fun AddModelScreen(navController: NavController, providerKey: String) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
hintContent = { AddModelScanHint() }
hintContent = getAddModelScanHint()
)
},
) { paddingValues ->

View File

@@ -80,7 +80,7 @@ import eu.gaudian.translator.view.composable.ClickableText
import eu.gaudian.translator.view.composable.PrimaryButton
import eu.gaudian.translator.view.composable.SecondaryButton
import eu.gaudian.translator.view.composable.TabItem
import eu.gaudian.translator.view.hints.ApiKeyHint
import eu.gaudian.translator.view.hints.getApiKeyHint
import eu.gaudian.translator.viewmodel.ApiKeyManagementState
import eu.gaudian.translator.viewmodel.ApiViewModel
import eu.gaudian.translator.viewmodel.ProviderState
@@ -121,7 +121,7 @@ fun ApiKeyScreen(navController: NavController) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
hintContent = { ApiKeyHint.Content() }
hintContent = getApiKeyHint()
)
}
) { paddingValues ->

View File

@@ -61,12 +61,8 @@ fun CustomVocabularyPromptScreen(
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
hintContent = {
Text(
//TODO make this nicer and own file
stringResource(R.string.hint_this_screen_lets_you_customize_)
)
}
hintContent = null //TODO: Add hint
)
}
) { paddingValues ->

View File

@@ -72,7 +72,7 @@ fun DictionaryOptionsScreen(
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
hintContent = { getDictionaryOptionsHint() }
hintContent = getDictionaryOptionsHint()
)
}
) { paddingValues ->

View File

@@ -70,12 +70,7 @@ fun TranslationSettingsScreen(
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
hintContent = {
Text(
//TODO make this nicer and an own file
stringResource(R.string.hint_use_this_screen_to_define)
)
}
hintContent = null //TODO add hint
)
}
) { paddingValues ->

View File

@@ -50,6 +50,7 @@ import eu.gaudian.translator.view.composable.AppScaffold
import eu.gaudian.translator.view.composable.AppSlider
import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.hints.VocabularyProgressHint
import eu.gaudian.translator.view.hints.getVocabularyProgressHint
import eu.gaudian.translator.viewmodel.SettingsViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel
import kotlin.math.exp
@@ -85,7 +86,7 @@ fun VocabularyProgressOptionsScreen(
}
},
// Here is the new hint content
hintContent = { VocabularyProgressHint() }
hintContent = getVocabularyProgressHint()
)
}
) { paddingValues ->

View File

@@ -74,7 +74,7 @@ import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.composable.SingleLanguageDropDown
import eu.gaudian.translator.view.dialogs.CategoryDropdown
import eu.gaudian.translator.view.dialogs.CreateCategoryListDialog
import eu.gaudian.translator.view.hints.SortingScreenHint
import eu.gaudian.translator.view.hints.getSortingScreenHint
import eu.gaudian.translator.viewmodel.CategoryViewModel
import eu.gaudian.translator.viewmodel.LanguageConfigViewModel
import eu.gaudian.translator.viewmodel.LanguageViewModel
@@ -236,7 +236,7 @@ fun VocabularySortingScreen(
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
hintContent = {SortingScreenHint.Content()}
hintContent = getSortingScreenHint()
)
},