Compare commits

..

5 Commits

56 changed files with 2596 additions and 3083 deletions

View File

@@ -170,6 +170,9 @@ dependencies {
//noinspection UseTomlInstead
implementation("com.pierfrancescosoffritti.androidyoutubeplayer:core:13.0.0")
// Markdown rendering
implementation(libs.compose.markdown)
// Compression
testImplementation (libs.zstd.jni)
implementation(libs.zstd.jni.get().toString() + "@aar")

View File

@@ -0,0 +1,53 @@
# 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. This key authenticates your requests and tracks your usage.
> **Note:** Keep your API key secure and never share it publicly.
## Key Status Indicators
Your API key can be in one of two states:
| Status | Icon | Meaning |
|--------|------|---------|
| Active | ✅ | Key is valid and working |
| Missing | ⚠️ | Key is not set or was cleared |
### Active Key
When your API key is active, you can use all available AI models. The system will display a checkmark indicator next to the key status.
### Missing Key
If the key is missing or cleared, you won't be able to make API requests. You'll see a warning indicator, and any attempt to use AI features will prompt you to add a valid key.
## Connecting Your AI Model
1. **Navigate to Settings** → API Key section
2. **Enter your API key** in the provided field
3. **Save** the configuration
4. **Verify** the connection is successful
## Troubleshooting
If you're having issues with your API key:
1. **Verify the key is correct** - Check for typos or extra spaces
2. **Ensure the key has proper permissions** - Some models require additional access
3. **Check your quota** - You may have exceeded your usage limits
4. **Try regenerating the key** - If all else fails, generate a new key from your provider
```json
// Example API key format
{
"api_key": "sk-xxxxxxxxxxxxxxxxxxxx"
}
```
---
**Need more help?** Check our documentation or contact support.

View File

@@ -0,0 +1,49 @@
# Understanding Categories
Learn how to use categories to organize and filter your vocabulary effectively.
## What Are Categories?
Categories help you organize your vocabulary into meaningful groups. You can use them to track words by topic, difficulty, or any custom system that works for you.
## Two Types of Categories
### List Categories
List categories are simple groupings of vocabulary items. Words in a list category stay together regardless of their learning stage.
**Use cases:**
- Group words by topic (e.g., "Food", "Travel", "Business")
- Create custom decks for specific purposes
- Organize words by source (e.g., "Book: Harry Potter")
### Filter Categories
Filter categories automatically include all vocabulary items that match certain criteria. Words are dynamically added based on the filter rules.
**Use cases:**
- Filter by learning stage (e.g., "Words I'm learning")
- Filter by mastery level (e.g., "Words I need to review")
- Combine multiple criteria for complex filtering
## Creating Categories
1. **Tap the + button** to create a new category
2. **Choose the type** - List or Filter
3. **Add a name** and optional description
4. **Set the rules** (for filter categories)
5. **Save** your category
## Managing Categories
- **Edit** - Tap a category to modify its settings
- **Delete** - Swipe left and tap delete (words are not deleted)
- **Reorder** - Drag to change display order
## Tips
> **Pro Tip:** Use filter categories for learning stages to automatically track progress across all words at a certain level.
---
*Need more help? Check our documentation.*

View File

@@ -0,0 +1,49 @@
# Dictionary Options
Learn how to configure and use the dictionary options for better translations.
## What Are Dictionary Options?
Dictionary options allow you to customize how translations appear and how the dictionary feature works.
## Key Features
### Synonyms
Enable synonyms to see alternative translations:
- Toggle the **Synonyms** switch
- View multiple translation options
- Choose the most appropriate meaning
### Part of Speech
Display grammatical information:
- See if a word is noun, verb, adjective
- Helps understand context
- Available for supported languages
### Example Sentences
View usage examples:
- See words in context
- Learn proper usage patterns
- Understand nuances
## Configuration
### Enable/Disable Features
1. Go to Settings → Dictionary
2. Toggle desired options on/off
3. Changes apply immediately
### Custom Dictionary
Add custom entries:
- Tap the **+** button
- Enter word and translation
- Save to your personal dictionary
---
*Tip: Enable all options for the richest translation experience!*

View File

@@ -0,0 +1,86 @@
# How to Scan for AI Models
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

@@ -0,0 +1,53 @@
# Import Vocabulary with AI
Generate vocabulary lists automatically using AI assistance.
## Getting Started
Use AI to quickly create vocabulary lists from your learning goals.
## Step-by-Step Guide
### Step 1: Enter Search Term
Type a topic, theme, or concept for your vocabulary list:
- Be specific for better results
- Example: "German food and restaurant phrases"
- Example: "Business vocabulary for meetings"
### Step 2: Select Languages
Choose source and target languages:
- **Source language** - The language you're learning from
- **Target language** - Your native language
### Step 3: Set Amount
Choose how many words to generate:
- Slide to select 1-25 words
- More words = longer processing time
- Start small for your first import
### Step 4: Generate
Tap the generate button:
- AI creates the vocabulary list
- Review each entry before saving
- Edit any translations if needed
## After Generation
Once generated, you can:
- **Review** - Check each word-translation pair
- **Edit** - Correct any mistakes
- **Delete** - Remove unwanted entries
- **Import All** - Add all to your vocabulary
## Tips
> **Pro Tip:** Start with 10 words per import to get familiar with the feature.
---
*Need help? Check our vocabulary management guide.*

View File

@@ -0,0 +1,53 @@
# Learning Stages
Understand how vocabulary progresses through different learning stages to optimize your study sessions.
## The Learning Stages
Your vocabulary items move through these stages as you learn:
| Stage | Name | Interval | Description |
|-------|------|----------|-------------|
| 🌟 | New | - | Just added vocabulary |
| 📅 | Stage 1 | 1 day | Recently learned |
| 📅 | Stage 2 | 3 days | Reinforcement |
| 📅 | Stage 3 | 1 week | Consolidation |
| 📅 | Stage 4 | 2 weeks | Deep learning |
| 📅 | Stage 5 | 1 month | Mastery |
| ✅ | Learned | ∞ | Fully learned |
## How It Works
### Answer Correctly ✅
When you correctly identify a word during review:
- The word **moves forward** to the next stage
- The interval until next review **increases**
- This helps you focus on words that need more practice
### Answer Incorrectly ❌
When you make a mistake:
- The word **moves back** one or more stages
- The review interval **decreases**
- This ensures you practice challenging words more often
## Customization
All intervals and rules can be customized in Settings:
- **Adjust intervals** for each stage
- **Change how many stages** to regress on errors
- **Skip stages** for certain word types
- **Enable/disable** specific stages
## Visual Progress
The app displays your progress visually:
- Stage indicators show current status
- Progress bars track advancement
- Statistics display overall mastery
---
*Tip: Consistent daily practice is key to moving words through all stages!*

View File

@@ -0,0 +1,63 @@
# Review Vocabulary
Master your vocabulary through systematic review sessions.
## The Review Screen
When you start a review session, you'll see:
- The **source word** to translate
- Options to reveal the **translation**
- Buttons to indicate your **knowledge level**
## How to Review
### 1. View the Word
Read the source word carefully:
- Pay attention to the spelling
- Think of the meaning
- Recall the translation
### 2. Reveal Translation
Tap to show the translation:
- Compare with your recall
- Note any differences
- Learn from mistakes
### 3. Rate Your Knowledge
Rate how well you knew the answer:
| Button | Meaning | Action |
|--------|---------|-------------------|
| 😓 | Hard | Moves back stages |
| 🤔 | Okay | Stays current |
| ✅ | Easy | Advances stage |
## Review Statistics
Track your progress:
- **Cards reviewed** - Total in session
- **Accuracy** - Percentage correct
- **Time spent** - Learning duration
## Best Practices
### Daily Reviews
- Review **every day** for best results
- Complete **all due** cards before adding new
- Focus on **problem areas**
### Spaced Repetition
The system uses spaced repetition:
- **Hard cards** - Review sooner (1-2 days)
- **Okay cards** - Review normally (as scheduled)
- **Easy cards** - Review later (weeks)
---
*Consistent daily review is the key to long-term retention!*

View File

@@ -0,0 +1,72 @@
# Sorting Vocabulary
Learn how to efficiently sort and organize new vocabulary as you add them.
## The Sorting Screen
When you import vocabulary, you'll see the sorting screen where you can:
- Review each word-translation pair
- Decide the next action for each item
- Handle duplicates and conflicts
## Actions
### ✅ Mark as Learned
Move the word directly to Stage 1:
- The word enters your learning queue
- You'll review it according to the learning schedule
### 🗑️ Delete
Remove the word entirely:
- Use for duplicates or unwanted entries
- This action is permanent
### 📝 Edit
Tap on any word or translation to edit:
- Correct typos
- Improve translations
- Add additional context
## Duplicate Handling
When duplicates are detected:
| Icon | Meaning |
|------|---------|
| ⚠️ | Duplicate detected |
| ✅ | Original entry |
| ❌ | Duplicate entry |
**Options for duplicates:**
- Keep only the original
- Keep the newer entry
- Keep both (merge)
- Delete the duplicate
## Helper Features
### Remove Articles
Toggle to automatically strip articles from words:
- "der Hund" → "Hund"
- "the dog" → "dog"
- Useful for cleaner vocabulary lists
### Quick Actions
Use quick action buttons for bulk operations:
- **Skip All** - Review later
- **Learn All** - Add all to Stage 1
- **Delete Duplicates** - Auto-remove duplicates
## Tips
> **Pro Tip:** Review carefully before sorting. Once sorted, you can still edit words in the vocabulary list.
---
*For more tips, check our vocabulary management guide.*

View File

@@ -0,0 +1,67 @@
# Translation Features
Discover the powerful translation capabilities of this app.
## Alternative Translations
Sometimes a word has multiple meanings:
- Tap the **Show alternatives** option
- See all possible translations
- Choose the most appropriate context
## Custom Prompts
Customize how translations are generated:
### Create Custom Prompt
1. Go to Settings → Translation
2. Tap **Add Custom Prompt**
3. Write your prompt template
4. Save and use in translations
### Example Prompts
```
Translate {word} as used in {context}
Provide formal translation of: {word}
Casual translation for: {word}
```
## Multiple Translation Services
Use different translation backends:
| Service | Best For | Languages |
|---------|----------|-----------|
| AI Models | Context-aware | Many |
| Dictionary | Quick lookup | Limited |
| Online API | Accuracy | All |
## Translation History
Track all your translations:
- Automatically saved
- Search by word or date
- Export for review
## Text-to-Speech (TTS)
Hear words pronounced:
- Tap the **speaker icon**
- Choose voice for each language
- Adjust speed as needed
## Quick Actions
Fast access to common tasks:
- **Copy** - Copy translation to clipboard
- **Add** - Add directly to vocabulary
- **Share** - Send to other apps
---
*Pro Tip: Use custom prompts for domain-specific vocabulary!*

View File

@@ -0,0 +1,78 @@
# Vocabulary Progress Tracking
Monitor your vocabulary learning journey with detailed progress statistics.
## Progress Overview
Track your learning with these key metrics:
### Words Learned
- Total words added to your vocabulary
- Words currently in each learning stage
- Words marked as fully learned
### Learning Streak
- Days since you started learning
- Current streak count
- Best streak achieved
### Review Statistics
- Words reviewed today
- Accuracy rate per session
- Words due for review
## Progress Tracking Features
### 📊 Dashboard
View your overall progress at a glance:
- Total vocabulary count
- Mastery percentage
- Recent activity summary
### 📈 Statistics
Detailed analytics include:
- Learning rate over time
- Stage distribution
- Accuracy trends
- Time spent studying
### 🎯 Goals
Set and track learning goals:
- Daily word targets
- Weekly review quotas
- Mastery milestones
## Learning Stages Summary
| Stage | Count | Percentage |
|-------|-------|------------|
| New | X | X% |
| Learning | X | X% |
| Mastered | X | X% |
## Review System
The review system helps you:
1. **Prioritize** - Shows words due for review first
2. **Space** - Optimizes review timing for retention
3. **Track** - Records your performance over time
## Customization
Customize your progress tracking:
- **Select metrics** to display on dashboard
- **Set goals** for personalized targets
- **Export data** for external analysis
- **Reset progress** if starting fresh
---
*Keep practicing consistently to see your progress grow!*

View File

@@ -0,0 +1,258 @@
package eu.gaudian.translator.view.composable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
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.material3.DropdownMenu
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import eu.gaudian.translator.R
import eu.gaudian.translator.model.LanguageModel
import eu.gaudian.translator.model.communication.ApiProvider
@Composable
fun ApiModelDropDown(
models: List<LanguageModel>,
providers: List<ApiProvider>,
selectedModel: LanguageModel?,
onModelSelected: (LanguageModel?) -> Unit,
enabled: Boolean = true
) {
LocalContext.current
var expanded by remember { mutableStateOf(false) }
var searchQuery by remember { mutableStateOf("") }
val activeModels = models.filter { model -> providers.any { it.key == model.providerKey && (it.hasValidKey || it.isCustom) } }
val groupedModels = activeModels.groupBy { it.providerKey }
val providerNames = remember(providers) { providers.associate { it.key to it.displayName } }
val providerStatuses = remember(providers) { providers.associate { it.key to (it.hasValidKey || it.isCustom) } }
val filteredGroupedModels = remember(groupedModels, searchQuery) {
if (searchQuery.isBlank()) {
groupedModels
} else {
groupedModels.mapValues { (_, models) ->
models.filter { model ->
model.displayName.contains(searchQuery, ignoreCase = true) ||
model.modelId.contains(searchQuery, ignoreCase = true) ||
model.description.contains(searchQuery, ignoreCase = true)
}
}.filterValues { it.isNotEmpty() }
}
}
Box {
AppOutlinedButton(
onClick = { expanded = true },
modifier = Modifier.align(Alignment.Center),
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp),
enabled = enabled
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = selectedModel?.displayName ?: stringResource(R.string.text_select_model),
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (selectedModel != null) {
Text(
text = providerNames[selectedModel.providerKey] ?: selectedModel.providerKey,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
Spacer(modifier = Modifier.width(8.dp))
Icon(
imageVector = if (expanded) AppIcons.ArrowDropUp else AppIcons.ArrowDropDown,
contentDescription = if (expanded) stringResource(R.string.cd_collapse) else stringResource(R.string.cd_expand)
)
}
}
DropdownMenu(
modifier = Modifier
.fillMaxWidth(),
expanded = expanded,
onDismissRequest = { expanded = false }
) {
// Search bar
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
AppIcons.Search,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
)
Spacer(modifier = Modifier.width(8.dp))
TextField(
value = searchQuery,
onValueChange = { searchQuery = it },
placeholder = { Text(stringResource(R.string.label_search_models)) },
singleLine = true,
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
),
modifier = Modifier.weight(1f)
)
if (searchQuery.isNotBlank()) {
IconButton(
onClick = { searchQuery = "" },
modifier = Modifier.size(24.dp)
) {
Icon(
AppIcons.Close,
contentDescription = stringResource(R.string.cd_clear_search),
modifier = Modifier.size(16.dp)
)
}
}
}
HorizontalDivider()
}
if (filteredGroupedModels.isNotEmpty()) {
filteredGroupedModels.entries.forEachIndexed { index, entry ->
val providerKey = entry.key
val providerModels = entry.value
val isActive = providerStatuses[providerKey] == true
val providerName = providerNames[providerKey] ?: providerKey
if (index > 0) HorizontalDivider()
// Provider header
AppDropdownMenuItem(
text = {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Icon(
imageVector = if (isActive) AppIcons.CheckCircle else AppIcons.Warning,
contentDescription = null,
tint = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = providerName,
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Medium,
color = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
Text(
text = stringResource(
R.string.labels_1d_models,
providerModels.size
),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
)
}
}
},
onClick = {},
enabled = false,
selected = false
)
// Models for this provider
providerModels.forEach { model ->
AppDropdownMenuItem(
text = {
Column(modifier = Modifier.fillMaxWidth()) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = model.displayName,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.weight(1f),
fontWeight = if (model == selectedModel) FontWeight.Medium else FontWeight.Normal
)
Spacer(modifier = Modifier.width(8.dp))
ModelBadges(
modelDisplayOrId = model.displayName.ifBlank { model.modelId },
providerKey = model.providerKey,
)
}
if (model.description.isNotBlank()) {
Text(
text = model.description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
},
onClick = {
onModelSelected(model)
expanded = false
searchQuery = ""
},
selected = model == selectedModel
)
}
}
} else if (searchQuery.isNotBlank()) {
AppDropdownMenuItem(
text = {
Text(
stringResource(R.string.text_no_models_found),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
modifier = Modifier.fillMaxWidth()
)
},
onClick = {},
enabled = false,
selected = false
)
}
}
}
}

