Compare commits
21 Commits
b95a2de747
...
glassmorph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d0bf4cb1c | ||
|
|
2b8b9a84a3 | ||
|
|
59f5f5e668 | ||
|
|
15f7eae068 | ||
|
|
8e610259ca | ||
|
|
7d18f8eb04 | ||
|
|
f4fcffe90a | ||
|
|
5e920c43b3 | ||
|
|
61a97a1119 | ||
|
|
2e0fe76fbf | ||
|
|
a715ab78e9 | ||
|
|
fa3524268a | ||
|
|
77b86208c3 | ||
|
|
03e9aeedae | ||
|
|
05a1b2b71a | ||
|
|
18474b072e | ||
|
|
858c73fd0d | ||
|
|
b8baf0cd84 | ||
|
|
d2e77083ad | ||
|
|
306d0c7432 | ||
|
|
f829174bcb |
2
.idea/deploymentTargetSelector.xml
generated
2
.idea/deploymentTargetSelector.xml
generated
@@ -4,7 +4,7 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2026-02-06T10:01:23.649270100Z">
|
||||
<DropdownSelection timestamp="2026-02-15T19:51:37.987601800Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\jonas\.android\avd\Medium_Phone_28.avd" />
|
||||
|
||||
@@ -22,8 +22,8 @@ android {
|
||||
applicationId = "eu.gaudian.translator"
|
||||
minSdk = 28
|
||||
targetSdk = 36
|
||||
versionCode = 22
|
||||
versionName = "0.4.1"
|
||||
versionCode = 23
|
||||
versionName = "0.5.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -130,6 +130,7 @@ dependencies {
|
||||
implementation(libs.androidx.room.runtime) // ADDED: Explicitly add runtime
|
||||
implementation(libs.androidx.room.ktx)
|
||||
implementation(libs.core.ktx)
|
||||
implementation(libs.androidx.compose.foundation.layout)
|
||||
ksp(libs.room.compiler) // CHANGED: Use ksp instead of implementation
|
||||
|
||||
// Networking
|
||||
@@ -170,6 +171,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")
|
||||
|
||||
70
app/src/main/assets/hints/api_key_hint.md
Normal file
70
app/src/main/assets/hints/api_key_hint.md
Normal file
@@ -0,0 +1,70 @@
|
||||
## What is an API Key?
|
||||
|
||||
An API key is like a password that lets your app talk to AI services. You need one to use an AI provider like OpenAI (ChatGPT), Anthropic, Mistral, or DeepSeek.
|
||||
|
||||
## Getting an API Key
|
||||
|
||||
Some providers offer a limited free use of their API, which should be sufficient for most functions of this app; however, it is recommended to use a faster and better-paid service.
|
||||
|
||||
### For Cloud Providers
|
||||
|
||||
1. Create an account on the provider's website
|
||||
2. Choose a plan or billing option or free tier if available
|
||||
3. Create a new key and copy it
|
||||
4. Paste it into this app
|
||||
|
||||
### For Local AI Servers
|
||||
|
||||
Running a local AI server (like Ollama or LM Studio), you don't need an API key. Just add a custom provider:
|
||||
|
||||
1. Tap **"Add Custom Provider"**
|
||||
2. Enter your local server IP and endpoint
|
||||
3. Tap **"Check Availability"** to test the connection
|
||||
|
||||
## Choosing a Model
|
||||
|
||||
### What are Models?
|
||||
|
||||
A model is a specific AI brain. Different models have different strengths:
|
||||
- **Smaller models**: Faster and cheaper
|
||||
- **Larger models**: Smarter but slower and more expensive
|
||||
|
||||
For pre-configured providers, some models are already added by default and proven to work with this app.
|
||||
|
||||
### Adding Models
|
||||
|
||||
1. Open a provider's details
|
||||
2. Tap **Add Model**
|
||||
3. Choose **Scan for Models** to find available ones automatically
|
||||
4. Select the models you want to use
|
||||
|
||||
### Assigning Models to Tasks
|
||||
|
||||
You can use different models for different features:
|
||||
|
||||
1. Go to the **Tasks** tab
|
||||
2. Select which model to use for:
|
||||
- **Translation**: Translates text between languages
|
||||
- **Exercises**: Creates practice exercises
|
||||
- **Vocabulary**: Generates vocabulary and synonyms
|
||||
- **Dictionary**: Looks up definitions
|
||||
|
||||
## Common Problems
|
||||
|
||||
### "Invalid API Key"
|
||||
- Check for typos or extra spaces
|
||||
- Make sure your key is still active and valid on the provider's website
|
||||
|
||||
|
||||
### "No Models Available"
|
||||
- Make sure your API key is valid first
|
||||
- If on a local network, make sure that your connection and endpoint is configured correctly
|
||||
|
||||
### Slow Responses
|
||||
- Try a faster provider, you might need to choose a paid option
|
||||
- Use a smaller model (look for names with "small" "light" "fast" "nano")
|
||||
|
||||
### Local Server Not Working
|
||||
- Make sure your local server is running
|
||||
- Check that the URL is correct
|
||||
- Your phone and computer might need to be on the same WiFi for local servers.
|
||||
40
app/src/main/assets/hints/category_hint.md
Normal file
40
app/src/main/assets/hints/category_hint.md
Normal file
@@ -0,0 +1,40 @@
|
||||
## What Are Categories?
|
||||
|
||||
Categories help you organize your vocabulary into meaningful groups. You can use them to track words by topic, language, stage, or any custom system that works for you.
|
||||
|
||||
## Two Types of Categories
|
||||
|
||||
### List Categories
|
||||
|
||||
List categories are simple groupings of vocabulary items. You can simply just add words to a list and they stay there forever.
|
||||
|
||||
**Use cases:**
|
||||
- Group words by topic (e.g., "Food", "Travel", "Business")
|
||||
- Create custom decks for specific purposes
|
||||
|
||||
### Filter Categories
|
||||
|
||||
Filter categories automatically include all vocabulary items that match certain criteria. Words are dynamically added or removed based on the filter rules.
|
||||
|
||||
**Use cases:**
|
||||
- Filter by learning stage (e.g., "Words I'm learning")
|
||||
- Filter by language
|
||||
- Combine multiple criteria for complex filtering (e.g., "Words in Spanish that I know already")
|
||||
|
||||
## 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** - Enter a category to modify its settings
|
||||
|
||||
## Tips
|
||||
|
||||
- Use filter categories for learning stages to automatically track progress across all words at a certain level.
|
||||
- The same vocabulary card can appear in several categories
|
||||
- You can also use categories to manage large groups of vocabulary items at once by using the "selct all" feature inside a category
|
||||
50
app/src/main/assets/hints/dictionary_hint.md
Normal file
50
app/src/main/assets/hints/dictionary_hint.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Dictionary Options
|
||||
# TODO REWRITE
|
||||
|
||||
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!*
|
||||
81
app/src/main/assets/hints/exercise_hint.md
Normal file
81
app/src/main/assets/hints/exercise_hint.md
Normal file
@@ -0,0 +1,81 @@
|
||||
## What Are Exercises?
|
||||
|
||||
Exercises help you practice and memorize your vocabulary through interactive activities. You can practice with flashcards, test your spelling, or challenge yourself with word puzzles.
|
||||
|
||||
## Exercise Types
|
||||
|
||||
### Flashcard Mode
|
||||
|
||||
A simple card shows one word, and you guess the translation. Tap to flip and reveal the answer, then mark it as correct or wrong.
|
||||
|
||||
### Spelling Practice
|
||||
|
||||
A word appears and you must type the translation yourself. This is great for memorizing spelling and reinforcing what you've learned.
|
||||
|
||||
### Multiple Choice
|
||||
|
||||
Four answer options are shown and you pick the correct translation. Good for quick recognition practice.
|
||||
|
||||
### Word Jumble
|
||||
|
||||
Letters are scrambled and you must click them in the correct order to spell the word. Excellent for mastering spelling.
|
||||
|
||||
## Starting an Exercise
|
||||
|
||||
1. **From the Dashboard** - Tap the Start Exercise button
|
||||
2. **From a Category** - Open any category and tap Start
|
||||
3. **From the Main Screen** - Tap the floating action button
|
||||
|
||||
## Before You Start
|
||||
|
||||
You can customize your exercise:
|
||||
|
||||
- **How many cards** - Use the slider to pick 1 to 100+ cards
|
||||
- **Quick select** - Tap 10, 25, 50, or 100 for fast selection
|
||||
- **Language direction** - Choose which language to translate from and to
|
||||
- **Exercise types** - Pick one type or mix several together
|
||||
- **Shuffle cards** - Randomize the order
|
||||
- **Shuffle languages** - Randomize which side (word/translation) is shown
|
||||
- **Training mode** - Practice without affecting your progress
|
||||
- **Due today only** - Only practice items scheduled for today
|
||||
|
||||
## Training Mode
|
||||
|
||||
When training mode is ON:
|
||||
- Your answers won't affect your progress stages
|
||||
- No statistics are recorded
|
||||
- Perfect for casual practice or testing yourself
|
||||
|
||||
When training mode is OFF (default):
|
||||
- Correct answers may advance items to higher stages
|
||||
- Progress is saved and tracked
|
||||
- Affects your learning statistics
|
||||
|
||||
## During an Exercise
|
||||
|
||||
The progress bar at the top shows:
|
||||
- **Green** - How many you've gotten right
|
||||
- **Red** - How many you've gotten wrong
|
||||
- **Total** - How many cards in this session
|
||||
|
||||
Tap the close button anytime to exit (you'll be asked to confirm).
|
||||
|
||||
## After Finishing
|
||||
|
||||
Your results screen shows:
|
||||
- Your overall score as a percentage
|
||||
- How many you got right and wrong
|
||||
- Total cards practiced
|
||||
|
||||
You can then:
|
||||
- **Start Over** - Begin a fresh exercise
|
||||
- **Repeat Wrong** - Practice only the cards you missed
|
||||
- **Finish** - Return to the main screen
|
||||
|
||||
## Tips
|
||||
|
||||
- **Mix it up** - Combine different exercise types for variety
|
||||
- **Daily practice** - Use "Due Today Only" for regular reviews
|
||||
- **Focus on mistakes** - Use "Repeat Wrong" to learn from errors
|
||||
- **Start small** - Begin with 10-25 cards and increase over time
|
||||
- **Use training mode** - When you want to practice without pressure
|
||||
34
app/src/main/assets/hints/find_ai_model.md
Normal file
34
app/src/main/assets/hints/find_ai_model.md
Normal file
@@ -0,0 +1,34 @@
|
||||
The scan feature searches for available AI models for your configured provider
|
||||
|
||||
> **Note:** Results depend on your API key permissions. The provider must support the OpenAI API format.
|
||||
|
||||
### Key Points
|
||||
|
||||
- Only public models are shown by default
|
||||
- Try again if no models are found
|
||||
|
||||
### Model Tiers
|
||||
|
||||
Not all models are suitable for every task:
|
||||
|
||||
- **Nano** - Fastest, good for simple tasks like translations
|
||||
- **Mini** - Balanced speed and capability
|
||||
- **Small** - Good for most tasks
|
||||
- **Medium** - More capable, recommended for execise and vocabulary generation
|
||||
- **Large** - Most capable, mostly paid, best results
|
||||
|
||||
## Tips for Success
|
||||
|
||||
1. **Verify your API key** is active and has correct permissions
|
||||
2. Choose a capable model that supports text generation
|
||||
3. For local providers, make sure your connection and endponts are set up 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
|
||||
|
||||
> Check the logs in case of validation error
|
||||
46
app/src/main/assets/hints/import_hint.md
Normal file
46
app/src/main/assets/hints/import_hint.md
Normal file
@@ -0,0 +1,46 @@
|
||||
Generate vocabulary lists automatically using AI assistance.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Use AI to quickly create vocabulary lists for a certain topic.
|
||||
|
||||
|
||||
### 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: "Things to do in Paris"
|
||||
- Example: "Difficult verbs that are confusing"
|
||||
|
||||
### Step 2: Select Languages
|
||||
|
||||
Choose source and target languages:
|
||||
- **Source language** - The first language of the flashcard
|
||||
- **Target language** - The second language of the flashcard
|
||||
|
||||
### 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
|
||||
|
||||
## After Generation
|
||||
|
||||
Once generated, you can:
|
||||
|
||||
- Choose which terms to keep
|
||||
- Optionally, add it to a category
|
||||
|
||||
## Tips
|
||||
|
||||
- In the settings, you can give additional instructions to the AI, like "Use only nouns" or "European Portuguese orthography"
|
||||
- Start with a small number of items to see how many words your AI can generate.
|
||||
- Check the logs in the settings in case of failure
|
||||
- Try out different providers and AI models as results can vary greatly
|
||||
44
app/src/main/assets/hints/learning_stages_hint.md
Normal file
44
app/src/main/assets/hints/learning_stages_hint.md
Normal file
@@ -0,0 +1,44 @@
|
||||
## The Learning Stages
|
||||
|
||||
Your vocabulary items move through these stages as you learn. In "daily" exercises you get presented with vocabulary items according to their interval.
|
||||
|
||||
| Stage | Interval | Description |
|
||||
|------|----------|-------------|
|
||||
| New | 1 day | Just added vocabulary |
|
||||
| Stage 1 | 3 days | Recently learned |
|
||||
| Stage 2 | 1 week | Reinforcement |
|
||||
| Stage 3 | 2 weeks | Consolidation |
|
||||
| Stage 4 | 1 month | Deep learning |
|
||||
| Stage 5 | 2 month | Mastery |
|
||||
| Learned | 3 months | Fully learned |
|
||||
|
||||
## How It Works
|
||||
|
||||
### Answer Correctly
|
||||
|
||||
When you correctly identify a word during an exercise:
|
||||
- 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 stage
|
||||
- The review interval **decreases**
|
||||
- This ensures you practice challenging words more often
|
||||
|
||||
## Customization
|
||||
|
||||
All intervals and rules can be customized:
|
||||
|
||||
- **Adjust intervals** for each stage
|
||||
- **Change how many attempts** it takes to move up a stage or get demoted
|
||||
- **Skip stages** You can also manually move items to a stage
|
||||
|
||||
## Visual Progress
|
||||
|
||||
In the dashboard, the app displays your progress visually:
|
||||
- Stage indicators show current status
|
||||
- Progress bars track advancement
|
||||
- Statistics display overall mastery
|
||||
64
app/src/main/assets/hints/review_hint.md
Normal file
64
app/src/main/assets/hints/review_hint.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Review Vocabulary
|
||||
# TODO REWRITE
|
||||
|
||||
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!*
|
||||
48
app/src/main/assets/hints/sorting_hint.md
Normal file
48
app/src/main/assets/hints/sorting_hint.md
Normal file
@@ -0,0 +1,48 @@
|
||||
After you imported vocabulary, you can sort vocabulary
|
||||
|
||||
- Review each word-translation pair
|
||||
- Decide the next action for each item
|
||||
- Handle duplicates and conflicts
|
||||
|
||||
## Actions
|
||||
|
||||
### Mark as Learned
|
||||
|
||||
If you already know the word, move the word directly to Stage "Learned". This prevents the word from reappearing in your exercises.
|
||||
|
||||
### 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, you can choose how to handle them:
|
||||
|
||||
**Options for duplicates:**
|
||||
- Keep only the original
|
||||
- Keep the newer entry
|
||||
- Keep both (merge): the newer entry will get deleted but all its information (categories) will be added the old item.
|
||||
- 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
|
||||
|
||||
## Tips
|
||||
|
||||
You can edit your flashcards at any point in the flashcard itself
|
||||
|
||||
68
app/src/main/assets/hints/translation_hint.md
Normal file
68
app/src/main/assets/hints/translation_hint.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Translation Features
|
||||
# TODO REWRITE
|
||||
|
||||
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!*
|
||||
7
app/src/main/assets/hints/vocabulary_progress_hint.md
Normal file
7
app/src/main/assets/hints/vocabulary_progress_hint.md
Normal file
@@ -0,0 +1,7 @@
|
||||
Monitor your vocabulary learning journey with detailed progress statistics.
|
||||
|
||||
## Progress Overview
|
||||
|
||||
Track your learning with these key metrics:
|
||||
|
||||
TODO Rewrite
|
||||
@@ -21,10 +21,10 @@
|
||||
"description": "Next-gen efficient architecture; outperforms older 70B models."
|
||||
},
|
||||
{
|
||||
"modelId": "deepseek-ai/DeepSeek-V3",
|
||||
"displayName": "DeepSeek V3",
|
||||
"modelId": "deepseek-ai/DeepSeek-V3.1",
|
||||
"displayName": "DeepSeek V3.1",
|
||||
"provider": "together",
|
||||
"description": "Top-tier open-source model specializing in code and logic."
|
||||
"description": "Latest 671B MoE model with hybrid thinking/non-thinking modes."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -37,10 +37,10 @@
|
||||
"isCustom": false,
|
||||
"models": [
|
||||
{
|
||||
"modelId": "ministral-8b-latest",
|
||||
"displayName": "Ministral 8B",
|
||||
"modelId": "mistral-medium-latest",
|
||||
"displayName": "Mistral Medium",
|
||||
"provider": "mistral",
|
||||
"description": "Extremely efficient edge model for low-latency tasks."
|
||||
"description": "Balanced performance and cost for a wide range of tasks."
|
||||
},
|
||||
{
|
||||
"modelId": "mistral-large-latest",
|
||||
@@ -58,17 +58,17 @@
|
||||
"websiteUrl": "https://platform.openai.com/",
|
||||
"isCustom": false,
|
||||
"models": [
|
||||
{
|
||||
"modelId": "gpt-5.2",
|
||||
"displayName": "GPT-5.2",
|
||||
"provider": "openai",
|
||||
"description": "Balanced performance with enhanced reasoning and creativity."
|
||||
},
|
||||
{
|
||||
"modelId": "gpt-5.1-instant",
|
||||
"displayName": "GPT-5.1 Instant",
|
||||
"provider": "openai",
|
||||
"description": "The standard high-speed efficiency model replacing older 'Nano' tiers."
|
||||
},
|
||||
{
|
||||
"modelId": "gpt-5-nano",
|
||||
"displayName": "GPT-5 Nano",
|
||||
"provider": "openai",
|
||||
"description": "Fast and cheap model sufficient for most tasks."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -81,16 +81,16 @@
|
||||
"isCustom": false,
|
||||
"models": [
|
||||
{
|
||||
"modelId": "claude-sonnet-5-20260203",
|
||||
"displayName": "Claude Sonnet 5",
|
||||
"modelId": "claude-opus-4-6",
|
||||
"displayName": "Claude Opus 4.6",
|
||||
"provider": "anthropic",
|
||||
"description": "Latest stable workhorse (Feb 2026), balancing speed and top-tier reasoning."
|
||||
"description": "Most intelligent model for building agents and coding with 1M context."
|
||||
},
|
||||
{
|
||||
"modelId": "claude-4.5-haiku",
|
||||
"displayName": "Claude 4.5 Haiku",
|
||||
"modelId": "claude-sonnet-4-5",
|
||||
"displayName": "Claude Sonnet 4.5",
|
||||
"provider": "anthropic",
|
||||
"description": "Fastest Claude model for pure speed and simple tasks."
|
||||
"description": "Best combination of speed and intelligence with extended thinking."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -110,9 +110,9 @@
|
||||
},
|
||||
{
|
||||
"modelId": "deepseek-chat",
|
||||
"displayName": "DeepSeek V3",
|
||||
"displayName": "DeepSeek V3.1",
|
||||
"provider": "deepseek",
|
||||
"description": "General purpose chat model, specialized in code and reasoning."
|
||||
"description": "Latest 671B MoE with hybrid thinking/non-thinking modes, 128K context."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -120,15 +120,15 @@
|
||||
"key": "gemini",
|
||||
"displayName": "Google Gemini",
|
||||
"baseUrl": "https://generativelanguage.googleapis.com/",
|
||||
"endpoint": "v1beta/models/gemini-3-flash-preview:generateContent",
|
||||
"endpoint": "v1beta/models/gemini-2.5-pro:generateContent",
|
||||
"websiteUrl": "https://ai.google/",
|
||||
"isCustom": false,
|
||||
"models": [
|
||||
{
|
||||
"modelId": "gemini-3-flash-preview",
|
||||
"displayName": "Gemini 3 Flash",
|
||||
"modelId": "gemini-2.5-pro",
|
||||
"displayName": "Gemini 2.5 Pro",
|
||||
"provider": "gemini",
|
||||
"description": "Current default: Massive context, grounded, and extremely fast."
|
||||
"description": "Stable release: State-of-the-art reasoning with 1M context."
|
||||
},
|
||||
{
|
||||
"modelId": "gemini-3-pro-preview",
|
||||
@@ -156,16 +156,10 @@
|
||||
"isCustom": false,
|
||||
"models": [
|
||||
{
|
||||
"modelId": "llama-4-scout-17b",
|
||||
"displayName": "Llama 4 Scout",
|
||||
"modelId": "meta-llama/llama-4-maverick",
|
||||
"displayName": "Llama 4 Maverick",
|
||||
"provider": "groq",
|
||||
"description": "Powerful Llama 4 model running at extreme speed."
|
||||
},
|
||||
{
|
||||
"modelId": "llama-3.3-70b-versatile",
|
||||
"displayName": "Llama 3.3 70B",
|
||||
"provider": "groq",
|
||||
"description": "Previous gen flagship, highly reliable and fast on Groq chips."
|
||||
"description": "400B MoE powerhouse with industry-leading image and text understanding."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -216,10 +210,10 @@
|
||||
"description": "World's fastest inference (2000+ tokens/sec) on Wafer-Scale Engines."
|
||||
},
|
||||
{
|
||||
"modelId": "llama3.1-8b",
|
||||
"displayName": "Llama 3.1 8B",
|
||||
"modelId": "llama-4-scout",
|
||||
"displayName": "Llama 4 Scout",
|
||||
"provider": "cerebras",
|
||||
"description": "Instant speed for simple tasks."
|
||||
"description": "High-quality 17B active param model running at 2,600 tokens/sec."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -238,10 +232,10 @@
|
||||
"description": "Hosted via the Hugging Face serverless router (Free tier limits apply)."
|
||||
},
|
||||
{
|
||||
"modelId": "microsoft/Phi-3.5-mini-instruct",
|
||||
"displayName": "Phi 3.5 Mini",
|
||||
"modelId": "Qwen/Qwen2.5-72B-Instruct",
|
||||
"displayName": "Qwen 2.5 72B",
|
||||
"provider": "huggingface",
|
||||
"description": "Highly capable small model from Microsoft."
|
||||
"description": "High-quality open model with excellent reasoning and multilingual capabilities."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ package eu.gaudian.translator.model.repository
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.Language
|
||||
import eu.gaudian.translator.model.parseLanguagesFromResources
|
||||
import eu.gaudian.translator.model.repository.DataStoreKeys.LANGUAGE_INIT_METADATA_KEY
|
||||
@@ -88,13 +89,12 @@ class LanguageRepository(private val context: Context) {
|
||||
// Check if we already have default languages saved
|
||||
val savedDefaultLanguages = loadLanguages(LanguageListType.DEFAULT).firstOrNull() ?: emptyList()
|
||||
|
||||
// Check if we need to re-parse languages (first run, version change, or language change)
|
||||
// Check if we need to reparse languages (first run, version change, or language change)
|
||||
val shouldReparse = shouldReparseLanguages(savedDefaultLanguages)
|
||||
|
||||
if (shouldReparse) {
|
||||
Log.d("LanguageRepository", "Parsing languages from resources")
|
||||
val parsedLanguages = parseLanguagesFromResources(context)
|
||||
wipeHistoryAndFavorites()
|
||||
saveLanguages(LanguageListType.DEFAULT, parsedLanguages)
|
||||
// Save the current app version and locale to detect changes next time
|
||||
saveLanguageInitializationMetadata()
|
||||
@@ -112,8 +112,11 @@ class LanguageRepository(private val context: Context) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if the number of languages matches expected count (51)
|
||||
if (savedLanguages.size != 51) {
|
||||
// Get expected language count from resources dynamically
|
||||
val expectedCount = context.resources.getStringArray(R.array.language_codes).size
|
||||
|
||||
// Check if the number of languages matches expected count from resources
|
||||
if (savedLanguages.size != expectedCount) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -167,15 +170,22 @@ class LanguageRepository(private val context: Context) {
|
||||
val customLanguages = loadLanguages(LanguageListType.CUSTOM).first()
|
||||
val master = (defaultLanguages + customLanguages).distinctBy { it.nameResId }
|
||||
|
||||
// Sanitize existing enabled IDs and initialize if empty
|
||||
// Get existing enabled IDs
|
||||
val existingEnabled: List<Int> = try {
|
||||
context.dataStore.loadObjectList<Int>(DataStoreKeys.ALL_LANGUAGES_KEY).firstOrNull() ?: emptyList()
|
||||
} catch (_: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
val masterIds = master.map { it.nameResId }.toSet()
|
||||
val sanitized = existingEnabled.filter { it in masterIds }
|
||||
val finalIds = sanitized.ifEmpty { master.filter { it.isSelected == true }.map { it.nameResId } }
|
||||
|
||||
// Sanitize existing enabled IDs (remove any that are no longer valid)
|
||||
val existingValidIds = existingEnabled.filter { it in masterIds }.toSet()
|
||||
|
||||
// Add any new languages from master that aren't already enabled
|
||||
val newLanguageIds = masterIds - existingValidIds
|
||||
|
||||
// Combine: keep existing enabled + add new languages
|
||||
val finalIds = (existingValidIds + newLanguageIds).toList()
|
||||
|
||||
context.dataStore.saveObjectList(DataStoreKeys.ALL_LANGUAGES_KEY, finalIds)
|
||||
|
||||
|
||||
@@ -102,8 +102,8 @@ class SettingsRepository(private val context: Context) {
|
||||
val intervalStage4 = Setting(context.dataStore, PrefKeys.INTERVAL_STAGE_4, 30)
|
||||
val intervalStage5 = Setting(context.dataStore, PrefKeys.INTERVAL_STAGE_5, 60)
|
||||
val intervalLearned = Setting(context.dataStore, PrefKeys.INTERVAL_LEARNED, 90)
|
||||
val criteriaCorrect = Setting(context.dataStore, PrefKeys.CRITERIA_CORRECT, 3)
|
||||
val criteriaWrong = Setting(context.dataStore, PrefKeys.CRITERIA_WRONG, 2)
|
||||
val criteriaCorrect = Setting(context.dataStore, PrefKeys.CRITERIA_CORRECT, 1)
|
||||
val criteriaWrong = Setting(context.dataStore, PrefKeys.CRITERIA_WRONG, 1)
|
||||
val showHints = Setting(context.dataStore, PrefKeys.SHOW_HINTS, true)
|
||||
val experimentalFeatures = Setting(context.dataStore, PrefKeys.EXPERIMENTAL_FEATURES, false)
|
||||
val tryWiktionaryFirst = Setting(context.dataStore, PrefKeys.TRY_WIKTIONARY_FIRST, false)
|
||||
|
||||
@@ -480,7 +480,9 @@ class VocabularyRepository private constructor(context: Context) {
|
||||
correctCount: Int, incorrectCount: Int,
|
||||
criteriaCorrect: Int, criteriaWrong: Int
|
||||
): VocabularyStage {
|
||||
val readyToAdvance = if (isCorrect) correctCount >= criteriaCorrect else incorrectCount >= criteriaWrong
|
||||
if (isCorrect) {
|
||||
// Correct answer: advance to next stage if criteria met
|
||||
val readyToAdvance = correctCount >= criteriaCorrect
|
||||
if (!readyToAdvance) return currentStage
|
||||
return when (currentStage) {
|
||||
VocabularyStage.NEW -> VocabularyStage.STAGE_1
|
||||
@@ -491,6 +493,20 @@ class VocabularyRepository private constructor(context: Context) {
|
||||
VocabularyStage.STAGE_5 -> VocabularyStage.LEARNED
|
||||
VocabularyStage.LEARNED -> VocabularyStage.LEARNED
|
||||
}
|
||||
} else {
|
||||
// Incorrect answer: demote to previous stage if criteria met
|
||||
val readyToDemote = incorrectCount >= criteriaWrong
|
||||
if (!readyToDemote) return currentStage
|
||||
return when (currentStage) {
|
||||
VocabularyStage.LEARNED -> VocabularyStage.STAGE_5
|
||||
VocabularyStage.STAGE_5 -> VocabularyStage.STAGE_4
|
||||
VocabularyStage.STAGE_4 -> VocabularyStage.STAGE_3
|
||||
VocabularyStage.STAGE_3 -> VocabularyStage.STAGE_2
|
||||
VocabularyStage.STAGE_2 -> VocabularyStage.STAGE_1
|
||||
VocabularyStage.STAGE_1 -> VocabularyStage.STAGE_1
|
||||
VocabularyStage.NEW -> VocabularyStage.NEW
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isItemFitForCategory(
|
||||
|
||||
@@ -9,6 +9,7 @@ import eu.gaudian.translator.ui.theme.themes.CitrusSplashTheme
|
||||
import eu.gaudian.translator.ui.theme.themes.CoffeeTheme
|
||||
import eu.gaudian.translator.ui.theme.themes.CrimsonTheme
|
||||
import eu.gaudian.translator.ui.theme.themes.CyberpunkTheme
|
||||
import eu.gaudian.translator.ui.theme.themes.DebugTheme
|
||||
import eu.gaudian.translator.ui.theme.themes.DefaultTheme
|
||||
import eu.gaudian.translator.ui.theme.themes.ForestTheme
|
||||
import eu.gaudian.translator.ui.theme.themes.NordTheme
|
||||
@@ -113,6 +114,7 @@ val AllThemes = listOf(
|
||||
SpaceTheme,
|
||||
CyberpunkTheme,
|
||||
SynthwaveTheme,
|
||||
DebugTheme,
|
||||
|
||||
|
||||
)
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
@file:Suppress("HardCodedStringLiteral")
|
||||
|
||||
package eu.gaudian.translator.ui.theme.themes
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import eu.gaudian.translator.ui.theme.AppTheme
|
||||
import eu.gaudian.translator.ui.theme.ThemeColorSet
|
||||
|
||||
val DebugTheme = AppTheme(
|
||||
name = "Debug",
|
||||
lightColors = ThemeColorSet(
|
||||
// Primary: Bright Red
|
||||
primary = Color(0xFFFF0000),
|
||||
onPrimary = Color(0xFFFFFFFF),
|
||||
primaryContainer = Color(0xFFFFCCCC),
|
||||
onPrimaryContainer = Color(0xFF660000),
|
||||
|
||||
// Secondary: Bright Blue
|
||||
secondary = Color(0xFF0000FF),
|
||||
onSecondary = Color(0xFFFFFFFF),
|
||||
secondaryContainer = Color(0xFFCCCCFF),
|
||||
onSecondaryContainer = Color(0xFF000066),
|
||||
|
||||
// Tertiary: Bright Green
|
||||
tertiary = Color(0xFF00FF00),
|
||||
onTertiary = Color(0xFF000000),
|
||||
tertiaryContainer = Color(0xFFCCFFCC),
|
||||
onTertiaryContainer = Color(0xFF006600),
|
||||
|
||||
// Error: Standard Material Red
|
||||
error = Color(0xFFBA1A1A),
|
||||
onError = Color(0xFFFFFFFF),
|
||||
errorContainer = Color(0xFFFFDAD6),
|
||||
onErrorContainer = Color(0xFF410002),
|
||||
|
||||
// Background: Light Gray
|
||||
background = Color(0xFFF5F5F5),
|
||||
onBackground = Color(0xFF333333),
|
||||
|
||||
// Surface: White
|
||||
surface = Color(0xFFFFFFFF),
|
||||
onSurface = Color(0xFF333333),
|
||||
surfaceVariant = Color(0xFFE0E0E0),
|
||||
onSurfaceVariant = Color(0xFF666666),
|
||||
|
||||
// Outline
|
||||
outline = Color(0xFF999999),
|
||||
outlineVariant = Color(0xFFCCCCCC),
|
||||
|
||||
// Scrim
|
||||
scrim = Color(0xFF000000),
|
||||
|
||||
// Inverse colors
|
||||
inverseSurface = Color(0xFF333333),
|
||||
inverseOnSurface = Color(0xFFFFFFFF),
|
||||
inversePrimary = Color(0xFFFF6666),
|
||||
|
||||
// Surface containers
|
||||
surfaceDim = Color(0xFFE8E8E8),
|
||||
surfaceBright = Color(0xFFFFFFFF),
|
||||
surfaceContainerLowest = Color(0xFFFFFFFF),
|
||||
surfaceContainerLow = Color(0xFFEEEEEE),
|
||||
surfaceContainer = Color(0xFFF5F5F5),
|
||||
surfaceContainerHigh = Color(0xFFFAFAFA),
|
||||
surfaceContainerHighest = Color(0xFFFFFFFF)
|
||||
),
|
||||
darkColors = ThemeColorSet(
|
||||
// Primary: Bright Cyan
|
||||
primary = Color(0xFF00FFFF),
|
||||
onPrimary = Color(0xFF000000),
|
||||
primaryContainer = Color(0xFF66FFFF),
|
||||
onPrimaryContainer = Color(0xFF003333),
|
||||
|
||||
// Secondary: Bright Magenta
|
||||
secondary = Color(0xFFFF00FF),
|
||||
onSecondary = Color(0xFF000000),
|
||||
secondaryContainer = Color(0xFFFF66FF),
|
||||
onSecondaryContainer = Color(0xFF330033),
|
||||
|
||||
// Tertiary: Bright Yellow
|
||||
tertiary = Color(0xFFFFFF00),
|
||||
onTertiary = Color(0xFF000000),
|
||||
tertiaryContainer = Color(0xFFFFFF66),
|
||||
onTertiaryContainer = Color(0xFF333300),
|
||||
|
||||
// Error: Standard Material Red
|
||||
error = Color(0xFFFFB4AB),
|
||||
onError = Color(0xFF690005),
|
||||
errorContainer = Color(0xFF93000A),
|
||||
onErrorContainer = Color(0xFFFFDAD6),
|
||||
|
||||
// Background: Dark Gray
|
||||
background = Color(0xFF121212),
|
||||
onBackground = Color(0xFFE0E0E0),
|
||||
|
||||
// Surface: Dark Gray
|
||||
surface = Color(0xFF1E1E1E),
|
||||
onSurface = Color(0xFFE0E0E0),
|
||||
surfaceVariant = Color(0xFF2D2D2D),
|
||||
onSurfaceVariant = Color(0xFFB0B0B0),
|
||||
|
||||
// Outline
|
||||
outline = Color(0xFF555555),
|
||||
outlineVariant = Color(0xFF333333),
|
||||
|
||||
// Scrim
|
||||
scrim = Color(0xFF000000),
|
||||
|
||||
// Inverse colors
|
||||
inverseSurface = Color(0xFFE0E0E0),
|
||||
inverseOnSurface = Color(0xFF121212),
|
||||
inversePrimary = Color(0xFF006666),
|
||||
|
||||
// Surface containers
|
||||
surfaceDim = Color(0xFF121212),
|
||||
surfaceBright = Color(0xFF333333),
|
||||
surfaceContainerLowest = Color(0xFF0A0A0A),
|
||||
surfaceContainerLow = Color(0xFF181818),
|
||||
surfaceContainer = Color(0xFF1E1E1E),
|
||||
surfaceContainerHigh = Color(0xFF252525),
|
||||
surfaceContainerHighest = Color(0xFF2D2D2D)
|
||||
)
|
||||
)
|
||||
@@ -60,6 +60,7 @@ val LANGUAGE_STRING_IDS: IntArray = intArrayOf(
|
||||
R.string.language_49,
|
||||
R.string.language_50,
|
||||
R.string.language_51,
|
||||
R.string.language_52,
|
||||
|
||||
R.string.native_language_1,
|
||||
R.string.native_language_2,
|
||||
@@ -112,6 +113,7 @@ val LANGUAGE_STRING_IDS: IntArray = intArrayOf(
|
||||
R.string.native_language_49,
|
||||
R.string.native_language_50,
|
||||
R.string.native_language_51,
|
||||
R.string.native_language_52,
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package eu.gaudian.translator.utils
|
||||
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.viewmodel.MessageAction
|
||||
import eu.gaudian.translator.viewmodel.MessageDisplayType
|
||||
|
||||
/**
|
||||
* Simplified status message IDs for internationalization.
|
||||
* Each ID stores its metadata directly, reducing repetitive mapping code.
|
||||
*/
|
||||
enum class StatusMessageId(
|
||||
val stringResId: Int,
|
||||
val defaultType: MessageDisplayType,
|
||||
val defaultTimeout: Int,
|
||||
val associatedAction: MessageAction? = null
|
||||
) {
|
||||
// Generic messages
|
||||
SUCCESS_GENERIC(R.string.message_success_generic, MessageDisplayType.SUCCESS, 3),
|
||||
INFO_GENERIC(R.string.message_info_generic, MessageDisplayType.INFO, 3),
|
||||
ERROR_GENERIC(R.string.message_error_generic, MessageDisplayType.ERROR, 5),
|
||||
LOADING_GENERIC(R.string.message_loading_generic, MessageDisplayType.LOADING, 0),
|
||||
TEST_INFO(R.string.message_test_info, MessageDisplayType.INFO, 3),
|
||||
TEST_SUCCESS(R.string.message_test_success, MessageDisplayType.SUCCESS, 3),
|
||||
TEST_ERROR(R.string.message_test_error, MessageDisplayType.ERROR, 5),
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Language related
|
||||
ERROR_LANGUAGE_NOT_SELECTED(R.string.message_error_language_not_selected, MessageDisplayType.ERROR, 5),
|
||||
ERROR_NO_WORDS_FOUND(R.string.message_error_no_words_found, MessageDisplayType.ERROR, 5),
|
||||
SUCCESS_LANGUAGE_REPLACED(R.string.message_success_language_replaced, MessageDisplayType.SUCCESS, 3),
|
||||
|
||||
// Vocabulary related
|
||||
SUCCESS_VOCABULARY_IMPORTED(R.string.message_success_vocabulary_imported, MessageDisplayType.SUCCESS, 3),
|
||||
ERROR_VOCABULARY_IMPORT_FAILED(R.string.message_error_vocabulary_import_failed, MessageDisplayType.ERROR, 5),
|
||||
SUCCESS_ITEMS_MERGED(R.string.message_success_items_merged, MessageDisplayType.SUCCESS, 3),
|
||||
SUCCESS_ITEMS_ADDED(R.string.message_success_items_added, MessageDisplayType.SUCCESS, 3),
|
||||
ERROR_ITEMS_ADD_FAILED(R.string.message_error_items_add_failed, MessageDisplayType.ERROR, 5),
|
||||
SUCCESS_ITEMS_DELETED(R.string.message_success_items_deleted, MessageDisplayType.SUCCESS, 3),
|
||||
ERROR_ITEMS_DELETE_FAILED(R.string.message_error_items_delete_failed, MessageDisplayType.ERROR, 5),
|
||||
ERROR_NO_CARDS_FOUND(R.string.message_error_no_cards_found, MessageDisplayType.ERROR, 5),
|
||||
SUCCESS_CARDS_LOADED(R.string.message_success_cards_loaded, MessageDisplayType.SUCCESS, 3),
|
||||
|
||||
// Grammar related
|
||||
SUCCESS_GRAMMAR_UPDATED(R.string.message_success_grammar_updated, MessageDisplayType.SUCCESS, 3),
|
||||
ERROR_GRAMMAR_FETCH_FAILED(R.string.message_error_grammar_fetch_failed, MessageDisplayType.ERROR, 5),
|
||||
LOADING_GRAMMAR_FETCH(R.string.message_loading_grammar_fetch, MessageDisplayType.LOADING, 0),
|
||||
|
||||
// File operations
|
||||
SUCCESS_FILE_SAVED(R.string.message_success_file_saved, MessageDisplayType.SUCCESS, 3),
|
||||
ERROR_FILE_SAVE_FAILED(R.string.message_error_file_save_failed, MessageDisplayType.ERROR, 5),
|
||||
ERROR_FILE_SAVE_CANCELLED(R.string.message_error_file_save_cancelled, MessageDisplayType.ERROR, 5),
|
||||
ERROR_FILE_PICKER_NOT_INITIALIZED(R.string.message_error_file_picker_not_initialized, MessageDisplayType.ERROR, 5),
|
||||
SUCCESS_CATEGORY_SAVED(R.string.message_success_category_saved, MessageDisplayType.SUCCESS, 3),
|
||||
ERROR_EXCEL_NOT_SUPPORTED(R.string.message_error_excel_not_supported, MessageDisplayType.ERROR, 5),
|
||||
|
||||
|
||||
// API Key related
|
||||
ERROR_API_KEY_MISSING(R.string.message_error_api_key_missing, MessageDisplayType.ERROR, 0, MessageAction.NAVIGATE_TO_API_KEYS),
|
||||
ERROR_API_KEY_INVALID(R.string.message_error_api_key_invalid, MessageDisplayType.ERROR, 0, MessageAction.NAVIGATE_TO_API_KEYS),
|
||||
|
||||
// Translation related
|
||||
LOADING_TRANSLATING(R.string.message_loading_translating, MessageDisplayType.LOADING, 0),
|
||||
SUCCESS_TRANSLATION_COMPLETED(R.string.message_success_translation_completed, MessageDisplayType.SUCCESS, 3),
|
||||
ERROR_TRANSLATION_FAILED(R.string.message_error_translation_failed, MessageDisplayType.ERROR, 5),
|
||||
|
||||
// Repository operations
|
||||
SUCCESS_REPOSITORY_WIPED(R.string.message_success_repository_wiped, MessageDisplayType.SUCCESS, 3),
|
||||
ERROR_REPOSITORY_WIPE_FAILED(R.string.message_error_repository_wipe_failed, MessageDisplayType.ERROR, 5),
|
||||
LOADING_CARD_SET(R.string.message_loading_card_set, MessageDisplayType.LOADING, 0),
|
||||
|
||||
// Stage operations
|
||||
SUCCESS_STAGE_UPDATED(R.string.message_success_stage_updated, MessageDisplayType.SUCCESS, 3),
|
||||
ERROR_STAGE_UPDATE_FAILED(R.string.message_error_stage_update_failed, MessageDisplayType.ERROR, 5),
|
||||
|
||||
// Category operations
|
||||
SUCCESS_CATEGORY_UPDATED(R.string.message_success_category_updated, MessageDisplayType.SUCCESS, 3),
|
||||
ERROR_CATEGORY_UPDATE_FAILED(R.string.message_error_category_update_failed, MessageDisplayType.ERROR, 5),
|
||||
|
||||
// Article removal
|
||||
SUCCESS_ARTICLES_REMOVED(R.string.message_success_articles_removed, MessageDisplayType.SUCCESS, 3),
|
||||
ERROR_ARTICLES_REMOVE_FAILED(R.string.message_error_articles_remove_failed, MessageDisplayType.ERROR, 5),
|
||||
|
||||
// Synonyms
|
||||
SUCCESS_SYNONYMS_GENERATED(R.string.message_success_synonyms_generated, MessageDisplayType.SUCCESS, 3),
|
||||
ERROR_SYNONYMS_GENERATION_FAILED(R.string.message_error_synonyms_generation_failed, MessageDisplayType.ERROR, 5),
|
||||
|
||||
// Operation status
|
||||
ERROR_OPERATION_FAILED(R.string.message_error_operation_failed, MessageDisplayType.ERROR, 5),
|
||||
LOADING_OPERATION_IN_PROGRESS(R.string.message_loading_operation_in_progress, MessageDisplayType.LOADING, 0);
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Convenience function to get the string resource ID from a StatusMessageId.
|
||||
* Kept for backward compatibility with existing code.
|
||||
*/
|
||||
fun StatusMessageId.getStringResId(): Int = this.stringResId
|
||||
|
||||
/**
|
||||
* Convenience function to get the default display type.
|
||||
*/
|
||||
fun StatusMessageId.getDefaultDisplayType(): MessageDisplayType = this.defaultType
|
||||
|
||||
/**
|
||||
* Convenience function to get the default timeout.
|
||||
*/
|
||||
fun StatusMessageId.getDefaultTimeoutSeconds(): Int = this.defaultTimeout
|
||||
|
||||
/**
|
||||
* Convenience function to get the associated action.
|
||||
*/
|
||||
fun StatusMessageId.getAssociatedAction(): MessageAction? = this.associatedAction
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,10 @@ import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* A sealed class representing all possible actions that can be sent to the status system.
|
||||
* Supports both legacy string-based messages and new ID-based messages for internationalization.
|
||||
*/
|
||||
sealed class StatusAction {
|
||||
// Legacy string-based actions (deprecated in favor of ID-based actions)
|
||||
data class ShowMessage(val text: String, val type: MessageDisplayType, val timeoutInSeconds: Int) : StatusAction()
|
||||
data class ShowPermanentMessage(val text: String, val type: MessageDisplayType) : StatusAction()
|
||||
object CancelPermanentMessage : StatusAction()
|
||||
@@ -20,31 +22,59 @@ sealed class StatusAction {
|
||||
object CancelLoadingOperation : StatusAction()
|
||||
object HideMessageBar : StatusAction()
|
||||
object CancelAllMessages : StatusAction()
|
||||
|
||||
data class ShowActionableMessage(val text: String, val type: MessageDisplayType, val action: MessageAction) : StatusAction()
|
||||
|
||||
// New ID-based actions for internationalization
|
||||
data class ShowMessageById(
|
||||
val messageId: StatusMessageId,
|
||||
val type: MessageDisplayType = messageId.defaultType,
|
||||
val timeoutInSeconds: Int = messageId.defaultTimeout
|
||||
) : StatusAction()
|
||||
data class ShowPermanentMessageById(
|
||||
val messageId: StatusMessageId,
|
||||
val type: MessageDisplayType = messageId.defaultType
|
||||
) : StatusAction()
|
||||
data class ShowActionableMessageById(
|
||||
val messageId: StatusMessageId,
|
||||
val type: MessageDisplayType = messageId.defaultType,
|
||||
val action: MessageAction = messageId.associatedAction ?: MessageAction.NAVIGATE_TO_API_KEYS
|
||||
) : StatusAction()
|
||||
}
|
||||
|
||||
/**
|
||||
* A singleton object that acts as a central event bus for status messages.
|
||||
* Any part of the app can trigger an action, and any StatusViewModel listening will receive it.
|
||||
*
|
||||
* NOTE: All message display requests should go through this service.
|
||||
*/
|
||||
object StatusMessageService {
|
||||
private val _actions = MutableSharedFlow<StatusAction>()
|
||||
val actions = _actions.asSharedFlow()
|
||||
private val scope = CoroutineScope(Dispatchers.Default)
|
||||
|
||||
suspend fun trigger(action: StatusAction) {
|
||||
/**
|
||||
* Triggers a status action. This is the primary way to display messages.
|
||||
* Internally launches a coroutine, so this function is not suspend.
|
||||
*/
|
||||
fun trigger(action: StatusAction) {
|
||||
Log.d("StatusMessageService", "Received action: $action")
|
||||
_actions.emit(action)
|
||||
}
|
||||
|
||||
fun triggerNonSuspend(action: StatusAction) {
|
||||
Log.d("StatusMessageService", "Received non-suspend action: $action")
|
||||
scope.launch {
|
||||
_actions.emit(action)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use trigger() instead.
|
||||
*/
|
||||
@Deprecated("Use trigger() instead", ReplaceWith("trigger(action)"))
|
||||
fun triggerNonSuspend(action: StatusAction) {
|
||||
trigger(action)
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use showMessageById() instead for internationalization support.
|
||||
*/
|
||||
@Deprecated("Use showMessageById() for internationalization support", ReplaceWith("showMessageById(messageId)"))
|
||||
@Suppress("unused")
|
||||
fun showSimpleMessage(text: String, type: MessageDisplayType = MessageDisplayType.INFO) {
|
||||
scope.launch {
|
||||
@@ -52,6 +82,10 @@ object StatusMessageService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use showErrorById() instead for internationalization support.
|
||||
*/
|
||||
@Deprecated("Use showErrorById() for internationalization support", ReplaceWith("showErrorById(messageId)"))
|
||||
fun showErrorMessage(text: String, timeoutInSeconds: Int = 5) {
|
||||
scope.launch {
|
||||
_actions.emit(StatusAction.ShowMessage(
|
||||
@@ -62,6 +96,10 @@ object StatusMessageService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use showLoadingById() instead for internationalization support.
|
||||
*/
|
||||
@Deprecated("Use showLoadingById() for internationalization support", ReplaceWith("showLoadingById(messageId)"))
|
||||
fun showLoadingMessage(text: String, timeoutInSeconds: Int = 0) {
|
||||
scope.launch {
|
||||
_actions.emit(StatusAction.ShowMessage(
|
||||
@@ -71,6 +109,10 @@ object StatusMessageService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use showInfoById() instead for internationalization support.
|
||||
*/
|
||||
@Deprecated("Use showInfoById() for internationalization support", ReplaceWith("showInfoById(messageId)"))
|
||||
fun showInfoMessage(text: String, timeoutInSeconds: Int = 3) {
|
||||
scope.launch {
|
||||
_actions.emit(StatusAction.ShowMessage(
|
||||
@@ -80,6 +122,10 @@ object StatusMessageService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use showSuccessById() instead for internationalization support.
|
||||
*/
|
||||
@Deprecated("Use showSuccessById() for internationalization support", ReplaceWith("showSuccessById(messageId)"))
|
||||
fun showSuccessMessage(text: String, timeoutInSeconds: Int = 3) {
|
||||
scope.launch {
|
||||
_actions.emit(StatusAction.ShowMessage(
|
||||
@@ -89,33 +135,102 @@ object StatusMessageService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use showPermanentMessageById() instead for internationalization support.
|
||||
*/
|
||||
@Deprecated("Use showPermanentMessageById() for internationalization support", ReplaceWith("showPermanentMessageById(messageId)"))
|
||||
fun showPermanentMessage(text: String, type: MessageDisplayType) {
|
||||
scope.launch {
|
||||
_actions.emit(StatusAction.ShowPermanentMessage(text, type))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use StatusAction.CancelPermanentMessage via trigger() if needed.
|
||||
*/
|
||||
@Deprecated("Use StatusAction.CancelPermanentMessage via trigger() if needed")
|
||||
fun cancelPermanentMessage() {
|
||||
scope.launch {
|
||||
_actions.emit(StatusAction.CancelPermanentMessage)
|
||||
}
|
||||
trigger(StatusAction.CancelPermanentMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use StatusAction.HideMessageBar via trigger() if needed.
|
||||
*/
|
||||
@Deprecated("Use StatusAction.HideMessageBar via trigger() if needed")
|
||||
fun hideMessageBar() {
|
||||
scope.launch {
|
||||
_actions.emit(StatusAction.HideMessageBar)
|
||||
}
|
||||
trigger(StatusAction.HideMessageBar)
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use StatusAction.CancelAllMessages via trigger() if needed.
|
||||
*/
|
||||
@Deprecated("Use StatusAction.CancelAllMessages via trigger() if needed")
|
||||
fun cancelAllMessages() {
|
||||
scope.launch {
|
||||
_actions.emit(StatusAction.CancelAllMessages)
|
||||
}
|
||||
trigger(StatusAction.CancelAllMessages)
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use showActionableMessageById() instead for internationalization support.
|
||||
*/
|
||||
@Deprecated("Use showActionableMessageById() for internationalization support", ReplaceWith("showActionableMessageById(messageId)"))
|
||||
fun showActionableMessage(text: String, type: MessageDisplayType, action: MessageAction) {
|
||||
scope.launch {
|
||||
_actions.emit(StatusAction.ShowActionableMessage(text, type, action))
|
||||
}
|
||||
}
|
||||
|
||||
// === NEW ID-BASED METHODS (for internationalization) ===
|
||||
|
||||
/**
|
||||
* Shows a message by its ID. The actual text is resolved by StatusViewModel using string resources.
|
||||
* @param messageId The StatusMessageId that maps to a string resource
|
||||
* @param type Optional override for the display type
|
||||
* @param timeoutInSeconds Optional override for the timeout
|
||||
*/
|
||||
fun showMessageById(
|
||||
messageId: StatusMessageId,
|
||||
type: MessageDisplayType = messageId.defaultType,
|
||||
timeoutInSeconds: Int = messageId.defaultTimeout
|
||||
) {
|
||||
trigger(StatusAction.ShowMessageById(messageId, type, timeoutInSeconds))
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a permanent message (until dismissed) by its ID.
|
||||
*/
|
||||
fun showPermanentMessageById(
|
||||
messageId: StatusMessageId,
|
||||
type: MessageDisplayType = messageId.defaultType
|
||||
) {
|
||||
trigger(StatusAction.ShowPermanentMessageById(messageId, type))
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows an actionable message by its ID with an optional action.
|
||||
*/
|
||||
fun showActionableMessageById(
|
||||
messageId: StatusMessageId,
|
||||
type: MessageDisplayType = messageId.defaultType,
|
||||
action: MessageAction = messageId.associatedAction ?: MessageAction.NAVIGATE_TO_API_KEYS
|
||||
) {
|
||||
trigger(StatusAction.ShowActionableMessageById(messageId, type, action))
|
||||
}
|
||||
|
||||
// Convenience methods for common message types
|
||||
|
||||
fun showErrorById(messageId: StatusMessageId, timeoutInSeconds: Int = messageId.defaultTimeout) {
|
||||
showMessageById(messageId, MessageDisplayType.ERROR, timeoutInSeconds)
|
||||
}
|
||||
|
||||
fun showSuccessById(messageId: StatusMessageId, timeoutInSeconds: Int = messageId.defaultTimeout) {
|
||||
showMessageById(messageId, MessageDisplayType.SUCCESS, timeoutInSeconds)
|
||||
}
|
||||
|
||||
fun showInfoById(messageId: StatusMessageId, timeoutInSeconds: Int = messageId.defaultTimeout) {
|
||||
showMessageById(messageId, MessageDisplayType.INFO, timeoutInSeconds)
|
||||
}
|
||||
|
||||
fun showLoadingById(messageId: StatusMessageId, timeoutInSeconds: Int = messageId.defaultTimeout) {
|
||||
showMessageById(messageId, MessageDisplayType.LOADING, timeoutInSeconds)
|
||||
}
|
||||
}
|
||||
@@ -117,6 +117,7 @@ class TranslationService(private val context: Context) {
|
||||
}
|
||||
|
||||
suspend fun translateSentence(sentence: String): Result<TranslationHistoryItem> = withContext(Dispatchers.IO) {
|
||||
val statusMessageService = StatusMessageService
|
||||
val additionalInstructions = settingsRepository.customPromptTranslation.flow.first()
|
||||
val selectedSource = languageRepository.loadSelectedSourceLanguage().first()
|
||||
val sourceLangName = selectedSource?.englishName ?: "Auto"
|
||||
|
||||
@@ -5,11 +5,9 @@ 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.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -42,7 +40,7 @@ import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.PrimaryButton
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
|
||||
@Composable
|
||||
fun IntroNavHost(onIntroFinished: () -> Unit) {
|
||||
val pages = listOf(
|
||||
@@ -55,9 +53,16 @@ fun IntroNavHost(onIntroFinished: () -> Unit) {
|
||||
title = stringResource(R.string.intro_title_ai_assistant),
|
||||
description = stringResource(R.string.intro_desc_ai_assistant),
|
||||
content = {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
IconContent(iconRes = R.drawable.ic_intro_ai_agents)
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), verticalArrangement = Arrangement.spacedBy(2.dp), modifier = Modifier.fillMaxWidth()) {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
SuggestionChip(onClick = { }, label = { Text(stringResource(R.string.text_mistral)) })
|
||||
SuggestionChip(onClick = { }, label = { Text(stringResource(R.string.text_your_own_ai)) })
|
||||
SuggestionChip(onClick = { }, label = { Text(stringResource(R.string.text_openai)) })
|
||||
@@ -89,7 +94,7 @@ fun IntroNavHost(onIntroFinished: () -> Unit) {
|
||||
IntroPageData(
|
||||
title = stringResource(R.string.intro_title_learning_journey),
|
||||
description = stringResource(R.string.intro_desc_learning_journey),
|
||||
content = { IconContent(iconRes = R.drawable.ic_intro_learning_journey)}
|
||||
content = { IconContent(iconRes = R.drawable.ic_intro_learning_journey) }
|
||||
),
|
||||
IntroPageData(
|
||||
title = stringResource(R.string.intro_title_categories),
|
||||
@@ -128,7 +133,6 @@ fun IntroNavHost(onIntroFinished: () -> Unit) {
|
||||
.statusBarsPadding()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
// Full-width Skip intro button aligned to end but sized like primary (fillMaxWidth)
|
||||
eu.gaudian.translator.view.composable.SecondaryButton(
|
||||
onClick = { onIntroFinished() },
|
||||
text = stringResource(R.string.intro_skip),
|
||||
@@ -145,7 +149,9 @@ fun IntroNavHost(onIntroFinished: () -> Unit) {
|
||||
) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.weight(1f)
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
) { pageIndex ->
|
||||
IntroPage(pageData = pages[pageIndex])
|
||||
}
|
||||
@@ -170,7 +176,7 @@ fun IntroNavHost(onIntroFinished: () -> Unit) {
|
||||
}
|
||||
},
|
||||
text = if (pagerState.currentPage < pages.size - 1) stringResource(R.string.next) else stringResource(R.string.get_started),
|
||||
icon = if (pagerState.currentPage < pages.size - 1)AppIcons.ArrowForwardNoChevron else null,
|
||||
icon = if (pagerState.currentPage < pages.size - 1) AppIcons.ArrowForwardNoChevron else null,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
@@ -189,9 +195,9 @@ private fun IntroPage(pageData: IntroPageData) {
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.CenterVertically),
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.fillMaxSize() // Fixed: This was previously fillMaxHeight()
|
||||
.padding(horizontal = 16.dp)
|
||||
.verticalScroll(rememberScrollState()) // Allow scrolling for larger hint content
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
@@ -240,9 +246,8 @@ private fun IconContent(iconRes: Int) {
|
||||
tint = Color.Unspecified,
|
||||
modifier = Modifier.size(250.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun FlashcardTopicsPreview() {
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -35,12 +36,14 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
@@ -56,8 +59,11 @@ import eu.gaudian.translator.MyApplication
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.ui.theme.AllFonts
|
||||
import eu.gaudian.translator.ui.theme.AllThemes
|
||||
import eu.gaudian.translator.ui.theme.ProvideSemanticColors
|
||||
import eu.gaudian.translator.ui.theme.buildColorScheme
|
||||
import eu.gaudian.translator.utils.Log
|
||||
import eu.gaudian.translator.utils.StatusAction
|
||||
import eu.gaudian.translator.utils.StatusMessageService
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppAlertDialog
|
||||
import eu.gaudian.translator.view.composable.BottomNavigationBar
|
||||
@@ -149,9 +155,7 @@ fun TranslatorApp(
|
||||
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val statusViewModel: StatusViewModel = hiltViewModel(activity)
|
||||
|
||||
|
||||
|
||||
val statusMessageService = StatusMessageService
|
||||
|
||||
val navController = rememberNavController()
|
||||
val statusState by statusViewModel.status.collectAsStateWithLifecycle()
|
||||
@@ -303,7 +307,7 @@ fun TranslatorApp(
|
||||
StatusMessageSystem(
|
||||
statusState = statusState,
|
||||
navController = navController,
|
||||
onDismiss = { statusViewModel.hideMessageBar() },
|
||||
onDismiss = { statusMessageService.trigger(StatusAction.HideMessageBar) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
)
|
||||
@@ -357,9 +361,12 @@ private fun AppTheme(
|
||||
val window = (view.context as Activity).window
|
||||
val windowInsetsController = WindowInsetsControllerCompat(window, view)
|
||||
|
||||
//window.statusBarColor = android.graphics.Color.TRANSPARENT
|
||||
//window.navigationBarColor = android.graphics.Color.TRANSPARENT
|
||||
//TODO remove eventually
|
||||
// We must keep this for older Android version!!!
|
||||
@Suppress("DEPRECATION")
|
||||
window.statusBarColor = colorScheme.surface.toArgb()
|
||||
//Elevation must be the same as BottomNavigationBar
|
||||
@Suppress("DEPRECATION")
|
||||
window.navigationBarColor = colorScheme.surfaceColorAtElevation(8.dp).toArgb()
|
||||
|
||||
windowInsetsController.isAppearanceLightStatusBars = !useDarkTheme
|
||||
windowInsetsController.isAppearanceLightNavigationBars = !useDarkTheme
|
||||
@@ -400,8 +407,10 @@ private fun AppTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = dynamicTypography,
|
||||
) {
|
||||
eu.gaudian.translator.ui.theme.ProvideSemanticColors {
|
||||
ProvideSemanticColors {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package eu.gaudian.translator.view.composable
|
||||
|
||||
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.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
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.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.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
|
||||
) {
|
||||
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() }
|
||||
}
|
||||
}
|
||||
|
||||
// Custom button content showing selected model and provider
|
||||
val buttonContent: @Composable () -> Unit = {
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AppDropdownContainer(
|
||||
expanded = expanded,
|
||||
onDismissRequest = {
|
||||
expanded = false
|
||||
searchQuery = ""
|
||||
},
|
||||
onExpandRequest = { expanded = true },
|
||||
buttonText = "", // Not used with custom button content
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = enabled,
|
||||
showSearch = true,
|
||||
searchQuery = searchQuery,
|
||||
onSearchQueryChange = { searchQuery = it },
|
||||
searchPlaceholder = stringResource(R.string.label_search_models),
|
||||
buttonContent = buttonContent
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.heightIn(max = 400.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
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(modifier = Modifier.padding(vertical = 4.dp))
|
||||
|
||||
// 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.view.hints.Hint
|
||||
import eu.gaudian.translator.view.hints.HintBottomSheet
|
||||
import eu.gaudian.translator.view.hints.LocalShowHints
|
||||
|
||||
@@ -48,7 +49,7 @@ import eu.gaudian.translator.view.hints.LocalShowHints
|
||||
fun AppDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
title: (@Composable () -> Unit)? = null,
|
||||
hintContent: @Composable (() -> Unit)? = null,
|
||||
hintContent: Hint? = null,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
// 1. Swipe Resistance: Prevent accidental dismissal
|
||||
@@ -98,7 +99,7 @@ fun AppDialog(
|
||||
if (showBottomSheet) {
|
||||
EnhancedHintBottomSheet(
|
||||
onDismissRequest = { showBottomSheet = false },
|
||||
content = hintContent,
|
||||
content = {hintContent?.Render()},
|
||||
parentTitle = title
|
||||
)
|
||||
}
|
||||
@@ -156,7 +157,7 @@ fun AppAlertDialog(
|
||||
@Composable
|
||||
private fun DialogHeader(
|
||||
title: (@Composable () -> Unit)?,
|
||||
hintContent: @Composable (() -> Unit)?,
|
||||
hintContent: Hint? = null,
|
||||
onHintClick: () -> Unit,
|
||||
onCloseClick: () -> Unit
|
||||
) {
|
||||
@@ -327,7 +328,6 @@ fun AppDialogPreview() {
|
||||
AppDialog(
|
||||
onDismissRequest = {},
|
||||
title = { Text("Dialog Title") },
|
||||
hintContent = { Text("This is a hint.") },
|
||||
content = {
|
||||
Column {
|
||||
Text("Content line 1")
|
||||
@@ -378,7 +378,6 @@ fun AppDialogLongContentPreview() {
|
||||
AppDialog(
|
||||
onDismissRequest = {},
|
||||
title = { Text("Long Content Dialog") },
|
||||
hintContent = { Text("Hint for long content dialog") },
|
||||
content = {
|
||||
Column {
|
||||
Text("This is a long content dialog to test scrolling")
|
||||
|
||||
@@ -2,30 +2,45 @@
|
||||
|
||||
package eu.gaudian.translator.view.composable
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
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
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
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.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -34,6 +49,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,31 +58,428 @@ 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.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
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
|
||||
|
||||
// =========================================
|
||||
// UNIFIED DROPDOWN STYLES & CONSTANTS
|
||||
// =========================================
|
||||
|
||||
object DropdownDefaults {
|
||||
val shape = RoundedCornerShape(8.dp)
|
||||
val itemPaddingHorizontal = 8.dp
|
||||
val itemPaddingVertical = 2.dp
|
||||
|
||||
@Composable
|
||||
fun containerColor(): Color = MaterialTheme.colorScheme.surface
|
||||
|
||||
@Composable
|
||||
fun itemBackground(selected: Boolean): Color {
|
||||
return if (selected) {
|
||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.6f)
|
||||
} else {
|
||||
Color.Transparent
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun itemContentColor(selected: Boolean, enabled: Boolean): Color {
|
||||
return when {
|
||||
!enabled -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||
selected -> MaterialTheme.colorScheme.primary
|
||||
else -> MaterialTheme.colorScheme.onSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A modern, custom dropdown menu composable that provides a styled text field with a dropdown list of options.
|
||||
* This implementation uses a custom dropdown for a more tailored look compared to the stock menu, behaving like a normal ExposedDropdownMenu.
|
||||
* Allows managing selection and expansion, making it a convenient wrapper for dropdowns.
|
||||
*
|
||||
* @param expanded Whether the dropdown menu is expanded.
|
||||
* @param onDismissRequest Callback invoked when the dropdown menu should be dismissed.
|
||||
* @param modifier Modifier for the composable.
|
||||
* @param label Composable for the label displayed in the text field.
|
||||
* @param enabled Whether the dropdown is enabled.
|
||||
* @param placeholder Optional placeholder text when no option is selected.
|
||||
* @param selectedText The text to display in the text field for the selected option.
|
||||
* @param onExpandRequest Callback invoked when the dropdown should expand.
|
||||
* @param content Composable content for the dropdown items, typically using AppDropdownMenuItem.
|
||||
* A drop-in replacement for [androidx.compose.material3.DropdownMenu] that opens
|
||||
* as a BottomSheet. Compatible with the standard M3 signature.
|
||||
*/
|
||||
|
||||
@Suppress("unused", "HardCodedStringLiteral")
|
||||
@Composable
|
||||
fun AppDropDownMenu(
|
||||
expanded: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
offset: DpOffset = DpOffset(0.dp, 0.dp), // Retained for signature compatibility
|
||||
scrollState: ScrollState = rememberScrollState(),
|
||||
properties: PopupProperties = PopupProperties(focusable = true), // Retained for signature compatibility
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
if (expanded) {
|
||||
// skipPartiallyExpanded = true ensures it behaves more like a menu
|
||||
// (fully open or completely closed) rather than a peekable sheet.
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismissRequest,
|
||||
sheetState = sheetState,
|
||||
// Container color, shape, etc., can be linked to your DropdownDefaults here if needed.
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(scrollState)
|
||||
) {
|
||||
// Execute standard DropdownMenuItems here
|
||||
content()
|
||||
|
||||
// Extra padding to ensure the last item isn't hidden behind the system navigation bar
|
||||
Spacer(modifier = Modifier.navigationBarsPadding().height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// UNIFIED DROPDOWN CONTAINER
|
||||
// =========================================
|
||||
|
||||
/**
|
||||
* A unified dropdown container that provides consistent styling and behavior
|
||||
* for all dropdown menus in the app.
|
||||
*
|
||||
* @param expanded Whether the dropdown is currently expanded
|
||||
* @param onDismissRequest Callback when the dropdown should be dismissed
|
||||
* @param onExpandRequest Callback when the dropdown should expand (click on button)
|
||||
* @param buttonText The text to display on the dropdown button
|
||||
* @param modifier Modifier for the container
|
||||
* @param enabled Whether the dropdown is enabled
|
||||
* @param showSearch Whether to show the search field at the top of the dropdown
|
||||
* @param searchQuery Current search query (only used if showSearch is true)
|
||||
* @param onSearchQueryChange Callback when search query changes (only used if showSearch is true)
|
||||
* @param searchPlaceholder Placeholder text for search field
|
||||
* @param showDoneButton Whether to show a "Done" button at the bottom (for multi-select)
|
||||
* @param onDoneClick Callback when Done button is clicked
|
||||
* @param buttonContent Custom content for the button (if null, uses default text-based button)
|
||||
* @param dropdownContent Content to display inside the dropdown menu
|
||||
*/
|
||||
@Composable
|
||||
fun AppDropdownContainer(
|
||||
expanded: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
onExpandRequest: () -> Unit,
|
||||
buttonText: String,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
showSearch: Boolean = false,
|
||||
searchQuery: String = "",
|
||||
onSearchQueryChange: ((String) -> Unit)? = null,
|
||||
searchPlaceholder: String? = null,
|
||||
showDoneButton: Boolean = false,
|
||||
onDoneClick: (() -> Unit)? = null,
|
||||
buttonContent: @Composable (() -> Unit)? = null,
|
||||
dropdownContent: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
// Dropdown Button
|
||||
if (buttonContent != null) {
|
||||
AppOutlinedButton(
|
||||
onClick = onExpandRequest,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = enabled
|
||||
) {
|
||||
buttonContent()
|
||||
}
|
||||
} else {
|
||||
AppOutlinedButton(
|
||||
shape = DropdownDefaults.shape,
|
||||
onClick = onExpandRequest,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = enabled
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = buttonText,
|
||||
modifier = Modifier.weight(1f),
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Icon(
|
||||
imageVector = if (expanded) AppIcons.ArrowDropUp else AppIcons.ArrowDropDown,
|
||||
contentDescription = if (expanded)
|
||||
stringResource(R.string.cd_collapse)
|
||||
else
|
||||
stringResource(R.string.cd_expand)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom Sheet "Dropdown" Menu
|
||||
if (expanded) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismissRequest,
|
||||
sheetState = sheetState,
|
||||
containerColor = DropdownDefaults.containerColor()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
// Pinned Search field (optional)
|
||||
if (showSearch && onSearchQueryChange != null) {
|
||||
DropdownSearchField(
|
||||
searchQuery = searchQuery,
|
||||
onSearchQueryChange = onSearchQueryChange,
|
||||
placeholder = {
|
||||
Text(searchPlaceholder ?: stringResource(R.string.text_search))
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
// Scrollable Content
|
||||
// Weight ensures this takes up available space without pushing
|
||||
// the done button off-screen if the list is very long.
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f, fill = false)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
dropdownContent()
|
||||
}
|
||||
|
||||
// Pinned Done button (optional, for multi-select)
|
||||
if (showDoneButton && onDoneClick != null) {
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
AppButton(
|
||||
onClick = {
|
||||
onDoneClick()
|
||||
onDismissRequest() // Often expected to close on 'Done'
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.label_done))
|
||||
}
|
||||
}
|
||||
|
||||
// Extra padding for the system navigation bar so the bottom
|
||||
// item/button isn't cut off by gesture hints or software keys.
|
||||
Spacer(modifier = Modifier.navigationBarsPadding().height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// UNIFIED DROPDOWN SEARCH FIELD
|
||||
// =========================================
|
||||
|
||||
/**
|
||||
* A standardized search field for dropdown menus.
|
||||
* Provides consistent styling across all dropdowns with search functionality.
|
||||
*/
|
||||
@Composable
|
||||
fun DropdownSearchField(
|
||||
modifier: Modifier = Modifier,
|
||||
searchQuery: String,
|
||||
onSearchQueryChange: (String) -> Unit,
|
||||
placeholder: @Composable () -> Unit = { Text(stringResource(R.string.text_search)) },
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = 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 = onSearchQueryChange,
|
||||
placeholder = placeholder,
|
||||
singleLine = true,
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
disabledContainerColor = Color.Transparent,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
cursorColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
if (searchQuery.isNotBlank()) {
|
||||
IconButton(
|
||||
onClick = { onSearchQueryChange("") },
|
||||
modifier = Modifier.size(24.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = AppIcons.Close,
|
||||
contentDescription = stringResource(R.string.cd_clear_search),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview(showBackground = true, name = "Search Field - Empty")
|
||||
@Composable
|
||||
fun DropdownSearchFieldEmptyPreview() {
|
||||
MaterialTheme {
|
||||
Surface {
|
||||
DropdownSearchField(
|
||||
searchQuery = "",
|
||||
onSearchQueryChange = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview(showBackground = true, name = "Search Field - Filled")
|
||||
@Composable
|
||||
fun DropdownSearchFieldFilledPreview() {
|
||||
MaterialTheme {
|
||||
Surface {
|
||||
DropdownSearchField(
|
||||
searchQuery = "English",
|
||||
onSearchQueryChange = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview(showBackground = true, name = "Search Field - With Close Button")
|
||||
@Composable
|
||||
fun DropdownSearchFieldWithClosePreview() {
|
||||
MaterialTheme {
|
||||
Surface {
|
||||
DropdownSearchField(
|
||||
searchQuery = "German",
|
||||
onSearchQueryChange = {}
|
||||
// Providing this triggers the right-most close icon
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview(showBackground = true, name = "Search Field - Interactive")
|
||||
@Composable
|
||||
fun DropdownSearchFieldInteractivePreview() {
|
||||
MaterialTheme {
|
||||
Surface {
|
||||
var query by remember { mutableStateOf("") }
|
||||
DropdownSearchField(
|
||||
searchQuery = query,
|
||||
onSearchQueryChange = { query = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// UNIFIED DROPDOWN HEADER
|
||||
// =========================================
|
||||
|
||||
/**
|
||||
* A standardized header for dropdown sections.
|
||||
* Provides consistent styling for section headers like "Favorites", "Recent", etc.
|
||||
*/
|
||||
@Composable
|
||||
fun DropdownHeader(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.Bold),
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f),
|
||||
modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// UNIFIED DROPDOWN ITEM COMPONENT
|
||||
// =========================================
|
||||
|
||||
@Composable
|
||||
fun AppDropdownMenuItem(
|
||||
text: @Composable () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
selected: Boolean = false,
|
||||
) {
|
||||
val contentColor by animateColorAsState(
|
||||
targetValue = DropdownDefaults.itemContentColor(selected, enabled),
|
||||
label = "contentColor"
|
||||
)
|
||||
val backgroundColor by animateColorAsState(
|
||||
targetValue = DropdownDefaults.itemBackground(selected),
|
||||
label = "backgroundColor"
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = DropdownDefaults.itemPaddingHorizontal,
|
||||
vertical = DropdownDefaults.itemPaddingVertical
|
||||
)
|
||||
.clip(DropdownDefaults.shape)
|
||||
.background(backgroundColor)
|
||||
.clickable(enabled = enabled) { onClick() }
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
leadingIcon?.invoke()
|
||||
if (leadingIcon != null) {
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
}
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
CompositionLocalProvider(LocalContentColor provides contentColor) {
|
||||
text()
|
||||
}
|
||||
}
|
||||
if (trailingIcon != null) {
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
trailingIcon()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A lightweight, modern dropdown menu composable with a clean text field and dropdown list.
|
||||
*/
|
||||
@Suppress("unused", "HardCodedStringLiteral")
|
||||
@Composable
|
||||
fun AppDropdownMenu(
|
||||
expanded: Boolean,
|
||||
@@ -77,11 +490,10 @@ fun AppDropdownMenu(
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
selectedText: String = "",
|
||||
onExpandRequest: () -> Unit = {},
|
||||
content: @Composable androidx.compose.foundation.layout.ColumnScope.() -> Unit,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
var textFieldSize by remember { mutableStateOf(Size.Zero) }
|
||||
|
||||
val interactionSource = remember { androidx.compose.foundation.interaction.MutableInteractionSource() }
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
Column(modifier = modifier) {
|
||||
OutlinedTextField(
|
||||
@@ -102,21 +514,20 @@ fun AppDropdownMenu(
|
||||
label = label,
|
||||
placeholder = placeholder,
|
||||
trailingIcon = {
|
||||
val icon = if (expanded) AppIcons.ArrowDropUp else AppIcons.ArrowDropDown
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
imageVector = if (expanded) AppIcons.ArrowDropUp else AppIcons.ArrowDropDown,
|
||||
contentDescription = if (expanded) stringResource(R.string.cd_collapse) else stringResource(R.string.cd_expand),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
shape = ComponentDefaults.DefaultShape,
|
||||
shape = DropdownDefaults.shape,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = ComponentDefaults.ALPHA_LOW),
|
||||
unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.6f),
|
||||
focusedLabelColor = MaterialTheme.colorScheme.primary,
|
||||
cursorColor = MaterialTheme.colorScheme.primary,
|
||||
disabledBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = ComponentDefaults.ALPHA_LOW),
|
||||
disabledLabelColor = MaterialTheme.colorScheme.onSurface.copy(alpha = ComponentDefaults.ALPHA_MEDIUM)
|
||||
disabledBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
|
||||
disabledLabelColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||
),
|
||||
enabled = enabled,
|
||||
interactionSource = interactionSource
|
||||
@@ -125,152 +536,51 @@ fun AppDropdownMenu(
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
modifier = Modifier
|
||||
.width(with(LocalDensity.current) { textFieldSize.width.toDp() }),
|
||||
offset = DpOffset(0.dp, 0.dp),
|
||||
scrollState = rememberScrollState(),
|
||||
modifier = Modifier.width(with(LocalDensity.current) { textFieldSize.width.toDp() }),
|
||||
offset = DpOffset(0.dp, 2.dp),
|
||||
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 = DropdownDefaults.shape,
|
||||
containerColor = DropdownDefaults.containerColor()
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
// =========================================
|
||||
// LEGACY COMPONENTS (UPDATED TO USE UNIFIED STYLES)
|
||||
// =========================================
|
||||
|
||||
@Composable
|
||||
fun AppDropdownMenuItem(
|
||||
text: @Composable () -> Unit,
|
||||
fun LargeDropdownMenuItem(
|
||||
text: String,
|
||||
selected: Boolean,
|
||||
enabled: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
selected: Boolean = false,
|
||||
) {
|
||||
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
|
||||
}
|
||||
val contentColor = DropdownDefaults.itemContentColor(selected, enabled)
|
||||
val backgroundColor = DropdownDefaults.itemBackground(selected)
|
||||
val fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = enabled) { onClick() }
|
||||
) {
|
||||
androidx.compose.foundation.layout.Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
||||
) {
|
||||
leadingIcon?.invoke()
|
||||
if (leadingIcon != null) {
|
||||
androidx.compose.foundation.layout.Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
CompositionLocalProvider(LocalContentColor provides contentColor) {
|
||||
text()
|
||||
}
|
||||
}
|
||||
if (trailingIcon != null) {
|
||||
androidx.compose.foundation.layout.Spacer(modifier = Modifier.width(8.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 = {}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = DropdownDefaults.itemPaddingHorizontal,
|
||||
vertical = DropdownDefaults.itemPaddingVertical
|
||||
)
|
||||
.clip(DropdownDefaults.shape)
|
||||
.background(backgroundColor)
|
||||
.clickable(enabled) { onClick() }
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp)
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.titleSmall.copy(fontWeight = fontWeight),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -282,12 +592,12 @@ fun <T> LargeDropdownMenu(
|
||||
items: List<T>,
|
||||
selectedIndex: Int = -1,
|
||||
onItemSelected: (index: Int, item: T) -> Unit,
|
||||
selectedItemToString: (T) -> String = { it.toString() },
|
||||
drawItem: @Composable (T, Boolean, Boolean, () -> Unit) -> Unit = { item, selected, itemEnabled, onClick ->
|
||||
selectedItemToString: (T) -> String = { item: T -> item.toString() },
|
||||
drawItem: @Composable (T, Boolean, Boolean, () -> Unit) -> Unit = { item: T, selected: Boolean, _: Boolean, onClick: () -> Unit ->
|
||||
LargeDropdownMenuItem(
|
||||
text = item.toString(),
|
||||
selected = selected,
|
||||
enabled = itemEnabled,
|
||||
enabled = true,
|
||||
onClick = onClick,
|
||||
)
|
||||
},
|
||||
@@ -308,7 +618,6 @@ fun <T> LargeDropdownMenu(
|
||||
readOnly = true,
|
||||
)
|
||||
|
||||
// Transparent clickable surface on top of OutlinedTextField
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -320,11 +629,11 @@ fun <T> LargeDropdownMenu(
|
||||
}
|
||||
|
||||
if (expanded) {
|
||||
Dialog(
|
||||
onDismissRequest = { expanded = true },
|
||||
) {
|
||||
Dialog(onDismissRequest = { expanded = false }) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
tonalElevation = 6.dp
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
if (selectedIndex > -1) {
|
||||
@@ -333,7 +642,11 @@ fun <T> LargeDropdownMenu(
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth(), state = listState) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = listState,
|
||||
contentPadding = PaddingValues(vertical = 8.dp)
|
||||
) {
|
||||
if (notSetLabel != null) {
|
||||
item {
|
||||
LargeDropdownMenuItem(
|
||||
@@ -344,7 +657,7 @@ fun <T> LargeDropdownMenu(
|
||||
)
|
||||
}
|
||||
}
|
||||
itemsIndexed(items) { index, item ->
|
||||
itemsIndexed(items) { index: Int, item: T ->
|
||||
val selectedItem = index == selectedIndex
|
||||
drawItem(
|
||||
item,
|
||||
@@ -354,10 +667,6 @@ fun <T> LargeDropdownMenu(
|
||||
onItemSelected(index, item)
|
||||
expanded = false
|
||||
}
|
||||
|
||||
if (index < items.lastIndex) {
|
||||
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -365,31 +674,7 @@ fun <T> LargeDropdownMenu(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LargeDropdownMenuItem(
|
||||
text: String,
|
||||
selected: Boolean,
|
||||
enabled: Boolean,
|
||||
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)
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalContentColor provides contentColor) {
|
||||
Box(modifier = Modifier
|
||||
.clickable(enabled) { onClick() }
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// ============== PREVIEWS ==============
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview(showBackground = true)
|
||||
@@ -415,6 +700,30 @@ fun LargeDropdownMenuItemSelectedPreview() {
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun AppDropdownMenuItemPreview() {
|
||||
AppDropdownMenuItem(
|
||||
text = { Text("Sample Item") },
|
||||
onClick = {},
|
||||
selected = false,
|
||||
enabled = true
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun AppDropdownMenuItemSelectedPreview() {
|
||||
AppDropdownMenuItem(
|
||||
text = { Text("Selected Item") },
|
||||
onClick = {},
|
||||
selected = true,
|
||||
enabled = true
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
@@ -426,53 +735,8 @@ fun LargeDropdownMenuPreview() {
|
||||
label = "Select Option",
|
||||
items = options,
|
||||
selectedIndex = selectedIndex,
|
||||
onItemSelected = { index, _ ->
|
||||
onItemSelected = { index: Int, _: String ->
|
||||
selectedIndex = index
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun LargeDropdownMenuExpandedPreview() {
|
||||
val options = listOf("Option 1", "Option 2", "Option 3", "Option 4", "Option 5", "Option 6")
|
||||
var selectedIndex by remember { mutableIntStateOf(2) }
|
||||
|
||||
// Simulate expanded state by showing the dropdown and the dialog content
|
||||
Column {
|
||||
LargeDropdownMenu(
|
||||
label = "Select Option",
|
||||
items = options,
|
||||
selectedIndex = selectedIndex,
|
||||
onItemSelected = { index, _ ->
|
||||
selectedIndex = index
|
||||
}
|
||||
)
|
||||
|
||||
// Manually show the expanded dialog content for preview
|
||||
Dialog(onDismissRequest = {}) {
|
||||
Surface(shape = RoundedCornerShape(12.dp)) {
|
||||
val listState = rememberLazyListState()
|
||||
LaunchedEffect("ScrollToSelected") {
|
||||
listState.scrollToItem(index = selectedIndex)
|
||||
}
|
||||
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth(), state = listState) {
|
||||
itemsIndexed(options) { index, item ->
|
||||
LargeDropdownMenuItem(
|
||||
text = item,
|
||||
selected = index == selectedIndex,
|
||||
enabled = true,
|
||||
onClick = { selectedIndex = index }
|
||||
)
|
||||
|
||||
if (index < options.lastIndex) {
|
||||
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -159,14 +159,14 @@ private fun MenuItem(
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.shadow(elevation = 8.dp, shape = ComponentDefaults.DefaultShape)
|
||||
.glassmorphic(shape = RoundedCornerShape(16.dp), alpha = 0.4f)
|
||||
.clickable(
|
||||
onClick = onClick,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null
|
||||
),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceContainer
|
||||
color = Color.Transparent // Allow glassmorphic modifier to handle color
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
@@ -197,15 +197,3 @@ private fun MenuItem(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun MenuItemPreview() {
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
MenuItem(
|
||||
text = "Menu Item",
|
||||
imageVector = AppIcons.Add,
|
||||
painter = null,
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
@@ -69,10 +69,8 @@ fun <T : TabItem> AppTabLayout(
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp, horizontal = 8.dp)
|
||||
.height(56.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||
shape = ComponentDefaults.CardShape
|
||||
)
|
||||
// Replace background with glassmorphic extension
|
||||
.glassmorphic(shape = ComponentDefaults.CardShape, alpha = 0.3f)
|
||||
) {
|
||||
val tabWidth = maxWidth / tabs.size
|
||||
|
||||
@@ -89,7 +87,7 @@ fun <T : TabItem> AppTabLayout(
|
||||
.fillMaxHeight()
|
||||
.padding(4.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f), // Glassy indicator
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -25,11 +25,14 @@ 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.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
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
|
||||
|
||||
@@ -40,14 +43,21 @@ fun AppTopAppBar(
|
||||
onNavigateBack: (() -> Unit)? = null,
|
||||
navigationIcon: @Composable (() -> Unit)? = null,
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),
|
||||
hintContent: @Composable (() -> Unit)? = null
|
||||
colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color.Transparent,
|
||||
scrolledContainerColor = Color.Transparent
|
||||
),
|
||||
hintContent: Hint? = null
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
var showBottomSheet by remember { mutableStateOf(false) }
|
||||
|
||||
Surface(
|
||||
modifier = modifier.glassmorphic(shape = RectangleShape, alpha = 0.2f),
|
||||
color = Color.Transparent
|
||||
) {
|
||||
TopAppBar(
|
||||
modifier = modifier.height(56.dp),
|
||||
modifier = Modifier.height(56.dp),
|
||||
windowInsets = WindowInsets(0.dp),
|
||||
colors = colors,
|
||||
title = {
|
||||
@@ -103,6 +113,7 @@ fun AppTopAppBar(
|
||||
},
|
||||
actions = actions
|
||||
)
|
||||
}
|
||||
|
||||
if (showBottomSheet) {
|
||||
HintBottomSheet(
|
||||
@@ -111,7 +122,9 @@ fun AppTopAppBar(
|
||||
showBottomSheet = false
|
||||
},
|
||||
sheetState = sheetState,
|
||||
content = hintContent
|
||||
content = {
|
||||
hintContent?.Render()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
@@ -28,6 +29,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
@@ -100,24 +102,25 @@ fun BottomNavigationBar(
|
||||
targetOffsetY = { it }
|
||||
)
|
||||
) {
|
||||
|
||||
val baseHeight = if (showLabels) 80.dp else 56.dp
|
||||
val density = LocalDensity.current
|
||||
val navBarDp = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() }
|
||||
val height = baseHeight + navBarDp
|
||||
|
||||
NavigationBar(
|
||||
modifier = modifier.height(height),
|
||||
containerColor = MaterialTheme.colorScheme.surface, // Cleaner look than surfaceVariant
|
||||
tonalElevation = 8.dp, // Slight elevation for depth
|
||||
modifier = modifier
|
||||
.height(height)
|
||||
// Apply glassmorphism on the top corners
|
||||
.glassmorphic(shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), alpha = 0.35f),
|
||||
containerColor = Color.Transparent, // Let the glass shine through
|
||||
tonalElevation = 0.dp,
|
||||
) {
|
||||
screens.forEach { screen ->
|
||||
val isSelected = screen == selectedItem
|
||||
val title = stringResource(id = screen.title)
|
||||
|
||||
// 1. Spring Animation for the Icon Scale
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = if (isSelected) 1.2f else 1.0f, // Subtle pop effect
|
||||
targetValue = if (isSelected) 1.2f else 1.0f,
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
@@ -129,7 +132,7 @@ fun BottomNavigationBar(
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
if (!isSelected) {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress) // 2. Tactile feedback
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onItemSelected(screen)
|
||||
}
|
||||
},
|
||||
@@ -145,17 +148,16 @@ fun BottomNavigationBar(
|
||||
}
|
||||
} else null,
|
||||
icon = {
|
||||
// 3. Crossfade between Outlined and Filled icons
|
||||
Crossfade(targetState = isSelected, label = "iconFade") { selected ->
|
||||
Icon(
|
||||
imageVector = if (selected) screen.selectedIcon else screen.unselectedIcon,
|
||||
contentDescription = title,
|
||||
modifier = Modifier.scale(scale) // Apply the spring scale
|
||||
modifier = Modifier.scale(scale)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
indicatorColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
indicatorColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f), // Glassy indicator
|
||||
selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
selectedTextColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
|
||||
@@ -5,6 +5,8 @@ package eu.gaudian.translator.view.composable
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -43,6 +45,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.draw.shadow
|
||||
@@ -55,49 +58,51 @@ import androidx.compose.ui.unit.dp
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||
import eu.gaudian.translator.ui.theme.semanticColors
|
||||
import eu.gaudian.translator.view.composable.ComponentDefaults.DefaultElevation
|
||||
|
||||
|
||||
object ComponentDefaults {
|
||||
// Sizing
|
||||
val DefaultButtonHeight = 48.dp
|
||||
val CardPadding = 8.dp
|
||||
|
||||
// Elevation
|
||||
val DefaultElevation = 0.dp
|
||||
val NoElevation = 0.dp
|
||||
|
||||
// Borders
|
||||
val DefaultBorderWidth = 1.dp
|
||||
|
||||
// Shapes
|
||||
val DefaultCornerRadius = 16.dp
|
||||
val CardClipRadius = 8.dp
|
||||
val CardClipRadius = 16.dp // Increased slightly for softer glass look
|
||||
val NoRounding = 0.dp
|
||||
val DefaultShape = RoundedCornerShape(DefaultCornerRadius)
|
||||
val CardClipShape = RoundedCornerShape(CardClipRadius)
|
||||
val CardShape = RoundedCornerShape(DefaultCornerRadius)
|
||||
val NoShape = RoundedCornerShape(NoRounding)
|
||||
|
||||
// Opacity Levels
|
||||
const val ALPHA_HIGH = 0.6f
|
||||
const val ALPHA_MEDIUM = 0.5f
|
||||
const val ALPHA_LOW = 0.3f
|
||||
const val ALPHA_MEDIUM = 0.4f
|
||||
const val ALPHA_LOW = 0.2f // Adjusted for glass
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A styled card container for displaying content with a consistent floating look.
|
||||
*
|
||||
* @param modifier The modifier to be applied to the card.
|
||||
* @param content The content to be displayed inside the card.
|
||||
* Standard Glassmorphism Modifier
|
||||
*/
|
||||
fun Modifier.glassmorphic(
|
||||
shape: Shape = ComponentDefaults.DefaultShape,
|
||||
alpha: Float = ComponentDefaults.ALPHA_LOW,
|
||||
borderAlpha: Float = 0.15f
|
||||
): Modifier = composed {
|
||||
this
|
||||
.shadow(elevation = 8.dp, shape = shape, spotColor = Color.Black.copy(alpha = 0.05f))
|
||||
.clip(shape)
|
||||
.background(MaterialTheme.colorScheme.surface.copy(alpha = alpha))
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = borderAlpha),
|
||||
shape = shape
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppCard(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String? = null,
|
||||
icon: ImageVector? = null, // New optional icon parameter
|
||||
icon: ImageVector? = null,
|
||||
text: String? = null,
|
||||
expandable: Boolean = false,
|
||||
initiallyExpanded: Boolean = false,
|
||||
@@ -110,25 +115,17 @@ fun AppCard(
|
||||
label = "Chevron Rotation"
|
||||
)
|
||||
|
||||
// Check if we need to render the header row
|
||||
// Updated to include icon in the check
|
||||
val hasHeader = title != null || text != null || expandable || icon != null
|
||||
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.shadow(
|
||||
DefaultElevation,
|
||||
shape = ComponentDefaults.CardShape
|
||||
)
|
||||
.clip(ComponentDefaults.CardClipShape)
|
||||
// Animate height changes when expanding/collapsing
|
||||
.glassmorphic(shape = ComponentDefaults.CardShape, alpha = 0.25f)
|
||||
.animateContentSize(),
|
||||
shape = ComponentDefaults.CardShape,
|
||||
color = MaterialTheme.colorScheme.surfaceContainer
|
||||
color = Color.Transparent // Let glassmorphic handle the background
|
||||
) {
|
||||
Column {
|
||||
// --- Header Row ---
|
||||
if (hasHeader) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -137,7 +134,6 @@ fun AppCard(
|
||||
.padding(ComponentDefaults.CardPadding),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 1. Optional Icon on the left
|
||||
if (icon != null) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
@@ -148,7 +144,6 @@ fun AppCard(
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
}
|
||||
|
||||
// 2. Title and Text Column
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
if (!title.isNullOrBlank()) {
|
||||
Text(
|
||||
@@ -157,12 +152,9 @@ fun AppCard(
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
|
||||
// Only show spacer if both title and text exist
|
||||
if (!title.isNullOrBlank() && !text.isNullOrBlank()) {
|
||||
Spacer(Modifier.size(4.dp))
|
||||
}
|
||||
|
||||
if (!text.isNullOrBlank()) {
|
||||
Text(
|
||||
text = text,
|
||||
@@ -172,7 +164,6 @@ fun AppCard(
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Expand Chevron (Far right)
|
||||
if (expandable) {
|
||||
Icon(
|
||||
imageVector = AppIcons.ArrowDropDown,
|
||||
@@ -184,15 +175,12 @@ fun AppCard(
|
||||
}
|
||||
}
|
||||
|
||||
// --- Content Area ---
|
||||
if (!expandable || isExpanded) {
|
||||
Column(
|
||||
modifier = Modifier.padding(
|
||||
start = ComponentDefaults.CardPadding,
|
||||
end = ComponentDefaults.CardPadding,
|
||||
bottom = ComponentDefaults.CardPadding,
|
||||
// If we have a header, remove the top padding so content sits closer to the title.
|
||||
// If no header (legacy behavior), keep the top padding.
|
||||
top = if (hasHeader) 0.dp else ComponentDefaults.CardPadding
|
||||
),
|
||||
content = content
|
||||
@@ -304,31 +292,27 @@ fun AppButton(
|
||||
modifier: Modifier? = Modifier,
|
||||
enabled: Boolean = true,
|
||||
shape: Shape? = null,
|
||||
colors: ButtonColors = ButtonDefaults.buttonColors(),
|
||||
elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
|
||||
colors: ButtonColors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f) // Glassy primary
|
||||
),
|
||||
elevation: ButtonElevation? = ButtonDefaults.buttonElevation(defaultElevation = 0.dp),
|
||||
border: BorderStroke? = null,
|
||||
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
content: @Composable RowScope.() -> Unit
|
||||
) {
|
||||
|
||||
val m = modifier ?: Modifier.height(ComponentDefaults.DefaultButtonHeight)
|
||||
val s = shape ?: ComponentDefaults.DefaultShape
|
||||
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = m,
|
||||
modifier = m.border(1.dp, MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.2f), s),
|
||||
enabled = enabled,
|
||||
shape = s,
|
||||
colors = colors,
|
||||
elevation = elevation,
|
||||
border = border,
|
||||
contentPadding = PaddingValues(
|
||||
start = 8.dp, // More horizontal padding
|
||||
end = 8.dp,
|
||||
top = 8.dp, // Default vertical padding
|
||||
bottom = 8.dp
|
||||
),
|
||||
contentPadding = PaddingValues(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 8.dp),
|
||||
interactionSource = interactionSource
|
||||
) {
|
||||
content()
|
||||
@@ -368,11 +352,7 @@ fun AppOutlinedButton(
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PrimaryButtonWithIconPreview() {
|
||||
PrimaryButton(onClick = { }, text = stringResource(R.string.primary_with_icon), icon = AppIcons.Add)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The secondary button for less prominent actions.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("AssignedValueIsNeverRead", "HardCodedStringLiteral")
|
||||
|
||||
package eu.gaudian.translator.view.composable
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -6,20 +8,19 @@ 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.heightIn
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
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
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -34,7 +35,6 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -83,7 +83,10 @@ fun BaseLanguageDropDown(
|
||||
else -> stringResource(R.string.label_language_none)
|
||||
}
|
||||
|
||||
Box(modifier = modifier) {
|
||||
Box(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
AppOutlinedButton(
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
onClick = { expanded = true },
|
||||
@@ -105,23 +108,27 @@ fun BaseLanguageDropDown(
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenu(modifier = modifier.fillMaxWidth(), expanded = expanded, onDismissRequest = {
|
||||
if (expanded) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = {
|
||||
expanded = false
|
||||
searchText = ""
|
||||
tempSelection = emptyList() // Also reset temp selection on dismiss
|
||||
}) {
|
||||
// Helper composable for a single language row in multiple selection mode
|
||||
tempSelection = emptyList()
|
||||
},
|
||||
sheetState = sheetState
|
||||
) {
|
||||
@Composable
|
||||
fun MultiSelectItem(language: Language) {
|
||||
val isSelected = tempSelection.contains(language)
|
||||
DropdownMenuItem(
|
||||
AppDropdownMenuItem(
|
||||
text = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||
AppCheckbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = {
|
||||
tempSelection = if (isSelected) tempSelection - language else tempSelection + language
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
selectedLanguagesCount = tempSelection.size
|
||||
onLanguagesSelected(tempSelection)
|
||||
}
|
||||
@@ -142,20 +149,18 @@ fun BaseLanguageDropDown(
|
||||
},
|
||||
onClick = {
|
||||
tempSelection = if (isSelected) tempSelection - language else tempSelection + language
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
selectedLanguagesCount = tempSelection.size
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Helper composable for a single language row in single selection mode
|
||||
@Composable
|
||||
fun SingleSelectItem(language: Language) {
|
||||
val languageNames = languages.map { it.name }
|
||||
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 {
|
||||
@@ -198,43 +203,22 @@ fun BaseLanguageDropDown(
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// --- Main Dropdown Content ---
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.heightIn(max = 900.dp) // Constrain the height
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
// Search bar with a back arrow
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton(onClick = { expanded = false; searchText = "" }) {
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.label_close))
|
||||
}
|
||||
TextField(
|
||||
value = searchText,
|
||||
onValueChange = { searchText = it },
|
||||
singleLine = true,
|
||||
DropdownSearchField(
|
||||
searchQuery = searchText,
|
||||
onSearchQueryChange = { searchText = it },
|
||||
placeholder = { Text(stringResource(R.string.text_search_3d)) },
|
||||
trailingIcon = {
|
||||
if (searchText.isNotBlank()) {
|
||||
IconButton(onClick = { searchText = "" }) {
|
||||
Icon(imageVector = AppIcons.Close, contentDescription = stringResource(R.string.cd_clear_search))
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
disabledContainerColor = Color.Transparent,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
|
||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
// Replaced height(max = 900.dp) with standard weight logic to allow proper scrolling bounds
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
val isSearching = searchText.isNotBlank()
|
||||
|
||||
if (isSearching) {
|
||||
@@ -256,80 +240,91 @@ fun BaseLanguageDropDown(
|
||||
} else if (alternateLanguages.isNotEmpty()) {
|
||||
val sortedAlternate = alternateLanguages.sortedBy { it.name }
|
||||
if (enableMultipleSelection) {
|
||||
Text(stringResource(R.string.text_all_languages), style = TextStyle(fontWeight = FontWeight.Bold), modifier = Modifier.padding(8.dp))
|
||||
DropdownHeader(text = stringResource(R.string.text_all_languages))
|
||||
sortedAlternate.forEach { language -> MultiSelectItem(language) }
|
||||
} else {
|
||||
Text(stringResource(R.string.text_all_languages), style = TextStyle(fontWeight = FontWeight.Bold), modifier = Modifier.padding(8.dp))
|
||||
DropdownHeader(text = stringResource(R.string.text_all_languages))
|
||||
sortedAlternate.forEach { language -> SingleSelectItem(language) }
|
||||
}
|
||||
|
||||
} else {
|
||||
if (enableMultipleSelection) {
|
||||
if (favoriteLanguages.isNotEmpty()) {
|
||||
Text(stringResource(R.string.text_favorites), style = TextStyle(fontWeight = FontWeight.Bold), modifier = Modifier.padding(8.dp))
|
||||
DropdownHeader(text = stringResource(R.string.text_favorites))
|
||||
favoriteLanguages.forEach { language -> MultiSelectItem(language) }
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
}
|
||||
val recentHistoryFiltered = languageHistory.filter { it !in favoriteLanguages }.takeLast(5)
|
||||
if (recentHistoryFiltered.isNotEmpty() && alternateLanguages.isEmpty()) {
|
||||
Text(stringResource(R.string.text_recent_history), style = TextStyle(fontWeight = FontWeight.Bold), modifier = Modifier.padding(8.dp))
|
||||
DropdownHeader(text = stringResource(R.string.text_recent_history))
|
||||
recentHistoryFiltered.forEach { language -> MultiSelectItem(language) }
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
}
|
||||
val remainingLanguages = languages.sortedBy { it.name }
|
||||
if (remainingLanguages.isNotEmpty()) {
|
||||
Text(stringResource(R.string.text_all_languages), style = TextStyle(fontWeight = FontWeight.Bold), modifier = Modifier.padding(8.dp))
|
||||
DropdownHeader(text = stringResource(R.string.text_all_languages))
|
||||
remainingLanguages.forEach { language -> MultiSelectItem(language) }
|
||||
}
|
||||
} else {
|
||||
// Logic for single selection default view
|
||||
if (showAutoOption) {
|
||||
DropdownMenuItem(text = { Text(stringResource(R.string.text_select_auto_recognition)) }, onClick = { onAutoSelected(); expanded = false; searchText = "" })
|
||||
LargeDropdownMenuItem(
|
||||
text = stringResource(R.string.text_select_auto_recognition),
|
||||
selected = false, // Set to true if you want to highlight it when active
|
||||
enabled = true,
|
||||
onClick = {
|
||||
onAutoSelected()
|
||||
expanded = false
|
||||
searchText = ""
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
if (showNoneOption) {
|
||||
DropdownMenuItem(text = { Text(stringResource(R.string.text_select_none)) }, onClick = { onNoneSelected(); expanded = false; searchText = "" })
|
||||
LargeDropdownMenuItem(
|
||||
text = stringResource(R.string.text_select_no_language),
|
||||
selected = false, // Set to true if you want to highlight it when active
|
||||
enabled = true,
|
||||
onClick = {
|
||||
onNoneSelected()
|
||||
expanded = false
|
||||
searchText = ""
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
if (favoriteLanguages.any {
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
it.code != "none" && it.code != "auto"
|
||||
}) {
|
||||
Text(stringResource(R.string.text_favorites), style = TextStyle(fontWeight = FontWeight.Bold), modifier = Modifier.padding(8.dp))
|
||||
DropdownHeader(text = stringResource(R.string.text_favorites))
|
||||
favoriteLanguages.filter {
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
it.code != "none" && it.code != "auto"
|
||||
}.forEach { language -> SingleSelectItem(language) }
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
}
|
||||
val recentHistoryFiltered = languageHistory.filter {
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
it !in favoriteLanguages && it.code != "none" && it.code != "auto"
|
||||
}.takeLast(5)
|
||||
if (recentHistoryFiltered.isNotEmpty()) {
|
||||
Text(stringResource(R.string.text_recent_history), style = TextStyle(fontWeight = FontWeight.Bold), modifier = Modifier.padding(8.dp))
|
||||
DropdownHeader(text = stringResource(R.string.text_recent_history))
|
||||
recentHistoryFiltered.forEach { language -> SingleSelectItem(language) }
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
}
|
||||
val remainingLanguages = languages.filter {
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
it !in favoriteLanguages && it !in recentHistoryFiltered && it.code != "none" && it.code != "auto"
|
||||
}.sortedBy { it.name }
|
||||
if (remainingLanguages.isNotEmpty()) {
|
||||
Text(stringResource(R.string.text_all_languages), style = TextStyle(fontWeight = FontWeight.Bold), modifier = Modifier.padding(8.dp))
|
||||
DropdownHeader(text = stringResource(R.string.text_all_languages))
|
||||
remainingLanguages.forEach { language -> SingleSelectItem(language) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Done button for multiple selection mode
|
||||
if (enableMultipleSelection) {
|
||||
HorizontalDivider()
|
||||
AppButton(
|
||||
onClick = {
|
||||
onLanguagesSelected(tempSelection)
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
selectedLanguagesCount = tempSelection.size
|
||||
expanded = false
|
||||
searchText = ""
|
||||
@@ -341,6 +336,10 @@ fun BaseLanguageDropDown(
|
||||
Text(stringResource(R.string.label_done))
|
||||
}
|
||||
}
|
||||
|
||||
// Provides breathing room for system gestures at bottom of sheet
|
||||
Spacer(modifier = Modifier.navigationBarsPadding().height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,10 +48,11 @@ import eu.gaudian.translator.view.composable.AppDialog
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedTextField
|
||||
import eu.gaudian.translator.view.composable.MultipleLanguageDropdown
|
||||
import eu.gaudian.translator.view.hints.CategoryHint
|
||||
import eu.gaudian.translator.view.hints.HintDefinition
|
||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
|
||||
|
||||
enum class DialogCategoryType { TAG, FILTER }
|
||||
|
||||
@Composable
|
||||
@@ -79,7 +80,7 @@ fun AddCategoryDialog(
|
||||
AppDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(R.string.label_add_category)) },
|
||||
hintContent = { CategoryHint() },
|
||||
hintContent = HintDefinition.CATEGORY.hint(),
|
||||
content = {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
|
||||
|
||||
@@ -41,6 +41,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.VocabularyItem
|
||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||
import eu.gaudian.translator.utils.StatusMessageService
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.LocalConnectionConfigured
|
||||
import eu.gaudian.translator.view.composable.AppButton
|
||||
@@ -50,7 +51,6 @@ import eu.gaudian.translator.view.composable.AppOutlinedTextField
|
||||
import eu.gaudian.translator.view.composable.SourceLanguageDropdown
|
||||
import eu.gaudian.translator.view.composable.TargetLanguageDropdown
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
import eu.gaudian.translator.viewmodel.StatusViewModel
|
||||
import eu.gaudian.translator.viewmodel.TranslationViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -67,12 +67,12 @@ fun AddVocabularyDialog(
|
||||
showMultiple: Boolean = true
|
||||
) {
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val statusViewModel: StatusViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val vocabularyViewModel = hiltViewModel<VocabularyViewModel>(viewModelStoreOwner = activity)
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val translationViewModel: TranslationViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val connectionConfigured = LocalConnectionConfigured.current
|
||||
val statusMessageService = StatusMessageService
|
||||
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ fun AddVocabularyDialog(
|
||||
selectedTranslations.clear()
|
||||
}
|
||||
.onFailure { exception ->
|
||||
statusViewModel.showErrorMessage(
|
||||
statusMessageService.showErrorMessage(
|
||||
textFailedToGetTranslations + exception.message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
@file:Suppress("AssignedValueIsNeverRead", "HardCodedStringLiteral")
|
||||
|
||||
package eu.gaudian.translator.view.dialogs
|
||||
|
||||
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.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
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
|
||||
@@ -22,157 +23,195 @@ 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.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.ui.theme.ThemePreviews
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppButton
|
||||
import eu.gaudian.translator.view.composable.AppCheckbox
|
||||
import eu.gaudian.translator.view.composable.AppDropdownContainer
|
||||
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.view.composable.DropdownHeader
|
||||
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(),
|
||||
val searchQuery: String = "",
|
||||
)
|
||||
|
||||
/**
|
||||
* Stateless dropdown content composable for category selection.
|
||||
* This component is fully controlled by its parameters and does not maintain any internal state.
|
||||
*/
|
||||
@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,
|
||||
onSearchQueryChange: (String) -> Unit = {},
|
||||
noneSelectable: Boolean = true,
|
||||
multipleSelectable: Boolean = false,
|
||||
onlyLists: Boolean = false,
|
||||
addCategory: Boolean = false
|
||||
addCategory: Boolean = false,
|
||||
enableSearch: 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())
|
||||
|
||||
// Filter categories by search query if search is enabled
|
||||
val filteredCategories = if (enableSearch && state.searchQuery.isNotBlank()) {
|
||||
selectableCategories.filter { category ->
|
||||
category.name.contains(state.searchQuery, ignoreCase = true)
|
||||
}
|
||||
} else {
|
||||
selectableCategories
|
||||
}
|
||||
var newCategoryName by remember { mutableStateOf("") }
|
||||
|
||||
val buttonText = when {
|
||||
state.selectedCategories.isEmpty() -> stringResource(R.string.text_select_category)
|
||||
state.selectedCategories.size == 1 -> state.selectedCategories.first()?.name
|
||||
?: stringResource(R.string.label_no_category)
|
||||
else -> stringResource(R.string.text_2d_categories_selected, state.selectedCategories.size)
|
||||
}
|
||||
|
||||
AppOutlinedButton(
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
onClick = { expanded = true },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
||||
AppDropdownContainer(
|
||||
expanded = state.expanded,
|
||||
onDismissRequest = { onExpand(false) },
|
||||
onExpandRequest = { onExpand(true) },
|
||||
buttonText = buttonText,
|
||||
modifier = modifier,
|
||||
showSearch = enableSearch,
|
||||
searchQuery = state.searchQuery,
|
||||
onSearchQueryChange = onSearchQueryChange,
|
||||
searchPlaceholder = stringResource(R.string.text_search),
|
||||
showDoneButton = multipleSelectable,
|
||||
onDoneClick = { onExpand(false) }
|
||||
) {
|
||||
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)
|
||||
},
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = 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 = { _ ->
|
||||
val newSelection = if (noneSelected) {
|
||||
state.selectedCategories.filterNotNull()
|
||||
} else {
|
||||
state.selectedCategories + listOf(null)
|
||||
}
|
||||
onCategorySelected(newSelection)
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
Text(stringResource(R.string.text_none))
|
||||
Text(
|
||||
text = stringResource(R.string.label_no_category),
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
},
|
||||
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)
|
||||
|
||||
filteredCategories.forEach { 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))
|
||||
}
|
||||
Text(category.name)
|
||||
Text(
|
||||
text = category.name,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
},
|
||||
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
|
||||
if (enableSearch && state.searchQuery.isNotBlank() && filteredCategories.isEmpty()) {
|
||||
AppDropdownMenuItem(
|
||||
text = {
|
||||
Text(stringResource(R.string.label_add_category))
|
||||
Text(
|
||||
text = stringResource(R.string.text_no_models_found),
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
},
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(4.dp)
|
||||
enabled = false
|
||||
)
|
||||
}
|
||||
|
||||
if (addCategory) {
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
|
||||
DropdownHeader(text = stringResource(R.string.label_add_category))
|
||||
|
||||
AppDropdownMenuItem(
|
||||
text = {
|
||||
@@ -181,26 +220,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))
|
||||
Spacer(modifier = Modifier.width(4.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,29 +241,191 @@ fun CategoryDropdown(
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = {} // No action on click
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
if (multipleSelectable) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
AppButton(
|
||||
onClick = { expanded = false },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.label_done))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
/**
|
||||
* Stateful wrapper for CategoryDropdown that manages its own state.
|
||||
*/
|
||||
@Composable
|
||||
fun CategoryDropdownPreview() {
|
||||
CategoryDropdown(
|
||||
onCategorySelected = {}
|
||||
fun CategoryDropdown(
|
||||
modifier: Modifier = Modifier,
|
||||
initialCategoryId: Int? = null,
|
||||
onCategorySelected: (List<VocabularyCategory?>) -> Unit,
|
||||
noneSelectable: Boolean? = true,
|
||||
multipleSelectable: Boolean = false,
|
||||
onlyLists: Boolean = false,
|
||||
addCategory: Boolean = false,
|
||||
enableSearch: Boolean = false,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var selectedCategories by remember {
|
||||
mutableStateOf<List<VocabularyCategory?>>(emptyList())
|
||||
}
|
||||
var newCategoryName by remember { mutableStateOf("") }
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
||||
|
||||
val initialCategory = remember(categories, initialCategoryId) {
|
||||
categories.find { it.id == initialCategoryId }
|
||||
}
|
||||
|
||||
remember(initialCategory) {
|
||||
if (initialCategory != null && selectedCategories.isEmpty()) {
|
||||
selectedCategories = listOf(initialCategory)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
CategoryDropdownContent(
|
||||
state = CategoryDropdownState(
|
||||
expanded = expanded,
|
||||
selectedCategories = selectedCategories,
|
||||
newCategoryName = newCategoryName,
|
||||
categories = categories,
|
||||
searchQuery = searchQuery,
|
||||
),
|
||||
onExpand = { isExpanded -> expanded = isExpanded },
|
||||
onCategorySelected = { newSelection ->
|
||||
selectedCategories = newSelection
|
||||
onCategorySelected(newSelection)
|
||||
},
|
||||
onNewCategoryNameChange = { newCategoryName = it },
|
||||
onAddCategory = { name ->
|
||||
val newCategory = TagCategory(id = 0, name = name)
|
||||
newCategoryName = ""
|
||||
categoryViewModel.createCategory(newCategory)
|
||||
//selectedCategories = selectedCategories + newCategory
|
||||
if (!multipleSelectable) {
|
||||
expanded = false
|
||||
}
|
||||
},
|
||||
onSearchQueryChange = { searchQuery = it },
|
||||
noneSelectable = noneSelectable == true,
|
||||
multipleSelectable = multipleSelectable,
|
||||
onlyLists = onlyLists,
|
||||
addCategory = addCategory,
|
||||
enableSearch = enableSearch,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ThemePreviews
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryDropdownCollapsedPreview() {
|
||||
Surface(modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.background) {
|
||||
CategoryDropdownContent(
|
||||
state = CategoryDropdownState(
|
||||
expanded = false,
|
||||
selectedCategories = emptyList(),
|
||||
categories = listOf(TagCategory(1, "Animals"), TagCategory(2, "Food")),
|
||||
),
|
||||
onExpand = {},
|
||||
onCategorySelected = {},
|
||||
onNewCategoryNameChange = {},
|
||||
onAddCategory = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryDropdownExpandedPreview() {
|
||||
Surface(modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.background) {
|
||||
CategoryDropdownContent(
|
||||
state = CategoryDropdownState(
|
||||
expanded = true,
|
||||
selectedCategories = listOf(TagCategory(1, "Animals")),
|
||||
categories = listOf(TagCategory(1, "Animals"), TagCategory(2, "Food"), TagCategory(3, "Travel")),
|
||||
),
|
||||
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"))
|
||||
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() {
|
||||
Surface(modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.background) {
|
||||
CategoryDropdownContent(
|
||||
state = CategoryDropdownState(
|
||||
expanded = true,
|
||||
selectedCategories = emptyList(),
|
||||
newCategoryName = "New Cat",
|
||||
categories = listOf(TagCategory(1, "Animals"), TagCategory(2, "Food")),
|
||||
),
|
||||
onExpand = {},
|
||||
onCategorySelected = {},
|
||||
onNewCategoryNameChange = {},
|
||||
onAddCategory = {},
|
||||
addCategory = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@ThemePreviews
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryDropdownWithSearchPreview() {
|
||||
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, "Technology"),
|
||||
TagCategory(5, "Sports")
|
||||
),
|
||||
searchQuery = "",
|
||||
),
|
||||
onExpand = {},
|
||||
onCategorySelected = {},
|
||||
onNewCategoryNameChange = {},
|
||||
onAddCategory = {},
|
||||
enableSearch = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,40 +6,45 @@ 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.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())
|
||||
|
||||
AppDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = {
|
||||
Text(text = stringResource(R.string.text_select_categories))
|
||||
}) {
|
||||
|
||||
|
||||
|
||||
}
|
||||
) {
|
||||
// Dropdown button and menu
|
||||
CategoryDropdown(
|
||||
onCategorySelected = { categories ->
|
||||
selectedCategory = categories
|
||||
},
|
||||
onCategorySelected = onCategorySelected,
|
||||
noneSelectable = false,
|
||||
multipleSelectable = true,
|
||||
onlyLists = true,
|
||||
addCategory = true
|
||||
addCategory = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Row(
|
||||
@@ -54,10 +59,11 @@ fun CategorySelectionDialog(
|
||||
|
||||
DialogButton(
|
||||
onClick = {
|
||||
onCategorySelected(selectedCategory)
|
||||
// The selected categories are handled by CategoryDropdown's internal state
|
||||
// and passed to onCategorySelected callback
|
||||
onDismissRequest()
|
||||
},
|
||||
enabled = true
|
||||
enabled = true // Always enabled since CategoryDropdown handles validation
|
||||
) {
|
||||
Text(stringResource(R.string.label_confirm))
|
||||
}
|
||||
|
||||
@@ -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.HintDefinition
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -109,7 +109,7 @@ fun ImportDialogContent(
|
||||
AppDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(descriptionText) },
|
||||
hintContent = { getImportVocabularyHint() },
|
||||
hintContent = HintDefinition.IMPORT.hint(),
|
||||
content = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -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
|
||||
@@ -42,12 +44,19 @@ fun StartExerciseDialog(
|
||||
) {
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val vocabularyViewModel : VocabularyViewModel = 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 selectedStages by remember { mutableStateOf<List<VocabularyStage>>(emptyList()) }
|
||||
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory>>(emptyList()) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
coroutineScope.launch {
|
||||
@@ -59,9 +68,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)) }) {
|
||||
|
||||
@@ -81,10 +87,13 @@ fun StartExerciseDialog(
|
||||
languages
|
||||
)
|
||||
CategoryDropdown(
|
||||
onCategorySelected = { categories ->
|
||||
selectedCategories = categories.filterIsInstance<VocabularyCategory>()
|
||||
onCategorySelected = { cats ->
|
||||
selectedCategories = cats.filterIsInstance<VocabularyCategory>()
|
||||
},
|
||||
multipleSelectable = true
|
||||
multipleSelectable = true,
|
||||
onlyLists = false, // Show both filters and lists
|
||||
addCategory = false,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
VocabularyStageDropDown(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
||||
@@ -34,7 +34,8 @@ import eu.gaudian.translator.view.composable.AppButton
|
||||
import eu.gaudian.translator.view.composable.AppCheckbox
|
||||
import eu.gaudian.translator.view.composable.AppScaffold
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.view.hints.getVocabularyReviewHint
|
||||
import eu.gaudian.translator.view.hints.HintDefinition
|
||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
|
||||
@Composable
|
||||
@@ -43,12 +44,15 @@ fun VocabularyReviewScreen(
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val vocabularyViewModel : VocabularyViewModel = hiltViewModel(activity)
|
||||
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()) }
|
||||
|
||||
LaunchedEffect(generatedItems) {
|
||||
val duplicateResults = vocabularyViewModel.findDuplicates(generatedItems)
|
||||
@@ -62,7 +66,7 @@ fun VocabularyReviewScreen(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = { Text(stringResource(R.string.found_items)) },
|
||||
hintContent = { getVocabularyReviewHint() }
|
||||
hintContent = HintDefinition.REVIEW.hint()
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
@@ -128,10 +132,14 @@ fun VocabularyReviewScreen(
|
||||
modifier = Modifier.padding(8.dp)
|
||||
)
|
||||
CategoryDropdown(
|
||||
onCategorySelected = { categories: List<VocabularyCategory?> ->
|
||||
selectedCategoryId = categories.filterNotNull().map { it.id }
|
||||
},
|
||||
onlyLists = true
|
||||
onCategorySelected = { selectedCategories = it },
|
||||
noneSelectable = false,
|
||||
multipleSelectable = true,
|
||||
onlyLists = true,
|
||||
addCategory = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -143,9 +151,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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
@file:Suppress("AssignedValueIsNeverRead")
|
||||
|
||||
package eu.gaudian.translator.view.dialogs
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.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
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -21,15 +16,13 @@ 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.unit.dp
|
||||
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.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
||||
import eu.gaudian.translator.view.composable.AppDropdownContainer
|
||||
import eu.gaudian.translator.view.composable.AppDropdownMenuItem
|
||||
|
||||
|
||||
@Composable
|
||||
@@ -44,46 +37,38 @@ fun VocabularyStageDropDown(
|
||||
var selectedStages by remember { mutableStateOf(preselectedStages) }
|
||||
val context = LocalContext.current
|
||||
|
||||
Box(
|
||||
modifier = modifier,
|
||||
contentAlignment = Alignment.CenterEnd
|
||||
) {
|
||||
AppOutlinedButton(
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
onClick = { expanded = true },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(text = when {
|
||||
val buttonText = when {
|
||||
selectedStages.isEmpty() -> stringResource(R.string.label_select_stage)
|
||||
selectedStages.size == 1 -> selectedStages.first()?.toString(context)?:stringResource(R.string.text_none)
|
||||
selectedStages.size == 1 -> selectedStages.first()?.toString(context) ?: stringResource(R.string.text_none)
|
||||
else -> stringResource(R.string.stages_selected, selectedStages.size)
|
||||
},
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Icon(
|
||||
imageVector = if (expanded) AppIcons.ArrowDropUp else AppIcons.ArrowDropDown,
|
||||
contentDescription = if (expanded) stringResource(R.string.cd_collapse) else stringResource(R.string.cd_expand)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
AppDropdownContainer(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
onExpandRequest = { expanded = true },
|
||||
buttonText = buttonText,
|
||||
modifier = modifier,
|
||||
showDoneButton = multipleSelectable,
|
||||
onDoneClick = { expanded = false }
|
||||
) {
|
||||
if (noneSelectable == true) {
|
||||
val noneSelected = selectedStages.contains(null)
|
||||
DropdownMenuItem(
|
||||
AppDropdownMenuItem(
|
||||
text = {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (multipleSelectable) {
|
||||
AppCheckbox(
|
||||
checked = noneSelected,
|
||||
onCheckedChange = {
|
||||
selectedStages = if (noneSelected) selectedStages.filterNotNull() else selectedStages + listOf(null)
|
||||
selectedStages = if (noneSelected) {
|
||||
selectedStages.filterNotNull()
|
||||
} else {
|
||||
selectedStages + listOf(null)
|
||||
}
|
||||
onStageSelected(selectedStages)
|
||||
}
|
||||
)
|
||||
@@ -111,14 +96,21 @@ fun VocabularyStageDropDown(
|
||||
|
||||
VocabularyStage.entries.forEach { stage ->
|
||||
val isSelected = selectedStages.contains(stage)
|
||||
DropdownMenuItem(
|
||||
AppDropdownMenuItem(
|
||||
text = {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (multipleSelectable) {
|
||||
AppCheckbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = {
|
||||
selectedStages = if (isSelected) selectedStages - stage else selectedStages + stage
|
||||
selectedStages = if (isSelected) {
|
||||
selectedStages - stage
|
||||
} else {
|
||||
selectedStages + stage
|
||||
}
|
||||
onStageSelected(selectedStages)
|
||||
}
|
||||
)
|
||||
@@ -143,19 +135,6 @@ fun VocabularyStageDropDown(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (multipleSelectable) {
|
||||
HorizontalDivider()
|
||||
AppButton(
|
||||
onClick = { expanded = false },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.label_done))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -20,6 +19,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
@@ -32,14 +32,14 @@ import androidx.compose.material.icons.filled.ContentPaste
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
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.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -67,6 +67,8 @@ import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
||||
import eu.gaudian.translator.view.composable.AppSwitch
|
||||
import eu.gaudian.translator.view.composable.DictionaryLanguageDropDown
|
||||
import eu.gaudian.translator.view.composable.DropdownDefaults
|
||||
import eu.gaudian.translator.view.composable.LargeDropdownMenuItem
|
||||
import eu.gaudian.translator.viewmodel.CorrectionViewModel
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -415,6 +417,7 @@ fun CorrectionScreenContent(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun ToneDropdown(
|
||||
selectedTone: CorrectionViewModel.Tone,
|
||||
@@ -447,20 +450,33 @@ private fun ToneDropdown(
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
if (expanded) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { expanded = false },
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp))
|
||||
sheetState = sheetState,
|
||||
containerColor = DropdownDefaults.containerColor()
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(R.string.text_none)) },
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(vertical = 8.dp) // Gives breathing room at top/bottom of list
|
||||
) {
|
||||
// Replaced with LargeDropdownMenuItem
|
||||
LargeDropdownMenuItem(
|
||||
text = stringResource(R.string.text_none),
|
||||
selected = selectedTone == CorrectionViewModel.Tone.NONE,
|
||||
enabled = enabled,
|
||||
onClick = {
|
||||
onToneSelected(CorrectionViewModel.Tone.NONE)
|
||||
expanded = false
|
||||
},
|
||||
enabled = enabled
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
|
||||
val options = listOf(
|
||||
CorrectionViewModel.Tone.FORMAL,
|
||||
CorrectionViewModel.Tone.CASUAL,
|
||||
@@ -471,16 +487,23 @@ private fun ToneDropdown(
|
||||
CorrectionViewModel.Tone.ACADEMIC,
|
||||
CorrectionViewModel.Tone.CREATIVE
|
||||
)
|
||||
|
||||
options.forEach { tone ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = labelFor(tone)) },
|
||||
// Replaced with LargeDropdownMenuItem
|
||||
LargeDropdownMenuItem(
|
||||
text = labelFor(tone),
|
||||
selected = selectedTone == tone,
|
||||
enabled = enabled,
|
||||
onClick = {
|
||||
onToneSelected(tone)
|
||||
expanded = false
|
||||
},
|
||||
enabled = enabled
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.navigationBarsPadding().height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
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.
|
||||
*/
|
||||
|
||||
@Composable
|
||||
fun getAddModelScanHint(): Hint {
|
||||
return Hint(
|
||||
titleRes = R.string.hint_scan_hint_title,
|
||||
elements = listOf(
|
||||
|
||||
HintElement.Section(
|
||||
title = stringResource(R.string.scan_hint_section_how_scan_works),
|
||||
content = listOf(
|
||||
HintElement.Text(stringResource(R.string.scan_hint_how_scan_works_paragraph)),
|
||||
HintElement.UIElement { Spacer(Modifier.height(8.dp)) },
|
||||
HintElement.UIElement { ReusedScanButtonPreview() },
|
||||
HintElement.UIElement { Spacer(Modifier.height(8.dp)) },
|
||||
HintElement.BulletList(
|
||||
listOf(
|
||||
stringResource(R.string.scan_hint_bullet_results_depend),
|
||||
stringResource(R.string.scan_hint_bullet_public_private),
|
||||
stringResource(R.string.scan_hint_bullet_try_again)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
HintElement.Divider,
|
||||
HintElement.Section(
|
||||
title = stringResource(R.string.scan_hint_section_why_missing),
|
||||
content = listOf(
|
||||
HintElement.InfoBadge(
|
||||
icon = AppIcons.Lock,
|
||||
text = stringResource(R.string.scan_hint_badge_restricted)
|
||||
),
|
||||
HintElement.InfoBadge(
|
||||
icon = AppIcons.Warning,
|
||||
text = stringResource(R.string.scan_hint_badge_not_suitable)
|
||||
),
|
||||
HintElement.InfoBadge(
|
||||
icon = AppIcons.CheckCircle,
|
||||
text = stringResource(R.string.scan_hint_badge_only_text_models)
|
||||
),
|
||||
HintElement.Divider,
|
||||
HintElement.Text(stringResource(R.string.scan_hint_focus_text_models)),
|
||||
HintElement.Divider,
|
||||
HintElement.Card {
|
||||
PerformanceTierChips()
|
||||
},
|
||||
HintElement.Divider,
|
||||
HintElement.InfoBadge(
|
||||
icon = AppIcons.Info,
|
||||
text = stringResource(R.string.scan_hint_most_tasks_small_models)
|
||||
)
|
||||
)
|
||||
),
|
||||
HintElement.Divider,
|
||||
HintElement.Section(
|
||||
title = stringResource(R.string.scan_hint_section_tips),
|
||||
content = listOf(
|
||||
HintElement.BulletList(
|
||||
listOf(
|
||||
stringResource(R.string.scan_hint_tip_verify_key),
|
||||
stringResource(R.string.scan_hint_tip_select_org),
|
||||
stringResource(R.string.scan_hint_tip_type_manually),
|
||||
stringResource(R.string.scan_hint_tip_instruct_chat_text)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
HintElement.Divider,
|
||||
HintElement.Section(
|
||||
title = stringResource(R.string.hint_scan_hint_section_visual_guide),
|
||||
content = listOf(
|
||||
HintElement.VisualStep(
|
||||
step = stringResource(R.string.hint_scan_hint_step_1),
|
||||
title = stringResource(R.string.hint_scan_hint_step1_title),
|
||||
description = stringResource(R.string.hint_scan_hint_step1_desc),
|
||||
trailing = {
|
||||
Icon(AppIcons.Search, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
),
|
||||
HintElement.VisualStep(
|
||||
step = stringResource(R.string.hint_scan_hint_step_2),
|
||||
title = stringResource(R.string.hint_scan_hint_step2_title),
|
||||
description = stringResource(R.string.hint_scan_hint_step2_desc),
|
||||
trailing = {
|
||||
SuggestionChip(
|
||||
onClick = {},
|
||||
label = { Text(stringResource(R.string.hint_scan_hint_label_text_chat)) },
|
||||
enabled = false,
|
||||
icon = {},
|
||||
colors = SuggestionChipDefaults.suggestionChipColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
labelColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
)
|
||||
}
|
||||
),
|
||||
HintElement.VisualStep(
|
||||
step = stringResource(R.string.hint_scan_hint_step_3),
|
||||
title = stringResource(R.string.hint_scan_hint_step3_title),
|
||||
description = stringResource(R.string.hint_scan_hint_step3_desc),
|
||||
trailing = {
|
||||
SmallPrimaryCard(text = stringResource(R.string.hint_scan_hint_add_validate))
|
||||
}
|
||||
)
|
||||
)
|
||||
),
|
||||
HintElement.Divider,
|
||||
HintElement.Section(
|
||||
title = stringResource(R.string.hint_scan_hint_section_cant_find),
|
||||
content = listOf(
|
||||
HintElement.Text(stringResource(R.string.hint_scan_hint_manual_add_paragraph))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ReusedScanButtonPreviewPreview() {
|
||||
ReusedScanButtonPreview()
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun AddModelScanHintPreview() {
|
||||
getAddModelScanHint().Render()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddModelScanHint() {
|
||||
getAddModelScanHint().Render()
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
@file:Suppress("HardCodedStringLiteral")
|
||||
|
||||
package eu.gaudian.translator.view.hints
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.navigation.NavController
|
||||
import eu.gaudian.translator.R
|
||||
|
||||
/**
|
||||
* Hint metadata mapping Markdown filenames to their string resource titles.
|
||||
* All hint-related operations are available as functions on each enum entry.
|
||||
*/
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
enum class HintDefinition(
|
||||
val markdownFile: String,
|
||||
val titleRes: Int
|
||||
) {
|
||||
ADD_MODEL_SCAN("find_ai_model", R.string.hint_scan_hint_title),
|
||||
API_KEY("api_key_hint", R.string.hint_how_to_connect_to_an_ai),
|
||||
CATEGORY("category_hint", R.string.category_hint_intro),
|
||||
DICTIONARY_OPTIONS("dictionary_hint", R.string.label_dictionary_options),
|
||||
EXERCISE("exercise_hint", R.string.label_exercise),
|
||||
IMPORT("import_hint", R.string.hint_how_to_generate_vocabulary_with_ai),
|
||||
LEARNING_STAGES("learning_stages_hint", R.string.learning_stages_title),
|
||||
REVIEW("review_hint", R.string.review_intro),
|
||||
SORTING("sorting_hint", R.string.sorting_hint_title),
|
||||
TRANSLATION("translation_hint", R.string.hint_translate_how_it_works),
|
||||
VOCABULARY_PROGRESS("vocabulary_progress_hint", R.string.hint_vocabulary_progress_hint_title);
|
||||
|
||||
/** Creates the Hint data class for this hint definition. */
|
||||
@Composable
|
||||
fun hint() = Hint(titleRes = titleRes, elements = listOf(HintElement.LocalizedMarkdown(markdownFile)))
|
||||
|
||||
/** Renders this hint's content. */
|
||||
@Composable
|
||||
fun Render() = hint().Render()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun hint(definition: HintDefinition): Hint = definition.hint()
|
||||
|
||||
@Composable fun HintContent(definition: HintDefinition) = definition.Render()
|
||||
@Composable fun HintScreen(navController: NavController, definition: HintDefinition) = HintScreen(
|
||||
navController = navController,
|
||||
title = stringResource(definition.titleRes),
|
||||
content = { definition.Render() }
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
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
|
||||
|
||||
@Composable
|
||||
override fun getTitle(): String = stringResource(R.string.hint_how_to_connect_to_an_ai)
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
HeaderWithIcon(
|
||||
title = stringResource(R.string.hint_how_to_connect_to_an_ai)
|
||||
)
|
||||
|
||||
HintSection(title = stringResource(R.string.connecting_your_ai_model)) {
|
||||
Text(
|
||||
text = stringResource(R.string.api_hint_intro_1),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.api_hint_intro_2),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
||||
|
||||
HintSection(title = stringResource(R.string.key_status_indicators_title)) {
|
||||
Text(
|
||||
text = stringResource(R.string.key_status_explanation),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
KeyStatus(hasKey = true)
|
||||
Text(
|
||||
text = stringResource(R.string.key_saved_and_active),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(start = 20.dp, bottom = 8.dp)
|
||||
)
|
||||
KeyStatus(hasKey = false)
|
||||
Text(
|
||||
text = stringResource(R.string.key_missing_or_cleared),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(start = 20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
||||
|
||||
HintSection(title = stringResource(R.string.troubleshooting_title)) {
|
||||
Text(
|
||||
text = stringResource(R.string.troubleshooting_intro),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.troubleshooting_bullets),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun ApiKeyHintPreview() {
|
||||
MaterialTheme {
|
||||
ApiKeyHint.Content()
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
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
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
HeaderWithIcon(
|
||||
title = stringResource(R.string.category_hint_intro)
|
||||
)
|
||||
|
||||
HintSection(title = stringResource(R.string.category_hint_intro)) {
|
||||
// Tag Category Explanation (formerly List)
|
||||
CategoryHintItem(
|
||||
icon = {
|
||||
Icon(
|
||||
AppIcons.FilterList,
|
||||
contentDescription = stringResource(R.string.content_desc_tag_category),
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
},
|
||||
title = stringResource(R.string.text_list),
|
||||
description = stringResource(R.string.hint_list_category)
|
||||
)
|
||||
|
||||
// Filter Category Explanation
|
||||
CategoryHintItem(
|
||||
icon = {
|
||||
Icon(
|
||||
AppIcons.FilterCategory,
|
||||
contentDescription = stringResource(R.string.content_desc_filter_category),
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
},
|
||||
title = stringResource(R.string.text_filter),
|
||||
description = stringResource(R.string.hint_filter_category_description)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun CategoryHintPreview() {
|
||||
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)
|
||||
)
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
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
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
HeaderWithIcon(
|
||||
title = stringResource(R.string.category_hint_intro)
|
||||
)
|
||||
|
||||
HintSection(title = stringResource(R.string.category_list_title)) {
|
||||
Text(
|
||||
text = stringResource(R.string.category_list_description),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
// Visual representation for List Category
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
WordItem(text = stringResource(R.string.example_word_apple))
|
||||
Icon(AppIcons.Add, contentDescription = stringResource(R.string.action_add), tint = MaterialTheme.colorScheme.secondary)
|
||||
CategoryBox(icon = AppIcons.FilterList, text = stringResource(R.string.example_category_my_fruit_list))
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
HintSection(title = stringResource(R.string.category_filter_title)) {
|
||||
Text(
|
||||
text = stringResource(R.string.category_filter_description),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
// Visual representation for Filter Category
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Row {
|
||||
WordItem(text = stringResource(R.string.example_word_dog), subtext = stringResource(R.string.stage_1))
|
||||
Spacer(modifier = Modifier.padding(horizontal = 4.dp))
|
||||
WordItem(text = stringResource(R.string.example_word_cat), subtext = stringResource(R.string.stage_1))
|
||||
}
|
||||
CategoryBox(icon = AppIcons.FilterCategory, text = stringResource(R.string.example_filter_stage_1))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CategoryHintScreen() {
|
||||
CategoryHintScreen.Content()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
@Composable
|
||||
fun CategoryHintScreenPreview() {
|
||||
MaterialTheme {
|
||||
CategoryHintScreen()
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
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
|
||||
|
||||
@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)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview for the migrated DictionaryOptionsHint.
|
||||
* It now calls getDictionaryOptionsHint() and then Render().
|
||||
*/
|
||||
@Preview
|
||||
@Composable
|
||||
fun DictionaryOptionsHintPreview() {
|
||||
getDictionaryOptionsHint().Render()
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
@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.
|
||||
*/
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -1,13 +1,21 @@
|
||||
@file:Suppress("HardCodedStringLiteral")
|
||||
|
||||
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.ui.tooling.preview.Preview
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.jeziellago.compose.markdowntext.MarkdownText
|
||||
import eu.gaudian.translator.utils.Log
|
||||
|
||||
private const val TAG = "MarkdownHint"
|
||||
|
||||
/**
|
||||
* Sealed class representing different types of elements that can be used in a Hint.
|
||||
@@ -15,67 +23,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,178 +45,85 @@ 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)
|
||||
) {
|
||||
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"
|
||||
|
||||
Log.d(TAG, "Loading hint: $fileName")
|
||||
Log.d(TAG, "Device locale: ${locale.language}_${locale.country}")
|
||||
Log.d(TAG, "Localized path: $localizedPath")
|
||||
|
||||
val localized = MarkdownHintLoader.loadFromAssets(context, localizedPath)
|
||||
if (localized != null) {
|
||||
Log.d(TAG, "Found localized version at: $localizedPath")
|
||||
localized
|
||||
} else {
|
||||
// Fall back to English default in hints folder
|
||||
val defaultPath = "hints/$fileName.md"
|
||||
Log.d(TAG, "Localized not found, trying default: $defaultPath")
|
||||
val default = MarkdownHintLoader.loadFromAssets(context, defaultPath)
|
||||
if (default != null) {
|
||||
Log.d(TAG, "Found default version at: $defaultPath")
|
||||
} else {
|
||||
Log.e(TAG, "No hint found for: $fileName (tried: $localizedPath, $defaultPath)")
|
||||
}
|
||||
default
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
if (content != null) {
|
||||
MarkdownText(
|
||||
markdown = content,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
)
|
||||
}
|
||||
// If content is null, nothing is rendered (empty hint)
|
||||
}
|
||||
}
|
||||
|
||||
// Preview Composables for each HintElement type
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview
|
||||
@Composable
|
||||
fun TextElementPreview() {
|
||||
RenderHintElement(HintElement.Text("This is a sample text element."))
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview
|
||||
@Composable
|
||||
fun HeaderElementPreview() {
|
||||
RenderHintElement(HintElement.Header("Sample Header", "Optional subtitle"))
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview
|
||||
@Composable
|
||||
fun SectionElementPreview() {
|
||||
RenderHintElement(
|
||||
HintElement.Section(
|
||||
title = "Sample Section",
|
||||
content = listOf(
|
||||
HintElement.Text("Content 1"),
|
||||
HintElement.Text("Content 2")
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview
|
||||
@Composable
|
||||
fun BulletListElementPreview() {
|
||||
RenderHintElement(
|
||||
HintElement.BulletList(
|
||||
listOf("Item 1", "Item 2", "Item 3")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview
|
||||
@Composable
|
||||
fun VisualStepElementPreview() {
|
||||
RenderHintElement(
|
||||
HintElement.VisualStep(
|
||||
step = "1",
|
||||
title = "Sample Step",
|
||||
description = "This is a description of the step.",
|
||||
trailing = {
|
||||
Icon(AppIcons.Info, contentDescription = null)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview
|
||||
@Composable
|
||||
fun InfoBadgeElementPreview() {
|
||||
RenderHintElement(
|
||||
HintElement.InfoBadge(
|
||||
icon = AppIcons.Info,
|
||||
text = "Sample info badge"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun DividerElementPreview() {
|
||||
RenderHintElement(HintElement.Divider)
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview
|
||||
@Composable
|
||||
fun CardElementPreview() {
|
||||
RenderHintElement(
|
||||
HintElement.Card {
|
||||
Text("Content inside card")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview
|
||||
@Composable
|
||||
fun ChipsElementPreview() {
|
||||
RenderHintElement(
|
||||
HintElement.Chips(
|
||||
listOf("Chip 1", "Chip 2", "Chip 3")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview
|
||||
@Composable
|
||||
fun SmallCardElementPreview() {
|
||||
RenderHintElement(HintElement.SmallCard("Sample Card"))
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview
|
||||
@Composable
|
||||
fun UIElementPreview() {
|
||||
RenderHintElement(
|
||||
HintElement.UIElement {
|
||||
Text("Custom UI Element")
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
package eu.gaudian.translator.view.hints
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.NavController
|
||||
|
||||
/**
|
||||
* Wrapper for Category Hint Screen
|
||||
*/
|
||||
@Composable
|
||||
fun CategoryHintScreenWrapper(navController: NavController) {
|
||||
HintScreen(
|
||||
navController = navController,
|
||||
title = CategoryHint.getTitle()
|
||||
) {
|
||||
CategoryHint.Content()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dictionary Hint Screen
|
||||
*/
|
||||
@Composable
|
||||
fun DictionaryHintScreen(navController: NavController) {
|
||||
HintScreen(
|
||||
navController = navController,
|
||||
title = getDictionaryOptionsHint().getTitle()
|
||||
) {
|
||||
getDictionaryOptionsHint()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import Hint Screen
|
||||
*/
|
||||
@Composable
|
||||
fun ImportHintScreen(navController: NavController) {
|
||||
HintScreen(
|
||||
navController = navController,
|
||||
title = getImportVocabularyHint().getTitle()
|
||||
) {
|
||||
getImportVocabularyHint()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorting Hint Screen
|
||||
*/
|
||||
@Composable
|
||||
fun SortingHintScreen(navController: NavController) {
|
||||
HintScreen(
|
||||
navController = navController,
|
||||
title = SortingScreenHint.getTitle()
|
||||
) {
|
||||
SortingScreenHint.Content()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stages Hint Screen
|
||||
*/
|
||||
@Composable
|
||||
fun StagesHintScreen(navController: NavController) {
|
||||
HintScreen(
|
||||
navController = navController,
|
||||
title = LearningStagesHint.getTitle()
|
||||
) {
|
||||
LearningStagesHint.Content()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translation Hint Screen
|
||||
*/
|
||||
@Composable
|
||||
fun TranslationHintScreen(navController: NavController) {
|
||||
HintScreen(
|
||||
navController = navController,
|
||||
title = getTranslationScreenHint().getTitle()
|
||||
) {
|
||||
getTranslationScreenHint()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan Hint Screen
|
||||
*/
|
||||
@Composable
|
||||
fun ScanHintScreen(navController: NavController) {
|
||||
HintScreen(
|
||||
navController = navController,
|
||||
title = getAddModelScanHint().getTitle()
|
||||
) {
|
||||
AddModelScanHint()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API Hint Screen
|
||||
*/
|
||||
@Composable
|
||||
fun ApiHintScreen(navController: NavController) {
|
||||
HintScreen(
|
||||
navController = navController,
|
||||
title = ApiKeyHint.getTitle()
|
||||
) {
|
||||
ApiKeyHint.Content()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vocabulary Progress Hint Screen
|
||||
*/
|
||||
@Composable
|
||||
fun VocabularyProgressHintScreen(navController: NavController) {
|
||||
HintScreen(
|
||||
navController = navController,
|
||||
title = VocabularyProgressHint.getTitle()
|
||||
) {
|
||||
VocabularyProgressHint.Content()
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||
import eu.gaudian.translator.view.LocalShowExperimentalFeatures
|
||||
import eu.gaudian.translator.view.composable.AppCard
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
@@ -41,38 +39,38 @@ fun HintsOverviewScreen(
|
||||
) {
|
||||
val showExperimental = LocalShowExperimentalFeatures.current
|
||||
|
||||
val importHint = getImportVocabularyHint()
|
||||
val addModelScanHint = getAddModelScanHint()
|
||||
val dictionaryOptionsHint = getDictionaryOptionsHint()
|
||||
val translationScreenHint = getTranslationScreenHint()
|
||||
// Get hints using the new function-based approach
|
||||
val importHint = HintDefinition.IMPORT.hint()
|
||||
val addModelScanHint = HintDefinition.ADD_MODEL_SCAN.hint()
|
||||
val dictionaryOptionsHint = HintDefinition.DICTIONARY_OPTIONS.hint()
|
||||
val translationScreenHint = HintDefinition.TRANSLATION.hint()
|
||||
val categoryHint = HintDefinition.CATEGORY.hint()
|
||||
val learningStagesHint = HintDefinition.LEARNING_STAGES.hint()
|
||||
val sortingScreenHint = HintDefinition.SORTING.hint()
|
||||
val vocabularyProgressHint = HintDefinition.VOCABULARY_PROGRESS.hint()
|
||||
val apiKeyHint = HintDefinition.API_KEY.hint()
|
||||
|
||||
|
||||
|
||||
|
||||
val hintGroups = remember(showExperimental, importHint) {
|
||||
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 {
|
||||
@@ -134,11 +132,7 @@ fun HintsOverviewScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun HintsOverviewScreenPreview() {
|
||||
HintsOverviewScreen(navController = rememberNavController())
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun HintHeader(
|
||||
@@ -177,12 +171,4 @@ private fun HintListItem(
|
||||
)
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun HintListItemPreview() {
|
||||
HintListItem(
|
||||
title = stringResource(R.string.category_hint_intro),
|
||||
icon = AppIcons.Category,
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
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.
|
||||
*/
|
||||
@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
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview for the migrated ImportVocabularyHint.
|
||||
* It now calls getImportVocabularyHint() and then Render().
|
||||
*/
|
||||
@Preview
|
||||
@Composable
|
||||
fun ImportVocabularyHintPreview() {
|
||||
getImportVocabularyHint().Render()
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the migrated Hint for VocabularyReview.
|
||||
* This function is @Composable to access stringResource.
|
||||
*/
|
||||
|
||||
@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))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview for the migrated VocabularyReviewHint.
|
||||
* It now calls getVocabularyReviewHint() and then Render().
|
||||
*/
|
||||
@Preview
|
||||
@Composable
|
||||
fun VocabularyReviewHintPreview() {
|
||||
getVocabularyReviewHint().Render()
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
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
|
||||
|
||||
@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))
|
||||
)
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
@Composable
|
||||
fun LearningStagesHintPreview() {
|
||||
MaterialTheme {
|
||||
Box(modifier = Modifier.padding(8.dp)) {
|
||||
LearningStagesHint()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
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 {
|
||||
|
||||
|
||||
fun getCurrentLocale(context: Context): Locale {
|
||||
return context.resources.configuration.locale
|
||||
}
|
||||
|
||||
/**
|
||||
* Load content from assets.
|
||||
*/
|
||||
fun loadFromAssets(context: Context, fileName: String): String? {
|
||||
return try {
|
||||
context.assets.open(fileName).bufferedReader().use { it.readText() }
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,239 +0,0 @@
|
||||
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.
|
||||
*/
|
||||
@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)
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun VerticalDividerPreview() {
|
||||
VerticalDivider()
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
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
|
||||
|
||||
@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))
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@Composable
|
||||
fun TranslationScreenHint() {
|
||||
getTranslationScreenHint().Render()
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
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)
|
||||
)
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
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
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
HeaderWithIcon(
|
||||
title = stringResource(R.string.hint_vocabulary_progress_hint_title)
|
||||
)
|
||||
|
||||
HintSection(title = stringResource(R.string.hint_vocabulary_progress_tracking_title)) {
|
||||
// Progress Tracking
|
||||
VocabularyProgressHintItem(
|
||||
icon = AppIcons.BarChart,
|
||||
title = stringResource(R.string.hint_vocabulary_progress_tracking_title),
|
||||
description = stringResource(R.string.hint_vocabulary_progress_tracking_desc)
|
||||
)
|
||||
|
||||
// Learning Stages
|
||||
VocabularyProgressHintItem(
|
||||
icon = AppIcons.Stages,
|
||||
title = stringResource(R.string.hint_vocabulary_learning_stages_title),
|
||||
description = stringResource(R.string.hint_vocabulary_learning_stages_desc)
|
||||
)
|
||||
|
||||
// Review System
|
||||
VocabularyProgressHintItem(
|
||||
icon = AppIcons.History,
|
||||
title = stringResource(R.string.hint_vocabulary_review_system_title),
|
||||
description = stringResource(R.string.hint_vocabulary_review_system_desc)
|
||||
)
|
||||
|
||||
// Customization
|
||||
VocabularyProgressHintItem(
|
||||
icon = AppIcons.Tune,
|
||||
title = stringResource(R.string.hint_vocabulary_customization_title),
|
||||
description = stringResource(R.string.hint_vocabulary_customization_desc)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ContentPreview() {
|
||||
VocabularyProgressHint.Content()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VocabularyProgressHint() {
|
||||
VocabularyProgressHint.Content()
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun VocabularyProgressHintPreview() {
|
||||
VocabularyProgressHint()
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun VocabularyProgressHintItem(
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
title: String,
|
||||
description: String
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = title,
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
Column {
|
||||
Text(text = title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(text = description, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview
|
||||
@Composable
|
||||
private fun VocabularyProgressHintItemPreview() {
|
||||
VocabularyProgressHintItem(
|
||||
icon = AppIcons.BarChart,
|
||||
title = "Progress Tracking",
|
||||
description = "Track your learning progress with detailed statistics."
|
||||
)
|
||||
}
|
||||
@@ -44,6 +44,9 @@ import androidx.navigation.NavController
|
||||
import eu.gaudian.translator.BuildConfig
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||
import eu.gaudian.translator.utils.StatusAction
|
||||
import eu.gaudian.translator.utils.StatusMessageId
|
||||
import eu.gaudian.translator.utils.StatusMessageService
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppCard
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
@@ -52,7 +55,6 @@ import eu.gaudian.translator.view.composable.AppSwitch
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.view.composable.SecondaryButton
|
||||
import eu.gaudian.translator.viewmodel.SettingsViewModel
|
||||
import eu.gaudian.translator.viewmodel.StatusViewModel
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@@ -371,7 +373,7 @@ private fun DeveloperOptions(
|
||||
val context = LocalContext.current
|
||||
|
||||
val activity = context.findActivity()
|
||||
val statusViewModel: StatusViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val statusMessageService = StatusMessageService
|
||||
val settingsViewModel: SettingsViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
|
||||
|
||||
@@ -420,38 +422,35 @@ private fun DeveloperOptions(
|
||||
)
|
||||
}
|
||||
|
||||
val loadingText = stringResource(R.string.text_loading_3d)
|
||||
val infoText = stringResource(R.string.text_sentence_this_is_an_info_message)
|
||||
val successText = stringResource(R.string.text_success_em)
|
||||
val errorText = stringResource(R.string.text_sentence_oops_something_went_wrong)
|
||||
|
||||
|
||||
SecondaryButton(
|
||||
onClick = { statusViewModel.showLoadingMessage(loadingText) },
|
||||
onClick = { statusMessageService.showMessageById(StatusMessageId.LOADING_GENERIC) },
|
||||
text = stringResource(R.string.text_show_loading),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
SecondaryButton(
|
||||
onClick = { statusViewModel.cancelLoadingOperation() },
|
||||
onClick = { statusMessageService.trigger(StatusAction.CancelLoadingOperation)},
|
||||
text = stringResource(R.string.text_cancel_loading),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
SecondaryButton(
|
||||
onClick = { statusViewModel.showInfoMessage(infoText) },
|
||||
onClick = { statusMessageService.showInfoById(StatusMessageId.TEST_INFO) },
|
||||
text = stringResource(R.string.text_show_info_message),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
SecondaryButton(
|
||||
onClick = { statusViewModel.showSuccessMessage(successText) },
|
||||
onClick = { statusMessageService.showSuccessById(StatusMessageId.TEST_SUCCESS) },
|
||||
text = stringResource(R.string.title_show_success_message),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
SecondaryButton(
|
||||
onClick = { statusViewModel.showErrorMessage(errorText, 2) },
|
||||
onClick = { statusMessageService.showErrorById(StatusMessageId.TEST_ERROR) },
|
||||
text = stringResource(R.string.text_show_error_message),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
SecondaryButton(
|
||||
onClick = { statusViewModel.showApiKeyMissingMessage() },
|
||||
onClick = { statusMessageService.showErrorById(StatusMessageId.ERROR_API_KEY_MISSING) },
|
||||
text = stringResource(R.string.show_api_key_missing_message),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
@@ -58,7 +58,7 @@ import eu.gaudian.translator.view.composable.AppScaffold
|
||||
import eu.gaudian.translator.view.composable.AppSwitch
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.view.composable.ModelBadges
|
||||
import eu.gaudian.translator.view.hints.AddModelScanHint
|
||||
import eu.gaudian.translator.view.hints.HintDefinition
|
||||
import eu.gaudian.translator.viewmodel.ApiViewModel
|
||||
|
||||
@Composable
|
||||
@@ -140,7 +140,7 @@ fun AddModelScreen(navController: NavController, providerKey: String) {
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||
}
|
||||
},
|
||||
hintContent = { AddModelScanHint() }
|
||||
hintContent = HintDefinition.ADD_MODEL_SCAN.hint()
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
|
||||
@@ -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.HintDefinition
|
||||
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 = HintDefinition.API_KEY.hint()
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
|
||||
@@ -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 = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -36,7 +36,7 @@ import eu.gaudian.translator.view.composable.AppScaffold
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.view.composable.OptionItemSwitch
|
||||
import eu.gaudian.translator.view.dictionary.DictionaryManagerContent
|
||||
import eu.gaudian.translator.view.hints.getDictionaryOptionsHint
|
||||
import eu.gaudian.translator.view.hints.HintDefinition
|
||||
import eu.gaudian.translator.viewmodel.ApiViewModel
|
||||
import eu.gaudian.translator.viewmodel.DictionaryViewModel
|
||||
import eu.gaudian.translator.viewmodel.SettingsViewModel
|
||||
@@ -72,7 +72,7 @@ fun DictionaryOptionsScreen(
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||
}
|
||||
},
|
||||
hintContent = { getDictionaryOptionsHint() }
|
||||
hintContent = HintDefinition.DICTIONARY_OPTIONS.hint()
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -70,15 +70,24 @@ fun LanguageOptionsScreen(
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
item {
|
||||
AppCard {
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
|
||||
AppCard(
|
||||
title = stringResource(R.string.text_select_languages),
|
||||
text = stringResource(R.string.text_language_settings_description),
|
||||
expandable = true,
|
||||
initiallyExpanded = false
|
||||
) {
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -109,7 +118,10 @@ fun LanguageOptionsScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
PrimaryButton(
|
||||
onClick = { showAddLanguageDialog = true },
|
||||
text = stringResource(R.string.text_add_custom_language),
|
||||
@@ -117,9 +129,9 @@ fun LanguageOptionsScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showAddLanguageDialog) {
|
||||
@Suppress("KotlinConstantConditions")
|
||||
AddCustomLanguageDialog(
|
||||
showDialog = showAddLanguageDialog,
|
||||
onDismiss = { showAddLanguageDialog = false },
|
||||
|
||||
@@ -47,7 +47,7 @@ fun MainSettingsScreen(
|
||||
Setting(R.string.settings_title_voice, AppIcons.TextToSpeech, SettingsRoutes.TTS_OPTIONS),
|
||||
Setting(R.string.label_logs, AppIcons.Log, SettingsRoutes.LOGS),
|
||||
Setting(R.string.label_languages, AppIcons.Language, SettingsRoutes.LANGUAGE_OPTIONS),
|
||||
//Setting(R.string.hint_settings_title_hints, AppIcons.Info, SettingsRoutes.HINTS_OVERVIEW)
|
||||
//Setting(R.string.hint_settings_title_help, AppIcons.Info, SettingsRoutes.HINTS_OVERVIEW)
|
||||
|
||||
),
|
||||
R.string.settings_header_translator to listOf(
|
||||
|
||||
@@ -7,16 +7,9 @@ import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navigation
|
||||
import eu.gaudian.translator.view.composable.Screen
|
||||
import eu.gaudian.translator.view.hints.ApiHintScreen
|
||||
import eu.gaudian.translator.view.hints.CategoryHintScreenWrapper
|
||||
import eu.gaudian.translator.view.hints.DictionaryHintScreen
|
||||
import eu.gaudian.translator.view.hints.HintDefinition
|
||||
import eu.gaudian.translator.view.hints.HintScreen
|
||||
import eu.gaudian.translator.view.hints.HintsOverviewScreen
|
||||
import eu.gaudian.translator.view.hints.ImportHintScreen
|
||||
import eu.gaudian.translator.view.hints.ScanHintScreen
|
||||
import eu.gaudian.translator.view.hints.SortingHintScreen
|
||||
import eu.gaudian.translator.view.hints.StagesHintScreen
|
||||
import eu.gaudian.translator.view.hints.TranslationHintScreen
|
||||
import eu.gaudian.translator.view.hints.VocabularyProgressHintScreen
|
||||
|
||||
// Defines the routes for the settings graph to avoid using raw strings
|
||||
object SettingsRoutes {
|
||||
@@ -114,31 +107,31 @@ fun NavGraphBuilder.settingsGraph(navController: NavController) {
|
||||
HintsOverviewScreen(navController = navController)
|
||||
}
|
||||
composable(SettingsRoutes.HINTS_CATEGORIES) {
|
||||
CategoryHintScreenWrapper(navController = navController)
|
||||
HintScreen(navController, HintDefinition.CATEGORY)
|
||||
}
|
||||
composable(SettingsRoutes.HINTS_DICTIONARY) {
|
||||
DictionaryHintScreen(navController = navController)
|
||||
HintScreen(navController, HintDefinition.DICTIONARY_OPTIONS)
|
||||
}
|
||||
composable(SettingsRoutes.HINTS_IMPORT) {
|
||||
ImportHintScreen(navController = navController)
|
||||
HintScreen(navController, HintDefinition.IMPORT)
|
||||
}
|
||||
composable(SettingsRoutes.HINTS_SORTING) {
|
||||
SortingHintScreen(navController = navController)
|
||||
HintScreen(navController, HintDefinition.SORTING)
|
||||
}
|
||||
composable(SettingsRoutes.HINTS_STAGES) {
|
||||
StagesHintScreen(navController = navController)
|
||||
HintScreen(navController, HintDefinition.LEARNING_STAGES)
|
||||
}
|
||||
composable(SettingsRoutes.HINTS_TRANSLATION) {
|
||||
TranslationHintScreen(navController = navController)
|
||||
HintScreen(navController, HintDefinition.TRANSLATION)
|
||||
}
|
||||
composable(SettingsRoutes.HINTS_SCAN) {
|
||||
ScanHintScreen(navController = navController)
|
||||
HintScreen(navController, HintDefinition.ADD_MODEL_SCAN)
|
||||
}
|
||||
composable(SettingsRoutes.HINTS_API) {
|
||||
ApiHintScreen(navController = navController)
|
||||
HintScreen(navController, HintDefinition.API_KEY)
|
||||
}
|
||||
composable(SettingsRoutes.HINTS_VOCABULARY_PROGRESS) {
|
||||
VocabularyProgressHintScreen(navController = navController)
|
||||
HintScreen(navController, HintDefinition.VOCABULARY_PROGRESS)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 ->
|
||||
|
||||
@@ -49,7 +49,7 @@ import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppScaffold
|
||||
import eu.gaudian.translator.view.composable.AppSlider
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.view.hints.VocabularyProgressHint
|
||||
import eu.gaudian.translator.view.hints.HintDefinition
|
||||
import eu.gaudian.translator.viewmodel.SettingsViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
import kotlin.math.exp
|
||||
@@ -85,7 +85,7 @@ fun VocabularyProgressOptionsScreen(
|
||||
}
|
||||
},
|
||||
// Here is the new hint content
|
||||
hintContent = { VocabularyProgressHint() }
|
||||
hintContent = HintDefinition.VOCABULARY_PROGRESS.hint()
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
|
||||
@@ -39,6 +39,8 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.navigation.NavController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.Language
|
||||
import eu.gaudian.translator.utils.StatusMessageId
|
||||
import eu.gaudian.translator.utils.StatusMessageService
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppButton
|
||||
import eu.gaudian.translator.view.composable.AppCard
|
||||
@@ -50,7 +52,6 @@ import eu.gaudian.translator.view.composable.PrimaryButton
|
||||
import eu.gaudian.translator.view.composable.SecondaryButton
|
||||
import eu.gaudian.translator.view.composable.SingleLanguageDropDown
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
import eu.gaudian.translator.viewmodel.StatusViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
|
||||
@Composable
|
||||
@@ -60,7 +61,7 @@ fun VocabularyRepositoryOptionsScreen(
|
||||
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val statusViewModel: StatusViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val statusMessageService = StatusMessageService
|
||||
|
||||
|
||||
val context = LocalContext.current
|
||||
@@ -73,7 +74,7 @@ fun VocabularyRepositoryOptionsScreen(
|
||||
context.contentResolver.openInputStream(it)?.use { inputStream ->
|
||||
val jsonString = inputStream.bufferedReader().use { reader -> reader.readText() }
|
||||
vocabularyViewModel.importVocabulary(jsonString)
|
||||
statusViewModel.showInfoMessage(repositoryStateImportedFrom + " " +it.path)
|
||||
statusMessageService.showInfoMessage(repositoryStateImportedFrom + " " +it.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,7 +146,7 @@ fun VocabularyRepositoryOptionsScreen(
|
||||
row.map { it.trim().trim('"') }
|
||||
}.filter { r -> r.any { it.isNotBlank() } }
|
||||
}
|
||||
val textExcelNotSupportedUseCsv = stringResource(R.string.text_excel_not_supported_use_csv)
|
||||
|
||||
val errorParsingTable = stringResource(R.string.error_parsing_table)
|
||||
val errorParsingTableWithReason = stringResource(R.string.error_parsing_table_with_reason)
|
||||
val importTableLauncher = rememberLauncherForActivityResult(
|
||||
@@ -159,7 +160,7 @@ fun VocabularyRepositoryOptionsScreen(
|
||||
val mime = context.contentResolver.getType(u)
|
||||
val isExcel = mime == "application/vnd.ms-excel" || mime == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
if (isExcel) {
|
||||
statusViewModel.showInfoMessage(textExcelNotSupportedUseCsv)
|
||||
statusMessageService.showErrorById(StatusMessageId.ERROR_EXCEL_NOT_SUPPORTED)
|
||||
return@let
|
||||
}
|
||||
context.contentResolver.openInputStream(u)?.use { inputStream ->
|
||||
@@ -173,12 +174,12 @@ fun VocabularyRepositoryOptionsScreen(
|
||||
parseError = null
|
||||
} else {
|
||||
parseError = errorParsingTable
|
||||
statusViewModel.showErrorMessage(parseError!!)
|
||||
statusMessageService.showErrorMessage(parseError!!)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
parseError = e.message
|
||||
statusViewModel.showErrorMessage(
|
||||
statusMessageService.showErrorMessage(
|
||||
(errorParsingTableWithReason + " " + e.message)
|
||||
)
|
||||
}
|
||||
@@ -394,13 +395,13 @@ fun VocabularyRepositoryOptionsScreen(
|
||||
val infoImportedItemsFrom = stringResource(R.string.info_imported_items_from)
|
||||
TextButton(onClick = {
|
||||
if (selectedColFirst == selectedColSecond) {
|
||||
statusViewModel.showErrorMessage(errorSelectTwoColumns)
|
||||
statusMessageService.showErrorMessage(errorSelectTwoColumns)
|
||||
return@TextButton
|
||||
}
|
||||
val langA = selectedLangFirst
|
||||
val langB = selectedLangSecond
|
||||
if (langA == null || langB == null) {
|
||||
statusViewModel.showErrorMessage(errorSelectLanguages)
|
||||
statusMessageService.showErrorMessage(errorSelectLanguages)
|
||||
return@TextButton
|
||||
}
|
||||
val startIdx = if (skipHeader) 1 else 0
|
||||
@@ -416,11 +417,11 @@ fun VocabularyRepositoryOptionsScreen(
|
||||
)
|
||||
}
|
||||
if (items.isEmpty()) {
|
||||
statusViewModel.showErrorMessage(errorNoRowsToImport)
|
||||
statusMessageService.showErrorMessage(errorNoRowsToImport)
|
||||
return@TextButton
|
||||
}
|
||||
vocabularyViewModel.addVocabularyItems(items)
|
||||
statusViewModel.showSuccessMessage(infoImportedItemsFrom + " " +items.size)
|
||||
statusMessageService.showSuccessMessage(infoImportedItemsFrom + " " +items.size)
|
||||
showTableImportDialog.value = false
|
||||
}) { Text(stringResource(R.string.label_import)) }
|
||||
},
|
||||
|
||||
@@ -59,7 +59,7 @@ import eu.gaudian.translator.view.NoConnectionScreen
|
||||
import eu.gaudian.translator.view.composable.AppCard
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedCard
|
||||
import eu.gaudian.translator.view.dialogs.AddVocabularyDialog
|
||||
import eu.gaudian.translator.view.hints.TranslationScreenHint
|
||||
import eu.gaudian.translator.view.hints.HintDefinition
|
||||
import eu.gaudian.translator.view.settings.SettingsRoutes
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
import eu.gaudian.translator.viewmodel.SettingsViewModel
|
||||
@@ -167,7 +167,7 @@ private fun LoadedTranslationContent(
|
||||
TopBarActions(
|
||||
languageViewModel = languageViewModel,
|
||||
onSettingsClick = onSettingsClick,
|
||||
hintContent = { TranslationScreenHint() }
|
||||
hintContent = { HintDefinition.TRANSLATION.Render() }
|
||||
)
|
||||
|
||||
AppCard(modifier = Modifier.padding(8.dp, end = 8.dp, bottom = 8.dp, top = 0.dp)) {
|
||||
|
||||
@@ -75,8 +75,6 @@ enum class VocabularyTab(
|
||||
Statistics(title = "label_all_vocabulary", icon = AppIcons.BarChart, route = "statistics")
|
||||
}
|
||||
|
||||
//Used to avoid the warning of unused variables in strings.xml
|
||||
|
||||
@Suppress("unused", "HardCodedStringLiteral", "UnusedVariable")
|
||||
@Composable
|
||||
fun Dummy() {
|
||||
@@ -297,7 +295,18 @@ fun MainVocabularyScreen(
|
||||
VocabularyTab.entries.find { it.route == currentRoute } ?: VocabularyTab.Dashboard
|
||||
}
|
||||
|
||||
val showFabText = selectedTab == VocabularyTab.Dashboard && !isScrolling
|
||||
val rawShowFabText = selectedTab == VocabularyTab.Dashboard && !isScrolling
|
||||
var showFabText by remember { mutableStateOf(rawShowFabText) }
|
||||
|
||||
LaunchedEffect(rawShowFabText) {
|
||||
if (rawShowFabText) {
|
||||
// Only delay when showing (true), hide immediately
|
||||
kotlinx.coroutines.delay(2000)
|
||||
showFabText = true
|
||||
} else {
|
||||
showFabText = false
|
||||
}
|
||||
}
|
||||
|
||||
val repoEmpty =
|
||||
vocabularyViewModel.vocabularyItems.collectAsState(initial = emptyList()).value.isEmpty()
|
||||
|
||||
@@ -6,7 +6,6 @@ import androidx.compose.foundation.BorderStroke
|
||||
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
|
||||
@@ -391,7 +390,6 @@ private fun ExerciseTypeSelector(
|
||||
onTypeSelected: (VocabularyExerciseType) -> Unit,
|
||||
) {
|
||||
// Using FlowRow for a more flexible layout that wraps to the next line if needed
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
FlowRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
|
||||
|
||||
@@ -238,7 +238,7 @@ fun VocabularyCardHost(
|
||||
listOf(currentVocabularyItem),
|
||||
it.mapNotNull { category -> category?.id }
|
||||
)
|
||||
showCategoryDialog = false
|
||||
//showCategoryDialog = false
|
||||
},
|
||||
onDismissRequest = { showCategoryDialog = false }
|
||||
)
|
||||
|
||||
@@ -77,7 +77,8 @@ fun VocabularyExerciseHostScreen(
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
|
||||
// Reset exercise state when starting fresh
|
||||
exerciseViewModel.resetExercise()
|
||||
|
||||
vocabularyViewModel.prepareExercise(
|
||||
categoryIdsAsJson,
|
||||
@@ -249,7 +250,15 @@ private fun ExerciseScreen(
|
||||
}
|
||||
|
||||
LaunchedEffect(currentExerciseState, score, wrongAnswers) {
|
||||
if (currentExerciseState == null && (score + wrongAnswers) >= totalItems && totalItems > 0) {
|
||||
// Only trigger completion when:
|
||||
// 1. Current exercise state is null (no more items to show)
|
||||
// 2. We have answered all items (score + wrong = total)
|
||||
// 3. We have at least one item to process
|
||||
// 4. We're not already in a completed state (prevent duplicate triggers)
|
||||
if (currentExerciseState == null &&
|
||||
(score + wrongAnswers) >= totalItems &&
|
||||
totalItems > 0 &&
|
||||
(score + wrongAnswers) > 0) {
|
||||
onFinish(score, wrongAnswers)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ private data class VocabularyFilterState(
|
||||
val searchQuery: String = "",
|
||||
val selectedStage: VocabularyStage? = null,
|
||||
val sortOrder: SortOrder = SortOrder.NEWEST_FIRST,
|
||||
val categoryId: Int? = null,
|
||||
val categoryIds: List<Int> = emptyList(),
|
||||
val dueTodayOnly: Boolean = false,
|
||||
val selectedLanguageIds: List<Int> = emptyList(),
|
||||
val selectedWordClass: String? = null
|
||||
@@ -133,7 +133,7 @@ fun VocabularyListScreen(
|
||||
var filterState by rememberSaveable {
|
||||
mutableStateOf(
|
||||
VocabularyFilterState(
|
||||
categoryId = categoryId,
|
||||
categoryIds = categoryId?.let { listOf(it) } ?: emptyList(),
|
||||
dueTodayOnly = showDueTodayOnly == true,
|
||||
selectedStage = stage
|
||||
)
|
||||
@@ -142,7 +142,7 @@ fun VocabularyListScreen(
|
||||
val isFilterActive by remember(filterState) {
|
||||
derivedStateOf {
|
||||
filterState.selectedStage != null ||
|
||||
(filterState.categoryId != null && filterState.categoryId != 0) ||
|
||||
filterState.categoryIds.isNotEmpty() ||
|
||||
filterState.dueTodayOnly ||
|
||||
filterState.selectedLanguageIds.isNotEmpty() ||
|
||||
!filterState.selectedWordClass.isNullOrBlank()
|
||||
@@ -165,7 +165,7 @@ fun VocabularyListScreen(
|
||||
vocabularyViewModel.filterVocabularyItems(
|
||||
languages = filterState.selectedLanguageIds,
|
||||
query = filterState.searchQuery.takeIf { it.isNotBlank() },
|
||||
categoryId = filterState.categoryId,
|
||||
categoryIds = filterState.categoryIds,
|
||||
stage = filterState.selectedStage,
|
||||
wordClass = filterState.selectedWordClass,
|
||||
dueTodayOnly = filterState.dueTodayOnly,
|
||||
@@ -179,7 +179,7 @@ fun VocabularyListScreen(
|
||||
|
||||
LaunchedEffect(categoryId, showDueTodayOnly, stage) {
|
||||
filterState = filterState.copy(
|
||||
categoryId = categoryId,
|
||||
categoryIds = categoryId?.let { listOf(it) } ?: emptyList(),
|
||||
dueTodayOnly = showDueTodayOnly == true,
|
||||
selectedStage = stage
|
||||
)
|
||||
@@ -382,7 +382,8 @@ fun VocabularyListScreen(
|
||||
languageViewModel = languageViewModel,
|
||||
languagesPresent = allLanguages.filter { it.nameResId in languagesPresent },
|
||||
hideCategory = categoryId != null && categoryId != 0,
|
||||
hideStage = stage != null
|
||||
hideStage = stage != null,
|
||||
categoryViewModel = categoryViewModel
|
||||
)
|
||||
}
|
||||
|
||||
@@ -394,7 +395,7 @@ fun VocabularyListScreen(
|
||||
selectedItems,
|
||||
it.mapNotNull { category -> category?.id }
|
||||
)
|
||||
showCategoryDialog = false
|
||||
//showCategoryDialog = false
|
||||
},
|
||||
onDismissRequest = { showCategoryDialog = false }
|
||||
)
|
||||
@@ -807,11 +808,12 @@ private fun FilterSortBottomSheet(
|
||||
onDismiss: () -> Unit,
|
||||
onApplyFilters: (VocabularyFilterState) -> Unit,
|
||||
hideCategory: Boolean = false,
|
||||
hideStage: Boolean = false
|
||||
hideStage: Boolean = false,
|
||||
categoryViewModel: CategoryViewModel
|
||||
) {
|
||||
var selectedStage by rememberSaveable { mutableStateOf(currentFilterState.selectedStage) }
|
||||
var dueTodayOnly by rememberSaveable { mutableStateOf(currentFilterState.dueTodayOnly) }
|
||||
var selectedCategoryId by rememberSaveable { mutableStateOf(currentFilterState.categoryId) }
|
||||
var selectedCategoryIds by rememberSaveable { mutableStateOf(currentFilterState.categoryIds) }
|
||||
var selectedLanguageIds by rememberSaveable { mutableStateOf(currentFilterState.selectedLanguageIds) }
|
||||
var selectedWordClass by rememberSaveable { mutableStateOf(currentFilterState.selectedWordClass) }
|
||||
|
||||
@@ -840,7 +842,7 @@ private fun FilterSortBottomSheet(
|
||||
TextButton(onClick = {
|
||||
if (!hideStage) selectedStage = null
|
||||
dueTodayOnly = false
|
||||
if (!hideCategory) selectedCategoryId = null
|
||||
if (!hideCategory) selectedCategoryIds = emptyList()
|
||||
selectedLanguageIds = emptyList()
|
||||
selectedWordClass = null
|
||||
}) {
|
||||
@@ -853,7 +855,7 @@ private fun FilterSortBottomSheet(
|
||||
currentFilterState.copy(
|
||||
selectedStage = selectedStage,
|
||||
dueTodayOnly = dueTodayOnly,
|
||||
categoryId = selectedCategoryId,
|
||||
categoryIds = selectedCategoryIds,
|
||||
selectedLanguageIds = selectedLanguageIds,
|
||||
selectedWordClass = selectedWordClass
|
||||
)
|
||||
@@ -893,16 +895,18 @@ private fun FilterSortBottomSheet(
|
||||
Text(stringResource(R.string.label_category), style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
CategoryDropdown(
|
||||
initialCategoryId = selectedCategoryId,
|
||||
initialCategoryId = selectedCategoryIds.firstOrNull(),
|
||||
onCategorySelected = { categories ->
|
||||
selectedCategoryId = categories.firstOrNull()?.id
|
||||
}
|
||||
selectedCategoryIds = categories.mapNotNull { it?.id }
|
||||
},
|
||||
multipleSelectable = true,
|
||||
noneSelectable = false
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
if (!hideStage) {
|
||||
Text(stringResource(R.string.filter_by_stage), style = MaterialTheme.typography.titleMedium)
|
||||
Text(stringResource(R.string.label_filter_by_stage), style = MaterialTheme.typography.titleMedium)
|
||||
FlowRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
@@ -955,6 +959,7 @@ private fun FilterSortBottomSheet(
|
||||
fun FilterSortBottomSheetPreview() {
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
FilterSortBottomSheet(
|
||||
currentFilterState = VocabularyFilterState(),
|
||||
languageViewModel = languageViewModel,
|
||||
@@ -962,6 +967,7 @@ fun FilterSortBottomSheetPreview() {
|
||||
onDismiss = {},
|
||||
onApplyFilters = {},
|
||||
hideCategory = false,
|
||||
hideStage = false
|
||||
hideStage = false,
|
||||
categoryViewModel = categoryViewModel
|
||||
)
|
||||
}
|
||||
@@ -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.HintDefinition
|
||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||
import eu.gaudian.translator.viewmodel.LanguageConfigViewModel
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
@@ -236,7 +236,7 @@ fun VocabularySortingScreen(
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||
}
|
||||
},
|
||||
hintContent = {SortingScreenHint.Content()}
|
||||
hintContent = HintDefinition.SORTING.hint()
|
||||
)
|
||||
},
|
||||
|
||||
@@ -299,6 +299,7 @@ fun VocabularySortingItem(
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val languageConfigViewModel: LanguageConfigViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
var wordFirst by remember { mutableStateOf(item.wordFirst) }
|
||||
var wordSecond by remember { mutableStateOf(item.wordSecond) }
|
||||
var selectedCategories by remember { mutableStateOf<List<Int>>(emptyList()) }
|
||||
@@ -313,6 +314,7 @@ fun VocabularySortingItem(
|
||||
var articlesLangSecond by remember { mutableStateOf(emptySet<String>()) }
|
||||
|
||||
var showDuplicateDialog by remember { mutableStateOf(false) }
|
||||
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
||||
|
||||
// NEW: Calculate if the item is valid for the "Done" button in faulty mode
|
||||
val isItemNowValid by remember(wordFirst, wordSecond, langFirst, langSecond) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import eu.gaudian.translator.utils.Log
|
||||
import eu.gaudian.translator.utils.StatusAction
|
||||
import eu.gaudian.translator.utils.StatusMessageId
|
||||
import eu.gaudian.translator.utils.StatusMessageService
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -45,6 +46,16 @@ enum class MessageDisplayType(val priority: Int) {
|
||||
ACTIONABLE_ERROR(5)
|
||||
}
|
||||
|
||||
/**
|
||||
* StatusViewModel is responsible for:
|
||||
* 1. Collecting status actions from StatusMessageService
|
||||
* 2. Managing the message queue
|
||||
* 3. Resolving StatusMessageId to actual strings
|
||||
* 4. Managing status state
|
||||
*
|
||||
* NOTE: All message display requests should go through StatusMessageService.
|
||||
* This ViewModel should NOT be called directly to display messages.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class StatusViewModel @Inject constructor(
|
||||
application: Application,
|
||||
@@ -67,9 +78,14 @@ class StatusViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles all status actions from StatusMessageService.
|
||||
* This is the main entry point for all status messages.
|
||||
*/
|
||||
private fun handleAction(action: StatusAction) {
|
||||
Log.d("StatusViewModel", "Received action: $action")
|
||||
when (action) {
|
||||
// Legacy string-based actions (deprecated but still supported for backward compatibility)
|
||||
is StatusAction.ShowMessage -> showMessageInternal(action.text, action.type, action.timeoutInSeconds)
|
||||
is StatusAction.ShowActionableMessage -> showPermanentActionableMessageInternal(action.text, action.type, action.action)
|
||||
is StatusAction.ShowPermanentMessage -> showPermanentMessageInternal(action.text, action.type)
|
||||
@@ -78,16 +94,66 @@ class StatusViewModel @Inject constructor(
|
||||
is StatusAction.CancelPermanentMessage -> cancelPermanentMessageInternal()
|
||||
is StatusAction.HideMessageBar -> hideMessageBarInternal()
|
||||
is StatusAction.CancelAllMessages -> cancelAllMessagesInternal()
|
||||
|
||||
// New ID-based actions for internationalization
|
||||
is StatusAction.ShowMessageById -> showMessageByIdInternal(
|
||||
action.messageId,
|
||||
action.type,
|
||||
action.timeoutInSeconds
|
||||
)
|
||||
is StatusAction.ShowPermanentMessageById -> showPermanentMessageByIdInternal(
|
||||
action.messageId,
|
||||
action.type
|
||||
)
|
||||
is StatusAction.ShowActionableMessageById -> showPermanentActionableMessageByIdInternal(
|
||||
action.messageId,
|
||||
action.type,
|
||||
action.action
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showApiKeyMissingMessage() = viewModelScope.launch {
|
||||
statusMessageService.showActionableMessage(
|
||||
text = "API Key is missing or invalid.",
|
||||
type = MessageDisplayType.ACTIONABLE_ERROR,
|
||||
action = MessageAction.NAVIGATE_TO_API_KEYS
|
||||
)
|
||||
/**
|
||||
* Resolves a StatusMessageId to its actual string text using Android string resources.
|
||||
*/
|
||||
private fun resolveMessageText(messageId: StatusMessageId): String {
|
||||
return try {
|
||||
getApplication<Application>().getString(messageId.stringResId)
|
||||
} catch (e: Exception) {
|
||||
Log.e("StatusViewModel", "Failed to resolve message string for ID: $messageId", e)
|
||||
"Message not available"
|
||||
}
|
||||
}
|
||||
|
||||
// --- ID-based internal methods ---
|
||||
|
||||
private fun showMessageByIdInternal(
|
||||
messageId: StatusMessageId,
|
||||
type: MessageDisplayType,
|
||||
timeoutInSeconds: Int
|
||||
) {
|
||||
val text = resolveMessageText(messageId)
|
||||
showMessageInternal(text, type, timeoutInSeconds)
|
||||
}
|
||||
|
||||
private fun showPermanentMessageByIdInternal(
|
||||
messageId: StatusMessageId,
|
||||
type: MessageDisplayType
|
||||
) {
|
||||
val text = resolveMessageText(messageId)
|
||||
showPermanentMessageInternal(text, type)
|
||||
}
|
||||
|
||||
private fun showPermanentActionableMessageByIdInternal(
|
||||
messageId: StatusMessageId,
|
||||
type: MessageDisplayType,
|
||||
action: MessageAction
|
||||
) {
|
||||
val text = resolveMessageText(messageId)
|
||||
showPermanentActionableMessageInternal(text, type, action)
|
||||
}
|
||||
|
||||
// --- Internal message display methods ---
|
||||
|
||||
private fun showPermanentActionableMessageInternal(message: String, type: MessageDisplayType, action: MessageAction) {
|
||||
cancelAllOperations() // Clear any other messages or loaders.
|
||||
@@ -99,54 +165,6 @@ class StatusViewModel @Inject constructor(
|
||||
_status.value = StatusState.Message(messageIdCounter++, message, type, action = null)
|
||||
}
|
||||
|
||||
fun showPermanentMessage(message: String, type: MessageDisplayType) = viewModelScope.launch {
|
||||
statusMessageService.showPermanentMessage(message, type)
|
||||
}
|
||||
|
||||
fun cancelPermanentMessage() = viewModelScope.launch {
|
||||
statusMessageService.cancelPermanentMessage()
|
||||
}
|
||||
|
||||
fun performLoadingOperation(block: suspend () -> Unit) = viewModelScope.launch {
|
||||
statusMessageService.trigger(StatusAction.PerformLoadingOperation(block))
|
||||
}
|
||||
|
||||
fun cancelLoadingOperation() = viewModelScope.launch {
|
||||
statusMessageService.trigger(StatusAction.CancelLoadingOperation)
|
||||
}
|
||||
|
||||
fun showInfoMessage(message: String, timeoutInSeconds: Int = 3) = viewModelScope.launch {
|
||||
statusMessageService.showInfoMessage(message, timeoutInSeconds)
|
||||
}
|
||||
|
||||
fun showLoadingMessage(message: String, timeoutInSeconds: Int = 0) = viewModelScope.launch { // Default timeout 0 for indefinite
|
||||
statusMessageService.showLoadingMessage(message, timeoutInSeconds)
|
||||
}
|
||||
|
||||
fun showErrorMessage(message: String, timeoutInSeconds: Int = 5) = viewModelScope.launch { // Default timeout 5 for errors
|
||||
statusMessageService.showErrorMessage(message, timeoutInSeconds)
|
||||
}
|
||||
|
||||
fun showSuccessMessage(message: String, timeoutInSeconds: Int = 3) = viewModelScope.launch {
|
||||
statusMessageService.showSuccessMessage(message, timeoutInSeconds)
|
||||
}
|
||||
|
||||
fun hideMessageBar() = viewModelScope.launch {
|
||||
statusMessageService.hideMessageBar()
|
||||
}
|
||||
|
||||
fun cancelAllMessages() = viewModelScope.launch {
|
||||
statusMessageService.cancelAllMessages()
|
||||
}
|
||||
|
||||
private fun cancelPermanentMessageInternal() {
|
||||
if (_status.value is StatusState.Message) {
|
||||
// This logic can be simplified or adjusted based on desired behavior for permanent messages
|
||||
_status.value = StatusState.Hidden
|
||||
processNextMessageInQueue()
|
||||
}
|
||||
}
|
||||
|
||||
private fun performLoadingOperationInternal(block: suspend () -> Unit) {
|
||||
cancelAllOperations()
|
||||
_status.value = StatusState.Loading
|
||||
@@ -159,7 +177,10 @@ class StatusViewModel @Inject constructor(
|
||||
Log.i("StatusViewModel", "Loading operation was cancelled.")
|
||||
} catch (e: Exception) {
|
||||
Log.e("StatusViewModel", "Loading operation failed.", e)
|
||||
showErrorMessage("Operation failed: ${e.localizedMessage ?: "Unknown error"}")
|
||||
// Trigger error message through StatusMessageService
|
||||
viewModelScope.launch {
|
||||
statusMessageService.showErrorById(StatusMessageId.ERROR_OPERATION_FAILED)
|
||||
}
|
||||
} finally {
|
||||
if (activeLoadingJob == this.coroutineContext[Job]) {
|
||||
if (_status.value == StatusState.Loading) {
|
||||
@@ -181,7 +202,38 @@ class StatusViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
// --- REVISED LOGIC ---
|
||||
private fun cancelPermanentMessageInternal() {
|
||||
if (_status.value is StatusState.Message) {
|
||||
_status.value = StatusState.Hidden
|
||||
processNextMessageInQueue()
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideMessageBarInternal() {
|
||||
messageDisplayJob?.cancel()
|
||||
messageDisplayJob = null
|
||||
if (_status.value is StatusState.Message) {
|
||||
_status.value = StatusState.Hidden
|
||||
}
|
||||
if (activeLoadingJob?.isActive != true) {
|
||||
processNextMessageInQueue()
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelAllMessagesInternal() {
|
||||
Log.d("StatusViewModel", "Cancelling all messages.")
|
||||
messageQueue.clear()
|
||||
messageDisplayJob?.cancel()
|
||||
messageDisplayJob = null
|
||||
if (_status.value is StatusState.Message) {
|
||||
_status.value = StatusState.Hidden
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a message with priority-based queuing.
|
||||
* High-priority messages interrupt lower-priority ones.
|
||||
*/
|
||||
private fun showMessageInternal(message: String, type: MessageDisplayType, timeoutInSeconds: Int) {
|
||||
val currentState = _status.value
|
||||
val currentPriority = (currentState as? StatusState.Message)?.type?.priority ?: -1
|
||||
@@ -204,29 +256,6 @@ class StatusViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideMessageBarInternal() {
|
||||
messageDisplayJob?.cancel()
|
||||
messageDisplayJob = null
|
||||
if (_status.value is StatusState.Message) {
|
||||
_status.value = StatusState.Hidden
|
||||
}
|
||||
if (activeLoadingJob?.isActive != true) {
|
||||
processNextMessageInQueue()
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelAllMessagesInternal() {
|
||||
Log.d("StatusViewModel", "Cancelling all messages.")
|
||||
messageQueue.clear()
|
||||
messageDisplayJob?.cancel()
|
||||
messageDisplayJob = null
|
||||
// Do not cancel activeLoadingJob here unless that's the desired behavior.
|
||||
// Assuming CancelAllMessages is for the message bar only.
|
||||
if (_status.value is StatusState.Message) {
|
||||
_status.value = StatusState.Hidden
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelAllOperations() {
|
||||
messageQueue.clear()
|
||||
messageDisplayJob?.cancel()
|
||||
@@ -236,7 +265,9 @@ class StatusViewModel @Inject constructor(
|
||||
_status.value = StatusState.Hidden
|
||||
}
|
||||
|
||||
// --- REVISED LOGIC ---
|
||||
/**
|
||||
* Processes the next message in the queue.
|
||||
*/
|
||||
private fun processNextMessageInQueue(timeoutInSeconds: Int = 3) {
|
||||
if (activeLoadingJob?.isActive == true) {
|
||||
Log.d("StatusViewModel", "Full-screen loading is active, not processing message queue now.")
|
||||
|
||||
@@ -15,6 +15,7 @@ import eu.gaudian.translator.model.repository.dataStore
|
||||
import eu.gaudian.translator.model.repository.loadObjectList
|
||||
import eu.gaudian.translator.model.repository.saveObjectList
|
||||
import eu.gaudian.translator.utils.Log
|
||||
import eu.gaudian.translator.utils.StatusMessageService
|
||||
import eu.gaudian.translator.utils.TextToSpeechHelper
|
||||
import eu.gaudian.translator.utils.TranslationService
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -31,6 +32,9 @@ class TranslationViewModel @Inject constructor(
|
||||
val languageRepository: LanguageRepository
|
||||
) : AndroidViewModel(application) {
|
||||
|
||||
private val statusMessageService = StatusMessageService
|
||||
|
||||
|
||||
// For back/forward navigation of history in the UI (like editors)
|
||||
private val _historyCursor = MutableStateFlow(-1)
|
||||
|
||||
@@ -112,11 +116,13 @@ class TranslationViewModel @Inject constructor(
|
||||
fun translateSentence(sentence: String) {
|
||||
val sentenceToTranslate = sentence.ifEmpty { _inputText.value }
|
||||
if (sentenceToTranslate.isBlank()) {
|
||||
statusMessageService.showSimpleMessage("Please enter a sentence to translate.")
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedTranslationModel.value == null) {
|
||||
Log.e("TranslationViewModel", "Cannot translate because no model is selected.")
|
||||
statusMessageService.showSimpleMessage("Cannot translate because no model is selected.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -151,6 +157,7 @@ class TranslationViewModel @Inject constructor(
|
||||
}
|
||||
.onFailure { exception ->
|
||||
Log.e("TranslationViewModel", "Translation failed: ${exception.message}")
|
||||
statusMessageService.showErrorMessage("Translation failed: ${exception.message}")
|
||||
}
|
||||
|
||||
_isTranslating.value = false
|
||||
|
||||
@@ -240,38 +240,12 @@ class VocabularyExerciseViewModel @Inject constructor(
|
||||
else -> if (shuffleLanguages) Random.nextBoolean() else false
|
||||
}
|
||||
|
||||
_exerciseState.value = when (randomType) {
|
||||
VocabularyExerciseType.GUESSING -> VocabularyExerciseState.Guessing(
|
||||
item = itemToUse,
|
||||
isSwitched = isSwitched
|
||||
)
|
||||
VocabularyExerciseType.SPELLING -> VocabularyExerciseState.Spelling(
|
||||
item = itemToUse,
|
||||
isSwitched = isSwitched
|
||||
)
|
||||
VocabularyExerciseType.MULTIPLE_CHOICE -> {
|
||||
val correctAnswer = if (isSwitched) itemToUse.wordFirst else itemToUse.wordSecond
|
||||
val options = generateMultipleChoiceOptions(correctAnswer, isSwitched)
|
||||
VocabularyExerciseState.MultipleChoice(
|
||||
item = itemToUse,
|
||||
isSwitched = isSwitched,
|
||||
options = options
|
||||
)
|
||||
}
|
||||
VocabularyExerciseType.WORD_JUMBLE -> {
|
||||
val wordToJumble = if (isSwitched) itemToUse.wordFirst else itemToUse.wordSecond
|
||||
VocabularyExerciseState.WordJumble(
|
||||
item = itemToUse,
|
||||
isSwitched = isSwitched,
|
||||
jumbledLetters = wordToJumble.toList().mapIndexed { index, char -> Pair(char, index) }.shuffled()
|
||||
)
|
||||
}
|
||||
}
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
Log.d("ExerciseDebug", "Item: ${itemToUse.wordFirst} (${itemToUse.languageFirstId}) / ${itemToUse.wordSecond} (${itemToUse.languageSecondId}), Switched: $isSwitched")
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
Log.d("ExerciseDebug", "Origin Lang: ${config.originLanguageId}, Target Lang: ${config.targetLanguageId}")
|
||||
|
||||
// Set the exercise state based on the random type
|
||||
_exerciseState.value = when (randomType) {
|
||||
VocabularyExerciseType.GUESSING -> VocabularyExerciseState.Guessing(
|
||||
item = itemToUse,
|
||||
@@ -344,18 +318,16 @@ class VocabularyExerciseViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkAnswer(answer: Any) {
|
||||
viewModelScope.launch {
|
||||
val state = _exerciseState.value ?: return@launch
|
||||
private suspend fun checkAnswer(answer: Any) {
|
||||
val state = _exerciseState.value ?: return
|
||||
val correctAnswer = if (state.isSwitched) state.item.wordFirst else state.item.wordSecond
|
||||
|
||||
// Check if the state is a Spelling type before proceeding with specific logic
|
||||
val isCorrect = when (state) {
|
||||
is VocabularyExerciseState.Spelling -> {
|
||||
val userAnswer = (answer as String).trim()
|
||||
val languageId = if (state.isSwitched) state.item.languageFirstId else state.item.languageSecondId
|
||||
val language = languageRepository.getLanguageById(languageId ?: 0)
|
||||
?: return@launch
|
||||
?: return
|
||||
|
||||
// Get articles for the language
|
||||
val articles = languageConfigRepository.getArticlesForLanguage(language.code)
|
||||
@@ -405,7 +377,6 @@ class VocabularyExerciseViewModel @Inject constructor(
|
||||
is VocabularyExerciseState.WordJumble -> state.copy(isCorrect = isCorrect, isRevealed = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateJumbledWord(assembledWord: List<Pair<Char, Int>>) {
|
||||
_exerciseState.value = when (val state = _exerciseState.value) {
|
||||
|
||||
@@ -26,6 +26,7 @@ import eu.gaudian.translator.model.repository.VocabularyFileSaver
|
||||
import eu.gaudian.translator.model.repository.VocabularyRepository
|
||||
import eu.gaudian.translator.utils.Log
|
||||
import eu.gaudian.translator.utils.StatusAction
|
||||
import eu.gaudian.translator.utils.StatusMessageId
|
||||
import eu.gaudian.translator.utils.StatusMessageService
|
||||
import eu.gaudian.translator.utils.StringHelper
|
||||
import eu.gaudian.translator.utils.VocabularyService
|
||||
@@ -774,7 +775,7 @@ class VocabularyViewModel @Inject constructor(
|
||||
statusService.hideMessageBar()
|
||||
if (_cardSet.value == null) {
|
||||
statusService.cancelAllMessages()
|
||||
statusService.showErrorMessage("No cards found for the specified filter", 3)
|
||||
statusService.showErrorById(StatusMessageId.ERROR_NO_CARDS_FOUND)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 & 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 & 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 & 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 & 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>
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
<item>cz,CZ,49</item>
|
||||
<item>he,IL,50</item>
|
||||
<item>hr,HR,51</item>
|
||||
<item>fil,PH,52</item>
|
||||
</string-array>
|
||||
|
||||
<string name="language_1">Englisch</string>
|
||||
@@ -105,6 +106,7 @@
|
||||
<string name="language_49">Tschechisch</string>
|
||||
<string name="language_50">Hebräisch</string>
|
||||
<string name="language_51">Kroatisch</string>
|
||||
<string name="language_52">Filipino</string>
|
||||
|
||||
|
||||
</resources>
|
||||
@@ -28,7 +28,6 @@
|
||||
<string name="cd_re_generate_definition">Definition neu erstellen</string>
|
||||
<string name="cd_clear_search">Suche löschen</string>
|
||||
<string name="cd_translation_history">Übersetzungsverlauf</string>
|
||||
<string name="label_quit_app">App beenden?</string>
|
||||
<string name="label_reload">Neu laden</string>
|
||||
<string name="title_single">Einzeln</string>
|
||||
<string name="title_widget_streak">Streak</string>
|
||||
@@ -42,7 +41,7 @@
|
||||
<string name="title_multiple">Mehrere</string>
|
||||
<string name="label_translation_settings">Übersetzung</string>
|
||||
<string name="reset_to_defaults">Auf Standard zurücksetzen</string>
|
||||
<string name="text_excel_not_supported_use_csv">Excel wird nicht unterstützt. Bitte CSV verwenden.</string>
|
||||
<string name="message_error_excel_not_supported">Excel wird nicht unterstützt. Bitte CSV verwenden.</string>
|
||||
<string name="error_parsing_table">Fehler beim Parsen der Tabelle</string>
|
||||
<string name="error_parsing_table_with_reason">Fehler beim Parsen der Tabelle: %1$s</string>
|
||||
<string name="label_import_table_csv_excel">Tabelle importieren (CSV)</string>
|
||||
@@ -66,7 +65,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 +128,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 +154,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 +197,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>
|
||||
@@ -214,7 +209,7 @@
|
||||
<string name="text_favorites">Favoriten</string>
|
||||
<string name="text_recent_history">Verlauf</string>
|
||||
<string name="text_select_auto_recognition">Automatische Erkennung auswählen</string>
|
||||
<string name="text_select_none">Keine auswählen</string>
|
||||
<string name="text_select_no_language">Keine auswählen</string>
|
||||
<string name="text_language_options">Sprachoptionen</string>
|
||||
<string name="text_select_all_languages">Alle Sprachen auswählen</string>
|
||||
<string name="text_delete_custom_language">Eigene Sprache löschen</string>
|
||||
@@ -263,7 +258,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>
|
||||
@@ -365,7 +359,7 @@
|
||||
<string name="days_2d">%1$d Tage</string>
|
||||
<string name="progress_by_category">Fortschritt nach Kategorie</string>
|
||||
<string name="label_apply_filters">Filter anwenden</string>
|
||||
<string name="filter_by_stage">Nach Stufe filtern</string>
|
||||
<string name="label_filter_by_stage">Nach Stufe filtern</string>
|
||||
<string name="label_category">Kategorie</string>
|
||||
<string name="language">Sprache</string>
|
||||
<string name="label_clear_all">Alle löschen</string>
|
||||
@@ -402,7 +396,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 +554,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 +568,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 +593,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 +600,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 +628,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 +643,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 +673,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 +796,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 +846,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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
<item>cz,CZ,49</item>
|
||||
<item>he,IL,50</item>
|
||||
<item>hr,HR,51</item>
|
||||
<item>fil,PH,52</item>
|
||||
</string-array>
|
||||
|
||||
<string name="language_1">Inglês</string>
|
||||
@@ -105,6 +106,7 @@
|
||||
<string name="language_49">Tcheco</string>
|
||||
<string name="language_50">Hebraico</string>
|
||||
<string name="language_51">Croata</string>
|
||||
<string name="language_52">Filipino</string>
|
||||
|
||||
|
||||
</resources>
|
||||
@@ -28,7 +28,6 @@
|
||||
<string name="cd_re_generate_definition">Gerar Definição Novamente</string>
|
||||
<string name="cd_clear_search">Limpar pesquisa</string>
|
||||
<string name="cd_translation_history">Histórico de Tradução</string>
|
||||
<string name="label_quit_app">Fechar o aplicativo?</string>
|
||||
<string name="label_reload">Recarregar</string>
|
||||
<string name="title_single">Único</string>
|
||||
<string name="title_widget_streak">Sequência</string>
|
||||
@@ -42,7 +41,7 @@
|
||||
<string name="title_multiple">Múltiplos</string>
|
||||
<string name="label_translation_settings">Configurações de Tradução</string>
|
||||
<string name="reset_to_defaults">Restaurar Padrões</string>
|
||||
<string name="text_excel_not_supported_use_csv">Excel não é suportado. Use CSV.</string>
|
||||
<string name="message_error_excel_not_supported">Excel não é suportado. Use CSV.</string>
|
||||
<string name="error_parsing_table">Erro ao analisar tabela</string>
|
||||
<string name="error_parsing_table_with_reason">Erro ao analisar tabela: %1$s</string>
|
||||
<string name="label_import_table_csv_excel">Importar Tabela (CSV)</string>
|
||||
@@ -66,7 +65,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 +126,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 +152,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 +194,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>
|
||||
@@ -211,7 +206,7 @@
|
||||
<string name="text_favorites">Favoritos</string>
|
||||
<string name="text_recent_history">Histórico</string>
|
||||
<string name="text_select_auto_recognition">Selecionar Reconhecimento Automático</string>
|
||||
<string name="text_select_none">Não selecionar nenhum</string>
|
||||
<string name="text_select_no_language">Não selecionar nenhum</string>
|
||||
<string name="text_language_options">Opções de Idioma</string>
|
||||
<string name="text_select_all_languages">Selecionar todos os idiomas</string>
|
||||
<string name="text_delete_custom_language">Excluir idioma personalizado</string>
|
||||
@@ -362,7 +357,7 @@
|
||||
<string name="days_2d">%1$d dias</string>
|
||||
<string name="progress_by_category">Progresso por Categoria</string>
|
||||
<string name="label_apply_filters">Aplicar Filtros</string>
|
||||
<string name="filter_by_stage">Filtrar por Estágio</string>
|
||||
<string name="label_filter_by_stage">Filtrar por Estágio</string>
|
||||
<string name="label_category">Categoria</string>
|
||||
<string name="language">Idioma</string>
|
||||
<string name="label_clear_all">Limpar Tudo</string>
|
||||
@@ -556,7 +551,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 +565,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 +590,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 +597,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,11 +625,10 @@
|
||||
<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>
|
||||
<string name="label_language_none">Nenhuma</string>
|
||||
<string name="label_language_none">Nenhum</string>
|
||||
<string name="label_grammar_inflections">Flexões</string>
|
||||
<string name="label_more">Mais</string>
|
||||
<string name="label_translations">Traduções</string>
|
||||
@@ -660,7 +638,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 +848,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>
|
||||
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
<string-array name="changelog_entries">
|
||||
<item>Version 0.3.0 \n• Enabled CSV Import for Vocabulary\n• Option to use a translation server for translations instead of AI models for some supported langugaes\n• UI bug fixes \n• Show word frequency \n• Performance optimizations \n• Improved translations (German and Portuguese)</item>
|
||||
<item>Version 0.4.0 \n• Added dictionary download (beta) \n• UI enhancements \n• Bugfixes \n• Re-designed vocabulary card with improved UI \n• More pre-configured providers \n• Improved performance</item>
|
||||
<item>Version 0.5.0 \n• Reworked hints and help content, added more instcructions and help \n• UI changes in the flashcards with a more intuitive design \n• Lots of bugfixes \n• Improved translations for German and Portuguese</item>
|
||||
<item> </item>
|
||||
|
||||
</string-array>
|
||||
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="hint_scan_hint_section_cant_find">Can’t find your model?</string>
|
||||
<string name="hint_scan_hint_manual_add_paragraph">You can add models manually. Enter the exact Model ID from your provider’s 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 & 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 & Validate to save the model and perform a quick check with your provider.</string>
|
||||
<string name="hint_scan_hint_add_validate">Add & 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 & 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 it’s correctly configured.</string>
|
||||
<string name="scan_hint_tip_type_manually">If a model you know exists doesn’t show up, try typing it manually using the Model ID shown in the provider’s 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>
|
||||
@@ -52,6 +52,7 @@
|
||||
<item>cz,CZ,49</item>
|
||||
<item>he,IL,50</item>
|
||||
<item>hr,HR,51</item>
|
||||
<item>fil,PH,52</item>
|
||||
</string-array>
|
||||
|
||||
<string name="language_1">English</string>
|
||||
@@ -105,6 +106,7 @@
|
||||
<string name="language_49">Czech</string>
|
||||
<string name="language_50">Hebrew</string>
|
||||
<string name="language_51">Croatian</string>
|
||||
<string name="language_52">Filipino</string>
|
||||
|
||||
|
||||
</resources>
|
||||
@@ -52,4 +52,5 @@
|
||||
<string name="native_language_49" translatable="false">Čeština</string>
|
||||
<string name="native_language_50" translatable="false">עברית</string>
|
||||
<string name="native_language_51" translatable="false">Hrvatski</string>
|
||||
<string name="native_language_52" translatable="false">Filipino</string>
|
||||
</resources>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -144,7 +140,7 @@
|
||||
<string name="fetching_grammar_details">Fetching Grammar Details</string>
|
||||
|
||||
<string name="filter_and_sort">Filter and Sort</string>
|
||||
<string name="filter_by_stage">Filter by Stage</string>
|
||||
<string name="label_filter_by_stage">Filter by Stage</string>
|
||||
<string name="filter_by_word_type">Filter by Word Type</string>
|
||||
|
||||
<string name="find_translations">Find Translations</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>
|
||||
|
||||
@@ -240,6 +225,7 @@
|
||||
<string name="label_amount_models">%1$d models</string>
|
||||
<string name="label_analyze_grammar">Analyze Grammar</string>
|
||||
<string name="label_appearance">Appearance</string>
|
||||
<string name="hint_settings_title_help">Help</string>
|
||||
<string name="label_apply_filters">Apply Filters</string>
|
||||
<string name="label_article">Article</string>
|
||||
<string name="label_backup_and_restore">Backup and Restore</string>
|
||||
@@ -330,7 +316,7 @@
|
||||
<string name="label_preview_second">Preview (first 5) for second column: %1$s</string>
|
||||
<string name="label_pronoun">Pronoun</string>
|
||||
<string name="label_providers">Providers</string>
|
||||
<string name="label_quit_app">Quit app?</string>
|
||||
<string name="label_quit_app">Quit App</string>
|
||||
<string name="label_quit_exercise_qm">Quit Exercise?</string>
|
||||
<string name="label_raw_data_2d">Raw Data:</string>
|
||||
<string name="label_related_words">Related Words</string>
|
||||
@@ -649,10 +635,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 +740,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 +780,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,8 +788,7 @@
|
||||
<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="message_error_excel_not_supported">Excel is not supported. Use CSV instead.</string>
|
||||
<string name="text_expand_widget">Expand Widget</string>
|
||||
<string name="text_explanation">Explanation</string>
|
||||
<string name="text_export_category">Export Category</string>
|
||||
@@ -835,7 +814,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>
|
||||
@@ -906,7 +884,7 @@
|
||||
<string name="text_select_category">Select Category</string>
|
||||
<string name="text_select_languages">Select Languages</string>
|
||||
<string name="text_select_model">Select Model</string>
|
||||
<string name="text_select_none">Select None</string>
|
||||
<string name="text_select_no_language">Select None</string>
|
||||
<string name="text_select_the_content_dictionary">Select the content to be generated for a dictionary entry.</string>
|
||||
<string name="text_select_translations_to_add">Select Translations to Add</string>
|
||||
<string name="text_selected">Selected</string>
|
||||
@@ -1049,4 +1027,91 @@
|
||||
<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>
|
||||
<string name="label_no_category">None</string>
|
||||
<string name="text_select">Select</string>
|
||||
<string name="text_search">Search</string>
|
||||
<string name="text_language_settings_description">Set what languages you want to use in the app. Languages that are not activated will not appear in this app. You can also add your own language to the list, or change an existing language (region/locale)</string>
|
||||
|
||||
<!-- Status Messages (for internationalization) -->
|
||||
<string name="message_success_generic">Success!</string>
|
||||
<string name="message_info_generic">Info</string>
|
||||
<string name="message_error_generic">An error occurred</string>
|
||||
<string name="message_loading_generic">Loading…</string>
|
||||
|
||||
<!-- Language related -->
|
||||
<string name="message_error_language_not_selected">Source and target languages must be selected.</string>
|
||||
<string name="message_error_no_words_found">No words found in the provided text.</string>
|
||||
<string name="message_success_language_replaced">Language ID updated for %1$d items.</string>
|
||||
|
||||
<!-- Vocabulary related -->
|
||||
<string name="message_success_vocabulary_imported">Vocabulary items imported successfully.</string>
|
||||
<string name="message_error_vocabulary_import_failed">Error importing vocabulary items: %1$s</string>
|
||||
<string name="message_success_items_merged">Items merged!</string>
|
||||
<string name="message_success_items_added">Successfully added %1$d new vocabulary items.</string>
|
||||
<string name="message_error_items_add_failed">Error adding items: %1$s</string>
|
||||
<string name="message_success_items_deleted">Successfully deleted vocabulary items.</string>
|
||||
<string name="message_error_items_delete_failed">Error deleting items: %1$s</string>
|
||||
<string name="message_error_no_cards_found">No cards found for the specified filter.</string>
|
||||
<string name="message_success_cards_loaded">Successfully loaded card set.</string>
|
||||
|
||||
<!-- Grammar related -->
|
||||
<string name="message_success_grammar_updated">Grammar details updated!</string>
|
||||
<string name="message_error_grammar_fetch_failed">Could not retrieve grammar details.</string>
|
||||
<string name="message_loading_grammar_fetch">Fetching grammar for %1$d items…</string>
|
||||
|
||||
<!-- File operations -->
|
||||
<string name="message_success_file_saved">File saved to %1$s</string>
|
||||
<string name="message_error_file_save_failed">Error saving file: %1$s</string>
|
||||
<string name="message_error_file_save_cancelled">File save cancelled or failed.</string>
|
||||
<string name="message_error_file_picker_not_initialized">Save File Launcher not initialized.</string>
|
||||
<string name="message_success_category_saved">Category saved to %1$s</string>
|
||||
|
||||
<!-- API Key related -->
|
||||
<string name="message_error_api_key_missing">API Key is missing or invalid.</string>
|
||||
<string name="message_error_api_key_invalid">API Key is missing or invalid.</string>
|
||||
|
||||
<!-- Translation related -->
|
||||
<string name="message_loading_translating">Translating %1$d words…</string>
|
||||
<string name="message_success_translation_completed">Translation completed.</string>
|
||||
<string name="message_error_translation_failed">Translation failed: %1$s</string>
|
||||
|
||||
<!-- Repository operations -->
|
||||
<string name="message_success_repository_wiped">All repository data deleted.</string>
|
||||
<string name="message_error_repository_wipe_failed">Failed to wipe repository: %1$s</string>
|
||||
<string name="message_loading_card_set">Loading card set</string>
|
||||
|
||||
<!-- Stage operations -->
|
||||
<string name="message_success_stage_updated">Stage updated successfully.</string>
|
||||
<string name="message_error_stage_update_failed">Error updating stage: %1$s</string>
|
||||
|
||||
<!-- Category operations -->
|
||||
<string name="message_success_category_updated">Category updated successfully.</string>
|
||||
<string name="message_error_category_update_failed">Error updating category: %1$s</string>
|
||||
|
||||
<!-- Article removal -->
|
||||
<string name="message_success_articles_removed">Articles removed successfully.</string>
|
||||
<string name="message_error_articles_remove_failed">Error removing articles: %1$s</string>
|
||||
|
||||
<!-- Synonyms -->
|
||||
<string name="message_success_synonyms_generated">Synonyms generated successfully.</string>
|
||||
<string name="message_error_synonyms_generation_failed">Failed to generate synonyms: %1$s</string>
|
||||
|
||||
<!-- Operation status -->
|
||||
<string name="message_error_operation_failed">Operation failed: %1$s</string>
|
||||
<string name="message_loading_operation_in_progress">Operation in progress…</string>
|
||||
<string name="message_test_info">This is a generic info message.</string>
|
||||
<string name="message_test_success">This is a test success message!</string>
|
||||
<string name="message_test_error">Oops, something went wrong :(</string>
|
||||
</resources>
|
||||
|
||||
270
docs/EXERCISES.md
Normal file
270
docs/EXERCISES.md
Normal file
@@ -0,0 +1,270 @@
|
||||
# Vocabulary Exercises Documentation
|
||||
|
||||
This document explains how the exercise system works in the Polly vocabulary learning app.
|
||||
|
||||
## Overview
|
||||
|
||||
The exercise system allows users to practice vocabulary through various interactive exercise types. Users can configure and start exercises from different screens, track their progress, and review their results.
|
||||
|
||||
## Exercise Types
|
||||
|
||||
The app supports four different exercise types:
|
||||
|
||||
### 1. Guessing Exercise (Flashcard Mode)
|
||||
|
||||
**How it works:**
|
||||
- A vocabulary card is displayed with one side (word or translation)
|
||||
- The user sees only one side of the card initially
|
||||
- The user guesses the answer before flipping the card
|
||||
|
||||
**User actions:**
|
||||
- Tap "Flip Card" to reveal the answer
|
||||
- Mark as "Wrong" or "Correct" after revealing
|
||||
- The card advances when "Next" is tapped
|
||||
|
||||
**Use case:** Ideal for quick recognition practice and memorization.
|
||||
|
||||
---
|
||||
|
||||
### 2. Spelling Exercise
|
||||
|
||||
**How it works:**
|
||||
- A vocabulary word is displayed as a prompt
|
||||
- The user must type the translation manually
|
||||
- The app checks if the typed answer is correct
|
||||
|
||||
**User actions:**
|
||||
- Type the translation in the text field
|
||||
- Tap "Check" to submit the answer
|
||||
- See immediate feedback on correctness
|
||||
- Tap "Next" to proceed to the next card
|
||||
|
||||
**Use case:** Excellent for practicing proper spelling and reinforcing memory through active recall.
|
||||
|
||||
---
|
||||
|
||||
### 3. Multiple Choice Exercise
|
||||
|
||||
**How it works:**
|
||||
- A word is displayed with four answer options
|
||||
- Only one option is the correct translation
|
||||
- The user selects their answer
|
||||
|
||||
**User actions:**
|
||||
- Tap one of the four options
|
||||
- The correct answer is highlighted after selection
|
||||
- Visual feedback shows if the choice was right or wrong
|
||||
- Tap "Next" to continue
|
||||
|
||||
**Use case:** Good for recognition practice with immediate feedback.
|
||||
|
||||
---
|
||||
|
||||
### 4. Word Jumble Exercise
|
||||
|
||||
**How it works:**
|
||||
- A word's letters are scrambled and displayed
|
||||
- The user must click letters in the correct order to form the word
|
||||
- Letters can be moved between the "available" and "assembled" areas
|
||||
|
||||
**User actions:**
|
||||
- Click a letter from the available pool to add it to the assembly area
|
||||
- Click an assembled letter to return it to the pool
|
||||
- Tap "Check" when the word is assembled
|
||||
- The correct answer is revealed if incorrect
|
||||
|
||||
**Use case:** Great for reinforcing spelling through active manipulation of letter order.
|
||||
|
||||
---
|
||||
|
||||
## Starting an Exercise
|
||||
|
||||
### Entry Points
|
||||
|
||||
1. **Dashboard Widget:** Tap "Start Exercise" from the ModernStartButtons widget
|
||||
2. **Category Detail Screen:** Tap "Start" on a specific category
|
||||
3. **Main Vocabulary Screen:** Tap the FAB (Floating Action Button)
|
||||
4. **Daily Exercise:** Access from the dashboard for daily review
|
||||
|
||||
### Configuration Options
|
||||
|
||||
Before starting an exercise, users can configure:
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| **Number of Cards** | Slider to select how many cards to practice (1 to total available) |
|
||||
| **Quick Select** | Preset buttons for 10, 25, 50, or 100 cards |
|
||||
| **Language Direction** | Choose origin and target languages |
|
||||
| **Exercise Types** | Select which exercise types to include (can combine multiple) |
|
||||
| **Shuffle Cards** | Randomize card order |
|
||||
| **Shuffle Languages** | Randomize which side (word/translation) is shown |
|
||||
| **Training Mode** | Practice without affecting progress/stages |
|
||||
| **Due Today Only** | Limit to items scheduled for today |
|
||||
|
||||
---
|
||||
|
||||
## Exercise Flow
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ START SCREEN │
|
||||
│ (Configuration) │
|
||||
└────────┬────────┘
|
||||
│ Tap "Start Exercise"
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ EXERCISE │
|
||||
│ PROGRESS │
|
||||
│ INDICATOR │
|
||||
├─────────────────┤
|
||||
│ CARD/QUESTION │
|
||||
│ DISPLAY │
|
||||
├─────────────────┤
|
||||
│ CONTROLS │
|
||||
│ (Reveal/Check) │
|
||||
└────────┬────────┘
|
||||
│ Complete all cards
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ RESULT SCREEN │
|
||||
│ (Score, │
|
||||
│ Retry, │
|
||||
│ Start Over) │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise Progress Indicator
|
||||
|
||||
During an exercise, a progress bar shows:
|
||||
|
||||
- **Green section:** Number of correct answers
|
||||
- **Red section:** Number of wrong answers
|
||||
- **Total:** Total cards in the exercise
|
||||
- **Close button:** Exit the exercise (shows confirmation dialog)
|
||||
|
||||
The progress bar animates as the user advances through cards.
|
||||
|
||||
---
|
||||
|
||||
## Exercise State Management
|
||||
|
||||
The app uses a state machine to manage exercise progress:
|
||||
|
||||
### States
|
||||
|
||||
| State | Description |
|
||||
|-------|-------------|
|
||||
| `START` | Configuration screen before starting |
|
||||
| `EXERCISE` | Active exercise in progress |
|
||||
| `RESULT` | Exercise completed, showing results |
|
||||
|
||||
### State Transitions
|
||||
|
||||
```
|
||||
START → EXERCISE (user taps "Start")
|
||||
EXERCISE → RESULT (all cards completed)
|
||||
RESULT → START (user taps "Start Over")
|
||||
RESULT → EXERCISE (user taps "Retry Wrong")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exercise Actions
|
||||
|
||||
The following user actions are available during exercises:
|
||||
|
||||
| Action | Description |
|
||||
|--------|-------------|
|
||||
| `Reveal` | Show the answer (Guessing exercise) |
|
||||
| `Submit` | Submit an answer (Spelling, Multiple Choice, Word Jumble) |
|
||||
| `Next` | Move to the next card |
|
||||
| `UpdateWordJumble` | Modify assembled letters (Word Jumble) |
|
||||
|
||||
---
|
||||
|
||||
## Results Screen
|
||||
|
||||
After completing an exercise, users see:
|
||||
|
||||
### Displayed Information
|
||||
- **Percentage Score:** Overall performance as a circular progress indicator
|
||||
- **Correct/Wrong Count:** Detailed breakdown of answers
|
||||
- **Total Items:** Number of cards practiced
|
||||
|
||||
### Available Actions
|
||||
- **Start Over:** Begin a new exercise from scratch
|
||||
- **Repeat Wrong:** Retry only the incorrectly answered cards
|
||||
- **Finish:** Return to the main screen
|
||||
|
||||
---
|
||||
|
||||
## Training Mode
|
||||
|
||||
When Training Mode is enabled:
|
||||
- Answers are not recorded for progress tracking
|
||||
- No stage progression occurs
|
||||
- Useful for casual practice or testing knowledge
|
||||
|
||||
When disabled (default):
|
||||
- Correct answers may advance item stages
|
||||
- Progress is saved for statistics
|
||||
- Affects due dates and learning progress
|
||||
|
||||
---
|
||||
|
||||
## Technical Architecture
|
||||
|
||||
### Key Components
|
||||
|
||||
1. **VocabularyExerciseHostScreen.kt**
|
||||
- Main container managing exercise flow
|
||||
- Handles screen states (START, EXERCISE, RESULT)
|
||||
- Coordinates between configuration and exercise screens
|
||||
|
||||
2. **VocabularyExercise.kt**
|
||||
- Defines exercise types (enum)
|
||||
- State classes for each exercise type
|
||||
- Action sealed class for user interactions
|
||||
|
||||
3. **VocabularyExerciseRenderer.kt**
|
||||
- Renders the appropriate UI based on exercise state
|
||||
- Handles display logic for each exercise type
|
||||
|
||||
4. **ExerciseControls.kt**
|
||||
- Input field and buttons for user interaction
|
||||
- Different controls based on exercise type
|
||||
|
||||
5. **ExerciseProgressIndicator.kt**
|
||||
- Visual progress bar during exercises
|
||||
- Animated updates for correct/wrong counts
|
||||
|
||||
---
|
||||
|
||||
## Related Screens
|
||||
|
||||
| Screen | Purpose |
|
||||
|--------|---------|
|
||||
| `StartScreen` | Configure exercise settings before starting |
|
||||
| `ResultScreen` | Show final score and options to continue |
|
||||
| `CategoryDetailScreen` | Start exercise for a specific category |
|
||||
| `DashboardContent` | Main dashboard with exercise shortcuts |
|
||||
|
||||
---
|
||||
|
||||
## Tips for Users
|
||||
|
||||
1. **Combine exercise types:** Mix Guessing, Spelling, and Word Jumble for variety
|
||||
2. **Use Training Mode:** For casual practice without affecting progress
|
||||
3. **Start small:** Use 10-25 cards initially, increase as comfortable
|
||||
4. **Review wrong answers:** Use "Repeat Wrong" to focus on difficult items
|
||||
5. **Daily exercises:** Use "Due Today Only" for spaced repetition practice
|
||||
|
||||
---
|
||||
|
||||
## Navigation During Exercise
|
||||
|
||||
- **Close button:** Exit exercise (confirms before closing)
|
||||
- **Back button:** Also triggers exit confirmation
|
||||
- Results screen options: Continue practicing or return to dashboard
|
||||
@@ -41,6 +41,9 @@ 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"
|
||||
foundationLayoutVersion = "1.10.3"
|
||||
|
||||
|
||||
[libraries]
|
||||
@@ -100,6 +103,8 @@ 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" }
|
||||
androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayoutVersion" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user