Refactor the hint system by consolidating hint definitions into a central HintDefinition enum and migrating individual hint files to a markdown-based approach.

This commit is contained in:
jonasgaudian
2026-02-15 21:46:11 +01:00
parent 7d18f8eb04
commit 8e610259ca
31 changed files with 117 additions and 1098 deletions

View File

@@ -1,87 +0,0 @@
# How to Scan for AI Models
# TODO REWRITE
This guide explains how to use the **Scan** feature to discover and add AI models to your app.
## How Scanning Works
The scan feature searches for available AI models on your device or network.
> **Note:** Results depend on your API key permissions.
### Key Points
- Only public models are shown by default
- Private models require additional setup
- Try again if no models are found
## Why Some Models Are Missing
Some models may not appear in the scan results due to:
| Reason | Description | Icon |
|--------|-------------|------|
| Restricted access | Model requires special permissions | 🔒 |
| Not suitable | Model type not supported | ⚠️ |
| Text only | Only text-based models are supported | ✓ |
### Model Tiers
We recommend these tiers for optimal performance:
- **Nano** - Fastest, for simple tasks
- **Mini** - Balanced speed and capability
- **Small** - Good for most tasks
- **Medium** - More capable, slower
- **Large** - Most capable, paid only
## Tips for Success
1. **Verify your API key** is active and has correct permissions
2. **Select the correct organization** from your account
3. **Type model names manually** if scanning doesn't find them
4. **Prefer instruct or chat models** for text generation
```kotlin
// Example: Manual model addition
val model = Model(
name = "llama3.2",
type = ModelType.TEXT,
provider = "ollama"
)
```
## Visual Guide
### Step 1: Initiate Scan
Click the scan button to search for available models.
### Step 2: Select Model Type
Choose between different model categories:
- **Text Chat** - For conversational AI
- **Instruct** - For direct instructions
- **Complete** - For text completion
### Step 3: Add & Validate
Add the selected model and validate it works correctly.
---
## Can't Find Your Model?
If your model doesn't appear in the scan results:
1. Check if the model is running locally or accessible via API
2. Verify network connectivity
3. Try adding it manually by entering the model details
> **Pro Tip:** You can always add models manually by clicking the "+" button in the models screen.
---
*Last updated: 2024-01-15*
*For more help, visit our documentation website.*

View File