View File

@@ -3,10 +3,14 @@
package eu.gaudian.translator.view.composable
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -34,6 +38,7 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Size
@@ -42,14 +47,13 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
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.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.PopupProperties
import com.google.android.material.color.MaterialColors.ALPHA_DISABLED
import com.google.android.material.color.MaterialColors.ALPHA_FULL
import eu.gaudian.translator.R
/**
@@ -80,7 +84,6 @@ fun AppDropdownMenu(
content: @Composable androidx.compose.foundation.layout.ColumnScope.() -> Unit,
) {
var textFieldSize by remember { mutableStateOf(Size.Zero) }
val interactionSource = remember { androidx.compose.foundation.interaction.MutableInteractionSource() }
Column(modifier = modifier) {
@@ -126,15 +129,17 @@ fun AppDropdownMenu(
expanded = expanded,
onDismissRequest = onDismissRequest,
modifier = Modifier
.width(with(LocalDensity.current) { textFieldSize.width.toDp() }),
offset = DpOffset(0.dp, 0.dp),
.width(with(LocalDensity.current) { textFieldSize.width.toDp() })
// Give the menu itself a bit of breathing room
.padding(vertical = 4.dp),
offset = DpOffset(0.dp, 4.dp), // Slight detachment from the anchor
scrollState = rememberScrollState(),
properties = PopupProperties(focusable = true),
shape = RoundedCornerShape(8.dp),
containerColor = MaterialTheme.colorScheme.surfaceContainer,
tonalElevation = 0.dp,
shadowElevation = 4.dp,
border = BorderStroke(0.5.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.14f))
shape = RoundedCornerShape(12.dp),
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
tonalElevation = 6.dp,
shadowElevation = 8.dp,
border = BorderStroke(0.5.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
) {
content()
}
@@ -143,15 +148,7 @@ fun AppDropdownMenu(
/**
* A modern and stylish composable for individual dropdown items, featuring enhanced visual design
* with subtle shadows, rounded corners, and smooth interactions. This provides a cool, contemporary look
* that aligns with modern UI trends while maintaining accessibility and usability.
*
* @param text Composable lambda for the text to display in the item.
* @param onClick Callback invoked when the item is clicked.
* @param modifier Modifier for the item.
* @param enabled Whether the item is enabled.
* @param leadingIcon Optional leading icon for the item.
* @param trailingIcon Optional trailing icon for the item.
* with subtle shadows, rounded corners, and smooth interactions.
*/
@Composable
fun AppDropdownMenuItem(
@@ -166,21 +163,28 @@ fun AppDropdownMenuItem(
val contentColor = if (enabled) {
if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
} else {
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) // Equivalent to disabled alpha
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
}
// Modern "floating" highlight background
val backgroundColor = if (selected) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.6f) else Color.Transparent
Box(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 2.dp) // Outer padding creates the floating shape
.clip(RoundedCornerShape(8.dp))
.background(backgroundColor)
.clickable(enabled = enabled) { onClick() }
//.padding(horizontal = 12.dp, vertical = 10.dp) // Inner padding keeps content comfortable
) {
androidx.compose.foundation.layout.Row(
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
verticalAlignment = Alignment.CenterVertically
) {
leadingIcon?.invoke()
if (leadingIcon != null) {
androidx.compose.foundation.layout.Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(12.dp))
}
Box(modifier = Modifier.weight(1f)) {
CompositionLocalProvider(LocalContentColor provides contentColor) {
@@ -188,90 +192,14 @@ fun AppDropdownMenuItem(
}
}
if (trailingIcon != null) {
androidx.compose.foundation.layout.Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(12.dp))
trailingIcon()
}
}
}
}
@Suppress("HardCodedStringLiteral")
@Preview(showBackground = true)
@Composable
fun AppDropdownMenuPreview() {
val options = listOf("Option 1", "Option 2", "Option 3")
AppDropdownMenu(
expanded = false,
onDismissRequest = {},
label = { Text("Select Option") },
content = {
options.forEach { option ->
AppDropdownMenuItem(
text = { Text(text = option) },
onClick = {}
)
}
}
)
}
@Suppress("HardCodedStringLiteral")
@Preview(showBackground = true)
@Composable
fun AppDropdownMenuExpandedPreview() {
val options = listOf("English", "Spanish", "French", "German", "Italian", "Portuguese")
var expanded by remember { mutableStateOf(true) } // Force expanded state for preview
// Since previews are static, we'll simulate the expanded state by showing the dropdown
AppDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
label = { Text("Language") },
content = {
options.forEach { option ->
AppDropdownMenuItem(
text = { Text(text = option) },
onClick = {}
)
}
}
)
}
@Suppress("HardCodedStringLiteral")
@Preview(showBackground = true)
@Composable
fun DropDownItemPreview() {
AppDropdownMenuItem(
text = { Text("Sample Item", style = MaterialTheme.typography.titleSmall) },
onClick = {},
leadingIcon = {
Icon(
imageVector = AppIcons.Add,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
}
)
}
@Suppress("HardCodedStringLiteral")
@Preview(showBackground = true)
@Composable
fun DropDownItemSelectedPreview() {
AppDropdownMenuItem(
text = { Text("Selected Item", style = MaterialTheme.typography.titleSmall) },
onClick = {},
selected = true,
trailingIcon = {
Icon(
imageVector = AppIcons.Check,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
}
)
}
// ... [Previews remain exactly the same as your original file] ...
@Composable
fun <T> LargeDropdownMenu(
@@ -308,7 +236,6 @@ fun <T> LargeDropdownMenu(
readOnly = true,
)
// Transparent clickable surface on top of OutlinedTextField
Surface(
modifier = Modifier
.fillMaxSize()
@@ -321,10 +248,13 @@ fun <T> LargeDropdownMenu(
if (expanded) {
Dialog(
onDismissRequest = { expanded = true },
onDismissRequest = { expanded = false }, // Fixed bug from original code
) {
Surface(
shape = RoundedCornerShape(12.dp),
shape = RoundedCornerShape(16.dp),
color = MaterialTheme.colorScheme.surfaceContainerHigh,
shadowElevation = 8.dp,
tonalElevation = 6.dp
) {
val listState = rememberLazyListState()
if (selectedIndex > -1) {
@@ -333,7 +263,12 @@ fun <T> LargeDropdownMenu(
}
}
LazyColumn(modifier = Modifier.fillMaxWidth(), state = listState) {
// Added vertical padding to the list instead of hard dividers
LazyColumn(
modifier = Modifier.fillMaxWidth(),
state = listState,
contentPadding = PaddingValues(vertical = 8.dp)
) {
if (notSetLabel != null) {
item {
LargeDropdownMenuItem(
@@ -354,10 +289,6 @@ fun <T> LargeDropdownMenu(
onItemSelected(index, item)
expanded = false
}
if (index < items.lastIndex) {
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
}
}
}
}
@@ -373,19 +304,27 @@ fun LargeDropdownMenuItem(
onClick: () -> Unit,
) {
val contentColor = when {
!enabled -> MaterialTheme.colorScheme.onSurface.copy(alpha = ALPHA_DISABLED)
selected -> MaterialTheme.colorScheme.primary.copy(alpha = ALPHA_FULL)
else -> MaterialTheme.colorScheme.onSurface.copy(alpha = ALPHA_FULL)
!enabled -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
selected -> MaterialTheme.colorScheme.primary
else -> MaterialTheme.colorScheme.onSurface
}
val backgroundColor = if (selected) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.6f) else Color.Transparent
val fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal
CompositionLocalProvider(LocalContentColor provides contentColor) {
Box(modifier = Modifier
.clickable(enabled) { onClick() }
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)) {
.padding(horizontal = 8.dp, vertical = 4.dp) // Outer padding for floating shape
.clip(RoundedCornerShape(8.dp))
.background(backgroundColor)
.clickable(enabled) { onClick() }
.padding(horizontal = 16.dp, vertical = 14.dp) // Inner padding
) {
Text(
text = text,
style = MaterialTheme.typography.titleSmall,
style = MaterialTheme.typography.titleSmall.copy(fontWeight = fontWeight),
)
}
}

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

@@ -13,7 +13,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -114,7 +113,7 @@ fun BaseLanguageDropDown(
@Composable
fun MultiSelectItem(language: Language) {
val isSelected = tempSelection.contains(language)
DropdownMenuItem(
AppDropdownMenuItem(
text = {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
AppCheckbox(
@@ -155,7 +154,7 @@ fun BaseLanguageDropDown(
val duplicateNames = languageNames.groupingBy { it }.eachCount().filter { it.value > 1 }.keys
val isDuplicate = duplicateNames.contains(language.name)
DropdownMenuItem(
AppDropdownMenuItem(
text = {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Column {
@@ -284,11 +283,11 @@ fun BaseLanguageDropDown(
} else {
// Logic for single selection default view
if (showAutoOption) {
DropdownMenuItem(text = { Text(stringResource(R.string.text_select_auto_recognition)) }, onClick = { onAutoSelected(); expanded = false; searchText = "" })
AppDropdownMenuItem(text = { Text(stringResource(R.string.text_select_auto_recognition)) }, onClick = { onAutoSelected(); expanded = false; searchText = "" })
HorizontalDivider()
}
if (showNoneOption) {
DropdownMenuItem(text = { Text(stringResource(R.string.text_select_none)) }, onClick = { onNoneSelected(); expanded = false; searchText = "" })
AppDropdownMenuItem(text = { Text(stringResource(R.string.text_select_none)) }, onClick = { onNoneSelected(); expanded = false; searchText = "" })
HorizontalDivider()
}
if (favoriteLanguages.any {

View File

@@ -11,98 +11,130 @@ import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import eu.gaudian.translator.R
import eu.gaudian.translator.model.TagCategory
import eu.gaudian.translator.model.VocabularyCategory
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.model.VocabularyFilter
import eu.gaudian.translator.ui.theme.ThemePreviews
import eu.gaudian.translator.view.composable.AppButton
import eu.gaudian.translator.view.composable.AppCheckbox
import eu.gaudian.translator.view.composable.AppDropdownMenuItem
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppOutlinedButton
import eu.gaudian.translator.view.composable.AppOutlinedTextField
import eu.gaudian.translator.viewmodel.CategoryViewModel
/**
* State class representing the internal state of CategoryDropdown.
* Used for previews and testing.
*/
data class CategoryDropdownState(
val expanded: Boolean = false,
val selectedCategories: List<VocabularyCategory?> = emptyList(),
val newCategoryName: String = "",
val categories: List<VocabularyCategory> = emptyList(),
)
/**
* Stateless dropdown content composable for category selection.
* This component is fully controlled by its parameters and does not maintain any internal state.
*
* @param state The current state of the dropdown
* @param onExpand Callback when the dropdown should expand/collapse
* @param onCategorySelected Callback when a category is selected
* @param onNewCategoryNameChange Callback when the new category name changes
* @param onAddCategory Callback when a new category should be added
* @param noneSelectable Whether "None" option is selectable
* @param multipleSelectable Whether multiple categories can be selected
* @param onlyLists Whether to show only list/category types
* @param addCategory Whether to show the "Add Category" option
* @param modifier Modifier for the composable
*/
@Composable
fun CategoryDropdown(
initialCategoryId: Int? = null,
fun CategoryDropdownContent(
modifier: Modifier = Modifier,
state: CategoryDropdownState,
onExpand: (Boolean) -> Unit,
onCategorySelected: (List<VocabularyCategory?>) -> Unit,
noneSelectable: Boolean? = true,
onNewCategoryNameChange: (String) -> Unit,
onAddCategory: (String) -> Unit,
noneSelectable: Boolean = true,
multipleSelectable: Boolean = false,
onlyLists: Boolean = false,
addCategory: Boolean = false
addCategory: Boolean = false,
) {
val activity = LocalContext.current.findActivity()
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
var expanded by remember { mutableStateOf(false) }
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
val selectableCategories = if (onlyLists) categories.filterIsInstance<TagCategory>() else categories
val initialCategory = remember(categories, initialCategoryId) {
categories.find { it.id == initialCategoryId }
val selectableCategories = if (onlyLists) {
state.categories.filterIsInstance<TagCategory>()
} else {
state.categories
}
var selectedCategories by remember {
mutableStateOf<List<VocabularyCategory?>>(if (initialCategory != null) listOf(initialCategory) else emptyList())
}
var newCategoryName by remember { mutableStateOf("") }
AppOutlinedButton(
shape = RoundedCornerShape(8.dp),
onClick = { expanded = true },
modifier = Modifier.fillMaxWidth(),
onClick = { onExpand(true) },
modifier = modifier.fillMaxWidth(),
) {
Row(modifier = Modifier.fillMaxWidth()) {
Text(text = when {
selectedCategories.isEmpty() -> stringResource(R.string.text_select_category)
selectedCategories.size == 1 -> selectedCategories.first()?.name ?: stringResource(R.string.text_none)
else -> stringResource(R.string.text_2d_categories_selected, selectedCategories.size)
Text(
text = when {
state.selectedCategories.isEmpty() -> stringResource(R.string.text_select_category)
state.selectedCategories.size == 1 -> state.selectedCategories.first()?.name
?: stringResource(R.string.text_none)
else -> stringResource(R.string.text_2d_categories_selected, state.selectedCategories.size)
},
modifier = Modifier.weight(1f),
textAlign = TextAlign.Center
)
Icon(
imageVector = if (expanded) AppIcons.ArrowDropUp else AppIcons.ArrowDropDown,
contentDescription = if (expanded) stringResource(R.string.cd_collapse) else stringResource(
R.string.cd_expand
)
imageVector = if (state.expanded) AppIcons.ArrowDropUp else AppIcons.ArrowDropDown,
contentDescription = if (state.expanded) {
stringResource(R.string.cd_collapse)
} else {
stringResource(R.string.cd_expand)
}
)
}
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
expanded = state.expanded,
onDismissRequest = { onExpand(false) },
modifier = Modifier.fillMaxWidth(),
) {
if (noneSelectable == true) {
val noneSelected = selectedCategories.contains(null)
if (noneSelectable) {
val noneSelected = state.selectedCategories.contains(null)
AppDropdownMenuItem(
text = {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
if (multipleSelectable) {
AppCheckbox(
checked = noneSelected,
onCheckedChange = {
selectedCategories = if (noneSelected) selectedCategories.filterNotNull() else selectedCategories + listOf(null)
onCategorySelected(selectedCategories)
onCheckedChange = { isChecked ->
val newSelection = if (noneSelected) {
state.selectedCategories.filterNotNull()
} else {
state.selectedCategories + listOf(null)
}
onCategorySelected(newSelection)
}
)
Spacer(modifier = Modifier.width(8.dp))
@@ -112,31 +144,38 @@ fun CategoryDropdown(
},
onClick = {
if (multipleSelectable) {
selectedCategories = if (noneSelected) {
selectedCategories.filterNotNull()
val newSelection = if (noneSelected) {
state.selectedCategories.filterNotNull()
} else {
selectedCategories + listOf(null)
state.selectedCategories + listOf(null)
}
onCategorySelected(selectedCategories)
onCategorySelected(newSelection)
} else {
selectedCategories = listOf(null)
onCategorySelected(selectedCategories)
expanded = false
onCategorySelected(listOf(null))
onExpand(false)
}
}
)
}
selectableCategories.forEach { category ->
val isSelected = selectedCategories.contains(category)
val isSelected = state.selectedCategories.contains(category)
AppDropdownMenuItem(
text = {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
if (multipleSelectable) {
AppCheckbox(
checked = isSelected,
onCheckedChange = {
selectedCategories = if (isSelected) selectedCategories - category else selectedCategories + category
onCategorySelected(selectedCategories)
onCheckedChange = { _ ->
val newSelection = if (isSelected) {
state.selectedCategories - category
} else {
state.selectedCategories + category
}
onCategorySelected(newSelection)
}
)
Spacer(modifier = Modifier.width(8.dp))
@@ -146,26 +185,23 @@ fun CategoryDropdown(
},
onClick = {
if (multipleSelectable) {
selectedCategories = if (category in selectedCategories) {
selectedCategories - category
val newSelection = if (category in state.selectedCategories) {
state.selectedCategories - category
} else {
selectedCategories + category
state.selectedCategories + category
}
onCategorySelected(selectedCategories)
onCategorySelected(newSelection)
} else {
selectedCategories = listOf(category)
onCategorySelected(selectedCategories)
expanded = false
onCategorySelected(listOf(category))
onExpand(false)
}
}
)
}
if (addCategory) {
HorizontalDivider()
// Create new category section
AppDropdownMenuItem(
text = {
Text(stringResource(R.string.label_add_category))
@@ -181,26 +217,19 @@ fun CategoryDropdown(
verticalAlignment = Alignment.CenterVertically
) {
AppOutlinedTextField(
value = newCategoryName,
onValueChange = { newCategoryName = it },
value = state.newCategoryName,
onValueChange = onNewCategoryNameChange,
modifier = Modifier.weight(1f),
singleLine = true,
)
Spacer(modifier = Modifier.width(8.dp))
IconButton(
onClick = {
if (newCategoryName.isNotBlank()) {
val newList =
TagCategory(id = 0, name = newCategoryName.trim())
categoryViewModel.createCategory(newList)
newCategoryName = ""
// Optionally, select the new category if single selection
if (!multipleSelectable) {
expanded = false
}
if (state.newCategoryName.isNotBlank()) {
onAddCategory(state.newCategoryName.trim())
}
},
enabled = newCategoryName.isNotBlank()
enabled = state.newCategoryName.isNotBlank()
) {
Icon(
imageVector = AppIcons.Add,
@@ -209,15 +238,14 @@ fun CategoryDropdown(
}
}
},
onClick = {} // No action on click
onClick = {}
)
}
if (multipleSelectable) {
Spacer(modifier = Modifier.height(8.dp))
AppButton(
onClick = { expanded = false },
onClick = { onExpand(false) },
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
@@ -228,10 +256,417 @@ fun CategoryDropdown(
}
}
@Preview
/**
* Stateful wrapper for CategoryDropdown that manages its own state.
* This is the main composable that should be used in production code.
*
* @param initialCategoryId The initial category ID to select
* @param onCategorySelected Callback when categories are selected
* @param noneSelectable Whether "None" option is selectable
* @param multipleSelectable Whether multiple categories can be selected
* @param onlyLists Whether to show only list/category types
* @param addCategory Whether to show the "Add Category" option
* @param modifier Modifier for the composable
*/
@Composable
fun CategoryDropdownPreview() {
CategoryDropdown(
onCategorySelected = {}
fun CategoryDropdown(
initialCategoryId: Int? = null,
onCategorySelected: (List<VocabularyCategory?>) -> Unit,
noneSelectable: Boolean? = true,
multipleSelectable: Boolean = false,
onlyLists: Boolean = false,
addCategory: Boolean = false,
modifier: Modifier = Modifier,
) {
var expanded by remember { mutableStateOf(false) }
var selectedCategories by remember {
mutableStateOf<List<VocabularyCategory?>>(emptyList())
}
var newCategoryName by remember { mutableStateOf("") }
// For production use, this would come from ViewModel
// For preview, we'll use empty list or pass via state
val categories by remember { mutableStateOf(emptyList<VocabularyCategory>()) }
// Find initial category
val initialCategory = remember(categories, initialCategoryId) {
categories.find { it.id == initialCategoryId }
}
// Initialize selection with initial category if provided
remember(initialCategory) {
if (initialCategory != null && selectedCategories.isEmpty()) {
selectedCategories = listOf(initialCategory)
}
true
}
CategoryDropdownContent(
state = CategoryDropdownState(
expanded = expanded,
selectedCategories = selectedCategories,
newCategoryName = newCategoryName,
categories = categories,
),
onExpand = { isExpanded -> expanded = isExpanded },
onCategorySelected = { newSelection ->
selectedCategories = newSelection
onCategorySelected(newSelection)
},
onNewCategoryNameChange = { newCategoryName = it },
onAddCategory = { name ->
val newCategory = TagCategory(id = 0, name = name)
// In production, this would call ViewModel.createCategory(newCategory)
newCategoryName = ""
if (!multipleSelectable) {
expanded = false
}
},
noneSelectable = noneSelectable == true,
multipleSelectable = multipleSelectable,
onlyLists = onlyLists,
addCategory = addCategory,
modifier = modifier,
)
}
// ============== PREVIEWS ==============
/**
* Preview provider for CategoryDropdownState
*/
@Suppress("HardCodedStringLiteral")
class CategoryDropdownStateProvider : PreviewParameterProvider<CategoryDropdownState> {
override val values = sequenceOf(
// Collapsed state - nothing selected
CategoryDropdownState(
expanded = false,
selectedCategories = emptyList(),
categories = listOf(
TagCategory(1, "Animals"),
TagCategory(2, "Food"),
VocabularyFilter(3, "Filters", languages = listOf(1, 2)),
)
),
// Collapsed state - one category selected
CategoryDropdownState(
expanded = false,
selectedCategories = listOf(TagCategory(1, "Animals")),
categories = listOf(
TagCategory(1, "Animals"),
TagCategory(2, "Food"),
TagCategory(3, "Travel"),
)
),
// Collapsed state - multiple categories selected
CategoryDropdownState(
expanded = false,
selectedCategories = listOf(
TagCategory(1, "Animals"),
TagCategory(3, "Travel"),
),
categories = listOf(
TagCategory(1, "Animals"),
TagCategory(2, "Food"),
TagCategory(3, "Travel"),
)
),
// Expanded state - nothing selected
CategoryDropdownState(
expanded = true,
selectedCategories = emptyList(),
categories = listOf(
TagCategory(1, "Animals"),
TagCategory(2, "Food"),
TagCategory(3, "Travel"),
)
),
// Expanded state - one selected
CategoryDropdownState(
expanded = true,
selectedCategories = listOf(TagCategory(2, "Food")),
categories = listOf(
TagCategory(1, "Animals"),
TagCategory(2, "Food"),
TagCategory(3, "Travel"),
)
),
// With "None" option selected
CategoryDropdownState(
expanded = true,
selectedCategories = listOf(null),
categories = listOf(
TagCategory(1, "Animals"),
TagCategory(2, "Food"),
)
),
// With add category option
CategoryDropdownState(
expanded = true,
selectedCategories = emptyList(),
newCategoryName = "New Cat",
categories = listOf(
TagCategory(1, "Animals"),
TagCategory(2, "Food"),
)
),
)
}
@ThemePreviews
@Preview(showBackground = true)
@Composable
fun CategoryDropdownCollapsedPreview(
@PreviewParameter(CategoryDropdownStateProvider::class) state: CategoryDropdownState
) {
Surface(
modifier = Modifier.padding(16.dp),
color = MaterialTheme.colorScheme.background
) {
CategoryDropdownContent(
state = state.copy(expanded = false),
onExpand = {},
onCategorySelected = {},
onNewCategoryNameChange = {},
onAddCategory = {},
)
}
}
@ThemePreviews
@Preview(showBackground = true)
@Composable
fun CategoryDropdownExpandedPreview(
@PreviewParameter(CategoryDropdownStateProvider::class) state: CategoryDropdownState
) {
Surface(
modifier = Modifier.padding(16.dp),
color = MaterialTheme.colorScheme.background
) {
CategoryDropdownContent(
state = state.copy(expanded = true),
onExpand = {},
onCategorySelected = {},
onNewCategoryNameChange = {},
onAddCategory = {},
)
}
}
@Suppress("HardCodedStringLiteral")
@ThemePreviews
@Preview(showBackground = true)
@Composable
fun CategoryDropdownMultipleSelectionPreview() {
val categories = listOf(
TagCategory(1, "Animals"),
TagCategory(2, "Food"),
TagCategory(3, "Travel"),
TagCategory(4, "Business"),
TagCategory(5, "Technology"),
)
var selectedCategories by remember {
mutableStateOf<List<VocabularyCategory?>>(listOf(categories[0], categories[2]))
}
Surface(
modifier = Modifier.padding(16.dp),
color = MaterialTheme.colorScheme.background
) {
CategoryDropdownContent(
state = CategoryDropdownState(
expanded = true,
selectedCategories = selectedCategories,
categories = categories,
),
onExpand = {},
onCategorySelected = { selectedCategories = it },
onNewCategoryNameChange = {},
onAddCategory = {},
multipleSelectable = true,
noneSelectable = true,
)
}
}
@Suppress("HardCodedStringLiteral")
@ThemePreviews
@Preview(showBackground = true)
@Composable
fun CategoryDropdownWithAddCategoryPreview() {
val categories = listOf(
TagCategory(1, "Animals"),
TagCategory(2, "Food"),
)
var newCategoryName by remember { mutableStateOf("New Category") }
Surface(
modifier = Modifier.padding(16.dp),
color = MaterialTheme.colorScheme.background
) {
CategoryDropdownContent(
state = CategoryDropdownState(
expanded = true,
selectedCategories = emptyList(),
newCategoryName = newCategoryName,
categories = categories,
),
onExpand = {},
onCategorySelected = {},
onNewCategoryNameChange = { newCategoryName = it },
onAddCategory = {},
addCategory = true,
)
}
}
@Suppress("HardCodedStringLiteral")
@ThemePreviews
@Preview(showBackground = true)
@Composable
fun CategoryDropdownOnlyListsPreview() {
val categories = listOf(
TagCategory(1, "Animals"),
TagCategory(2, "Food"),
VocabularyFilter(3, "Language Pair EN-DE", languages = listOf(1, 2)),
TagCategory(4, "Travel"),
)
Surface(
modifier = Modifier.padding(16.dp),
color = MaterialTheme.colorScheme.background
) {
CategoryDropdownContent(
state = CategoryDropdownState(
expanded = true,
selectedCategories = emptyList(),
categories = categories,
),
onExpand = {},
onCategorySelected = {},
onNewCategoryNameChange = {},
onAddCategory = {},
onlyLists = true,
)
}
}
@Suppress("HardCodedStringLiteral")
@ThemePreviews
@Preview(showBackground = true)
@Composable
fun CategoryDropdownNoNoneOptionPreview() {
val categories = listOf(
TagCategory(1, "Animals"),
TagCategory(2, "Food"),
)
Surface(
modifier = Modifier.padding(16.dp),
color = MaterialTheme.colorScheme.background
) {
CategoryDropdownContent(
state = CategoryDropdownState(
expanded = true,
selectedCategories = emptyList(),
categories = categories,
),
onExpand = {},
onCategorySelected = {},
onNewCategoryNameChange = {},
onAddCategory = {},
noneSelectable = false,
)
}
}
@ThemePreviews
@Preview(showBackground = true)
@Composable
fun CategoryDropdownEmptyPreview() {
Surface(
modifier = Modifier.padding(16.dp),
color = MaterialTheme.colorScheme.background
) {
CategoryDropdownContent(
state = CategoryDropdownState(
expanded = false,
selectedCategories = emptyList(),
categories = emptyList(),
),
onExpand = {},
onCategorySelected = {},
onNewCategoryNameChange = {},
onAddCategory = {},
)
}
}
@Suppress("HardCodedStringLiteral")
@ThemePreviews
@Preview(showBackground = true)
@Composable
fun CategoryDropdownStatefulPreview() {
var expanded by remember { mutableStateOf(true) }
var selectedCategories by remember {
mutableStateOf<List<VocabularyCategory?>>(listOf(TagCategory(1, "Animals")))
}
Surface(
modifier = Modifier.padding(16.dp),
color = MaterialTheme.colorScheme.background
) {
CategoryDropdownContent(
state = CategoryDropdownState(
expanded = expanded,
selectedCategories = selectedCategories,
categories = listOf(
TagCategory(1, "Animals"),
TagCategory(2, "Food"),
TagCategory(3, "Travel"),
),
),
onExpand = { expanded = it },
onCategorySelected = { selectedCategories = it },
onNewCategoryNameChange = {},
onAddCategory = {},
multipleSelectable = true,
noneSelectable = true,
)
}
}
@Suppress("HardCodedStringLiteral")
@ThemePreviews
@Preview(showBackground = true)
@Composable
fun CategoryDropdownFullExpandedPreview() {
Surface(
modifier = Modifier.padding(16.dp),
color = MaterialTheme.colorScheme.background
) {
CategoryDropdownContent(
state = CategoryDropdownState(
expanded = true,
selectedCategories = emptyList(),
categories = listOf(
TagCategory(1, "Animals"),
TagCategory(2, "Food"),
TagCategory(3, "Travel"),
TagCategory(4, "Business"),
TagCategory(5, "Technology"),
TagCategory(6, "Sports"),
TagCategory(7, "Music"),
TagCategory(8, "Art"),
),
),
onExpand = {},
onCategorySelected = {},
onNewCategoryNameChange = {},
onAddCategory = {},
addCategory = true,
multipleSelectable = true,
noneSelectable = true,
)
}
}

View File

@@ -6,40 +6,65 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import eu.gaudian.translator.R
import eu.gaudian.translator.model.TagCategory
import eu.gaudian.translator.model.VocabularyCategory
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppDialog
import eu.gaudian.translator.view.composable.DialogButton
import eu.gaudian.translator.viewmodel.CategoryViewModel
@Composable
fun CategorySelectionDialog(
onCategorySelected: (List<VocabularyCategory?>) -> Unit,
onDismissRequest: () -> Unit,
) {
var selectedCategory by remember { mutableStateOf<List<VocabularyCategory?>>(emptyList()) }
val activity = LocalContext.current.findActivity()
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
AppDialog(onDismissRequest = onDismissRequest, title = {
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory?>>(emptyList()) }
var newCategoryName by remember { mutableStateOf("") }
var expanded by remember { mutableStateOf(false) }
AppDialog(
onDismissRequest = onDismissRequest,
title = {
Text(text = stringResource(R.string.text_select_categories))
}) {
CategoryDropdown(
onCategorySelected = { categories ->
selectedCategory = categories
}
) {
// Dropdown button and menu
CategoryDropdownContent(
state = CategoryDropdownState(
expanded = expanded,
selectedCategories = selectedCategories,
newCategoryName = newCategoryName,
categories = categories,
),
onExpand = { isExpanded -> expanded = isExpanded },
onCategorySelected = { selectedCategories = it },
onNewCategoryNameChange = { newCategoryName = it },
onAddCategory = { name ->
val newCategory = TagCategory(id = 0, name = name.trim())
categoryViewModel.createCategory(newCategory)
newCategoryName = ""
},
noneSelectable = false,
multipleSelectable = true,
onlyLists = true,
addCategory = true
addCategory = true,
modifier = Modifier.fillMaxWidth(),
)
Row(
@@ -54,10 +79,10 @@ fun CategorySelectionDialog(
DialogButton(
onClick = {
onCategorySelected(selectedCategory)
onCategorySelected(selectedCategories)
onDismissRequest()
},
enabled = true
enabled = selectedCategories.isNotEmpty()
) {
Text(stringResource(R.string.label_confirm))
}

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

@@ -9,6 +9,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -27,6 +28,7 @@ import eu.gaudian.translator.model.VocabularyStage
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppDialog
import eu.gaudian.translator.view.composable.MultipleLanguageDropdown
import eu.gaudian.translator.viewmodel.CategoryViewModel
import eu.gaudian.translator.viewmodel.LanguageViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel
import kotlinx.coroutines.launch
@@ -43,11 +45,19 @@ fun StartExerciseDialog(
val activity = LocalContext.current.findActivity()
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
val coroutineScope = rememberCoroutineScope()
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
var lids by remember { mutableStateOf<List<Int>>(emptyList()) }
var languages by remember { mutableStateOf<List<Language>>(emptyList()) }
// Map displayed Language to its DB id (lid) using position mapping from load
var languageIdMap by remember { mutableStateOf<Map<Language, Int>>(emptyMap()) }
var selectedLanguages by remember { mutableStateOf<List<Language>>(emptyList()) }
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory>>(emptyList()) }
var selectedStages by remember { mutableStateOf<List<VocabularyStage>>(emptyList()) }
var expanded by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
coroutineScope.launch {
@@ -59,9 +69,6 @@ fun StartExerciseDialog(
languageIdMap = languages.mapIndexed { index, lang -> lang to lids.getOrNull(index) }.filter { it.second != null }.associate { it.first to it.second!! }
}
}
var selectedLanguages by remember { mutableStateOf<List<Language>>(emptyList()) }
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory>>(emptyList()) }
var selectedStages by remember { mutableStateOf<List<VocabularyStage>>(emptyList()) }
AppDialog(onDismissRequest = onDismiss, title = { Text(stringResource(R.string.label_start_exercise)) }) {
@@ -80,11 +87,23 @@ fun StartExerciseDialog(
},
languages
)
CategoryDropdown(
onCategorySelected = { categories ->
selectedCategories = categories.filterIsInstance<VocabularyCategory>()
CategoryDropdownContent(
state = CategoryDropdownState(
expanded = expanded,
selectedCategories = selectedCategories,
newCategoryName = "",
categories = categories,
),
onExpand = { isExpanded -> expanded = isExpanded },
onCategorySelected = { cats ->
selectedCategories = cats.filterIsInstance<VocabularyCategory>()
},
multipleSelectable = true
onNewCategoryNameChange = {},
onAddCategory = {},
multipleSelectable = true,
onlyLists = false, // Show both filters and lists
addCategory = false,
modifier = Modifier.fillMaxWidth(),
)
VocabularyStageDropDown(
modifier = Modifier.fillMaxWidth(),

View File

@@ -27,6 +27,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import eu.gaudian.translator.R
import eu.gaudian.translator.model.TagCategory
import eu.gaudian.translator.model.VocabularyCategory
import eu.gaudian.translator.model.VocabularyItem
import eu.gaudian.translator.utils.findActivity
@@ -35,6 +36,7 @@ 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.viewmodel.CategoryViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel
@Composable
@@ -44,11 +46,16 @@ fun VocabularyReviewScreen(
) {
val activity = LocalContext.current.findActivity()
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(activity)
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
val generatedItems: List<VocabularyItem> by vocabularyViewModel.generatedVocabularyItems.collectAsState()
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
val selectedItems = remember { mutableStateListOf<VocabularyItem>() }
val duplicates = remember { mutableStateListOf<Boolean>() }
var selectedCategoryId by remember { mutableStateOf<List<Int>>(emptyList()) }
LocalContext.current
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory?>>(emptyList()) }
var newCategoryName by remember { mutableStateOf("") }
var expanded by remember { mutableStateOf(false) }
LaunchedEffect(generatedItems) {
val duplicateResults = vocabularyViewModel.findDuplicates(generatedItems)
@@ -62,7 +69,7 @@ fun VocabularyReviewScreen(
topBar = {
AppTopAppBar(
title = { Text(stringResource(R.string.found_items)) },
hintContent = { getVocabularyReviewHint() }
hintContent = getVocabularyReviewHint()
)
},
) { paddingValues ->
@@ -127,11 +134,28 @@ fun VocabularyReviewScreen(
style = MaterialTheme.typography.labelLarge,
modifier = Modifier.padding(8.dp)
)
CategoryDropdown(
onCategorySelected = { categories: List<VocabularyCategory?> ->
selectedCategoryId = categories.filterNotNull().map { it.id }
CategoryDropdownContent(
state = CategoryDropdownState(
expanded = expanded,
selectedCategories = selectedCategories,
newCategoryName = newCategoryName,
categories = categories,
),
onExpand = { isExpanded -> expanded = isExpanded },
onCategorySelected = { selectedCategories = it },
onNewCategoryNameChange = { newCategoryName = it },
onAddCategory = { name ->
val newCategory = TagCategory(id = 0, name = name.trim())
categoryViewModel.createCategory(newCategory)
newCategoryName = ""
},
onlyLists = true
noneSelectable = false,
multipleSelectable = true,
onlyLists = true,
addCategory = true,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Row(
modifier = Modifier
@@ -143,9 +167,13 @@ fun VocabularyReviewScreen(
Text(stringResource(R.string.label_cancel))
}
Spacer(modifier = Modifier.width(8.dp))
AppButton(onClick = {
onConfirm(selectedItems.toList(), selectedCategoryId)
}) {
AppButton(
onClick = {
val selectedCategoryIds = selectedCategories.filterNotNull().map { it.id }
onConfirm(selectedItems.toList(), selectedCategoryIds)
},
enabled = selectedItems.isNotEmpty()
) {
Text(stringResource(R.string.label_add_, selectedItems.size))
}
}

View File

@@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
@@ -28,6 +27,7 @@ import eu.gaudian.translator.R
import eu.gaudian.translator.model.VocabularyStage
import eu.gaudian.translator.view.composable.AppButton
import eu.gaudian.translator.view.composable.AppCheckbox
import eu.gaudian.translator.view.composable.AppDropdownMenuItem
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppOutlinedButton
@@ -76,7 +76,7 @@ fun VocabularyStageDropDown(
) {
if (noneSelectable == true) {
val noneSelected = selectedStages.contains(null)
DropdownMenuItem(
AppDropdownMenuItem(
text = {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
if (multipleSelectable) {
@@ -111,7 +111,7 @@ fun VocabularyStageDropDown(
VocabularyStage.entries.forEach { stage ->
val isSelected = selectedStages.contains(stage)
DropdownMenuItem(
AppDropdownMenuItem(
text = {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
if (multipleSelectable) {

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.LocalizedMarkdown("example_hint")
)
)
)
),
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))
)
)
)
)
}
@Preview
@Composable
fun ReusedScanButtonPreviewPreview() {
ReusedScanButtonPreview()
}
@Preview

View File

@@ -1,125 +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
object ApiKeyHint: LegacyHint() {
override val titleRes: Int = R.string.hint_how_to_connect_to_an_ai
/**
* Migrated API Key hint using markdown-based format.
* Content is loaded from localized assets/hints based on device locale.
*/
@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)
fun getApiKeyHint() = Hint (
titleRes = R.string.hint_how_to_connect_to_an_ai,
elements = listOf(
HintElement.LocalizedMarkdown("api_key_hint")
)
)
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
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)
}
}
fun ApiKeyHint() {
getApiKeyHint().Render()
}
@Preview(showBackground = true)
@Preview
@Composable
fun ApiKeyHintPreview() {
MaterialTheme {
ApiKeyHint.Content()
ApiKeyHint()
}
}

View File

@@ -1,116 +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
object CategoryHint : LegacyHint() {
override val titleRes: Int = R.string.category_hint_intro
/**
* Migrated Category hint using markdown-based format.
* Content is loaded from localized assets/hints based on device locale.
*/
@Composable
override fun Content() {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
HeaderWithIcon(
title = stringResource(R.string.category_hint_intro)
fun getCategoryHint(): Hint {
return Hint(
titleRes = R.string.category_hint_intro,
elements = listOf(
HintElement.LocalizedMarkdown("category_hint")
)
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 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() {
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,128 +1,30 @@
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
object CategoryHintScreen : LegacyHint() {
override val titleRes: Int = R.string.category_hint_intro
/**
* Migrated Category Hint Screen using markdown-based format.
* Content is loaded from localized assets/hints based on device locale.
*/
@Composable
override fun Content() {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
HeaderWithIcon(
title = stringResource(R.string.category_hint_intro)
fun getCategoryHintScreen(): Hint {
return Hint(
titleRes = R.string.category_hint_intro,
elements = listOf(
HintElement.LocalizedMarkdown("category_hint")
)
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 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 {

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

@@ -0,0 +1,145 @@
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,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) {
@@ -100,225 +71,3 @@ 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
}
}
// Preview Composables for each HintElement type
@Suppress("HardCodedStringLiteral")
@Preview
@Composable
fun TextElementPreview() {
RenderHintElement(HintElement.Text("This is a sample text element."))
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)
}
}
@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
@@ -259,3 +135,12 @@ fun UIElementPreview() {
}
)
}
@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
}
}
AppOutlinedCard {

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
)
}
}
)
)
)
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,200 +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
)
object LearningStagesHint : LegacyHint() {
override val titleRes: Int = R.string.learning_stages_title
/**
* Migrated Learning Stages hint using markdown-based format.
* Content is loaded from localized assets/hints based on device locale.
*/
@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))
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()
}
}
}

View File

@@ -0,0 +1,147 @@
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

@@ -0,0 +1,217 @@
package eu.gaudian.translator.view.hints
import android.content.Context
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"
}
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) {
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,239 +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
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,122 +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
object VocabularyProgressHint : LegacyHint() {
override val titleRes: Int = R.string.hint_vocabulary_progress_hint_title
/**
* Migrated Vocabulary Progress hint using markdown-based format.
* Content is loaded from localized assets/hints based on device locale.
*/
@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)
fun getVocabularyProgressHint(): Hint {
return Hint(
titleRes = R.string.hint_vocabulary_progress_hint_title,
elements = listOf(
HintElement.LocalizedMarkdown("vocabulary_progress_hint")
)
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()
}
@Composable
fun VocabularyProgressHint() {
VocabularyProgressHint.Content()
getVocabularyProgressHint().Render()
}
@Preview
@Composable
private fun VocabularyProgressHintPreview() {
fun VocabularyProgressHintPreview() {
MaterialTheme {
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)
}
}
}
@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

@@ -66,6 +66,7 @@ import eu.gaudian.translator.R
import eu.gaudian.translator.model.LanguageModel
import eu.gaudian.translator.model.communication.ApiProvider
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.ApiModelDropDown
import eu.gaudian.translator.view.composable.AppAlertDialog
import eu.gaudian.translator.view.composable.AppButton
import eu.gaudian.translator.view.composable.AppCard
@@ -79,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
@@ -120,7 +121,7 @@ fun ApiKeyScreen(navController: NavController) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
hintContent = { ApiKeyHint.Content() }
hintContent = getApiKeyHint()
)
}
) { paddingValues ->

View File

@@ -1,48 +1,28 @@
package eu.gaudian.translator.view.settings
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.PaddingValues
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.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import eu.gaudian.translator.R
import eu.gaudian.translator.model.LanguageModel
import eu.gaudian.translator.model.communication.ApiProvider
import eu.gaudian.translator.ui.theme.ThemePreviews
import eu.gaudian.translator.view.composable.ApiModelDropDown
import eu.gaudian.translator.view.composable.AppButton
import eu.gaudian.translator.view.composable.AppCard
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppOutlinedButton
import eu.gaudian.translator.view.composable.InspiringSearchField
import eu.gaudian.translator.view.composable.ModelBadges
data class PromptSettingsState(
val availableModels: List<LanguageModel> = emptyList(),
@@ -118,232 +98,7 @@ fun BasePromptSettingsScreen(
}
}
@Composable
fun ApiModelDropDown(
models: List<LanguageModel>,
providers: List<ApiProvider>,
selectedModel: LanguageModel?,
onModelSelected: (LanguageModel?) -> Unit,
enabled: Boolean = true
) {
LocalContext.current
var expanded by remember { mutableStateOf(false) }
var searchQuery by remember { mutableStateOf("") }
val activeModels = models.filter { model -> providers.any { it.key == model.providerKey && (it.hasValidKey || it.isCustom) } }
val groupedModels = activeModels.groupBy { it.providerKey }
val providerNames = remember(providers) { providers.associate { it.key to it.displayName } }
val providerStatuses = remember(providers) { providers.associate { it.key to (it.hasValidKey || it.isCustom) } }
val filteredGroupedModels = remember(groupedModels, searchQuery) {
if (searchQuery.isBlank()) {
groupedModels
} else {
groupedModels.mapValues { (_, models) ->
models.filter { model ->
model.displayName.contains(searchQuery, ignoreCase = true) ||
model.modelId.contains(searchQuery, ignoreCase = true) ||
model.description.contains(searchQuery, ignoreCase = true)
}
}.filterValues { it.isNotEmpty() }
}
}
Box {
AppOutlinedButton(
onClick = { expanded = true },
modifier = Modifier.align(Alignment.Center),
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp),
enabled = enabled
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = selectedModel?.displayName ?: stringResource(R.string.text_select_model),
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (selectedModel != null) {
Text(
text = providerNames[selectedModel.providerKey] ?: selectedModel.providerKey,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
Spacer(modifier = Modifier.width(8.dp))
Icon(
imageVector = if (expanded) AppIcons.ArrowDropUp else AppIcons.ArrowDropDown,
contentDescription = if (expanded) stringResource(R.string.cd_collapse) else stringResource(R.string.cd_expand)
)
}
}
DropdownMenu(
modifier = Modifier
.fillMaxWidth(),
expanded = expanded,
onDismissRequest = { expanded = false }
) {
// Search bar
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
AppIcons.Search,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
)
Spacer(modifier = Modifier.width(8.dp))
TextField(
value = searchQuery,
onValueChange = { searchQuery = it },
placeholder = { Text(stringResource(R.string.label_search_models)) },
singleLine = true,
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
),
modifier = Modifier.weight(1f)
)
if (searchQuery.isNotBlank()) {
IconButton(
onClick = { searchQuery = "" },
modifier = Modifier.size(24.dp)
) {
Icon(
AppIcons.Close,
contentDescription = stringResource(R.string.cd_clear_search),
modifier = Modifier.size(16.dp)
)
}
}
}
HorizontalDivider()
}
if (filteredGroupedModels.isNotEmpty()) {
filteredGroupedModels.entries.forEachIndexed { index, entry ->
val providerKey = entry.key
val providerModels = entry.value
val isActive = providerStatuses[providerKey] == true
val providerName = providerNames[providerKey] ?: providerKey
if (index > 0) HorizontalDivider()
// Provider header
DropdownMenuItem(
text = {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Icon(
imageVector = if (isActive) AppIcons.CheckCircle else AppIcons.Warning,
contentDescription = null,
tint = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = providerName,
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Medium,
color = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
Text(
text = stringResource(
R.string.labels_1d_models,
providerModels.size
),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
)
}
}
},
enabled = false,
onClick = {}
)
// Models for this provider
providerModels.forEach { model ->
DropdownMenuItem(
text = {
Column(modifier = Modifier.fillMaxWidth()) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = model.displayName,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.weight(1f),
fontWeight = if (model == selectedModel) FontWeight.Medium else FontWeight.Normal
)
Spacer(modifier = Modifier.width(8.dp))
ModelBadges(
modelDisplayOrId = model.displayName.ifBlank { model.modelId },
providerKey = model.providerKey,
)
}
if (model.description.isNotBlank()) {
Text(
text = model.description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
},
onClick = {
onModelSelected(model)
expanded = false
searchQuery = ""
},
modifier = if (model == selectedModel) {
Modifier.background(
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
} else {
Modifier
}
)
}
}
} else if (searchQuery.isNotBlank()) {
DropdownMenuItem(
text = {
Text(
stringResource(R.string.text_no_models_found),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
modifier = Modifier.fillMaxWidth()
)
},
enabled = false,
onClick = {}
)
}
}
}
}

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

@@ -29,6 +29,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import eu.gaudian.translator.R
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.ApiModelDropDown
import eu.gaudian.translator.view.composable.AppCard
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppOutlinedTextField

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()
)
},

View File

@@ -1,129 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="hint_scan_hint_section_cant_find">Findest du dein Modell nicht?</string>
<string name="hint_scan_hint_manual_add_paragraph">Du kannst Modelle manuell hinzufügen. Gib die exakte Modell-ID aus der Dokumentation deines Anbieters und einen Anzeigenamen ein. Die App verwendet diese ID für alle API-Aufrufe.</string>
<string name="hint_scan_hint_chip_nano">nano</string>
<string name="hint_scan_hint_chip_mini">mini</string>
<string name="hint_scan_hint_chip_small">klein</string>
<string name="hint_scan_hint_chip_medium">mittel</string>
<string name="hint_scan_hint_chip_large_paid">groß / bezahlt</string>
<string name="hint_scan_hint_section_visual_guide">Vom Scan zur Auswahl eine schnelle visuelle Anleitung</string>
<string name="hint_scan_hint_step_1">1</string>
<string name="hint_scan_hint_step_2">2</string>
<string name="hint_scan_hint_step_3">3</string>
<string name="hint_scan_hint_step1_title">Starte den Scan</string>
<string name="hint_scan_hint_step1_desc">Tippe auf die Scan-Schaltfläche, um verfügbare Modelle von deinem Anbieter abzurufen.</string>
<string name="hint_scan_hint_step2_title">Filtern &amp; auswählen</string>
<string name="hint_scan_hint_step2_desc">Durchsuche die Liste. Bevorzuge Modelle, die für Text/Chat markiert sind. Einige Anbieter kennzeichnen kostenlose/kostenpflichtige Modelle unterschiedlich.</string>
<string name="hint_scan_hint_label_text_chat">Text/Chat</string>
<string name="hint_scan_hint_step3_title">Validieren</string>
<string name="hint_scan_hint_step3_desc">Nutze „Hinzufügen &amp; Validieren“, um das Modell zu speichern und eine schnelle Überprüfung bei deinem Anbieter durchzuführen.</string>
<string name="hint_scan_hint_add_validate">Hinzufügen &amp; Validieren</string>
<string name="hint_translation_context_aware_title">Kontextbezogene Übersetzung</string>
<string name="hint_translation_context_aware_desc">Erhalte Übersetzungen, die den Kontext deines Gesprächs verstehen, für genauere Ergebnisse.</string>
<string name="hint_vocabulary_progress_hint_title">Fortschrittsverfolgung für Vokabeln</string>
<string name="hint_vocabulary_progress_tracking_title">Fortschrittsverfolgung</string>
<string name="hint_vocabulary_progress_tracking_desc">Verfolge deinen Lernfortschritt mit detaillierten Statistiken und visuellen Indikatoren.</string>
<string name="hint_vocabulary_learning_stages_title">Lernstufen</string>
<string name="hint_vocabulary_learning_stages_desc">Wörter durchlaufen beim Lernen verschiedene Stufen, mit zunehmenden Intervallen zwischen den Wiederholungen.</string>
<string name="hint_vocabulary_review_system_title">Wiederholungssystem</string>
<string name="hint_vocabulary_review_system_desc">Das System der verteilten Wiederholung stellt sicher, dass du Wörter in optimalen Intervallen wiederholst, um sie langfristig zu behalten.</string>
<string name="hint_vocabulary_customization_title">Anpassung</string>
<string name="hint_vocabulary_customization_desc">Passe Lernkriterien, tägliche Ziele und Wiederholungsintervalle an deinen Lernstil an.</string>
<string name="hint_title_hints_overview">Hilfe und Anleitungen</string>
<string name="hint_hints_overview_intro">Hilfebereich</string>
<string name="hint_hints_overview_description">Alle Hinweise, die es in dieser App gibt, findest du auch hier.</string>
<string name="hint_hints_header_basics">Erste Schritte</string>
<string name="hint_hints_header_vocabulary">Vokabelverwaltung</string>
<string name="hint_hints_header_advanced">Erweiterte Funktionen</string>
<string name="hint_list_category">Eine „Liste“ ist eine einfache Kategorie, zu der du beliebige Vokabeln manuell hinzufügen kannst. Sie ist wie ein benutzerdefinierter Ordner für deine Wörter.</string>
<string name="api_hint_intro_1">Um alle Funktionen nutzen zu können, muss die App eine Verbindung zu einem Dienst für große Sprachmodelle (LLM) herstellen. Dies geschieht über einen API-Anbieter.</string>
<string name="api_hint_intro_2">Du kannst deinen API-Schlüssel zu einem vorkonfigurierten Anbieter (wie OpenAI oder Google) hinzufügen oder einen benutzerdefinierten Anbieter hinzufügen, um dich mit einem anderen Dienst, wie einem lokalen Modell, zu verbinden. Jeder Anbieter muss mit dem OpenAI-API-Standard kompatibel sein.</string>
<string name="key_status_indicators_title">Schlüssel-Statusanzeigen</string>
<string name="key_status_explanation">Jede Anbieterkarte zeigt den Status deines API-Schlüssels an:</string>
<string name="key_saved_and_active">Das bedeutet, dein Schlüssel ist gespeichert und aktiv.</string>
<string name="key_missing_or_cleared">Das bedeutet, der API-Schlüssel fehlt oder wurde gelöscht.</string>
<string name="troubleshooting_title">Fehlerbehebung</string>
<string name="troubleshooting_intro">Wenn du Probleme hast, überprüfe bitte Folgendes:</string>
<string name="troubleshooting_bullets">• Stelle sicher, dass dein API-Schlüssel gültig ist und die nötigen Berechtigungen hat.\n• Überprüfe deine Netzwerkverbindung.\n• Sieh dir den Tab „Netzwerk-Logs“ für detaillierte Fehlermeldungen an.</string>
<string name="category_hint_intro">Du kannst zwei Arten von Kategorien erstellen, um deine Vokabeln zu organisieren:</string>
<string name="content_desc_tag_category">Tag-Kategorie</string>
<string name="content_desc_filter_category">Filter-Kategorie</string>
<string name="hint_filter_category_description">Filter können Elemente abgleichen nach: keinem Sprachfilter, einer Liste von Sprachen oder einem Wörterbuchpaar. Du kannst optional auch nach Lernstufen filtern. Sprachliste und Wörterbuchpaar schließen sich gegenseitig aus.</string>
<string name="category_hint_item_preview_description">Erstelle einen manuellen Tag, um Wörter deiner Wahl zu gruppieren.</string>
<string name="category_list_title">Listenkategorie</string>
<string name="category_list_description">Füge manuell jedes gewünschte Wort zu dieser Kategorie hinzu. Sie ist perfekt, um eigene Lernlisten für ein bestimmtes Thema oder Kapitel zu erstellen.</string>
<string name="example_word_apple">Apple</string>
<string name="action_add">Hinzufügen</string>
<string name="example_category_my_fruit_list">Meine Obstliste</string>
<string name="category_filter_title">Filterkategorie</string>
<string name="category_filter_description">Diese Kategorie gruppiert Wörter automatisch basierend auf von dir festgelegten Regeln, wie ihrer Lernstufe oder Sprache. Das ist eine dynamische, automatische Art der Organisation.</string>
<string name="example_word_dog">Dog</string>
<string name="example_word_cat">Cat</string>
<string name="example_filter_stage_1">„Stufe 1“-Filter</string>
<string name="hint_dict_options_step1_title">Schritt 1: Konfiguriere die KI</string>
<string name="hint_dict_options_step2_title">Schritt 2: Wähle Inhalte aus</string>
<string name="hint_dict_options_step2_desc">Nutze als Nächstes die Schalter, um auszuwählen, welche spezifischen Abschnitte (wie Synonyme, Antonyme usw.) bei einer Wörterbuchsuche enthalten sein sollen.</string>
<string name="eg_synonyms">z.B. Synonyme</string>
<string name="example_toggle">Beispiel-Schalter</string>
<string name="import_ai_intro">Lass die KI Vokabeln für dich finden. So nutzt du diese Funktion:</string>
<string name="import_step1_title">1. Gib einen Suchbegriff ein</string>
<string name="search_term_placeholder">Was man im Zoo machen kann</string>
<string name="import_step2_title">2. Wähle deine Sprachen aus</string>
<string name="import_step2_desc">Wähle die Sprache, aus der du lernen möchtest (Quelle), und die Sprache, die du lernen möchtest (Ziel).</string>
<string name="import_step3_title">3. Wähle die Anzahl der Wörter</string>
<string name="import_step3_desc">Verwende den Schieberegler, um auszuwählen, wie viele Wörter du generieren möchtest (bis zu 25).</string>
<string name="import_after_generating">Nach dem Generieren kannst du die Wörter überprüfen, bevor du sie hinzufügst.</string>
<string name="example_word_der_apfel">der Apfel</string>
<string name="example_word_the_apple">the apple</string>
<string name="example_word_der_hund">der Hund</string>
<string name="example_word_the_dog">the dog</string>
<string name="review_intro">Überprüfe die generierten Vokabeln, bevor du sie zu deiner Sammlung hinzufügst.</string>
<string name="review_select_items_title">Elemente auswählen</string>
<string name="review_select_items_desc">Verwende die Kontrollkästchen, um die Wörter auszuwählen, die du behalten möchtest. Du kannst auch das Kontrollkästchen oben verwenden, um alle Elemente auf einmal aus- oder abzuwählen.</string>
<string name="duplicate">Duplikat</string>
<string name="duplicate_handling_title">Umgang mit Duplikaten</string>
<string name="duplicate_handling_desc">Die App erkennt automatisch, ob ein Wort bereits in deinem Vokabular vorhanden ist. Diese Duplikate sind standardmäßig nicht ausgewählt, um Unordnung zu vermeiden.</string>
<string name="add_to_list_optional">Zu einer Liste hinzufügen (Optional)</string>
<string name="add_to_list_optional_desc">Du kannst die ausgewählten Wörter direkt zu einer deiner bestehenden Vokabellisten hinzufügen, indem du eine aus dem Dropdown-Menü unten auswählst.</string>
<string name="interval_1_day">1 Tag</string>
<string name="interval_3_days">3 Tage</string>
<string name="interval_1_week">1 Woche</string>
<string name="interval_2_weeks">2 Wochen</string>
<string name="interval_1_month">1 Monat</string>
<string name="hint_scan_hint_title">Das richtige KI-Modell finden</string>
<string name="scan_hint_section_how_scan_works">Wie der Scan funktioniert</string>
<string name="scan_hint_how_scan_works_paragraph">Wenn du auf „Nach Modellen suchen“ tippst, fragt die App deinen ausgewählten API-Anbieter nach einer Liste verfügbarer Modelle. Der Anbieter antwortet mit den Modellen, die für dich sichtbar sind.</string>
<string name="scan_hint_bullet_results_depend">Die Ergebnisse hängen von deinem Konto, deiner Organisation und der Anbieterkonfiguration ab.</string>
<string name="scan_hint_bullet_public_private">Einige Anbieter geben nur öffentliche Modelle zurück; private oder Unternehmensmodelle erfordern möglicherweise zusätzliche Berechtigungen.</string>
<string name="scan_hint_bullet_try_again">Wenn du kürzlich Berechtigungen oder Kontingente geändert hast, versuche es nach einer kurzen Verzögerung erneut.</string>
<string name="scan_hint_section_why_missing">Warum einige Modelle möglicherweise nicht angezeigt werden</string>
<string name="scan_hint_badge_restricted">Eingeschränkt oder nicht für dein Konto/deine Organisation zulässig</string>
<string name="scan_hint_badge_not_suitable">Nicht für diese Aufgabe geeignet (z. B. nur Bild, nur Audio oder nur Embeddings)</string>
<string name="scan_hint_badge_only_text_models">Es werden nur textfähige Modelle mit Textvervollständigung/Chat angezeigt</string>
<string name="scan_hint_focus_text_models">Die App konzentriert sich auf Modelle, die Text lesen und schreiben können. Für Übersetzung, Wörterbuch- und Vokabelgenerierung muss das Modell Text-Prompts unterstützen und Textvervollständigungen zurückgeben (Chat/Completions-API).</string>
<string name="scan_hint_most_tasks_small_models">Die meisten Aufgaben funktionieren hervorragend mit schnellen, kleinen Modellen (z. B. nano/mini/klein). Für die Erstellung ganzer Übungen kann ein größeres oder kostenpflichtiges Modell erforderlich sein.</string>
<string name="scan_hint_section_tips">Tipps &amp; Fehlerbehebung</string>
<string name="scan_hint_tip_verify_key">Überprüfe, ob dein API-Schlüssel gültig ist und die Berechtigung hat, auf die gewünschten Modelle zuzugreifen.</string>
<string name="scan_hint_tip_select_org">Einige Anbieter erfordern die Auswahl einer Organisation/eines Projekts. Stelle sicher, dass dies korrekt konfiguriert ist.</string>
<string name="scan_hint_tip_type_manually">Wenn ein Modell, von dem du weißt, dass es existiert, nicht angezeigt wird, versuche, es manuell über die Modell-ID aus den Dokumenten des Anbieters einzugeben.</string>
<string name="scan_hint_tip_instruct_chat_text">Suche nach Modellen, die als „Instruct“, „Chat“ oder „Text“ gekennzeichnet sind. Diese passen normalerweise am besten.</string>
<string name="hint_translate_how_it_works">Wie die Übersetzung funktioniert</string>
<string name="hint_translate_alternative_translations_title">Alternative Übersetzungen</string>
<string name="hint_translate_alternative_translations_desc">Tippe auf ein Wort in der Übersetzung, um alternative Bedeutungen zu sehen und die passendste auszuwählen.</string>
<string name="hint_translate_custom_prompts_title">Eigene Übersetzungs-Prompts</string>
<string name="hint_translate_custom_prompts_desc">Passe in den Einstellungen an, wie Übersetzungen mit KI-Prompts erstellt werden. Wähle aus Beispiel-Prompts oder erstelle deine eigenen.</string>
<string name="hint_translate_multiple_services_title">Mehrere Übersetzungsdienste</string>
<string name="hint_translate_multiple_services_desc">Wechsle zwischen KI-gestützter Übersetzung oder einem Übersetzungsdienst für verschiedene Optionen im Übersetzungsstil.</string>
<string name="hint_translate_history_title">Übersetzungsverlauf</string>
<string name="hint_translate_history_desc">Greife auf deinen Übersetzungsverlauf zu, um frühere Übersetzungen wiederzuverwenden.</string>
<string name="hint_translate_tts_title">Text-zu-Sprache</string>
<string name="hint_translate_tts_desc">Höre dir Übersetzungen mit Text-zu-Sprache-Unterstützung an. Konfiguriere Stimmen für verschiedene Sprachen in den Einstellungen.</string>
<string name="hint_translate_quick_actions_title">Schnellaktionen</string>
<string name="hint_translate_quick_actions_desc">Kopiere Übersetzungen in die Zwischenablage, teile sie oder füge Wörter mit einem Tippen zu deinem Vokabular hinzu.</string>
<string name="hint_translate_model_selection_title">Auswahl des KI-Modells</string>
<string name="hint_translate_model_selection_desc">Wähle aus verschiedenen KI-Modellen für Übersetzungsqualität und -geschwindigkeit.</string>
<string name="hint_dict_options_step1_desc">Zuerst wählst du das beste KI-Modell für deine Bedürfnisse aus und schreibst optional eine benutzerdefinierte Aufforderung, um zu steuern, wie es Wörterbuchinhalte generiert.</string>
</resources>

View File

@@ -66,7 +66,6 @@
<string name="menu_create_youtube_exercise">YouTube-Übung erstellen</string>
<string name="text_youtube_link">YouTube-Link</string>
<string name="text_customize_the_intervals">Passe die Intervalle und Kriterien für das Verschieben von Vokabeln an. Karten in niedrigeren Stufen werden öfter abgefragt.</string>
<string name="text_enter_your_custom_prompt">Gib deinen benutzerdefinierten Prompt ein</string>
<string name="text_developed_by_jonas_gaudian">Entwickelt von Jonas Gaudian</string>
<string name="text_visit_my_website">Besuche meine Webseite</string>
<string name="contact_developer_title">Entwickler kontaktieren</string>
@@ -130,7 +129,6 @@
<string name="text_enter_api_key">API-Schlüssel eingeben</string>
<string name="text_save_key">Schlüssel speichern</string>
<string name="text_select_model">Modell auswählen</string>
<string name="text_example_prompts">Beispiel-Prompts</string>
<string name="title_title_preview_title">Vorschau-Titel</string>
<string name="text_none">Keine</string>
<string name="text_manual_vocabulary_list">Manuelle Vokabelliste</string>
@@ -157,7 +155,6 @@
<string name="text_vocabulary_prompt">Vokabular-Prompt</string>
<string name="text_here_you_can_set_a_custom_">Hier kannst du einen benutzerdefinierten Prompt festlegen, um zu definieren, wie neue Vokabeln erstellt werden.</string>
<string name="text_select_the_content_dictionary">Wähle die Inhalte aus, die für einen Wörterbucheintrag erstellt werden sollen.</string>
<string name="text_custom_dictionary_prompt">Benutzerdefinierter Wörterbuch-Prompt</string>
<string name="text_save_prompt">Prompt speichern</string>
<string name="text_light">Hell</string>
<string name="text_dark">Dunkel</string>
@@ -201,7 +198,6 @@
<string name="text_let_ai_find_vocabulary_for_you">Lass die KI Vokabeln für dich finden!</string>
<string name="text_search_term">Suchbegriff</string>
<string name="text_hint">Tipp</string>
<string name="text_hint_you_can_search">Hinweis: Du kannst nach jedem Begriff suchen, z.B. „Was man im Zoo machen kann“ oder „unregelmäßige Verben“!</string>
<string name="text_select_languages">Sprachen auswählen</string>
<string name="text_select_amount">Anzahl auswählen</string>
<string name="text_amount_2d">Anzahl: %1$d</string>
@@ -263,7 +259,6 @@
<string name="text_authentication_is_required_and_has_failed">Authentifizierung erforderlich und fehlgeschlagen oder nicht vorhanden.</string>
<string name="text_401_unauthorized">401 Unauthorized</string>
<string name="text_403_forbidden">403 Forbidden</string>
<string name="the_server_understood_the_request_but_is_refusing_to_authorize_it">Der Server hat die Anfrage verstanden, verweigert aber die Autorisierung.</string>
<string name="text_404_not_found">404 Not Found</string>
<string name="the_requested_resource_could_not_be_found">Die angeforderte Ressource wurde nicht gefunden.</string>
<string name="text_429_too_many_requests">429 Too Many Requests</string>
@@ -402,7 +397,6 @@
<string name="delete_model">Modell löschen</string>
<string name="system_theme">System-Theme</string>
<string name="system_default_font">System-Standard-Schriftart</string>
<string name="label_analyze_grammar">Grammatik analysieren</string>
<string name="show_contextual_hints">Kontextbezogene Hinweise anzeigen</string>
<string name="display_info_buttons_for_on_screen_help">Info-Buttons auf dem Bildschirm für Hilfe anzeigen.</string>
<string name="got_it">Verstanden!</string>
@@ -561,7 +555,6 @@
<string name="error_no_text_to_edit">Fehler: Kein Text zum Bearbeiten</string>
<string name="not_launched_with_text_to_edit">Nicht mit zu bearbeitendem Text gestartet</string>
<string name="text_a_simple_list_to">Eine einfache Liste, um deine Vokabeln manuell zu sortieren</string>
<string name="connecting_your_ai_model">Verbinde dein KI-Modell</string>
<string name="settings_title_voice">Stimme</string>
<string name="default_value">Standard</string>
<string name="label_speaking_speed">Sprechgeschwindigkeit</string>
@@ -576,17 +569,11 @@
<string name="previous_month">Vorheriger Monat</string>
<string name="next_month">Nächster Monat</string>
<string name="show_api_key_missing_message">Meldung "API-Schlüssel fehlt" anzeigen</string>
<string name="sorting_hint_intro_text">Hier sortierst du deine neuen Vokabeln. Du kannst Rechtschreibung und Übersetzungen korrigieren und Einträge Kategorien zuweisen.</string>
<string name="sorting_hint_helper_text">Die App hilft dir auch, Duplikate zu erkennen oder Artikel für sauberere Vokabellisten zu entfernen.</string>
<string name="sorting_hint_chip_duplicate">Duplikat</string>
<string name="sorting_hint_decide_next_action">Wenn du fertig bist, entscheide, was mit dem Eintrag geschehen soll:</string>
<string name="label_move_first_stage">In die erste Stufe verschieben</string>
<string name="sorting_hint_title">Vokabeln sortieren</string>
<string name="text_optional">" (optional)"</string>
<string name="text_check_availability">Verfügbarkeit prüfen</string>
<string name="text_no_valid_api_configuration_could_be_found">Keine gültige API-Konfiguration gefunden. Bitte konfiguriere zuerst einen API-Anbieter in den Einstellungen.</string>
<string name="cd_tag_category">Tag-Kategorie</string>
<string name="hint_this_screen_lets_you_customize_">Hier kannst du die Anweisungen zum Erstellen neuer Vokabeln anpassen, z.B. ob Definitionen oder Beispielsätze enthalten sein sollen.</string>
<string name="text_try_wiktionary_first">Zuerst Wiktionary versuchen</string>
<string name="text_try_first_finding_the_word_on">Versuche zuerst, das Wort auf Wiktionary zu finden, bevor eine KI-Antwort generiert wird.</string>
<string name="text_question_of">Frage %1$d von %2$d</string>
@@ -607,13 +594,6 @@
<string name="intro_if_you_need_help_you">Wenn du Hilfe brauchst, findest du Hinweise in allen Bereichen der App.</string>
<string name="text_navigation_bar_labels">Navigationsleisten-Beschriftungen</string>
<string name="text_show_text_labels_on_the_main_navigation_bar">Textbeschriftungen in der Hauptnavigationsleiste anzeigen.</string>
<string name="hint_how_it_works">So funktioniert\'s</string>
<string name="hint_answer_correctly">Antworte richtig</string>
<string name="hint_the_word_moves">Das Wort steigt eine Stufe auf und du siehst es nach einer längeren Pause wieder.</string>
<string name="hint_answer_incorrectly">Antworte falsch</string>
<string name="hint_the_word_moves_back_another_stage_this_helps_you_focus_on_">Das Wort steigt eine Stufe ab. So konzentrierst du dich auf schwierige Vokabeln.</string>
<string name="hint_customizable">Anpassbar</string>
<string name="hint_you_can_costumize_all_intervals_and_rules_in_the_settings">Du kannst alle Intervalle und Regeln in den Einstellungen anpassen.</string>
<string name="text_word_pair_settings">Wortpaar-Einstellungen</string>
<string name="text_amount_of_questions_2d">Anzahl der Fragen: %1$d</string>
<string name="text_shuffle_questions">Fragen mischen</string>
@@ -621,8 +601,6 @@
<string name="text_match_the_pairs">Bilde die Paare</string>
<string name="text_word_pair_exercise">Wortpaar-Übung</string>
<string name="text_training_mode_description">Trainingsmodus ist aktiv: Antworten beeinflussen den Fortschritt nicht.</string>
<string name="cd_start_exercise">Übung starten</string>
<string name="hint_use_this_screen_to_define">"Verwende diesen Bildschirm, um eine benutzerdefinierte Anweisung für das KI-Übersetzungsmodell zu definieren. Du kannst den Ton, den Stil oder das Format der Übersetzung festlegen."</string>
<string name="text_days">" Tage"</string>
<string name="label_add_vocabulary">Vokabel hinzufügen</string>
<string name="label_create_vocabulary_with_ai">Vokabular mit KI erstellen</string>
@@ -651,7 +629,6 @@
<string name="text_repeat_wrong">Falsche wiederholen</string>
<string name="text_start_over">Von vorne beginnen</string>
<string name="label_dictionary_options">Wörterbuch-Optionen</string>
<string name="hint_dictionary_desc">So funktioniert das Wörterbuch:</string>
<string name="text_paste_or_open_a_">Füge einen YouTube-Link ein oder öffne ihn, um hier die Untertitel zu sehen.</string>
<string name="text_error_2d">Fehler: %1$s</string>
<string name="text_repeat_wrong_guesses">Falsche Antworten wiederholen</string>
@@ -667,7 +644,6 @@
<string name="future">Zukunft</string>
<string name="label_action_correct">Korrigieren</string>
<string name="text_daily_goal_description">Wie viele Wörter möchtest du jeden Tag richtig beantworten?</string>
<string name="hint_example_hint_scan_for_models_hint">Beispiel-Tipp Scanne nach Modellen Tipp</string>
<string name="hint_how_to_connect_to_an_ai">Wie man sich mit einer KI verbindet</string>
<string name="hint_how_to_generate_vocabulary_with_ai">Wie man Vokabeln mit KI generiert</string>
<string name="label_dictionary_manager">Wörterbuch-Manager</string>
@@ -698,7 +674,6 @@
<string name="text_finish_video_and_start_exercise">Video beenden und Übung starten</string>
<string name="text_orphaned_file_deleted_successfully">Verwaistes File erfolgreich gelöscht</string>
<string name="text_please_select_a_dictionary_language_first">Wähle zuerst eine Wörterbuch-Sprache aus.</string>
<string name="text_these_files_exist_locally">Diese Dateien existieren lokal, sind aber nicht im Server-Manifest enthalten. Sie könnten von älteren Versionen stammen.</string>
<string name="text_use_downloaded_dictionary">Heruntergeladenes Wörterbuch verwenden</string>
<string name="label_unknown_dictionary_d">Unbekanntes Wörterbuch (%1$s)</string>
<string name="text_dictionary_manager_description">Du kannst Wörterbücher für bestimmte Sprachen herunterladen, die du statt der KI-Erzeugung für den Wörterbuchinhalt verwenden kannst.</string>
@@ -822,7 +797,6 @@
<string name="pos_verb">Verb</string>
<string name="delete_all_dictionaries_title">Alle Wörterbücher löschen?</string>
<string name="delete_all_dictionaries_confirmation">Damit löschst du alle heruntergeladenen Wörterbücher von deinem Handy.</string>
<string name="text_orphaned_file_description">Diese Datei existiert lokal, ist aber nicht im Servermanifest oder hat fehlende Assets. Sie könnte aus einer älteren Version stammen oder ein fehlgeschlagener Download sein.</string>
<string name="label_unknown">Unbekannt</string>
<string name="label_interjection">Ausruf</string>
<string name="label_article">Artikel</string>
@@ -873,5 +847,21 @@
<string name="text_translation_instructions">Setze ein Modell für die Übersetzung und gib optionale Anweisungen, wie übersetzt werden soll.</string>
<string name="label_all_categories">Alle Kategorien</string>
<string name="text_description_dictionary_prompt">Setze ein Modell zum Generieren von Wörterbuchinhalten und gib optionale Anweisungen.</string>
<string name="hint_vocabulary_progress_hint_title">Vokabel-Fortschrittsverfolgung</string>
<string name="hint_title_hints_overview">Hilfe und Anleitungen</string>
<string name="hint_hints_overview_intro">Hilfe-Center</string>
<string name="hint_hints_overview_description">Alle Hinweise, die in dieser App enthalten sind, können auch hier gefunden werden.</string>
<string name="hint_hints_header_basics">Erste Schritte</string>
<string name="hint_hints_header_vocabulary">Wortschatzverwaltung</string>
<string name="label_analyze_grammar">Grammatik analysieren</string>
<string name="text_orphaned_file_description">Diese Datei existiert lokal, ist aber nicht im Server-Manifest oder es fehlen Assets. Sie könnte von einer älteren Version oder einem fehlgeschlagenen Download stammen.</string>
<string name="text_these_files_exist_locally">Diese Dateien existieren lokal, sind aber nicht im Server-Manifest. Sie könnten von älteren Versionen stammen.</string>
<string name="the_server_understood_the_request_but_is_refusing_to_authorize_it">Der Server hat die Anfrage verstanden, weigert sich aber, sie zu autorisieren.</string>
<string name="hint_hints_header_advanced">Erweiterte Funktionen</string>
<string name="category_hint_intro">Du kannst zwei Arten von Kategorien erstellen, um deinen Wortschatz zu organisieren:</string>
<string name="review_intro">Überprüfe das generierte Vokabular, bevor du es zu deiner Sammlung hinzufügst.</string>
<string name="duplicate">Duplikat</string>
<string name="hint_scan_hint_title">Das richtige AI-Modell finden</string>
<string name="hint_translate_how_it_works">Wie Übersetzung funktioniert</string>
</resources>

View File

@@ -1,129 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="hint_scan_hint_section_cant_find">Não consegue encontrar o seu modelo?</string>
<string name="hint_scan_hint_manual_add_paragraph">Você pode adicionar modelos manualmente. Insira o ID do modelo exato da documentação do seu provedor e um nome de exibição amigável. O aplicativo usará o ID para todas as chamadas de API.</string>
<string name="hint_scan_hint_chip_nano">nano</string>
<string name="hint_scan_hint_chip_mini">mini</string>
<string name="hint_scan_hint_chip_small">pequeno</string>
<string name="hint_scan_hint_chip_medium">médio</string>
<string name="hint_scan_hint_chip_large_paid">grande / pago</string>
<string name="hint_scan_hint_section_visual_guide">Da busca à seleção um guia visual rápido</string>
<string name="hint_scan_hint_step_1">1</string>
<string name="hint_scan_hint_step_2">2</string>
<string name="hint_scan_hint_step_3">3</string>
<string name="hint_scan_hint_step1_title">Inicie a busca</string>
<string name="hint_scan_hint_step1_desc">Toque no botão de busca para obter os modelos disponíveis do seu provedor.</string>
<string name="hint_scan_hint_step2_title">Filtre e escolha</string>
<string name="hint_scan_hint_step2_desc">Navegue pela lista. Prefira modelos marcados para texto/chat. Alguns provedores rotulam modelos gratuitos/pagos de forma diferente.</string>
<string name="hint_scan_hint_label_text_chat">Texto/Chat</string>
<string name="hint_scan_hint_step3_title">Validar</string>
<string name="hint_scan_hint_step3_desc">Use \"Adicionar e Validar\" para salvar o modelo e realizar uma verificação rápida com o seu provedor.</string>
<string name="hint_scan_hint_add_validate">Adicionar e Validar</string>
<string name="hint_translation_context_aware_title">Tradução Consciente do Contexto</string>
<string name="hint_translation_context_aware_desc">Obtenha traduções que entendem o contexto da sua conversa para resultados mais precisos.</string>
<string name="hint_vocabulary_progress_hint_title">Acompanhamento de Progresso do Vocabulário</string>
<string name="hint_vocabulary_progress_tracking_title">Acompanhamento de Progresso</string>
<string name="hint_vocabulary_progress_tracking_desc">Acompanhe seu progresso de aprendizado com estatísticas detalhadas e indicadores visuais.</string>
<string name="hint_vocabulary_learning_stages_title">Estágios de Aprendizagem</string>
<string name="hint_vocabulary_learning_stages_desc">As palavras passam por estágios à medida que você aprende, com intervalos crescentes entre as revisões.</string>
<string name="hint_vocabulary_review_system_title">Sistema de Revisão</string>
<string name="hint_vocabulary_review_system_desc">O sistema de repetição espaçada garante que você revise as palavras em intervalos ideais para retenção a longo prazo.</string>
<string name="hint_vocabulary_customization_title">Personalização</string>
<string name="hint_vocabulary_customization_desc">Personalize critérios de aprendizado, metas diárias e intervalos de revisão para combinar com seu estilo de aprendizado.</string>
<string name="hint_title_hints_overview">Ajuda e Instruções</string>
<string name="hint_hints_overview_intro">Central de Ajuda</string>
<string name="hint_hints_overview_description">Todas as dicas que estão neste aplicativo também podem ser encontradas aqui.</string>
<string name="hint_hints_header_basics">Começando</string>
<string name="hint_hints_header_vocabulary">Gerenciamento de Vocabulário</string>
<string name="hint_hints_header_advanced">Recursos Avançados</string>
<string name="hint_list_category">Uma \'Lista\' é uma categoria simples onde você pode adicionar manualmente qualquer item de vocabulário que desejar. É como uma pasta personalizada para suas palavras.</string>
<string name="api_hint_intro_1">Para usar todos os recursos, o aplicativo precisa se conectar a um serviço de Modelo de Linguagem Grande (LLM). Isso é feito através de um Provedor de API.</string>
<string name="api_hint_intro_2">Você pode adicionar sua Chave de API a um provedor pré-configurado (como OpenAI ou Google) ou adicionar um provedor personalizado para se conectar a um serviço diferente, como um modelo local. Qualquer provedor deve ser compatível com o padrão da API da OpenAI.</string>
<string name="key_status_indicators_title">Indicadores de Status da Chave</string>
<string name="key_status_explanation">Cada cartão de provedor mostra o status da sua chave de API:</string>
<string name="key_saved_and_active">Isso significa que sua chave está salva e ativa.</string>
<string name="key_missing_or_cleared">Isso significa que a chave de API está faltando ou foi removida.</string>
<string name="troubleshooting_title">Solução de Problemas</string>
<string name="troubleshooting_intro">Se você está tendo problemas, verifique o seguinte:</string>
<string name="troubleshooting_bullets">• Verifique se sua chave de API é válida e tem permissões.\n• Verifique sua conexão de rede.\n• Veja a aba de Logs de Rede para mensagens de erro detalhadas.</string>
<string name="category_hint_intro">Você pode criar dois tipos de categorias para organizar seu vocabulário:</string>
<string name="content_desc_tag_category">Categoria de Tag</string>
<string name="content_desc_filter_category">Categoria de Filtro</string>
<string name="hint_filter_category_description">O filtro pode corresponder itens por: nenhum filtro de idioma, uma lista de idiomas ou um par de dicionário. Você também pode opcionalmente filtrar por estágios de estudo. A lista de idiomas e o par de dicionário são mutuamente exclusivos.</string>
<string name="category_hint_item_preview_description">Crie uma tag manual para agrupar as palavras que você escolher.</string>
<string name="category_list_title">Categoria de Lista</string>
<string name="category_list_description">Adicione manualmente qualquer palavra que desejar a esta categoria. É perfeito para criar listas de estudo personalizadas para um tópico ou capítulo específico.</string>
<string name="example_word_apple">Apple</string>
<string name="action_add">Adicionar</string>
<string name="example_category_my_fruit_list">Minha Lista de Frutas</string>
<string name="category_filter_title">Categoria de Filtro</string>
<string name="category_filter_description">Esta categoria agrupa palavras automaticamente com base em regras que você define, como seu estágio de aprendizado ou idioma. É uma forma dinâmica e automática de organizar.</string>
<string name="example_word_dog">Dog</string>
<string name="example_word_cat">Cat</string>
<string name="example_filter_stage_1">Filtro \"Estágio 1\"</string>
<string name="hint_dict_options_step1_title">Passo 1: Configure a IA</string>
<string name="hint_dict_options_step2_title">Passo 2: Selecione o Conteúdo</string>
<string name="hint_dict_options_step2_desc">Em seguida, use os seletores para escolher quais seções específicas (como sinônimos, antônimos, etc.) devem ser incluídas em uma consulta de dicionário.</string>
<string name="eg_synonyms">ex., Sinônimos</string>
<string name="example_toggle">Exemplo de Seletor</string>
<string name="import_ai_intro">Deixe a IA encontrar vocabulário para você. Veja como usar este recurso:</string>
<string name="import_step1_title">1. Insira um termo de busca</string>
<string name="search_term_placeholder">Coisas para fazer no zoológico</string>
<string name="import_step2_title">2. Selecione seus idiomas</string>
<string name="import_step2_desc">Escolha o idioma do qual você quer aprender (origem) e o idioma que você quer aprender (destino).</string>
<string name="import_step3_title">3. Selecione a quantidade de palavras</string>
<string name="import_step3_desc">Use o controle deslizante para escolher quantas palavras você deseja gerar (até 25).</string>
<string name="import_after_generating">Após gerar, você poderá revisar as palavras antes de adicioná-las.</string>
<string name="example_word_der_apfel">der Apfel</string>
<string name="example_word_the_apple">the apple</string>
<string name="example_word_der_hund">der Hund</string>
<string name="example_word_the_dog">the dog</string>
<string name="review_intro">Revise o vocabulário gerado antes de adicioná-lo à sua coleção.</string>
<string name="review_select_items_title">Selecionar Itens</string>
<string name="review_select_items_desc">Use as caixas de seleção para selecionar as palavras que você deseja manter. Você também pode usar a caixa de seleção no topo para selecionar ou desmarcar todos os itens de uma vez.</string>
<string name="duplicate">Duplicado</string>
<string name="duplicate_handling_title">Tratamento de Duplicatas</string>
<string name="duplicate_handling_desc">O aplicativo detecta automaticamente se uma palavra já existe no seu vocabulário. Essas duplicatas são desmarcadas por padrão para evitar desordem.</string>
<string name="add_to_list_optional">Adicionar a uma Lista (Opcional)</string>
<string name="add_to_list_optional_desc">Você pode adicionar diretamente as palavras selecionadas a uma de suas listas de vocabulário existentes, escolhendo uma no menu suspenso na parte inferior.</string>
<string name="interval_1_day">1 Dia</string>
<string name="interval_3_days">3 Dias</string>
<string name="interval_1_week">1 Semana</string>
<string name="interval_2_weeks">2 Semanas</string>
<string name="interval_1_month">1 Mês</string>
<string name="hint_scan_hint_title">Encontrando o modelo de IA certo</string>
<string name="scan_hint_section_how_scan_works">Como a busca funciona</string>
<string name="scan_hint_how_scan_works_paragraph">Quando você toca em \"Procurar Modelos\", o aplicativo solicita ao seu provedor de API selecionado uma lista de modelos disponíveis. O provedor responde com os modelos que estão visíveis para você.</string>
<string name="scan_hint_bullet_results_depend">Os resultados dependem da sua conta, organização e configuração do provedor.</string>
<string name="scan_hint_bullet_public_private">Alguns provedores retornam apenas modelos públicos; modelos privados ou empresariais podem exigir permissões adicionais.</string>
<string name="scan_hint_bullet_try_again">Se você alterou permissões ou cotas recentemente, tente novamente após um curto período.</string>
<string name="scan_hint_section_why_missing">Por que alguns modelos podem não aparecer</string>
<string name="scan_hint_badge_restricted">Restrito ou não permitido para sua conta/organização</string>
<string name="scan_hint_badge_not_suitable">Não adequado para esta tarefa (por exemplo, apenas imagem, apenas áudio ou apenas embeddings)</string>
<string name="scan_hint_badge_only_text_models">Apenas modelos capazes de processar texto com conclusão de texto/chat são mostrados</string>
<string name="scan_hint_focus_text_models">O aplicativo foca em modelos que podem ler e escrever texto. Para tradução, dicionário e geração de vocabulário, o modelo deve suportar prompts de texto e retornar conclusões de texto (API de chat/completions).</string>
<string name="scan_hint_most_tasks_small_models">A maioria das tarefas funciona muito bem com modelos rápidos e pequenos (por exemplo, nano/mini/pequeno). Para gerar exercícios completos, um modelo maior ou pago pode ser necessário.</string>
<string name="scan_hint_section_tips">Dicas e Solução de Problemas</string>
<string name="scan_hint_tip_verify_key">Verifique se sua chave de API é válida e tem permissão para acessar os modelos desejados.</string>
<string name="scan_hint_tip_select_org">Alguns provedores exigem que uma organização/projeto seja selecionado. Certifique-se de que esteja configurado corretamente.</string>
<string name="scan_hint_tip_type_manually">Se um modelo que você sabe que existe não aparecer, tente digitá-lo manualmente usando o ID do modelo mostrado na documentação do provedor.</string>
<string name="scan_hint_tip_instruct_chat_text">Procure por modelos marcados como \'Instruct\', \'Chat\' ou \'Text\'. Geralmente, esses são os mais adequados.</string>
<string name="hint_translate_how_it_works">Como a tradução funciona</string>
<string name="hint_translate_alternative_translations_title">Traduções Alternativas</string>
<string name="hint_translate_alternative_translations_desc">Toque em qualquer palavra na tradução para ver significados alternativos e escolher o que melhor se encaixa.</string>
<string name="hint_translate_custom_prompts_title">Prompts de Tradução Personalizados</string>
<string name="hint_translate_custom_prompts_desc">Personalize como as traduções são geradas usando prompts de IA nas Configurações. Escolha entre prompts de exemplo ou crie os seus.</string>
<string name="hint_translate_multiple_services_title">Múltiplos Serviços de Tradução</string>
<string name="hint_translate_multiple_services_desc">Alterne entre a tradução com IA ou um serviço de tradução para diferentes opções de estilos de tradução.</string>
<string name="hint_translate_history_title">Histórico de Tradução</string>
<string name="hint_translate_history_desc">Acesse seu histórico de traduções para reutilizar traduções anteriores.</string>
<string name="hint_translate_tts_title">Texto para Fala</string>
<string name="hint_translate_tts_desc">Ouça as traduções com suporte a texto para fala. Configure vozes para diferentes idiomas nas Configurações.</string>
<string name="hint_translate_quick_actions_title">Ações Rápidas</string>
<string name="hint_translate_quick_actions_desc">Copie traduções para a área de transferência, compartilhe-as ou adicione palavras ao seu vocabulário com um toque.</string>
<string name="hint_translate_model_selection_title">Seleção de Modelo de IA</string>
<string name="hint_translate_model_selection_desc">Escolha entre diferentes modelos de IA para qualidade e velocidade de tradução.</string>
<string name="hint_dict_options_step1_desc">Primeiro, selecione o melhor modelo de IA para suas necessidades e opcionalmente escreva um prompt personalizado para guiar como ele gera conteúdo do dicionário.</string>
</resources>

View File

@@ -66,7 +66,6 @@
<string name="menu_create_youtube_exercise">Criar Exercício do YouTube</string>
<string name="text_youtube_link">Link do YouTube</string>
<string name="text_customize_the_intervals">Personalize os intervalos e critérios para mover os cartões de vocabulário. Cartões em estágios iniciais são perguntados com mais frequência.</string>
<string name="text_enter_your_custom_prompt">Insira seu prompt personalizado</string>
<string name="text_developed_by_jonas_gaudian">Desenvolvido por Jonas Gaudian</string>
<string name="text_visit_my_website">Visite meu site</string>
<string name="contact_developer_title">Contatar desenvolvedor</string>
@@ -128,7 +127,6 @@
<string name="text_enter_api_key">Inserir Chave de API</string>
<string name="text_save_key">Salvar Chave</string>
<string name="text_select_model">Selecionar Modelo</string>
<string name="text_example_prompts">Prompts de Exemplo</string>
<string name="title_title_preview_title">Título de Prévia</string>
<string name="text_none">Nenhum</string>
<string name="text_manual_vocabulary_list">Lista de vocabulário manual</string>
@@ -155,7 +153,6 @@
<string name="text_vocabulary_prompt">Prompt de Vocabulário</string>
<string name="text_here_you_can_set_a_custom_">Aqui você pode definir um prompt personalizado para definir como novos itens de vocabulário são gerados.</string>
<string name="text_select_the_content_dictionary">Selecione o conteúdo a ser gerado para uma entrada de dicionário.</string>
<string name="text_custom_dictionary_prompt">Prompt Personalizado do Dicionário</string>
<string name="text_save_prompt">Salvar Prompt</string>
<string name="text_light">Claro</string>
<string name="text_dark">Escuro</string>
@@ -198,7 +195,6 @@
<string name="text_generate">Gerar</string>
<string name="text_let_ai_find_vocabulary_for_you">Deixe a IA encontrar vocabulário para você!</string>
<string name="text_search_term">Termo de Busca</string>
<string name="text_hint_you_can_search">Dica: Você pode buscar qualquer termo, ex. \"Coisas para fazer no zoológico\" ou \"verbos irregulares\"!</string>
<string name="text_select_languages">Selecionar Idiomas</string>
<string name="text_select_amount">Selecionar Quantidade</string>
<string name="text_amount_2d">Quantidade: %1$d</string>
@@ -556,7 +552,6 @@
<string name="error_no_text_to_edit">Erro: Nenhum texto para editar</string>
<string name="not_launched_with_text_to_edit">Não iniciado com texto para editar</string>
<string name="text_a_simple_list_to">Uma lista simples para organizar o seu vocabulário manualmente</string>
<string name="connecting_your_ai_model">Conectando seu Modelo de IA</string>
<string name="settings_title_voice">Voz</string>
<string name="default_value">Padrão</string>
<string name="label_speaking_speed">Velocidade da Fala</string>
@@ -571,17 +566,11 @@
<string name="previous_month">Mês Anterior</string>
<string name="next_month">Próximo Mês</string>
<string name="show_api_key_missing_message">Mostrar Mensagem de Chave de API Ausente</string>
<string name="sorting_hint_intro_text">Nesta tela, você organiza o seu novo vocabulário. Pode corrigir a ortografia e as traduções, e atribuir itens a categorias.</string>
<string name="sorting_hint_helper_text">O app também ajuda a detetar duplicados ou remover artigos para listas de vocabulário mais limpas.</string>
<string name="sorting_hint_chip_duplicate">Duplicado</string>
<string name="sorting_hint_decide_next_action">Quando terminar, decida o que fazer com o item:</string>
<string name="label_move_first_stage">Mover para o Primeiro Estágio</string>
<string name="sorting_hint_title">Organização de Vocabulário</string>
<string name="text_optional">" (opcional)"</string>
<string name="text_check_availability">Verificar disponibilidade</string>
<string name="text_no_valid_api_configuration_could_be_found">Nenhuma configuração de API válida foi encontrada. Antes de usar o app, configure pelo menos um provedor de API.</string>
<string name="cd_tag_category">Categoria de Tag</string>
<string name="hint_this_screen_lets_you_customize_">Esta tela permite personalizar as instruções para gerar novas entradas de vocabulário, controlando quais informações incluir.</string>
<string name="text_try_wiktionary_first">Tentar Wikcionário Primeiro</string>
<string name="text_try_first_finding_the_word_on">Tente primeiro encontrar a palavra no Wikcionário antes de gerar uma resposta de IA</string>
<string name="text_question_of">Pergunta %1$d de %2$d</string>
@@ -602,13 +591,6 @@
<string name="intro_if_you_need_help_you">Se precisar de ajuda, você pode encontrar dicas em todas as seções do aplicativo.</string>
<string name="text_navigation_bar_labels">Rótulos da Barra de Navegação</string>
<string name="text_show_text_labels_on_the_main_navigation_bar">Mostrar rótulos de texto na barra de navegação principal.</string>
<string name="hint_how_it_works">Como Funciona</string>
<string name="hint_answer_correctly">Responda Corretamente</string>
<string name="hint_the_word_moves">A palavra avança para o próximo estágio, e você a verá novamente após um intervalo maior.</string>
<string name="hint_answer_incorrectly">Responda Incorretamente</string>
<string name="hint_the_word_moves_back_another_stage_this_helps_you_focus_on_">A palavra volta um estágio. Isso ajuda você a focar no vocabulário que acha difícil.</string>
<string name="hint_customizable">Personalizável</string>
<string name="hint_you_can_costumize_all_intervals_and_rules_in_the_settings">Você pode personalizar todos os intervalos e regras nas configurações.</string>
<string name="text_word_pair_settings">Configurações de Pares de Palavras</string>
<string name="text_amount_of_questions_2d">Quantidade de perguntas: %1$d</string>
<string name="text_shuffle_questions">Embaralhar perguntas</string>
@@ -616,8 +598,6 @@
<string name="text_match_the_pairs">Combine os pares</string>
<string name="text_word_pair_exercise">Exercício de Pares de Palavras</string>
<string name="text_training_mode_description">Modo de treino ativado: respostas não afetarão o progresso.</string>
<string name="cd_start_exercise">Iniciar Exercício</string>
<string name="hint_use_this_screen_to_define">"Use esta tela para definir uma instrução personalizada para o modelo de tradução de IA. Você pode especificar o tom, estilo ou formato da tradução."</string>
<string name="text_days">" dias"</string>
<string name="label_add_vocabulary">Adicionar Vocabulário</string>
<string name="label_create_vocabulary_with_ai">Criar Vocabulário com IA</string>
@@ -646,7 +626,6 @@
<string name="text_repeat_wrong">Repetir Erradas</string>
<string name="text_start_over">Começar de Novo</string>
<string name="label_dictionary_options">Opções do Dicionário</string>
<string name="hint_dictionary_desc">É assim que o dicionário funciona:</string>
<string name="text_paste_or_open_a_">Cole ou abra um link do YouTube para ver as legendas aqui.</string>
<string name="text_error_2d">Erro: %1$s</string>
<string name="text_repeat_wrong_guesses">Repetir Respostas Erradas</string>
@@ -660,7 +639,6 @@
<string name="label_noun">Substantivo</string>
<string name="label_action_correct">Corrigir</string>
<string name="text_daily_goal_description">Quantas palavras você quer acertar por dia?</string>
<string name="hint_example_hint_scan_for_models_hint">Dica de exemplo: Procure por modelos</string>
<string name="hint_how_to_connect_to_an_ai">Como se conectar a uma IA</string>
<string name="hint_how_to_generate_vocabulary_with_ai">Como gerar vocabulário com IA</string>
<string name="label_dictionary_manager">Gerenciador de dicionários</string>
@@ -871,5 +849,17 @@
<string name="text_language_direction_explanation">Você pode definir uma preferência opcional sobre qual idioma deve vir primeiro ou segundo.</string>
<string name="label_all_categories">Todas as Categorias</string>
<string name="text_description_dictionary_prompt">Defina um modelo para gerar conteúdo do dicionário e dê instruções opcionais.</string>
<string name="hint_vocabulary_progress_hint_title">Acompanhamento de Progresso de Vocabulário</string>
<string name="hint_title_hints_overview">Ajuda e Instruções</string>
<string name="hint_hints_overview_intro">Central de Ajuda</string>
<string name="hint_hints_overview_description">Todas as dicas que estão neste aplicativo também podem ser encontradas aqui.</string>
<string name="hint_hints_header_basics">Primeiros Passos</string>
<string name="hint_hints_header_vocabulary">Gerenciamento de Vocabulário</string>
<string name="hint_hints_header_advanced">Recursos Avançados</string>
<string name="category_hint_intro">Você pode criar dois tipos de categorias para organizar seu vocabulário:</string>
<string name="review_intro">Revise o vocabulário gerado antes de adicioná-lo à sua coleção.</string>
<string name="duplicate">Duplicado</string>
<string name="hint_scan_hint_title">Encontrando o modelo de IA certo</string>
<string name="hint_translate_how_it_works">Como funciona a tradução</string>
</resources>

View File

@@ -1,153 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="hint_scan_hint_section_cant_find">Cant find your model?</string>
<string name="hint_scan_hint_manual_add_paragraph">You can add models manually. Enter the exact Model ID from your providers documentation and a friendly Display Name. The app will use the ID for all API calls.</string>
<string name="hint_scan_hint_chip_nano">nano</string>
<string name="hint_scan_hint_chip_mini">mini</string>
<string name="hint_scan_hint_chip_small">small</string>
<string name="hint_scan_hint_chip_medium">medium</string>
<string name="hint_scan_hint_chip_large_paid">large / paid</string>
<string name="hint_scan_hint_section_visual_guide">From scan to selection quick visual guide</string>
<string name="hint_scan_hint_step_1">1</string>
<string name="hint_scan_hint_step_2">2</string>
<string name="hint_scan_hint_step_3">3</string>
<string name="hint_scan_hint_step1_title">Start the scan</string>
<string name="hint_scan_hint_step1_desc">Tap the scan button to fetch available models from your provider.</string>
<string name="hint_scan_hint_step2_title">Filter &amp; choose</string>
<string name="hint_scan_hint_step2_desc">Browse the list. Prefer models marked for text/chat. Some providers label free/paid models differently.</string>
<string name="hint_scan_hint_label_text_chat">Text/Chat</string>
<string name="hint_scan_hint_step3_title">Validate</string>
<string name="hint_scan_hint_step3_desc">Use Add &amp; Validate to save the model and perform a quick check with your provider.</string>
<string name="hint_scan_hint_add_validate">Add &amp; Validate</string>
<!-- Translation Screen Hint Strings -->
<!-- Translation Screen Hint Strings - Updated -->
<string name="hint_translation_context_aware_title">Context-Aware Translation</string>
<string name="hint_translation_context_aware_desc">Get translations that understand the context of your conversation for more accurate results.</string>
<!-- Vocabulary Progress Hint Strings -->
<string name="hint_vocabulary_progress_hint_title">Vocabulary Progress Tracking</string>
<string name="hint_vocabulary_progress_tracking_title">Progress Tracking</string>
<string name="hint_vocabulary_progress_tracking_desc">Track your learning progress with detailed statistics and visual indicators.</string>
<string name="hint_vocabulary_learning_stages_title">Learning Stages</string>
<string name="hint_vocabulary_learning_stages_desc">Words move through stages as you learn, with increasing intervals between reviews.</string>
<string name="hint_vocabulary_review_system_title">Review System</string>
<string name="hint_vocabulary_review_system_desc">The spaced repetition system ensures you review words at optimal intervals for long-term retention.</string>
<string name="hint_vocabulary_customization_title">Customization</string>
<string name="hint_vocabulary_customization_desc">Customize learning criteria, daily goals, and review intervals to match your learning style.</string>
<!-- Sorting Screen Hint Strings -->
<!-- API Key Hint Strings -->
<string name="hint_title_hints_overview">Help and Instructions</string>
<string name="hint_hints_overview_intro">Help Center</string>
<string name="hint_hints_overview_description">All hints that are in this app can be found here as well.</string>
<!-- Hint Categories -->
<string name="hint_hints_header_basics">Getting Started</string>
<string name="hint_hints_header_vocabulary">Vocabulary Management</string>
<string name="hint_hints_header_advanced">Advanced Features</string>
<string name="hint_list_category">A \'List\' is a simple category where you can manually add any vocabulary item you want. It\'s like a custom folder for your words.</string>
<string name="api_hint_intro_1">To use all features, the app needs to connect to a Large Language Model (LLM) service. This is done through an API Provider.</string>
<string name="api_hint_intro_2">You can add your API Key to a pre-configured provider (like OpenAI or Google) or add a custom provider to connect to a different service, like a local model. Any provider has to be compatible with the OpenAI API standard.</string>
<string name="key_status_indicators_title">Key Status Indicators</string>
<string name="key_status_explanation">Each provider card shows the status of your API key:</string>
<string name="key_saved_and_active">This means your key is saved and active.</string>
<string name="key_missing_or_cleared">This means the API key is missing or has been cleared.</string>
<string name="troubleshooting_title">Troubleshooting</string>
<string name="troubleshooting_intro">If you\'re having issues, please check the following:</string>
<string name="troubleshooting_bullets">• Ensure your API key is valid and has permissions.\n• Check your network connection.\n• View the Network Logs tab for detailed error messages.</string>
<string name="category_hint_intro">You can create two types of categories to organize your vocabulary:</string>
<string name="content_desc_tag_category">Tag Category</string>
<string name="content_desc_filter_category">Filter Category</string>
<string name="hint_filter_category_description">Filter can match items by: no language filter, a list of languages, or a dictionary pair. You can also optionally filter by study stages. Language list and dictionary pair are mutually exclusive.</string>
<string name="category_hint_item_preview_description">Create a manual tag to group words you choose.</string>
<string name="category_list_title">List Category</string>
<string name="category_list_description">Manually add any word you want to this category. It\'s perfect for creating custom study lists for a specific topic or chapter.</string>
<string name="example_word_apple">Apple</string>
<string name="action_add">Add</string>
<string name="example_category_my_fruit_list">My Fruit List</string>
<string name="category_filter_title">Filter Category</string>
<string name="category_filter_description">This category automatically groups words based on rules you set, like their learning stage or language. It\'s a dynamic, hands-free way to organize.</string>
<string name="example_word_dog">Dog</string>
<string name="example_word_cat">Cat</string>
<string name="example_filter_stage_1">"Stage 1" Filter</string>
<string name="hint_dict_options_step1_title">Step 1: Configure the AI</string>
<string name="hint_dict_options_step1_desc">First, select the best AI model for your needs and optionally write a custom prompt to guide how it generates dictionary content.</string>
<string name="hint_dict_options_step2_title">Step 2: Select Content</string>
<string name="hint_dict_options_step2_desc">Next, use the toggles to choose which specific sections (like synonyms, antonyms, etc.) should be included in a dictionary lookup.</string>
<string name="eg_synonyms">e.g., Synonyms</string>
<string name="example_toggle">Example Toggle</string>
<string name="import_ai_intro">Let AI find vocabulary for you. Here\s how to use this feature:</string>
<string name="import_step1_title">1. Enter a search term</string>
<string name="search_term_placeholder">Things to do at the zoo</string>
<string name="import_step2_title">2. Select your languages</string>
<string name="import_step2_desc">Choose the language you want to learn from (source) and the language you want to learn (target).</string>
<string name="import_step3_title">3. Select the amount of words</string>
<string name="import_step3_desc">Use the slider to choose how many words you want to generate (up to 25).</string>
<string name="import_after_generating">After generating, you will be able to review the words before adding them.</string>
<string name="example_word_der_apfel">der Apfel</string>
<string name="example_word_the_apple">the apple</string>
<string name="example_word_der_hund">der Hund</string>
<string name="example_word_the_dog">the dog</string>
<string name="review_intro">Review the generated vocabulary before adding it to your collection.</string>
<string name="review_select_items_title">Select Items</string>
<string name="review_select_items_desc">Use the checkboxes to select the words you want to keep. You can also use the checkbox at the top to select or deselect all items at once.</string>
<string name="duplicate">Duplicate</string>
<string name="duplicate_handling_title">Duplicate Handling</string>
<string name="duplicate_handling_desc">The app automatically detects if a word already exists in your vocabulary. These duplicates are unselected by default to avoid clutter.</string>
<string name="add_to_list_optional">Add to a List (Optional)</string>
<string name="add_to_list_optional_desc">You can directly add the selected words to one of your existing vocabulary lists by choosing one from the dropdown menu at the bottom.</string>
<string name="interval_1_day">1 Day</string>
<string name="interval_3_days">3 Days</string>
<string name="interval_1_week">1 Week</string>
<string name="interval_2_weeks">2 Weeks</string>
<string name="interval_1_month">1 Month</string>
<string name="hint_scan_hint_title">Finding the right AI model</string>
<string name="scan_hint_section_how_scan_works">How Scan works</string>
<string name="scan_hint_how_scan_works_paragraph">When you tap Scan for Models, the app asks your selected API provider for a list of available models. The provider responds with the models that are visible to you.</string>
<string name="scan_hint_bullet_results_depend">Results depend on your account, organization, and provider configuration.</string>
<string name="scan_hint_bullet_public_private">Some providers only return public models; private or enterprise models may require additional permissions.</string>
<string name="scan_hint_bullet_try_again">If you recently changed permissions or quotas, try again after a short delay.</string>
<string name="scan_hint_section_why_missing">Why some models may not appear</string>
<string name="scan_hint_badge_restricted">Restricted or not allowed for your account/organization</string>
<string name="scan_hint_badge_not_suitable">Not suitable for this task (e.g., image-only, audio-only, or embeddings-only)</string>
<string name="scan_hint_badge_only_text_models">Only text-capable models with text completion/chat are shown</string>
<string name="scan_hint_focus_text_models">The app focuses on models that can read and write text. For translation, dictionary and vocabulary generation, the model must support text prompts and return text completions (chat/completions API).</string>
<string name="scan_hint_most_tasks_small_models">Most tasks work great with fast, small models (e.g., nano/mini/small). For generating full exercises, a larger or paid model may be required.</string>
<string name="scan_hint_section_tips">Tips &amp; Troubleshooting</string>
<string name="scan_hint_tip_verify_key">Verify that your API key is valid and has permission to access the desired models.</string>
<string name="scan_hint_tip_select_org">Some providers require an organization/project to be selected. Make sure its correctly configured.</string>
<string name="scan_hint_tip_type_manually">If a model you know exists doesnt show up, try typing it manually using the Model ID shown in the providers docs.</string>
<string name="scan_hint_tip_instruct_chat_text">Look for models tagged as Instruct, Chat, or Text. Those are typically the best fit.</string>
<!-- Translation Screen Hint Strings - New -->
<string name="hint_translate_how_it_works">How translation works</string>
<string name="hint_translate_alternative_translations_title">Alternative Translations</string>
<string name="hint_translate_alternative_translations_desc">Tap any word in the translation to see alternative meanings and choose the best fit.</string>
<string name="hint_translate_custom_prompts_title">Custom Translation Prompts</string>
<string name="hint_translate_custom_prompts_desc">Customize how translations are generated using AI prompts in Settings. Choose from example prompts or create your own.</string>
<string name="hint_translate_multiple_services_title">Multiple Translation Services</string>
<string name="hint_translate_multiple_services_desc">Switch between AI-powered translation or a translation service for different translation styles options.</string>
<string name="hint_translate_history_title">Translation History</string>
<string name="hint_translate_history_desc">Access your translation history to reuse previous translations.</string>
<string name="hint_translate_tts_title">Text-to-Speech</string>
<string name="hint_translate_tts_desc">Listen to translations with text-to-speech support. Configure voices for different languages in Settings.</string>
<string name="hint_translate_quick_actions_title">Quick Actions</string>
<string name="hint_translate_quick_actions_desc">Copy translations to clipboard, share them, or add words to your vocabulary with one tap.</string>
<string name="hint_translate_model_selection_title">AI Model Selection</string>
<string name="hint_translate_model_selection_desc">Choose from different AI models for translation quality and speed.</string>
</resources>

View File

@@ -12,10 +12,8 @@
<string name="cd_paste">Paste</string>
<string name="cd_re_generate_definition">Re-generate Definition</string>
<string name="cd_search">Search</string>
<string name="cd_start_exercise">Start Exercise</string>
<string name="cd_success">Success</string>
<string name="cd_switch_languages">Switch Languages</string>
<string name="cd_tag_category">Tag Category</string>
<string name="cd_target_met">Target Met</string>
<string name="cd_text_to_speech">Text to Speech</string>
<string name="cd_toggle_menu">Toggle Menu</string>
@@ -30,8 +28,6 @@
<string name="label_colloquial">Colloquial</string>
<string name="connecting_your_ai_model">Connecting Your AI Model</string>
<string name="contact_developer_description">Contact me for bug reports, ideas, feature requests, and more.</string>
<string name="contact_developer_title">Contact developer</string>
@@ -176,19 +172,8 @@
<string name="hide_context">Hide</string>
<string name="hint">Hint: %1$s</string>
<string name="hint_answer_correctly">Answer Correctly</string>
<string name="hint_answer_incorrectly">Answer Incorrectly</string>
<string name="hint_customizable">Customizable</string>
<string name="hint_dictionary_desc">This is how the dictionary works:</string>
<string name="hint_example_hint_scan_for_models_hint">Example Hint Scan for Models Hint</string>
<string name="hint_how_it_works">How It Works</string>
<string name="hint_how_to_connect_to_an_ai">How to connect to an AI</string>
<string name="hint_how_to_generate_vocabulary_with_ai">How to generate Vocabulary with AI</string>
<string name="hint_the_word_moves">The word moves to the next stage, and you\'ll see it again after a longer break.</string>
<string name="hint_the_word_moves_back_another_stage_this_helps_you_focus_on_">The word moves back another stage. This helps you focus on vocabulary you find difficult.</string>
<string name="hint_this_screen_lets_you_customize_">This screen lets you customize the instructions for generating new vocabulary entries. You can control what information is included, like definitions, example sentences, or phonetic transcriptions.</string>
<string name="hint_use_this_screen_to_define">"Use this screen to define a custom instruction for the AI translation model. You can specify the tone, style, or format of the translation. "</string>
<string name="hint_you_can_costumize_all_intervals_and_rules_in_the_settings">You can costumize all intervals and rules in the settings.</string>
<string name="imperative">Imperative</string>
@@ -649,10 +634,6 @@
<string name="sort_by_size">Sort by Size</string>
<string name="sort_new_vocabulary">Sort New Vocabulary</string>
<string name="sorting_hint_chip_duplicate">Duplicate</string>
<string name="sorting_hint_decide_next_action">When you\'re done, decide what to do with the item:</string>
<string name="sorting_hint_helper_text">The app also helps you detect duplicates or remove articles for cleaner vocabulary lists.</string>
<string name="sorting_hint_intro_text">On this screen, you sort your new vocabulary. You can correct spelling and translations, and assign items to categories.</string>
<string name="sorting_hint_title">Vocabulary Sorting</string>
<string name="label_speaking_speed">Speaking Speed</string>
@@ -758,7 +739,6 @@
<string name="text_copy_corrected_text">Copy corrected text</string>
<string name="text_correct_em">Correct!</string>
<string name="text_could_not_fetch_a_new_word">Could not fetch a new word.</string>
<string name="text_custom_dictionary_prompt">Custom Dictionary Prompt</string>
<string name="text_custom_exercise">Custom Exercise</string>
<string name="text_customize_the_intervals">Customize the intervals and criteria for moving vocabulary cards between stages. Cards in lower stages should be asked more often than those in higher stages.</string>
<string name="text_daily_exercise">Daily Exercise</string>
@@ -799,7 +779,6 @@
<string name="text_enter_model_details_yourself">Enter model details yourself</string>
<string name="text_enter_text_to_correct">Enter text to correct</string>
<string name="text_enter_text_to_translate">Enter text to translate</string>
<string name="text_enter_your_custom_prompt">Enter your custom prompt</string>
<string name="text_error_2d">Error: %1$s</string>
<string name="text_error_deleting_dictionaries">Error deleting dictionaries: %1$s</string>
<string name="text_error_deleting_dictionary">Error deleting dictionary: %1$s</string>
@@ -808,7 +787,6 @@
<string name="text_error_generating_questions">Error generating questions: %1$s</string>
<string name="text_error_loading_stored_values">Error loading stored values: %1$s</string>
<string name="text_error_saving_entry">Error saving entry: %1$s</string>
<string name="text_example_prompts">Example Prompts</string>
<string name="text_excel_not_supported_use_csv">Excel is not supported. Use CSV instead.</string>
<string name="text_expand_widget">Expand Widget</string>
<string name="text_explanation">Explanation</string>
@@ -835,7 +813,6 @@
<string name="text_translation_instructions">Set model for translation and give optional instructions on how to translate.</string>
<string name="text_here_you_can_set_a_custom_">Here you can set a custom prompt for the AI vocabulary model. This allows you to define how new vocabulary entries are generated.</string>
<string name="text_hint">Hint</string>
<string name="text_hint_you_can_search">Hint: You can search for any term, e.g. \"Things to do at the zoo\" or \"irregular verbs\"!</string>
<string name="text_in_progress">In Progress</string>
<string name="text_incorrect_em">Incorrect!</string>
<string name="text_infrequent">Rare</string>
@@ -1049,4 +1026,16 @@
<string name="label_read_aloud">Read Aloud</string>
<string name="label_all_categories">All Categories</string>
<string name="text_description_dictionary_prompt">Set a model for generating dictionary content and give optional instructions.</string>
<string name="hint_vocabulary_progress_hint_title">Vocabulary Progress Tracking</string>
<string name="hint_title_hints_overview">Help and Instructions</string>
<string name="hint_hints_overview_intro">Help Center</string>
<string name="hint_hints_overview_description">All hints that are in this app can be found here as well.</string>
<string name="hint_hints_header_basics">Getting Started</string>
<string name="hint_hints_header_vocabulary">Vocabulary Management</string>
<string name="hint_hints_header_advanced">Advanced Features</string>
<string name="category_hint_intro">You can create two types of categories to organize your vocabulary:</string>
<string name="review_intro">Review the generated vocabulary before adding it to your collection.</string>
<string name="duplicate">Duplicate</string>
<string name="hint_scan_hint_title">Finding the right AI model</string>
<string name="hint_translate_how_it_works">How translation works</string>
</resources>

View File

@@ -41,6 +41,8 @@ room = "2.8.4"
coreKtxVersion = "1.7.0"
truth = "1.4.5"
zstdJni = "1.5.7-7"
composeMarkdown = "0.5.8"
jitpack = "1.0.10"
[libraries]
@@ -100,6 +102,7 @@ hilt-android = { module = "com.google.dagger:hilt-android", version = "2.59.1" }
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version = "2.59.1" }
hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.3.0" }
mockk = { module = "io.mockk:mockk", version = "1.14.9" }
compose-markdown = { module = "com.github.jeziellago:compose-markdown", version.ref = "composeMarkdown" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }

View File

@@ -14,15 +14,13 @@ pluginManagement {
}
}
dependencyResolutionManagement {
@Suppress("UnstableApiUsage")
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
@Suppress("UnstableApiUsage")
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}
rootProject.name = "Translator"
include(":app")