@@ -41,6 +41,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import eu.gaudian.translator.R
import eu.gaudian.translator.view.hints.Hint
import eu.gaudian.translator.view.hints.HintBottomSheet
import eu.gaudian.translator.view.hints.LocalShowHints
@@ -48,7 +49,7 @@ import eu.gaudian.translator.view.hints.LocalShowHints
fun AppDialog(
onDismissRequest: () -> Unit,
title: (@Composable () -> Unit)? = null,
hintContent: @Composable (() -> Unit)? = null,
hintContent: Hint? = null,
content: @Composable () -> Unit
) {
// 1. Swipe Resistance: Prevent accidental dismissal
@@ -98,7 +99,7 @@ fun AppDialog(
if (showBottomSheet) {
EnhancedHintBottomSheet(
onDismissRequest = { showBottomSheet = false },
content = hintContent,
content = {hintContent?.Render()},
parentTitle = title
)
}
@@ -156,7 +157,7 @@ fun AppAlertDialog(
@Composable
private fun DialogHeader(
title: (@Composable () -> Unit)?,
hintContent: @Composable (() -> Unit)?,
hintContent: Hint? = null,
onHintClick: () -> Unit,
onCloseClick: () -> Unit
) {
@@ -327,7 +328,6 @@ fun AppDialogPreview() {
AppDialog(
onDismissRequest = {},
title = { Text("Dialog Title") },
hintContent = { Text("This is a hint.") },
content = {
Column {
Text("Content line 1")
@@ -378,7 +378,6 @@ fun AppDialogLongContentPreview() {
AppDialog(
onDismissRequest = {},
title = { Text("Long Content Dialog") },
hintContent = { Text("Hint for long content dialog") },
content = {
Column {
Text("This is a long content dialog to test scrolling")

View File

@@ -48,7 +48,7 @@ import eu.gaudian.translator.view.composable.AppDialog
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppOutlinedTextField
import eu.gaudian.translator.view.composable.MultipleLanguageDropdown
import eu.gaudian.translator.view.hints.CategoryHint
import eu.gaudian.translator.view.hints.HintDefinition
import eu.gaudian.translator.viewmodel.CategoryViewModel
import eu.gaudian.translator.viewmodel.LanguageViewModel
@@ -80,7 +80,7 @@ fun AddCategoryDialog(
AppDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.label_add_category)) },
hintContent = { CategoryHint() },
hintContent = HintDefinition.CATEGORY.hint(),
content = {
Column(modifier = Modifier.fillMaxWidth()) {
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {

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.ImportVocabularyHint
import eu.gaudian.translator.view.hints.HintDefinition
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 = { ImportVocabularyHint() },
hintContent = HintDefinition.IMPORT.hint(),
content = {
Column(
modifier = Modifier

View File

@@ -34,7 +34,7 @@ import eu.gaudian.translator.view.composable.AppButton
import eu.gaudian.translator.view.composable.AppCheckbox
import eu.gaudian.translator.view.composable.AppScaffold
import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.hints.getVocabularyReviewHint
import eu.gaudian.translator.view.hints.HintDefinition
import eu.gaudian.translator.viewmodel.CategoryViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel
@@ -66,7 +66,7 @@ fun VocabularyReviewScreen(
topBar = {
AppTopAppBar(
title = { Text(stringResource(R.string.found_items)) },
hintContent = getVocabularyReviewHint()
hintContent = HintDefinition.REVIEW.hint()
)
},
) { paddingValues ->

View File

@@ -1,30 +0,0 @@
package eu.gaudian.translator.view.hints
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import eu.gaudian.translator.R
/**
* 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.LocalizedMarkdown("example_hint")
)
)
}
@Preview
@Composable
fun AddModelScanHintPreview() {
getAddModelScanHint().Render()
}
@Composable
fun AddModelScanHint() {
getAddModelScanHint().Render()
}

View File

@@ -0,0 +1,55 @@
@file:Suppress("HardCodedStringLiteral")
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
/**
* Hint metadata mapping Markdown filenames to their string resource titles.
* All hint-related operations are available as functions on each enum entry.
*/
@Suppress("HardCodedStringLiteral")
enum class HintDefinition(
val markdownFile: String,
val titleRes: Int
) {
ADD_MODEL_SCAN("find_ai_model", R.string.hint_scan_hint_title),
API_KEY("api_key_hint", R.string.hint_how_to_connect_to_an_ai),
CATEGORY("category_hint", R.string.category_hint_intro),
DICTIONARY_OPTIONS("dictionary_hint", R.string.label_dictionary_options),
EXERCISE("exercise_hint", R.string.label_exercise),
IMPORT("import_hint", R.string.hint_how_to_generate_vocabulary_with_ai),
LEARNING_STAGES("learning_stages_hint", R.string.learning_stages_title),
REVIEW("review_hint", R.string.review_intro),
SORTING("sorting_hint", R.string.sorting_hint_title),
TRANSLATION("translation_hint", R.string.hint_translate_how_it_works),
VOCABULARY_PROGRESS("vocabulary_progress_hint", R.string.hint_vocabulary_progress_hint_title);
/** Creates the Hint data class for this hint definition. */
@Composable
fun hint() = Hint(titleRes = titleRes, elements = listOf(HintElement.LocalizedMarkdown(markdownFile)))
/** Renders this hint's content. */
@Composable
fun Render() = hint().Render()
}
@Composable
fun hint(definition: HintDefinition): Hint = definition.hint()
@Composable fun HintContent(definition: HintDefinition) = definition.Render()
@Composable fun HintScreen(navController: NavController, definition: HintDefinition) = HintScreen(
navController = navController,
title = stringResource(definition.titleRes),
content = { definition.Render() }
)

View File

@@ -1,33 +0,0 @@
package eu.gaudian.translator.view.hints
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import eu.gaudian.translator.R
/**
* Migrated API Key hint using markdown-based format.
* Content is loaded from localized assets/hints based on device locale.
*/
@Composable
fun getApiKeyHint() = Hint (
titleRes = R.string.hint_how_to_connect_to_an_ai,
elements = listOf(
HintElement.LocalizedMarkdown("api_key_hint")
)
)
@Composable
fun ApiKeyHint() {
getApiKeyHint().Render()
}
@Preview
@Composable
fun ApiKeyHintPreview() {
MaterialTheme {
ApiKeyHint()
}
}

View File

@@ -1,33 +0,0 @@
package eu.gaudian.translator.view.hints
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import eu.gaudian.translator.R
/**
* Migrated Category hint using markdown-based format.
* Content is loaded from localized assets/hints based on device locale.
*/
@Composable
fun getCategoryHint(): Hint {
return Hint(
titleRes = R.string.category_hint_intro,
elements = listOf(
HintElement.LocalizedMarkdown("category_hint")
)
)
}
@Composable
fun CategoryHint() {
getCategoryHint().Render()
}
@Preview
@Composable
fun CategoryHintPreview() {
MaterialTheme {
CategoryHint()
}
}

View File

@@ -1,33 +0,0 @@
package eu.gaudian.translator.view.hints
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import eu.gaudian.translator.R
/**
* Migrated Category Hint Screen using markdown-based format.
* Content is loaded from localized assets/hints based on device locale.
*/
@Composable
fun getCategoryHintScreen(): Hint {
return Hint(
titleRes = R.string.category_hint_intro,
elements = listOf(
HintElement.LocalizedMarkdown("category_hint")
)
)
}
@Composable
fun CategoryHintScreen() {
getCategoryHintScreen().Render()
}
@Preview
@Composable
fun CategoryHintScreenPreview() {
MaterialTheme {
CategoryHintScreen()
}
}

View File

@@ -1,25 +0,0 @@
package eu.gaudian.translator.view.hints
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import eu.gaudian.translator.R
/**
* 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.LocalizedMarkdown("dictionary_hint")
)
)
}
@Preview
@Composable
fun DictionaryOptionsHint() {
getDictionaryOptionsHint().Render()
}

View File

@@ -1,145 +0,0 @@
package eu.gaudian.translator.view.hints
import android.content.Context
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import eu.gaudian.translator.R
/**
* Example of a migrated hint using the new markdown-based approach.
*
* This demonstrates how to migrate from the old LegacyHint format to the new
* markdown-based format.
*
* Benefits:
* - Easier to manage and translate (no code changes needed)
* - Better separation of concerns
* - Consistent styling across all hints
* - Support for rich formatting (tables, code blocks, etc.)
*/
/**
* Example 1: Loading markdown content from string (simple).
*/
@Composable
fun getApiKeyMarkdownHint(): String {
return """
# How to Connect to an AI Model
This guide explains how to connect your app to an AI model using an API key.
## Getting Started
To use AI models in your app, you need to provide a valid API key.
> **Note:** Keep your API key secure and never share it publicly.
## Key Status Indicators
| Status | Icon | Meaning |
|--------|------|---------|
| Active | ✅ | Key is valid and working |
| Missing | ⚠️ | Key is not set |
## Troubleshooting
1. Verify the key is correct
2. Ensure proper permissions
3. Check your quota
```json
{
"api_key": "sk-xxxxxxxxxxxx"
}
```
""".trimIndent()
}
@Composable
fun ApiKeyMarkdownHint() {
val content = getApiKeyMarkdownHint()
MarkdownHint(
markdownContent = content,
title = stringResource(R.string.hint_how_to_connect_to_an_ai)
)
}
/**
* Example 2: Loading markdown from assets file (recommended for production).
*/
@Composable
fun loadMarkdownFromAssets(fileName: String): String {
val context = LocalContext.current
return try {
context.assets.open("hints/$fileName").bufferedReader().use { it.readText() }
} catch (e: Exception) {
"Error loading markdown file: ${e.message}"
}
}
/**
* Data class for programmatic hint loading.
*/
data class MarkdownHintDefinition(
val fileName: String,
val titleRes: Int
) {
fun loadContent(context: Context): String {
return context.assets.open("hints/$fileName").bufferedReader().use { it.readText() }
}
}
/**
* Pre-defined hints ready for migration.
*/
object MarkdownHints {
val API_KEY = MarkdownHintDefinition(
fileName = "api_key_hint.md",
titleRes = R.string.hint_how_to_connect_to_an_ai
)
val CATEGORY = MarkdownHintDefinition(
fileName = "category_hint.md",
titleRes = R.string.category_hint_intro
)
val LEARNING_STAGES = MarkdownHintDefinition(
fileName = "learning_stages_hint.md",
titleRes = R.string.learning_stages_title
)
val SORTING = MarkdownHintDefinition(
fileName = "sorting_hint.md",
titleRes = R.string.sorting_hint_title
)
val VOCABULARY_PROGRESS = MarkdownHintDefinition(
fileName = "vocabulary_progress_hint.md",
titleRes = R.string.hint_vocabulary_progress_hint_title
)
}
/**
* Preview for the migrated API Key hint.
*/
@Preview
@Composable
fun ApiKeyMarkdownHintPreview() {
MaterialTheme {
ApiKeyMarkdownHint()
}
}
/**
* Preview for loading from assets.
*/
@Preview
@Composable
fun LoadFromAssetsPreview() {
MaterialTheme {
val content = loadMarkdownFromAssets("example_hint.md")
MarkdownHint(
markdownContent = content,
title = stringResource(R.string.hint_title_hints_overview)
)
}
}

View File

@@ -1,7 +0,0 @@
package eu.gaudian.translator.view.hints
/**
* This file is kept for reference only.
* All hints are now migrated to markdown-based format.
* See individual hint files for implementations.
*/

View File

@@ -1,3 +1,5 @@
@file:Suppress("HardCodedStringLiteral")
package eu.gaudian.translator.view.hints
import androidx.compose.foundation.layout.Column
@@ -9,9 +11,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import dev.jeziellago.compose.markdowntext.MarkdownText
import eu.gaudian.translator.utils.Log
private const val TAG = "MarkdownHint"
@@ -25,7 +27,7 @@ sealed class HintElement {
data class UIElement(val composable: @Composable () -> Unit) : HintElement()
/**
* A localized markdown file element.
* 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)
@@ -56,7 +58,7 @@ fun RenderHintElement(element: HintElement) {
androidx.compose.foundation.layout.Row(
modifier = Modifier.padding(top = 4.dp)
) {
androidx.compose.material3.Text(
Text(
text = "[DEBUG: ${element.fileName}]",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.error
@@ -68,7 +70,7 @@ fun RenderHintElement(element: HintElement) {
}
/**
* Composable to render localized markdown content.
* 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.
*/
@@ -85,23 +87,23 @@ fun LocalizedMarkdownContent(
// 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")
Log.d(TAG, "Loading hint: $fileName")
Log.d(TAG, "Device locale: ${locale.language}_${locale.country}")
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")
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")
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")
Log.d(TAG, "Found default version at: $defaultPath")
} else {
android.util.Log.e(TAG, "No hint found for: $fileName (tried: $localizedPath, $defaultPath)")
Log.e(TAG, "No hint found for: $fileName (tried: $localizedPath, $defaultPath)")
}
default
}
@@ -125,22 +127,3 @@ fun LocalizedMarkdownContent(
}
@Suppress("HardCodedStringLiteral")
@Preview
@Composable
fun UIElementPreview() {
RenderHintElement(
HintElement.UIElement {
Text("Custom UI Element")
}
)
}
@Suppress("HardCodedStringLiteral")
@Preview
@Composable
fun LocalizedMarkdownElementPreview() {
RenderHintElement(
HintElement.LocalizedMarkdown("example_hint")
)
}

View File

@@ -1,123 +0,0 @@
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
*/
@Composable
fun CategoryHintScreenWrapper(navController: NavController) {
HintScreen(
navController = navController,
title = stringResource(R.string.category_hint_intro)
) {
CategoryHint()
}
}
/**
* Dictionary Hint Screen
*/
@Composable
fun DictionaryHintScreen(navController: NavController) {
HintScreen(
navController = navController,
title = stringResource(R.string.label_dictionary_options)
) {
getDictionaryOptionsHint().Render()
}
}
/**
* Import Hint Screen
*/
@Composable
fun ImportHintScreen(navController: NavController) {
HintScreen(
navController = navController,
title = stringResource(R.string.hint_how_to_generate_vocabulary_with_ai)
) {
getImportVocabularyHint()
}
}
/**
* Sorting Hint Screen
*/
@Composable
fun SortingHintScreen(navController: NavController) {
HintScreen(
navController = navController,
title = stringResource(R.string.sorting_hint_title)
) {
SortingScreenHint()
}
}
/**
* Stages Hint Screen
*/
@Composable
fun StagesHintScreen(navController: NavController) {
HintScreen(
navController = navController,
title = stringResource(R.string.learning_stages_title)
) {
LearningStagesHint()
}
}
/**
* Translation Hint Screen
*/
@Composable
fun TranslationHintScreen(navController: NavController) {
HintScreen(
navController = navController,
title = stringResource(R.string.hint_translate_how_it_works)
) {
getTranslationScreenHint().Render()
}
}
/**
* Scan Hint Screen
*/
@Composable
fun ScanHintScreen(navController: NavController) {
HintScreen(
navController = navController,
title = stringResource(R.string.hint_scan_hint_title)
) {
AddModelScanHint()
}
}
/**
* API Hint Screen
*/
@Composable
fun ApiHintScreen(navController: NavController) {
HintScreen(
navController = navController,
title = stringResource(R.string.hint_how_to_connect_to_an_ai)
) {
ApiKeyHint()
}
}
/**
* Vocabulary Progress Hint Screen
*/
@Composable
fun VocabularyProgressHintScreen(navController: NavController) {
HintScreen(
navController = navController,
title = stringResource(R.string.hint_vocabulary_progress_hint_title)
) {
VocabularyProgressHint()
}
}

View File

@@ -18,9 +18,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import eu.gaudian.translator.R
import eu.gaudian.translator.ui.theme.ThemePreviews
import eu.gaudian.translator.view.LocalShowExperimentalFeatures
import eu.gaudian.translator.view.composable.AppCard
import eu.gaudian.translator.view.composable.AppIcons
@@ -42,15 +40,16 @@ 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 importHint = HintDefinition.IMPORT.hint()
val addModelScanHint = HintDefinition.ADD_MODEL_SCAN.hint()
val dictionaryOptionsHint = HintDefinition.DICTIONARY_OPTIONS.hint()
val translationScreenHint = HintDefinition.TRANSLATION.hint()
val categoryHint = HintDefinition.CATEGORY.hint()
val learningStagesHint = HintDefinition.LEARNING_STAGES.hint()
val sortingScreenHint = HintDefinition.SORTING.hint()
val vocabularyProgressHint = HintDefinition.VOCABULARY_PROGRESS.hint()
val apiKeyHint = HintDefinition.API_KEY.hint()
val hintGroups = remember(showExperimental, importHint, addModelScanHint, dictionaryOptionsHint, translationScreenHint) {
val allGroups = listOf(
@@ -133,11 +132,7 @@ fun HintsOverviewScreen(
}
}
@ThemePreviews
@Composable
fun HintsOverviewScreenPreview() {
HintsOverviewScreen(navController = rememberNavController())
}
@Composable
private fun HintHeader(
@@ -176,12 +171,4 @@ private fun HintListItem(
)
}
@ThemePreviews
@Composable
fun HintListItemPreview() {
HintListItem(
title = stringResource(R.string.category_hint_intro),
icon = AppIcons.Category,
onClick = {}
)
}

View File

@@ -1,45 +0,0 @@
package eu.gaudian.translator.view.hints
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import eu.gaudian.translator.R
/**
* 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.LocalizedMarkdown("import_hint")
)
)
}
@Preview
@Composable
fun ImportVocabularyHint() {
getImportVocabularyHint().Render()
}
/**
* 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(
HintElement.LocalizedMarkdown("review_hint")
)
)
}
@Composable
fun VocabularyReviewHint() {
getVocabularyReviewHint()
}

View File

@@ -1,33 +0,0 @@
package eu.gaudian.translator.view.hints
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import eu.gaudian.translator.R
/**
* Migrated Learning Stages hint using markdown-based format.
* Content is loaded from localized assets/hints based on device locale.
*/
@Composable
fun getLearningStagesHint(): Hint {
return Hint(
titleRes = R.string.learning_stages_title,
elements = listOf(
HintElement.LocalizedMarkdown("learning_stages_hint")
)
)
}
@Composable
fun LearningStagesHint() {
getLearningStagesHint().Render()
}
@Preview
@Composable
fun LearningStagesHintPreview() {
MaterialTheme {
LearningStagesHint()
}
}

View File

@@ -1,147 +0,0 @@
package eu.gaudian.translator.view.hints
import androidx.compose.foundation.layout.Column
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
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 dev.jeziellago.compose.markdowntext.MarkdownText
import eu.gaudian.translator.R
/**
* Markdown-styled hint content using the jeziellago compose-markdown library.
* This provides beautiful, consistent rendering of markdown content with
* support for headings, lists, tables, code blocks, and more.
*
* Usage:
* - Create a .md file in assets/hints/ (e.g., "api_key_hint.md")
* - Load it using context.assets.open() or pass raw markdown string
* - Use MarkdownHint composable for styled rendering
*
* Supported markdown:
* - Headings (# ## ###)
* - Bold (**text**) and italic (*text*)
* - Lists (- item, 1. item)
* - Tables (| col | col |)
* - Code blocks (```code```)
* - Blockquotes (> quote)
* - Links ([text](url))
*/
@Composable
fun MarkdownHint(
markdownContent: String,
modifier: Modifier = Modifier,
title: String? = null
) {
val scrollState = rememberScrollState()
Column(
modifier = modifier
.fillMaxWidth()
.verticalScroll(scrollState)
.padding(16.dp)
) {
// Optional title with icon header
title?.let {
HeaderWithIcon(
title = it,
subtitle = null
)
Spacer(modifier = Modifier.height(16.dp))
}
// Render markdown
MarkdownText(
markdown = markdownContent,
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurface
)
)
}
}
/**
* Preview for MarkdownHint with sample content.
*/
@Preview
@Composable
fun MarkdownHintPreview() {
val sampleMarkdown = """
# Welcome to the Hint System
This is a **markdown-based** hint that provides _beautiful_ and consistent styling.
## Features
- **Rich text formatting** - bold, italic, strikethrough
- **Headings** - H1 through H6 levels
- **Lists** - ordered and unordered
- **Code blocks** - with syntax highlighting
- **Tables** - for structured data
- **Links** - styled clickable references
- **Blockquotes** - for highlighted content
## How to Use
1. Create a `.md` file in `assets/hints/`
2. Load it using `context.assets.open("hints/your_hint.md")`
3. Pass the content to `MarkdownHint()`
> **Tip:** You can also embed UI elements by combining markdown with HintElements!
## Code Example
```kotlin
val markdown = loadMarkdownFromAssets("hints/scan_hint.md")
MarkdownHint(
markdownContent = markdown,
title = "Scan Hint"
)
```
## Table Example
| Feature | Status | Priority |
|---------|--------|----------|
| Headings | ✅ | High |
| Lists | ✅ | High |
| Tables | ✅ | Medium |
---
*Last updated: 2024-01-15*
""".trimIndent()
MaterialTheme {
MarkdownHint(
markdownContent = sampleMarkdown,
title = stringResource(R.string.hint_title_hints_overview)
)
}
}
/**
* Data class for loading markdown content from assets.
*/
data class MarkdownHintData(
val fileName: String,
val titleRes: Int
) {
/**
* Load and return the markdown content as a string.
*/
fun loadContent(androidContext: android.content.Context): String {
return androidContext.assets.open("hints/$fileName")
.bufferedReader()
.use { it.readText() }
}
}

View File

@@ -4,7 +4,7 @@ import android.content.Context
import java.util.Locale
/**
* Internationalization system for markdown hints.
* Internationalization system for Markdown hints.
*
* This follows Android's locale-qualified resource pattern:
* - assets/hints/ - Default (English)
@@ -18,131 +18,22 @@ import java.util.Locale
*/
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"
}
fun getCurrentLocale(context: Context): Locale {
return 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) {
} catch (_: 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.
*/
@@ -163,55 +54,3 @@ object MarkdownHintLoader {
}
}
/**
* 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,33 +0,0 @@
package eu.gaudian.translator.view.hints
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import eu.gaudian.translator.R
/**
* Migrated Sorting Screen hint using markdown-based format.
* Content is loaded from localized assets/hints based on device locale.
*/
@Composable
fun getSortingScreenHint(): Hint {
return Hint(
titleRes = R.string.sorting_hint_title,
elements = listOf(
HintElement.LocalizedMarkdown("sorting_hint")
)
)
}
@Composable
fun SortingScreenHint() {
getSortingScreenHint()
}
@Preview
@Composable
fun SortingScreenHintPreview() {
MaterialTheme {
SortingScreenHint()
}
}

View File

@@ -1,28 +0,0 @@
package eu.gaudian.translator.view.hints
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import eu.gaudian.translator.R
/**
* 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.LocalizedMarkdown("translation_hint")
)
)
@Composable
fun TranslationScreenHint() {
getTranslationScreenHint().Render()
}
@Preview
@Composable
fun TranslationScreenHintPreview() {
getTranslationScreenHint().Render()
}

View File

@@ -1,33 +0,0 @@
package eu.gaudian.translator.view.hints
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import eu.gaudian.translator.R
/**
* Migrated Vocabulary Progress hint using markdown-based format.
* Content is loaded from localized assets/hints based on device locale.
*/
@Composable
fun getVocabularyProgressHint(): Hint {
return Hint(
titleRes = R.string.hint_vocabulary_progress_hint_title,
elements = listOf(
HintElement.LocalizedMarkdown("vocabulary_progress_hint")
)
)
}
@Composable
fun VocabularyProgressHint() {
getVocabularyProgressHint().Render()
}
@Preview
@Composable
fun VocabularyProgressHintPreview() {
MaterialTheme {
VocabularyProgressHint()
}
}

View File

@@ -58,8 +58,7 @@ import eu.gaudian.translator.view.composable.AppScaffold
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.view.hints.HintDefinition
import eu.gaudian.translator.viewmodel.ApiViewModel
@Composable
@@ -141,7 +140,7 @@ fun AddModelScreen(navController: NavController, providerKey: String) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
hintContent = getAddModelScanHint()
hintContent = HintDefinition.ADD_MODEL_SCAN.hint()
)
},
) { 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.getApiKeyHint
import eu.gaudian.translator.view.hints.HintDefinition
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 = getApiKeyHint()
hintContent = HintDefinition.API_KEY.hint()
)
}
) { paddingValues ->

View File

@@ -36,7 +36,7 @@ import eu.gaudian.translator.view.composable.AppScaffold
import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.composable.OptionItemSwitch
import eu.gaudian.translator.view.dictionary.DictionaryManagerContent
import eu.gaudian.translator.view.hints.getDictionaryOptionsHint
import eu.gaudian.translator.view.hints.HintDefinition
import eu.gaudian.translator.viewmodel.ApiViewModel
import eu.gaudian.translator.viewmodel.DictionaryViewModel
import eu.gaudian.translator.viewmodel.SettingsViewModel
@@ -72,7 +72,7 @@ fun DictionaryOptionsScreen(
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
hintContent = getDictionaryOptionsHint()
hintContent = HintDefinition.DICTIONARY_OPTIONS.hint()
)
}
) { paddingValues ->

View File

@@ -7,16 +7,9 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import androidx.navigation.navigation
import eu.gaudian.translator.view.composable.Screen
import eu.gaudian.translator.view.hints.ApiHintScreen
import eu.gaudian.translator.view.hints.CategoryHintScreenWrapper
import eu.gaudian.translator.view.hints.DictionaryHintScreen
import eu.gaudian.translator.view.hints.HintDefinition
import eu.gaudian.translator.view.hints.HintScreen
import eu.gaudian.translator.view.hints.HintsOverviewScreen
import eu.gaudian.translator.view.hints.ImportHintScreen
import eu.gaudian.translator.view.hints.ScanHintScreen
import eu.gaudian.translator.view.hints.SortingHintScreen
import eu.gaudian.translator.view.hints.StagesHintScreen
import eu.gaudian.translator.view.hints.TranslationHintScreen
import eu.gaudian.translator.view.hints.VocabularyProgressHintScreen
// Defines the routes for the settings graph to avoid using raw strings
object SettingsRoutes {
@@ -114,31 +107,31 @@ fun NavGraphBuilder.settingsGraph(navController: NavController) {
HintsOverviewScreen(navController = navController)
}
composable(SettingsRoutes.HINTS_CATEGORIES) {
CategoryHintScreenWrapper(navController = navController)
HintScreen(navController, HintDefinition.CATEGORY)
}
composable(SettingsRoutes.HINTS_DICTIONARY) {
DictionaryHintScreen(navController = navController)
HintScreen(navController, HintDefinition.DICTIONARY_OPTIONS)
}
composable(SettingsRoutes.HINTS_IMPORT) {
ImportHintScreen(navController = navController)
HintScreen(navController, HintDefinition.IMPORT)
}
composable(SettingsRoutes.HINTS_SORTING) {
SortingHintScreen(navController = navController)
HintScreen(navController, HintDefinition.SORTING)
}
composable(SettingsRoutes.HINTS_STAGES) {
StagesHintScreen(navController = navController)
HintScreen(navController, HintDefinition.LEARNING_STAGES)
}
composable(SettingsRoutes.HINTS_TRANSLATION) {
TranslationHintScreen(navController = navController)
HintScreen(navController, HintDefinition.TRANSLATION)
}
composable(SettingsRoutes.HINTS_SCAN) {
ScanHintScreen(navController = navController)
HintScreen(navController, HintDefinition.ADD_MODEL_SCAN)
}
composable(SettingsRoutes.HINTS_API) {
ApiHintScreen(navController = navController)
HintScreen(navController, HintDefinition.API_KEY)
}
composable(SettingsRoutes.HINTS_VOCABULARY_PROGRESS) {
VocabularyProgressHintScreen(navController = navController)
HintScreen(navController, HintDefinition.VOCABULARY_PROGRESS)
}
}
}

View File

@@ -49,8 +49,7 @@ import eu.gaudian.translator.view.composable.AppIcons
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.view.hints.HintDefinition
import eu.gaudian.translator.viewmodel.SettingsViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel
import kotlin.math.exp
@@ -86,7 +85,7 @@ fun VocabularyProgressOptionsScreen(
}
},
// Here is the new hint content
hintContent = getVocabularyProgressHint()
hintContent = HintDefinition.VOCABULARY_PROGRESS.hint()
)
}
) { paddingValues ->

View File

@@ -59,7 +59,7 @@ import eu.gaudian.translator.view.NoConnectionScreen
import eu.gaudian.translator.view.composable.AppCard
import eu.gaudian.translator.view.composable.AppOutlinedCard
import eu.gaudian.translator.view.dialogs.AddVocabularyDialog
import eu.gaudian.translator.view.hints.TranslationScreenHint
import eu.gaudian.translator.view.hints.HintDefinition
import eu.gaudian.translator.view.settings.SettingsRoutes
import eu.gaudian.translator.viewmodel.LanguageViewModel
import eu.gaudian.translator.viewmodel.SettingsViewModel
@@ -167,7 +167,7 @@ private fun LoadedTranslationContent(
TopBarActions(
languageViewModel = languageViewModel,
onSettingsClick = onSettingsClick,
hintContent = { TranslationScreenHint() }
hintContent = { HintDefinition.TRANSLATION.Render() }
)
AppCard(modifier = Modifier.padding(8.dp, end = 8.dp, bottom = 8.dp, top = 0.dp)) {

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.getSortingScreenHint
import eu.gaudian.translator.view.hints.HintDefinition
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 = getSortingScreenHint()
hintContent = HintDefinition.SORTING.hint()
)
},