Compare commits
16 Commits
858c73fd0d
...
glassmorph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d0bf4cb1c | ||
|
|
2b8b9a84a3 | ||
|
|
59f5f5e668 | ||
|
|
15f7eae068 | ||
|
|
8e610259ca | ||
|
|
7d18f8eb04 | ||
|
|
f4fcffe90a | ||
|
|
5e920c43b3 | ||
|
|
61a97a1119 | ||
|
|
2e0fe76fbf | ||
|
|
a715ab78e9 | ||
|
|
fa3524268a | ||
|
|
77b86208c3 | ||
|
|
03e9aeedae | ||
|
|
05a1b2b71a | ||
|
|
18474b072e |
2
.idea/deploymentTargetSelector.xml
generated
2
.idea/deploymentTargetSelector.xml
generated
@@ -4,7 +4,7 @@
|
|||||||
<selectionStates>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<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">
|
<Target type="DEFAULT_BOOT">
|
||||||
<handle>
|
<handle>
|
||||||
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\jonas\.android\avd\Medium_Phone_28.avd" />
|
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\jonas\.android\avd\Medium_Phone_28.avd" />
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ android {
|
|||||||
applicationId = "eu.gaudian.translator"
|
applicationId = "eu.gaudian.translator"
|
||||||
minSdk = 28
|
minSdk = 28
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 22
|
versionCode = 23
|
||||||
versionName = "0.4.1"
|
versionName = "0.5.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
@@ -130,6 +130,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.room.runtime) // ADDED: Explicitly add runtime
|
implementation(libs.androidx.room.runtime) // ADDED: Explicitly add runtime
|
||||||
implementation(libs.androidx.room.ktx)
|
implementation(libs.androidx.room.ktx)
|
||||||
implementation(libs.core.ktx)
|
implementation(libs.core.ktx)
|
||||||
|
implementation(libs.androidx.compose.foundation.layout)
|
||||||
ksp(libs.room.compiler) // CHANGED: Use ksp instead of implementation
|
ksp(libs.room.compiler) // CHANGED: Use ksp instead of implementation
|
||||||
|
|
||||||
// Networking
|
// Networking
|
||||||
|
|||||||
@@ -1,53 +1,70 @@
|
|||||||
# How to Connect to an AI Model
|
## What is an API Key?
|
||||||
|
|
||||||
This guide explains how to connect your app to an AI model using 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 Started
|
## Getting an API Key
|
||||||
|
|
||||||
To use AI models in your app, you need to provide a valid API key. This key authenticates your requests and tracks your usage.
|
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.
|
||||||
|
|
||||||
> **Note:** Keep your API key secure and never share it publicly.
|
### For Cloud Providers
|
||||||
|
|
||||||
## Key Status Indicators
|
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
|
||||||
|
|
||||||
Your API key can be in one of two states:
|
### For Local AI Servers
|
||||||
|
|
||||||
| Status | Icon | Meaning |
|
Running a local AI server (like Ollama or LM Studio), you don't need an API key. Just add a custom provider:
|
||||||
|--------|------|---------|
|
|
||||||
| Active | ✅ | Key is valid and working |
|
|
||||||
| Missing | ⚠️ | Key is not set or was cleared |
|
|
||||||
|
|
||||||
### Active Key
|
1. Tap **"Add Custom Provider"**
|
||||||
|
2. Enter your local server IP and endpoint
|
||||||
|
3. Tap **"Check Availability"** to test the connection
|
||||||
|
|
||||||
When your API key is active, you can use all available AI models. The system will display a checkmark indicator next to the key status.
|
## Choosing a Model
|
||||||
|
|
||||||
### Missing Key
|
### What are Models?
|
||||||
|
|
||||||
If the key is missing or cleared, you won't be able to make API requests. You'll see a warning indicator, and any attempt to use AI features will prompt you to add a valid key.
|
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
|
||||||
|
|
||||||
## Connecting Your AI Model
|
For pre-configured providers, some models are already added by default and proven to work with this app.
|
||||||
|
|
||||||
1. **Navigate to Settings** → API Key section
|
### Adding Models
|
||||||
2. **Enter your API key** in the provided field
|
|
||||||
3. **Save** the configuration
|
|
||||||
4. **Verify** the connection is successful
|
|
||||||
|
|
||||||
## Troubleshooting
|
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
|
||||||
|
|
||||||
If you're having issues with your API key:
|
### Assigning Models to Tasks
|
||||||
|
|
||||||
1. **Verify the key is correct** - Check for typos or extra spaces
|
You can use different models for different features:
|
||||||
2. **Ensure the key has proper permissions** - Some models require additional access
|
|
||||||
3. **Check your quota** - You may have exceeded your usage limits
|
|
||||||
4. **Try regenerating the key** - If all else fails, generate a new key from your provider
|
|
||||||
|
|
||||||
```json
|
1. Go to the **Tasks** tab
|
||||||
// Example API key format
|
2. Select which model to use for:
|
||||||
{
|
- **Translation**: Translates text between languages
|
||||||
"api_key": "sk-xxxxxxxxxxxxxxxxxxxx"
|
- **Exercises**: Creates practice exercises
|
||||||
}
|
- **Vocabulary**: Generates vocabulary and synonyms
|
||||||
```
|
- **Dictionary**: Looks up definitions
|
||||||
|
|
||||||
---
|
## Common Problems
|
||||||
|
|
||||||
**Need more help?** Check our documentation or contact support.
|
### "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.
|
||||||
|
|||||||
@@ -1,30 +1,25 @@
|
|||||||
# Understanding Categories
|
|
||||||
|
|
||||||
Learn how to use categories to organize and filter your vocabulary effectively.
|
|
||||||
|
|
||||||
## What Are Categories?
|
## What Are Categories?
|
||||||
|
|
||||||
Categories help you organize your vocabulary into meaningful groups. You can use them to track words by topic, difficulty, or any custom system that works for you.
|
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
|
## Two Types of Categories
|
||||||
|
|
||||||
### List Categories
|
### List Categories
|
||||||
|
|
||||||
List categories are simple groupings of vocabulary items. Words in a list category stay together regardless of their learning stage.
|
List categories are simple groupings of vocabulary items. You can simply just add words to a list and they stay there forever.
|
||||||
|
|
||||||
**Use cases:**
|
**Use cases:**
|
||||||
- Group words by topic (e.g., "Food", "Travel", "Business")
|
- Group words by topic (e.g., "Food", "Travel", "Business")
|
||||||
- Create custom decks for specific purposes
|
- Create custom decks for specific purposes
|
||||||
- Organize words by source (e.g., "Book: Harry Potter")
|
|
||||||
|
|
||||||
### Filter Categories
|
### Filter Categories
|
||||||
|
|
||||||
Filter categories automatically include all vocabulary items that match certain criteria. Words are dynamically added based on the filter rules.
|
Filter categories automatically include all vocabulary items that match certain criteria. Words are dynamically added or removed based on the filter rules.
|
||||||
|
|
||||||
**Use cases:**
|
**Use cases:**
|
||||||
- Filter by learning stage (e.g., "Words I'm learning")
|
- Filter by learning stage (e.g., "Words I'm learning")
|
||||||
- Filter by mastery level (e.g., "Words I need to review")
|
- Filter by language
|
||||||
- Combine multiple criteria for complex filtering
|
- Combine multiple criteria for complex filtering (e.g., "Words in Spanish that I know already")
|
||||||
|
|
||||||
## Creating Categories
|
## Creating Categories
|
||||||
|
|
||||||
@@ -36,14 +31,10 @@ Filter categories automatically include all vocabulary items that match certain
|
|||||||
|
|
||||||
## Managing Categories
|
## Managing Categories
|
||||||
|
|
||||||
- **Edit** - Tap a category to modify its settings
|
- **Edit** - Enter a category to modify its settings
|
||||||
- **Delete** - Swipe left and tap delete (words are not deleted)
|
|
||||||
- **Reorder** - Drag to change display order
|
|
||||||
|
|
||||||
## Tips
|
## Tips
|
||||||
|
|
||||||
> **Pro Tip:** Use filter categories for learning stages to automatically track progress across all words at a certain level.
|
- 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
|
||||||
|
|
||||||
*Need more help? Check our documentation.*
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
# Dictionary Options
|
# Dictionary Options
|
||||||
|
# TODO REWRITE
|
||||||
|
|
||||||
Learn how to configure and use the dictionary options for better translations.
|
Learn how to configure and use the dictionary options for better translations.
|
||||||
|
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
# How to Scan for AI Models
|
|
||||||
|
|
||||||
This guide explains how to use the **Scan** feature to discover and add AI models to your app.
|
|
||||||
|
|
||||||
## How Scanning Works
|
|
||||||
|
|
||||||
The scan feature searches for available AI models on your device or network.
|
|
||||||
|
|
||||||
> **Note:** Results depend on your API key permissions.
|
|
||||||
|
|
||||||
### Key Points
|
|
||||||
|
|
||||||
- Only public models are shown by default
|
|
||||||
- Private models require additional setup
|
|
||||||
- Try again if no models are found
|
|
||||||
|
|
||||||
## Why Some Models Are Missing
|
|
||||||
|
|
||||||
Some models may not appear in the scan results due to:
|
|
||||||
|
|
||||||
| Reason | Description | Icon |
|
|
||||||
|--------|-------------|------|
|
|
||||||
| Restricted access | Model requires special permissions | 🔒 |
|
|
||||||
| Not suitable | Model type not supported | ⚠️ |
|
|
||||||
| Text only | Only text-based models are supported | ✓ |
|
|
||||||
|
|
||||||
### Model Tiers
|
|
||||||
|
|
||||||
We recommend these tiers for optimal performance:
|
|
||||||
|
|
||||||
- **Nano** - Fastest, for simple tasks
|
|
||||||
- **Mini** - Balanced speed and capability
|
|
||||||
- **Small** - Good for most tasks
|
|
||||||
- **Medium** - More capable, slower
|
|
||||||
- **Large** - Most capable, paid only
|
|
||||||
|
|
||||||
## Tips for Success
|
|
||||||
|
|
||||||
1. **Verify your API key** is active and has correct permissions
|
|
||||||
2. **Select the correct organization** from your account
|
|
||||||
3. **Type model names manually** if scanning doesn't find them
|
|
||||||
4. **Prefer instruct or chat models** for text generation
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
// Example: Manual model addition
|
|
||||||
val model = Model(
|
|
||||||
name = "llama3.2",
|
|
||||||
type = ModelType.TEXT,
|
|
||||||
provider = "ollama"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Visual Guide
|
|
||||||
|
|
||||||
### Step 1: Initiate Scan
|
|
||||||
|
|
||||||
Click the scan button to search for available models.
|
|
||||||
|
|
||||||
### Step 2: Select Model Type
|
|
||||||
|
|
||||||
Choose between different model categories:
|
|
||||||
|
|
||||||
- **Text Chat** - For conversational AI
|
|
||||||
- **Instruct** - For direct instructions
|
|
||||||
- **Complete** - For text completion
|
|
||||||
|
|
||||||
### Step 3: Add & Validate
|
|
||||||
|
|
||||||
Add the selected model and validate it works correctly.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Can't Find Your Model?
|
|
||||||
|
|
||||||
If your model doesn't appear in the scan results:
|
|
||||||
|
|
||||||
1. Check if the model is running locally or accessible via API
|
|
||||||
2. Verify network connectivity
|
|
||||||
3. Try adding it manually by entering the model details
|
|
||||||
|
|
||||||
> **Pro Tip:** You can always add models manually by clicking the "+" button in the models screen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Last updated: 2024-01-15*
|
|
||||||
*For more help, visit our documentation website.*
|
|
||||||
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
|
||||||
@@ -1,25 +1,23 @@
|
|||||||
# Import Vocabulary with AI
|
|
||||||
|
|
||||||
Generate vocabulary lists automatically using AI assistance.
|
Generate vocabulary lists automatically using AI assistance.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
Use AI to quickly create vocabulary lists from your learning goals.
|
Use AI to quickly create vocabulary lists for a certain topic.
|
||||||
|
|
||||||
## Step-by-Step Guide
|
|
||||||
|
|
||||||
### Step 1: Enter Search Term
|
### Step 1: Enter Search Term
|
||||||
|
|
||||||
Type a topic, theme, or concept for your vocabulary list:
|
Type a topic, theme, or concept for your vocabulary list:
|
||||||
- Be specific for better results
|
- Be specific for better results
|
||||||
- Example: "German food and restaurant phrases"
|
- Example: "German food and restaurant phrases"
|
||||||
- Example: "Business vocabulary for meetings"
|
- Example: "Things to do in Paris"
|
||||||
|
- Example: "Difficult verbs that are confusing"
|
||||||
|
|
||||||
### Step 2: Select Languages
|
### Step 2: Select Languages
|
||||||
|
|
||||||
Choose source and target languages:
|
Choose source and target languages:
|
||||||
- **Source language** - The language you're learning from
|
- **Source language** - The first language of the flashcard
|
||||||
- **Target language** - Your native language
|
- **Target language** - The second language of the flashcard
|
||||||
|
|
||||||
### Step 3: Set Amount
|
### Step 3: Set Amount
|
||||||
|
|
||||||
@@ -32,22 +30,17 @@ Choose how many words to generate:
|
|||||||
|
|
||||||
Tap the generate button:
|
Tap the generate button:
|
||||||
- AI creates the vocabulary list
|
- AI creates the vocabulary list
|
||||||
- Review each entry before saving
|
|
||||||
- Edit any translations if needed
|
|
||||||
|
|
||||||
## After Generation
|
## After Generation
|
||||||
|
|
||||||
Once generated, you can:
|
Once generated, you can:
|
||||||
|
|
||||||
- **Review** - Check each word-translation pair
|
- Choose which terms to keep
|
||||||
- **Edit** - Correct any mistakes
|
- Optionally, add it to a category
|
||||||
- **Delete** - Remove unwanted entries
|
|
||||||
- **Import All** - Add all to your vocabulary
|
|
||||||
|
|
||||||
## Tips
|
## Tips
|
||||||
|
|
||||||
> **Pro Tip:** Start with 10 words per import to get familiar with the feature.
|
- 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
|
||||||
*Need help? Check our vocabulary management guide.*
|
|
||||||
@@ -1,53 +1,44 @@
|
|||||||
# Learning Stages
|
|
||||||
|
|
||||||
Understand how vocabulary progresses through different learning stages to optimize your study sessions.
|
|
||||||
|
|
||||||
## The Learning Stages
|
## The Learning Stages
|
||||||
|
|
||||||
Your vocabulary items move through these stages as you learn:
|
Your vocabulary items move through these stages as you learn. In "daily" exercises you get presented with vocabulary items according to their interval.
|
||||||
|
|
||||||
| Stage | Name | Interval | Description |
|
| Stage | Interval | Description |
|
||||||
|-------|------|----------|-------------|
|
|------|----------|-------------|
|
||||||
| 🌟 | New | - | Just added vocabulary |
|
| New | 1 day | Just added vocabulary |
|
||||||
| 📅 | Stage 1 | 1 day | Recently learned |
|
| Stage 1 | 3 days | Recently learned |
|
||||||
| 📅 | Stage 2 | 3 days | Reinforcement |
|
| Stage 2 | 1 week | Reinforcement |
|
||||||
| 📅 | Stage 3 | 1 week | Consolidation |
|
| Stage 3 | 2 weeks | Consolidation |
|
||||||
| 📅 | Stage 4 | 2 weeks | Deep learning |
|
| Stage 4 | 1 month | Deep learning |
|
||||||
| 📅 | Stage 5 | 1 month | Mastery |
|
| Stage 5 | 2 month | Mastery |
|
||||||
| ✅ | Learned | ∞ | Fully learned |
|
| Learned | 3 months | Fully learned |
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
### Answer Correctly ✅
|
### Answer Correctly
|
||||||
|
|
||||||
When you correctly identify a word during review:
|
When you correctly identify a word during an exercise:
|
||||||
- The word **moves forward** to the next stage
|
- The word **moves forward** to the next stage
|
||||||
- The interval until next review **increases**
|
- The interval until next review **increases**
|
||||||
- This helps you focus on words that need more practice
|
- This helps you focus on words that need more practice
|
||||||
|
|
||||||
### Answer Incorrectly ❌
|
### Answer Incorrectly
|
||||||
|
|
||||||
When you make a mistake:
|
When you make a mistake:
|
||||||
- The word **moves back** one or more stages
|
- The word **moves back** one stage
|
||||||
- The review interval **decreases**
|
- The review interval **decreases**
|
||||||
- This ensures you practice challenging words more often
|
- This ensures you practice challenging words more often
|
||||||
|
|
||||||
## Customization
|
## Customization
|
||||||
|
|
||||||
All intervals and rules can be customized in Settings:
|
All intervals and rules can be customized:
|
||||||
|
|
||||||
- **Adjust intervals** for each stage
|
- **Adjust intervals** for each stage
|
||||||
- **Change how many stages** to regress on errors
|
- **Change how many attempts** it takes to move up a stage or get demoted
|
||||||
- **Skip stages** for certain word types
|
- **Skip stages** You can also manually move items to a stage
|
||||||
- **Enable/disable** specific stages
|
|
||||||
|
|
||||||
## Visual Progress
|
## Visual Progress
|
||||||
|
|
||||||
The app displays your progress visually:
|
In the dashboard, the app displays your progress visually:
|
||||||
- Stage indicators show current status
|
- Stage indicators show current status
|
||||||
- Progress bars track advancement
|
- Progress bars track advancement
|
||||||
- Statistics display overall mastery
|
- Statistics display overall mastery
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Tip: Consistent daily practice is key to moving words through all stages!*
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# Review Vocabulary
|
# Review Vocabulary
|
||||||
|
# TODO REWRITE
|
||||||
|
|
||||||
Master your vocabulary through systematic review sessions.
|
Master your vocabulary through systematic review sessions.
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
# Sorting Vocabulary
|
After you imported vocabulary, you can sort vocabulary
|
||||||
|
|
||||||
Learn how to efficiently sort and organize new vocabulary as you add them.
|
|
||||||
|
|
||||||
## The Sorting Screen
|
|
||||||
|
|
||||||
When you import vocabulary, you'll see the sorting screen where you can:
|
|
||||||
|
|
||||||
- Review each word-translation pair
|
- Review each word-translation pair
|
||||||
- Decide the next action for each item
|
- Decide the next action for each item
|
||||||
@@ -12,19 +6,17 @@ When you import vocabulary, you'll see the sorting screen where you can:
|
|||||||
|
|
||||||
## Actions
|
## Actions
|
||||||
|
|
||||||
### ✅ Mark as Learned
|
### Mark as Learned
|
||||||
|
|
||||||
Move the word directly to Stage 1:
|
If you already know the word, move the word directly to Stage "Learned". This prevents the word from reappearing in your exercises.
|
||||||
- The word enters your learning queue
|
|
||||||
- You'll review it according to the learning schedule
|
|
||||||
|
|
||||||
### 🗑️ Delete
|
### Delete
|
||||||
|
|
||||||
Remove the word entirely:
|
Remove the word entirely:
|
||||||
- Use for duplicates or unwanted entries
|
- Use for duplicates or unwanted entries
|
||||||
- This action is permanent
|
- This action is permanent
|
||||||
|
|
||||||
### 📝 Edit
|
### Edit
|
||||||
|
|
||||||
Tap on any word or translation to edit:
|
Tap on any word or translation to edit:
|
||||||
- Correct typos
|
- Correct typos
|
||||||
@@ -33,18 +25,12 @@ Tap on any word or translation to edit:
|
|||||||
|
|
||||||
## Duplicate Handling
|
## Duplicate Handling
|
||||||
|
|
||||||
When duplicates are detected:
|
When duplicates are detected, you can choose how to handle them:
|
||||||
|
|
||||||
| Icon | Meaning |
|
|
||||||
|------|---------|
|
|
||||||
| ⚠️ | Duplicate detected |
|
|
||||||
| ✅ | Original entry |
|
|
||||||
| ❌ | Duplicate entry |
|
|
||||||
|
|
||||||
**Options for duplicates:**
|
**Options for duplicates:**
|
||||||
- Keep only the original
|
- Keep only the original
|
||||||
- Keep the newer entry
|
- Keep the newer entry
|
||||||
- Keep both (merge)
|
- Keep both (merge): the newer entry will get deleted but all its information (categories) will be added the old item.
|
||||||
- Delete the duplicate
|
- Delete the duplicate
|
||||||
|
|
||||||
## Helper Features
|
## Helper Features
|
||||||
@@ -56,17 +42,7 @@ Toggle to automatically strip articles from words:
|
|||||||
- "the dog" → "dog"
|
- "the dog" → "dog"
|
||||||
- Useful for cleaner vocabulary lists
|
- Useful for cleaner vocabulary lists
|
||||||
|
|
||||||
### Quick Actions
|
|
||||||
|
|
||||||
Use quick action buttons for bulk operations:
|
|
||||||
- **Skip All** - Review later
|
|
||||||
- **Learn All** - Add all to Stage 1
|
|
||||||
- **Delete Duplicates** - Auto-remove duplicates
|
|
||||||
|
|
||||||
## Tips
|
## Tips
|
||||||
|
|
||||||
> **Pro Tip:** Review carefully before sorting. Once sorted, you can still edit words in the vocabulary list.
|
You can edit your flashcards at any point in the flashcard itself
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*For more tips, check our vocabulary management guide.*
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# Translation Features
|
# Translation Features
|
||||||
|
# TODO REWRITE
|
||||||
|
|
||||||
Discover the powerful translation capabilities of this app.
|
Discover the powerful translation capabilities of this app.
|
||||||
|
|
||||||
|
|||||||
@@ -1,78 +1,7 @@
|
|||||||
# Vocabulary Progress Tracking
|
|
||||||
|
|
||||||
Monitor your vocabulary learning journey with detailed progress statistics.
|
Monitor your vocabulary learning journey with detailed progress statistics.
|
||||||
|
|
||||||
## Progress Overview
|
## Progress Overview
|
||||||
|
|
||||||
Track your learning with these key metrics:
|
Track your learning with these key metrics:
|
||||||
|
|
||||||
### Words Learned
|
TODO Rewrite
|
||||||
|
|
||||||
- Total words added to your vocabulary
|
|
||||||
- Words currently in each learning stage
|
|
||||||
- Words marked as fully learned
|
|
||||||
|
|
||||||
### Learning Streak
|
|
||||||
|
|
||||||
- Days since you started learning
|
|
||||||
- Current streak count
|
|
||||||
- Best streak achieved
|
|
||||||
|
|
||||||
### Review Statistics
|
|
||||||
|
|
||||||
- Words reviewed today
|
|
||||||
- Accuracy rate per session
|
|
||||||
- Words due for review
|
|
||||||
|
|
||||||
## Progress Tracking Features
|
|
||||||
|
|
||||||
### 📊 Dashboard
|
|
||||||
|
|
||||||
View your overall progress at a glance:
|
|
||||||
- Total vocabulary count
|
|
||||||
- Mastery percentage
|
|
||||||
- Recent activity summary
|
|
||||||
|
|
||||||
### 📈 Statistics
|
|
||||||
|
|
||||||
Detailed analytics include:
|
|
||||||
- Learning rate over time
|
|
||||||
- Stage distribution
|
|
||||||
- Accuracy trends
|
|
||||||
- Time spent studying
|
|
||||||
|
|
||||||
### 🎯 Goals
|
|
||||||
|
|
||||||
Set and track learning goals:
|
|
||||||
- Daily word targets
|
|
||||||
- Weekly review quotas
|
|
||||||
- Mastery milestones
|
|
||||||
|
|
||||||
## Learning Stages Summary
|
|
||||||
|
|
||||||
| Stage | Count | Percentage |
|
|
||||||
|-------|-------|------------|
|
|
||||||
| New | X | X% |
|
|
||||||
| Learning | X | X% |
|
|
||||||
| Mastered | X | X% |
|
|
||||||
|
|
||||||
## Review System
|
|
||||||
|
|
||||||
The review system helps you:
|
|
||||||
|
|
||||||
1. **Prioritize** - Shows words due for review first
|
|
||||||
2. **Space** - Optimizes review timing for retention
|
|
||||||
3. **Track** - Records your performance over time
|
|
||||||
|
|
||||||
## Customization
|
|
||||||
|
|
||||||
Customize your progress tracking:
|
|
||||||
|
|
||||||
- **Select metrics** to display on dashboard
|
|
||||||
- **Set goals** for personalized targets
|
|
||||||
- **Export data** for external analysis
|
|
||||||
- **Reset progress** if starting fresh
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Keep practicing consistently to see your progress grow!*
|
|
||||||
@@ -21,10 +21,10 @@
|
|||||||
"description": "Next-gen efficient architecture; outperforms older 70B models."
|
"description": "Next-gen efficient architecture; outperforms older 70B models."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"modelId": "deepseek-ai/DeepSeek-V3",
|
"modelId": "deepseek-ai/DeepSeek-V3.1",
|
||||||
"displayName": "DeepSeek V3",
|
"displayName": "DeepSeek V3.1",
|
||||||
"provider": "together",
|
"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,
|
"isCustom": false,
|
||||||
"models": [
|
"models": [
|
||||||
{
|
{
|
||||||
"modelId": "ministral-8b-latest",
|
"modelId": "mistral-medium-latest",
|
||||||
"displayName": "Ministral 8B",
|
"displayName": "Mistral Medium",
|
||||||
"provider": "mistral",
|
"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",
|
"modelId": "mistral-large-latest",
|
||||||
@@ -58,17 +58,17 @@
|
|||||||
"websiteUrl": "https://platform.openai.com/",
|
"websiteUrl": "https://platform.openai.com/",
|
||||||
"isCustom": false,
|
"isCustom": false,
|
||||||
"models": [
|
"models": [
|
||||||
|
{
|
||||||
|
"modelId": "gpt-5.2",
|
||||||
|
"displayName": "GPT-5.2",
|
||||||
|
"provider": "openai",
|
||||||
|
"description": "Balanced performance with enhanced reasoning and creativity."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"modelId": "gpt-5.1-instant",
|
"modelId": "gpt-5.1-instant",
|
||||||
"displayName": "GPT-5.1 Instant",
|
"displayName": "GPT-5.1 Instant",
|
||||||
"provider": "openai",
|
"provider": "openai",
|
||||||
"description": "The standard high-speed efficiency model replacing older 'Nano' tiers."
|
"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,
|
"isCustom": false,
|
||||||
"models": [
|
"models": [
|
||||||
{
|
{
|
||||||
"modelId": "claude-sonnet-5-20260203",
|
"modelId": "claude-opus-4-6",
|
||||||
"displayName": "Claude Sonnet 5",
|
"displayName": "Claude Opus 4.6",
|
||||||
"provider": "anthropic",
|
"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",
|
"modelId": "claude-sonnet-4-5",
|
||||||
"displayName": "Claude 4.5 Haiku",
|
"displayName": "Claude Sonnet 4.5",
|
||||||
"provider": "anthropic",
|
"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",
|
"modelId": "deepseek-chat",
|
||||||
"displayName": "DeepSeek V3",
|
"displayName": "DeepSeek V3.1",
|
||||||
"provider": "deepseek",
|
"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",
|
"key": "gemini",
|
||||||
"displayName": "Google Gemini",
|
"displayName": "Google Gemini",
|
||||||
"baseUrl": "https://generativelanguage.googleapis.com/",
|
"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/",
|
"websiteUrl": "https://ai.google/",
|
||||||
"isCustom": false,
|
"isCustom": false,
|
||||||
"models": [
|
"models": [
|
||||||
{
|
{
|
||||||
"modelId": "gemini-3-flash-preview",
|
"modelId": "gemini-2.5-pro",
|
||||||
"displayName": "Gemini 3 Flash",
|
"displayName": "Gemini 2.5 Pro",
|
||||||
"provider": "gemini",
|
"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",
|
"modelId": "gemini-3-pro-preview",
|
||||||
@@ -156,16 +156,10 @@
|
|||||||
"isCustom": false,
|
"isCustom": false,
|
||||||
"models": [
|
"models": [
|
||||||
{
|
{
|
||||||
"modelId": "llama-4-scout-17b",
|
"modelId": "meta-llama/llama-4-maverick",
|
||||||
"displayName": "Llama 4 Scout",
|
"displayName": "Llama 4 Maverick",
|
||||||
"provider": "groq",
|
"provider": "groq",
|
||||||
"description": "Powerful Llama 4 model running at extreme speed."
|
"description": "400B MoE powerhouse with industry-leading image and text understanding."
|
||||||
},
|
|
||||||
{
|
|
||||||
"modelId": "llama-3.3-70b-versatile",
|
|
||||||
"displayName": "Llama 3.3 70B",
|
|
||||||
"provider": "groq",
|
|
||||||
"description": "Previous gen flagship, highly reliable and fast on Groq chips."
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -216,10 +210,10 @@
|
|||||||
"description": "World's fastest inference (2000+ tokens/sec) on Wafer-Scale Engines."
|
"description": "World's fastest inference (2000+ tokens/sec) on Wafer-Scale Engines."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"modelId": "llama3.1-8b",
|
"modelId": "llama-4-scout",
|
||||||
"displayName": "Llama 3.1 8B",
|
"displayName": "Llama 4 Scout",
|
||||||
"provider": "cerebras",
|
"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)."
|
"description": "Hosted via the Hugging Face serverless router (Free tier limits apply)."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"modelId": "microsoft/Phi-3.5-mini-instruct",
|
"modelId": "Qwen/Qwen2.5-72B-Instruct",
|
||||||
"displayName": "Phi 3.5 Mini",
|
"displayName": "Qwen 2.5 72B",
|
||||||
"provider": "huggingface",
|
"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.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.model.Language
|
import eu.gaudian.translator.model.Language
|
||||||
import eu.gaudian.translator.model.parseLanguagesFromResources
|
import eu.gaudian.translator.model.parseLanguagesFromResources
|
||||||
import eu.gaudian.translator.model.repository.DataStoreKeys.LANGUAGE_INIT_METADATA_KEY
|
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
|
// Check if we already have default languages saved
|
||||||
val savedDefaultLanguages = loadLanguages(LanguageListType.DEFAULT).firstOrNull() ?: emptyList()
|
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)
|
val shouldReparse = shouldReparseLanguages(savedDefaultLanguages)
|
||||||
|
|
||||||
if (shouldReparse) {
|
if (shouldReparse) {
|
||||||
Log.d("LanguageRepository", "Parsing languages from resources")
|
Log.d("LanguageRepository", "Parsing languages from resources")
|
||||||
val parsedLanguages = parseLanguagesFromResources(context)
|
val parsedLanguages = parseLanguagesFromResources(context)
|
||||||
wipeHistoryAndFavorites()
|
|
||||||
saveLanguages(LanguageListType.DEFAULT, parsedLanguages)
|
saveLanguages(LanguageListType.DEFAULT, parsedLanguages)
|
||||||
// Save the current app version and locale to detect changes next time
|
// Save the current app version and locale to detect changes next time
|
||||||
saveLanguageInitializationMetadata()
|
saveLanguageInitializationMetadata()
|
||||||
@@ -112,8 +112,11 @@ class LanguageRepository(private val context: Context) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the number of languages matches expected count (51)
|
// Get expected language count from resources dynamically
|
||||||
if (savedLanguages.size != 51) {
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,15 +170,22 @@ class LanguageRepository(private val context: Context) {
|
|||||||
val customLanguages = loadLanguages(LanguageListType.CUSTOM).first()
|
val customLanguages = loadLanguages(LanguageListType.CUSTOM).first()
|
||||||
val master = (defaultLanguages + customLanguages).distinctBy { it.nameResId }
|
val master = (defaultLanguages + customLanguages).distinctBy { it.nameResId }
|
||||||
|
|
||||||
// Sanitize existing enabled IDs and initialize if empty
|
// Get existing enabled IDs
|
||||||
val existingEnabled: List<Int> = try {
|
val existingEnabled: List<Int> = try {
|
||||||
context.dataStore.loadObjectList<Int>(DataStoreKeys.ALL_LANGUAGES_KEY).firstOrNull() ?: emptyList()
|
context.dataStore.loadObjectList<Int>(DataStoreKeys.ALL_LANGUAGES_KEY).firstOrNull() ?: emptyList()
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
val masterIds = master.map { it.nameResId }.toSet()
|
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)
|
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 intervalStage4 = Setting(context.dataStore, PrefKeys.INTERVAL_STAGE_4, 30)
|
||||||
val intervalStage5 = Setting(context.dataStore, PrefKeys.INTERVAL_STAGE_5, 60)
|
val intervalStage5 = Setting(context.dataStore, PrefKeys.INTERVAL_STAGE_5, 60)
|
||||||
val intervalLearned = Setting(context.dataStore, PrefKeys.INTERVAL_LEARNED, 90)
|
val intervalLearned = Setting(context.dataStore, PrefKeys.INTERVAL_LEARNED, 90)
|
||||||
val criteriaCorrect = Setting(context.dataStore, PrefKeys.CRITERIA_CORRECT, 3)
|
val criteriaCorrect = Setting(context.dataStore, PrefKeys.CRITERIA_CORRECT, 1)
|
||||||
val criteriaWrong = Setting(context.dataStore, PrefKeys.CRITERIA_WRONG, 2)
|
val criteriaWrong = Setting(context.dataStore, PrefKeys.CRITERIA_WRONG, 1)
|
||||||
val showHints = Setting(context.dataStore, PrefKeys.SHOW_HINTS, true)
|
val showHints = Setting(context.dataStore, PrefKeys.SHOW_HINTS, true)
|
||||||
val experimentalFeatures = Setting(context.dataStore, PrefKeys.EXPERIMENTAL_FEATURES, false)
|
val experimentalFeatures = Setting(context.dataStore, PrefKeys.EXPERIMENTAL_FEATURES, false)
|
||||||
val tryWiktionaryFirst = Setting(context.dataStore, PrefKeys.TRY_WIKTIONARY_FIRST, 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,
|
correctCount: Int, incorrectCount: Int,
|
||||||
criteriaCorrect: Int, criteriaWrong: Int
|
criteriaCorrect: Int, criteriaWrong: Int
|
||||||
): VocabularyStage {
|
): 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
|
if (!readyToAdvance) return currentStage
|
||||||
return when (currentStage) {
|
return when (currentStage) {
|
||||||
VocabularyStage.NEW -> VocabularyStage.STAGE_1
|
VocabularyStage.NEW -> VocabularyStage.STAGE_1
|
||||||
@@ -491,6 +493,20 @@ class VocabularyRepository private constructor(context: Context) {
|
|||||||
VocabularyStage.STAGE_5 -> VocabularyStage.LEARNED
|
VocabularyStage.STAGE_5 -> VocabularyStage.LEARNED
|
||||||
VocabularyStage.LEARNED -> 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(
|
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.CoffeeTheme
|
||||||
import eu.gaudian.translator.ui.theme.themes.CrimsonTheme
|
import eu.gaudian.translator.ui.theme.themes.CrimsonTheme
|
||||||
import eu.gaudian.translator.ui.theme.themes.CyberpunkTheme
|
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.DefaultTheme
|
||||||
import eu.gaudian.translator.ui.theme.themes.ForestTheme
|
import eu.gaudian.translator.ui.theme.themes.ForestTheme
|
||||||
import eu.gaudian.translator.ui.theme.themes.NordTheme
|
import eu.gaudian.translator.ui.theme.themes.NordTheme
|
||||||
@@ -113,6 +114,7 @@ val AllThemes = listOf(
|
|||||||
SpaceTheme,
|
SpaceTheme,
|
||||||
CyberpunkTheme,
|
CyberpunkTheme,
|
||||||
SynthwaveTheme,
|
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_49,
|
||||||
R.string.language_50,
|
R.string.language_50,
|
||||||
R.string.language_51,
|
R.string.language_51,
|
||||||
|
R.string.language_52,
|
||||||
|
|
||||||
R.string.native_language_1,
|
R.string.native_language_1,
|
||||||
R.string.native_language_2,
|
R.string.native_language_2,
|
||||||
@@ -112,6 +113,7 @@ val LANGUAGE_STRING_IDS: IntArray = intArrayOf(
|
|||||||
R.string.native_language_49,
|
R.string.native_language_49,
|
||||||
R.string.native_language_50,
|
R.string.native_language_50,
|
||||||
R.string.native_language_51,
|
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.
|
* 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 {
|
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 ShowMessage(val text: String, val type: MessageDisplayType, val timeoutInSeconds: Int) : StatusAction()
|
||||||
data class ShowPermanentMessage(val text: String, val type: MessageDisplayType) : StatusAction()
|
data class ShowPermanentMessage(val text: String, val type: MessageDisplayType) : StatusAction()
|
||||||
object CancelPermanentMessage : StatusAction()
|
object CancelPermanentMessage : StatusAction()
|
||||||
@@ -20,31 +22,59 @@ sealed class StatusAction {
|
|||||||
object CancelLoadingOperation : StatusAction()
|
object CancelLoadingOperation : StatusAction()
|
||||||
object HideMessageBar : StatusAction()
|
object HideMessageBar : StatusAction()
|
||||||
object CancelAllMessages : StatusAction()
|
object CancelAllMessages : StatusAction()
|
||||||
|
|
||||||
data class ShowActionableMessage(val text: String, val type: MessageDisplayType, val action: MessageAction) : 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.
|
* 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.
|
* 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 {
|
object StatusMessageService {
|
||||||
private val _actions = MutableSharedFlow<StatusAction>()
|
private val _actions = MutableSharedFlow<StatusAction>()
|
||||||
val actions = _actions.asSharedFlow()
|
val actions = _actions.asSharedFlow()
|
||||||
private val scope = CoroutineScope(Dispatchers.Default)
|
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")
|
Log.d("StatusMessageService", "Received action: $action")
|
||||||
_actions.emit(action)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun triggerNonSuspend(action: StatusAction) {
|
|
||||||
Log.d("StatusMessageService", "Received non-suspend action: $action")
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
_actions.emit(action)
|
_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")
|
@Suppress("unused")
|
||||||
fun showSimpleMessage(text: String, type: MessageDisplayType = MessageDisplayType.INFO) {
|
fun showSimpleMessage(text: String, type: MessageDisplayType = MessageDisplayType.INFO) {
|
||||||
scope.launch {
|
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) {
|
fun showErrorMessage(text: String, timeoutInSeconds: Int = 5) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
_actions.emit(StatusAction.ShowMessage(
|
_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) {
|
fun showLoadingMessage(text: String, timeoutInSeconds: Int = 0) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
_actions.emit(StatusAction.ShowMessage(
|
_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) {
|
fun showInfoMessage(text: String, timeoutInSeconds: Int = 3) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
_actions.emit(StatusAction.ShowMessage(
|
_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) {
|
fun showSuccessMessage(text: String, timeoutInSeconds: Int = 3) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
_actions.emit(StatusAction.ShowMessage(
|
_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) {
|
fun showPermanentMessage(text: String, type: MessageDisplayType) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
_actions.emit(StatusAction.ShowPermanentMessage(text, type))
|
_actions.emit(StatusAction.ShowPermanentMessage(text, type))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use StatusAction.CancelPermanentMessage via trigger() if needed.
|
||||||
|
*/
|
||||||
|
@Deprecated("Use StatusAction.CancelPermanentMessage via trigger() if needed")
|
||||||
fun cancelPermanentMessage() {
|
fun cancelPermanentMessage() {
|
||||||
scope.launch {
|
trigger(StatusAction.CancelPermanentMessage)
|
||||||
_actions.emit(StatusAction.CancelPermanentMessage)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use StatusAction.HideMessageBar via trigger() if needed.
|
||||||
|
*/
|
||||||
|
@Deprecated("Use StatusAction.HideMessageBar via trigger() if needed")
|
||||||
fun hideMessageBar() {
|
fun hideMessageBar() {
|
||||||
scope.launch {
|
trigger(StatusAction.HideMessageBar)
|
||||||
_actions.emit(StatusAction.HideMessageBar)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use StatusAction.CancelAllMessages via trigger() if needed.
|
||||||
|
*/
|
||||||
|
@Deprecated("Use StatusAction.CancelAllMessages via trigger() if needed")
|
||||||
fun cancelAllMessages() {
|
fun cancelAllMessages() {
|
||||||
scope.launch {
|
trigger(StatusAction.CancelAllMessages)
|
||||||
_actions.emit(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) {
|
fun showActionableMessage(text: String, type: MessageDisplayType, action: MessageAction) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
_actions.emit(StatusAction.ShowActionableMessage(text, type, action))
|
_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) {
|
suspend fun translateSentence(sentence: String): Result<TranslationHistoryItem> = withContext(Dispatchers.IO) {
|
||||||
|
val statusMessageService = StatusMessageService
|
||||||
val additionalInstructions = settingsRepository.customPromptTranslation.flow.first()
|
val additionalInstructions = settingsRepository.customPromptTranslation.flow.first()
|
||||||
val selectedSource = languageRepository.loadSelectedSourceLanguage().first()
|
val selectedSource = languageRepository.loadSelectedSourceLanguage().first()
|
||||||
val sourceLangName = selectedSource?.englishName ?: "Auto"
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
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 eu.gaudian.translator.view.composable.PrimaryButton
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun IntroNavHost(onIntroFinished: () -> Unit) {
|
fun IntroNavHost(onIntroFinished: () -> Unit) {
|
||||||
val pages = listOf(
|
val pages = listOf(
|
||||||
@@ -55,9 +53,16 @@ fun IntroNavHost(onIntroFinished: () -> Unit) {
|
|||||||
title = stringResource(R.string.intro_title_ai_assistant),
|
title = stringResource(R.string.intro_title_ai_assistant),
|
||||||
description = stringResource(R.string.intro_desc_ai_assistant),
|
description = stringResource(R.string.intro_desc_ai_assistant),
|
||||||
content = {
|
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)
|
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_mistral)) })
|
||||||
SuggestionChip(onClick = { }, label = { Text(stringResource(R.string.text_your_own_ai)) })
|
SuggestionChip(onClick = { }, label = { Text(stringResource(R.string.text_your_own_ai)) })
|
||||||
SuggestionChip(onClick = { }, label = { Text(stringResource(R.string.text_openai)) })
|
SuggestionChip(onClick = { }, label = { Text(stringResource(R.string.text_openai)) })
|
||||||
@@ -89,7 +94,7 @@ fun IntroNavHost(onIntroFinished: () -> Unit) {
|
|||||||
IntroPageData(
|
IntroPageData(
|
||||||
title = stringResource(R.string.intro_title_learning_journey),
|
title = stringResource(R.string.intro_title_learning_journey),
|
||||||
description = stringResource(R.string.intro_desc_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(
|
IntroPageData(
|
||||||
title = stringResource(R.string.intro_title_categories),
|
title = stringResource(R.string.intro_title_categories),
|
||||||
@@ -128,7 +133,6 @@ fun IntroNavHost(onIntroFinished: () -> Unit) {
|
|||||||
.statusBarsPadding()
|
.statusBarsPadding()
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
.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(
|
eu.gaudian.translator.view.composable.SecondaryButton(
|
||||||
onClick = { onIntroFinished() },
|
onClick = { onIntroFinished() },
|
||||||
text = stringResource(R.string.intro_skip),
|
text = stringResource(R.string.intro_skip),
|
||||||
@@ -145,7 +149,9 @@ fun IntroNavHost(onIntroFinished: () -> Unit) {
|
|||||||
) {
|
) {
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
) { pageIndex ->
|
) { pageIndex ->
|
||||||
IntroPage(pageData = pages[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),
|
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()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -189,9 +195,9 @@ private fun IntroPage(pageData: IntroPageData) {
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.CenterVertically),
|
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.CenterVertically),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxSize() // Fixed: This was previously fillMaxHeight()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.verticalScroll(rememberScrollState()) // Allow scrolling for larger hint content
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
Text(
|
Text(
|
||||||
@@ -240,9 +246,8 @@ private fun IconContent(iconRes: Int) {
|
|||||||
tint = Color.Unspecified,
|
tint = Color.Unspecified,
|
||||||
modifier = Modifier.size(250.dp)
|
modifier = Modifier.size(250.dp)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun FlashcardTopicsPreview() {
|
private fun FlashcardTopicsPreview() {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
import androidx.compose.material3.dynamicLightColorScheme
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -35,12 +36,14 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
@@ -56,8 +59,11 @@ import eu.gaudian.translator.MyApplication
|
|||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.ui.theme.AllFonts
|
import eu.gaudian.translator.ui.theme.AllFonts
|
||||||
import eu.gaudian.translator.ui.theme.AllThemes
|
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.ui.theme.buildColorScheme
|
||||||
import eu.gaudian.translator.utils.Log
|
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.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppAlertDialog
|
import eu.gaudian.translator.view.composable.AppAlertDialog
|
||||||
import eu.gaudian.translator.view.composable.BottomNavigationBar
|
import eu.gaudian.translator.view.composable.BottomNavigationBar
|
||||||
@@ -149,9 +155,7 @@ fun TranslatorApp(
|
|||||||
|
|
||||||
val activity = LocalContext.current.findActivity()
|
val activity = LocalContext.current.findActivity()
|
||||||
val statusViewModel: StatusViewModel = hiltViewModel(activity)
|
val statusViewModel: StatusViewModel = hiltViewModel(activity)
|
||||||
|
val statusMessageService = StatusMessageService
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val statusState by statusViewModel.status.collectAsStateWithLifecycle()
|
val statusState by statusViewModel.status.collectAsStateWithLifecycle()
|
||||||
@@ -303,7 +307,7 @@ fun TranslatorApp(
|
|||||||
StatusMessageSystem(
|
StatusMessageSystem(
|
||||||
statusState = statusState,
|
statusState = statusState,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
onDismiss = { statusViewModel.hideMessageBar() },
|
onDismiss = { statusMessageService.trigger(StatusAction.HideMessageBar) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
)
|
)
|
||||||
@@ -357,9 +361,12 @@ private fun AppTheme(
|
|||||||
val window = (view.context as Activity).window
|
val window = (view.context as Activity).window
|
||||||
val windowInsetsController = WindowInsetsControllerCompat(window, view)
|
val windowInsetsController = WindowInsetsControllerCompat(window, view)
|
||||||
|
|
||||||
//window.statusBarColor = android.graphics.Color.TRANSPARENT
|
// We must keep this for older Android version!!!
|
||||||
//window.navigationBarColor = android.graphics.Color.TRANSPARENT
|
@Suppress("DEPRECATION")
|
||||||
//TODO remove eventually
|
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.isAppearanceLightStatusBars = !useDarkTheme
|
||||||
windowInsetsController.isAppearanceLightNavigationBars = !useDarkTheme
|
windowInsetsController.isAppearanceLightNavigationBars = !useDarkTheme
|
||||||
@@ -400,8 +407,10 @@ private fun AppTheme(
|
|||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
typography = dynamicTypography,
|
typography = dynamicTypography,
|
||||||
) {
|
) {
|
||||||
eu.gaudian.translator.ui.theme.ProvideSemanticColors {
|
ProvideSemanticColors {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,19 @@
|
|||||||
package eu.gaudian.translator.view.composable
|
package eu.gaudian.translator.view.composable
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -24,8 +21,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
@@ -42,7 +37,6 @@ fun ApiModelDropDown(
|
|||||||
onModelSelected: (LanguageModel?) -> Unit,
|
onModelSelected: (LanguageModel?) -> Unit,
|
||||||
enabled: Boolean = true
|
enabled: Boolean = true
|
||||||
) {
|
) {
|
||||||
LocalContext.current
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
var searchQuery by remember { mutableStateOf("") }
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
|
|
||||||
@@ -65,13 +59,8 @@ fun ApiModelDropDown(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Box {
|
// Custom button content showing selected model and provider
|
||||||
AppOutlinedButton(
|
val buttonContent: @Composable () -> Unit = {
|
||||||
onClick = { expanded = true },
|
|
||||||
modifier = Modifier.align(Alignment.Center),
|
|
||||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp),
|
|
||||||
enabled = enabled
|
|
||||||
) {
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
@@ -101,56 +90,27 @@ fun ApiModelDropDown(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DropdownMenu(
|
AppDropdownContainer(
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onDismissRequest = { expanded = false }
|
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
|
||||||
) {
|
) {
|
||||||
// Search bar
|
Column(
|
||||||
Column {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.heightIn(max = 400.dp)
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
.verticalScroll(rememberScrollState())
|
||||||
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()) {
|
if (filteredGroupedModels.isNotEmpty()) {
|
||||||
filteredGroupedModels.entries.forEachIndexed { index, entry ->
|
filteredGroupedModels.entries.forEachIndexed { index, entry ->
|
||||||
val providerKey = entry.key
|
val providerKey = entry.key
|
||||||
@@ -158,7 +118,7 @@ fun ApiModelDropDown(
|
|||||||
val isActive = providerStatuses[providerKey] == true
|
val isActive = providerStatuses[providerKey] == true
|
||||||
val providerName = providerNames[providerKey] ?: providerKey
|
val providerName = providerNames[providerKey] ?: providerKey
|
||||||
|
|
||||||
if (index > 0) HorizontalDivider()
|
if (index > 0) HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||||
|
|
||||||
// Provider header
|
// Provider header
|
||||||
AppDropdownMenuItem(
|
AppDropdownMenuItem(
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import eu.gaudian.translator.R
|
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.HintBottomSheet
|
||||||
import eu.gaudian.translator.view.hints.LocalShowHints
|
import eu.gaudian.translator.view.hints.LocalShowHints
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@ import eu.gaudian.translator.view.hints.LocalShowHints
|
|||||||
fun AppDialog(
|
fun AppDialog(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
title: (@Composable () -> Unit)? = null,
|
title: (@Composable () -> Unit)? = null,
|
||||||
hintContent: @Composable (() -> Unit)? = null,
|
hintContent: Hint? = null,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
// 1. Swipe Resistance: Prevent accidental dismissal
|
// 1. Swipe Resistance: Prevent accidental dismissal
|
||||||
@@ -98,7 +99,7 @@ fun AppDialog(
|
|||||||
if (showBottomSheet) {
|
if (showBottomSheet) {
|
||||||
EnhancedHintBottomSheet(
|
EnhancedHintBottomSheet(
|
||||||
onDismissRequest = { showBottomSheet = false },
|
onDismissRequest = { showBottomSheet = false },
|
||||||
content = hintContent,
|
content = {hintContent?.Render()},
|
||||||
parentTitle = title
|
parentTitle = title
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -156,7 +157,7 @@ fun AppAlertDialog(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun DialogHeader(
|
private fun DialogHeader(
|
||||||
title: (@Composable () -> Unit)?,
|
title: (@Composable () -> Unit)?,
|
||||||
hintContent: @Composable (() -> Unit)?,
|
hintContent: Hint? = null,
|
||||||
onHintClick: () -> Unit,
|
onHintClick: () -> Unit,
|
||||||
onCloseClick: () -> Unit
|
onCloseClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -327,7 +328,6 @@ fun AppDialogPreview() {
|
|||||||
AppDialog(
|
AppDialog(
|
||||||
onDismissRequest = {},
|
onDismissRequest = {},
|
||||||
title = { Text("Dialog Title") },
|
title = { Text("Dialog Title") },
|
||||||
hintContent = { Text("This is a hint.") },
|
|
||||||
content = {
|
content = {
|
||||||
Column {
|
Column {
|
||||||
Text("Content line 1")
|
Text("Content line 1")
|
||||||
@@ -378,7 +378,6 @@ fun AppDialogLongContentPreview() {
|
|||||||
AppDialog(
|
AppDialog(
|
||||||
onDismissRequest = {},
|
onDismissRequest = {},
|
||||||
title = { Text("Long Content Dialog") },
|
title = { Text("Long Content Dialog") },
|
||||||
hintContent = { Text("Hint for long content dialog") },
|
|
||||||
content = {
|
content = {
|
||||||
Column {
|
Column {
|
||||||
Text("This is a long content dialog to test scrolling")
|
Text("This is a long content dialog to test scrolling")
|
||||||
|
|||||||
@@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
package eu.gaudian.translator.view.composable
|
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.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -14,22 +17,30 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
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.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -48,6 +59,8 @@ import androidx.compose.ui.layout.onGloballyPositioned
|
|||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -56,100 +69,362 @@ import androidx.compose.ui.window.Dialog
|
|||||||
import androidx.compose.ui.window.PopupProperties
|
import androidx.compose.ui.window.PopupProperties
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
|
|
||||||
/**
|
// =========================================
|
||||||
* A modern, custom dropdown menu composable that provides a styled text field with a dropdown list of options.
|
// UNIFIED DROPDOWN STYLES & CONSTANTS
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun AppDropdownMenu(
|
|
||||||
expanded: Boolean,
|
|
||||||
onDismissRequest: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
label: @Composable (() -> Unit)? = null,
|
|
||||||
enabled: Boolean = true,
|
|
||||||
placeholder: @Composable (() -> Unit)? = null,
|
|
||||||
selectedText: String = "",
|
|
||||||
onExpandRequest: () -> Unit = {},
|
|
||||||
content: @Composable androidx.compose.foundation.layout.ColumnScope.() -> Unit,
|
|
||||||
) {
|
|
||||||
var textFieldSize by remember { mutableStateOf(Size.Zero) }
|
|
||||||
val interactionSource = remember { androidx.compose.foundation.interaction.MutableInteractionSource() }
|
|
||||||
|
|
||||||
Column(modifier = modifier) {
|
object DropdownDefaults {
|
||||||
OutlinedTextField(
|
val shape = RoundedCornerShape(8.dp)
|
||||||
value = selectedText,
|
val itemPaddingHorizontal = 8.dp
|
||||||
onValueChange = {},
|
val itemPaddingVertical = 2.dp
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
@Composable
|
||||||
.onGloballyPositioned { coordinates ->
|
fun containerColor(): Color = MaterialTheme.colorScheme.surface
|
||||||
textFieldSize = coordinates.size.toSize()
|
|
||||||
|
@Composable
|
||||||
|
fun itemBackground(selected: Boolean): Color {
|
||||||
|
return if (selected) {
|
||||||
|
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.6f)
|
||||||
|
} else {
|
||||||
|
Color.Transparent
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.clickable(
|
|
||||||
enabled = enabled,
|
|
||||||
onClick = onExpandRequest,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
indication = null
|
|
||||||
),
|
|
||||||
readOnly = true,
|
|
||||||
label = label,
|
|
||||||
placeholder = placeholder,
|
|
||||||
trailingIcon = {
|
|
||||||
val icon = if (expanded) AppIcons.ArrowDropUp else AppIcons.ArrowDropDown
|
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = if (expanded) stringResource(R.string.cd_collapse) else stringResource(R.string.cd_expand),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
},
|
|
||||||
shape = ComponentDefaults.DefaultShape,
|
|
||||||
colors = OutlinedTextFieldDefaults.colors(
|
|
||||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
|
||||||
unfocusedBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = ComponentDefaults.ALPHA_LOW),
|
|
||||||
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)
|
|
||||||
),
|
|
||||||
enabled = enabled,
|
|
||||||
interactionSource = interactionSource
|
|
||||||
)
|
|
||||||
|
|
||||||
DropdownMenu(
|
@Composable
|
||||||
expanded = expanded,
|
fun itemContentColor(selected: Boolean, enabled: Boolean): Color {
|
||||||
onDismissRequest = onDismissRequest,
|
return when {
|
||||||
modifier = Modifier
|
!enabled -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||||
.width(with(LocalDensity.current) { textFieldSize.width.toDp() })
|
selected -> MaterialTheme.colorScheme.primary
|
||||||
// Give the menu itself a bit of breathing room
|
else -> MaterialTheme.colorScheme.onSurface
|
||||||
.padding(vertical = 4.dp),
|
|
||||||
offset = DpOffset(0.dp, 4.dp), // Slight detachment from the anchor
|
|
||||||
scrollState = rememberScrollState(),
|
|
||||||
properties = PopupProperties(focusable = true),
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
|
||||||
tonalElevation = 6.dp,
|
|
||||||
shadowElevation = 8.dp,
|
|
||||||
border = BorderStroke(0.5.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
|
|
||||||
) {
|
|
||||||
content()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A modern and stylish composable for individual dropdown items, featuring enhanced visual design
|
* A drop-in replacement for [androidx.compose.material3.DropdownMenu] that opens
|
||||||
* with subtle shadows, rounded corners, and smooth interactions.
|
* 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
|
@Composable
|
||||||
fun AppDropdownMenuItem(
|
fun AppDropdownMenuItem(
|
||||||
text: @Composable () -> Unit,
|
text: @Composable () -> Unit,
|
||||||
@@ -160,23 +435,25 @@ fun AppDropdownMenuItem(
|
|||||||
trailingIcon: @Composable (() -> Unit)? = null,
|
trailingIcon: @Composable (() -> Unit)? = null,
|
||||||
selected: Boolean = false,
|
selected: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val contentColor = if (enabled) {
|
val contentColor by animateColorAsState(
|
||||||
if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
|
targetValue = DropdownDefaults.itemContentColor(selected, enabled),
|
||||||
} else {
|
label = "contentColor"
|
||||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
)
|
||||||
}
|
val backgroundColor by animateColorAsState(
|
||||||
|
targetValue = DropdownDefaults.itemBackground(selected),
|
||||||
// Modern "floating" highlight background
|
label = "backgroundColor"
|
||||||
val backgroundColor = if (selected) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.6f) else Color.Transparent
|
)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 8.dp, vertical = 2.dp) // Outer padding creates the floating shape
|
.padding(
|
||||||
.clip(RoundedCornerShape(8.dp))
|
horizontal = DropdownDefaults.itemPaddingHorizontal,
|
||||||
|
vertical = DropdownDefaults.itemPaddingVertical
|
||||||
|
)
|
||||||
|
.clip(DropdownDefaults.shape)
|
||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
.clickable(enabled = enabled) { onClick() }
|
.clickable(enabled = enabled) { onClick() }
|
||||||
//.padding(horizontal = 12.dp, vertical = 10.dp) // Inner padding keeps content comfortable
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -199,7 +476,112 @@ fun AppDropdownMenuItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... [Previews remain exactly the same as your original file] ...
|
/**
|
||||||
|
* A lightweight, modern dropdown menu composable with a clean text field and dropdown list.
|
||||||
|
*/
|
||||||
|
@Suppress("unused", "HardCodedStringLiteral")
|
||||||
|
@Composable
|
||||||
|
fun AppDropdownMenu(
|
||||||
|
expanded: Boolean,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
label: @Composable (() -> Unit)? = null,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
placeholder: @Composable (() -> Unit)? = null,
|
||||||
|
selectedText: String = "",
|
||||||
|
onExpandRequest: () -> Unit = {},
|
||||||
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
var textFieldSize by remember { mutableStateOf(Size.Zero) }
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = selectedText,
|
||||||
|
onValueChange = {},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.onGloballyPositioned { coordinates ->
|
||||||
|
textFieldSize = coordinates.size.toSize()
|
||||||
|
}
|
||||||
|
.clickable(
|
||||||
|
enabled = enabled,
|
||||||
|
onClick = onExpandRequest,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = null
|
||||||
|
),
|
||||||
|
readOnly = true,
|
||||||
|
label = label,
|
||||||
|
placeholder = placeholder,
|
||||||
|
trailingIcon = {
|
||||||
|
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 = DropdownDefaults.shape,
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||||
|
unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.6f),
|
||||||
|
focusedLabelColor = MaterialTheme.colorScheme.primary,
|
||||||
|
cursorColor = MaterialTheme.colorScheme.primary,
|
||||||
|
disabledBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
|
||||||
|
disabledLabelColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||||
|
),
|
||||||
|
enabled = enabled,
|
||||||
|
interactionSource = interactionSource
|
||||||
|
)
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
modifier = Modifier.width(with(LocalDensity.current) { textFieldSize.width.toDp() }),
|
||||||
|
offset = DpOffset(0.dp, 2.dp),
|
||||||
|
properties = PopupProperties(focusable = true),
|
||||||
|
shape = DropdownDefaults.shape,
|
||||||
|
containerColor = DropdownDefaults.containerColor()
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================
|
||||||
|
// LEGACY COMPONENTS (UPDATED TO USE UNIFIED STYLES)
|
||||||
|
// =========================================
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LargeDropdownMenuItem(
|
||||||
|
text: String,
|
||||||
|
selected: Boolean,
|
||||||
|
enabled: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
val contentColor = DropdownDefaults.itemContentColor(selected, enabled)
|
||||||
|
val backgroundColor = DropdownDefaults.itemBackground(selected)
|
||||||
|
val fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal
|
||||||
|
|
||||||
|
CompositionLocalProvider(LocalContentColor provides contentColor) {
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun <T> LargeDropdownMenu(
|
fun <T> LargeDropdownMenu(
|
||||||
@@ -210,12 +592,12 @@ fun <T> LargeDropdownMenu(
|
|||||||
items: List<T>,
|
items: List<T>,
|
||||||
selectedIndex: Int = -1,
|
selectedIndex: Int = -1,
|
||||||
onItemSelected: (index: Int, item: T) -> Unit,
|
onItemSelected: (index: Int, item: T) -> Unit,
|
||||||
selectedItemToString: (T) -> String = { it.toString() },
|
selectedItemToString: (T) -> String = { item: T -> item.toString() },
|
||||||
drawItem: @Composable (T, Boolean, Boolean, () -> Unit) -> Unit = { item, selected, itemEnabled, onClick ->
|
drawItem: @Composable (T, Boolean, Boolean, () -> Unit) -> Unit = { item: T, selected: Boolean, _: Boolean, onClick: () -> Unit ->
|
||||||
LargeDropdownMenuItem(
|
LargeDropdownMenuItem(
|
||||||
text = item.toString(),
|
text = item.toString(),
|
||||||
selected = selected,
|
selected = selected,
|
||||||
enabled = itemEnabled,
|
enabled = true,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -247,13 +629,10 @@ fun <T> LargeDropdownMenu(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
Dialog(
|
Dialog(onDismissRequest = { expanded = false }) {
|
||||||
onDismissRequest = { expanded = false }, // Fixed bug from original code
|
|
||||||
) {
|
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
shadowElevation = 8.dp,
|
|
||||||
tonalElevation = 6.dp
|
tonalElevation = 6.dp
|
||||||
) {
|
) {
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
@@ -263,7 +642,6 @@ fun <T> LargeDropdownMenu(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Added vertical padding to the list instead of hard dividers
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
state = listState,
|
state = listState,
|
||||||
@@ -279,7 +657,7 @@ fun <T> LargeDropdownMenu(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
itemsIndexed(items) { index, item ->
|
itemsIndexed(items) { index: Int, item: T ->
|
||||||
val selectedItem = index == selectedIndex
|
val selectedItem = index == selectedIndex
|
||||||
drawItem(
|
drawItem(
|
||||||
item,
|
item,
|
||||||
@@ -296,39 +674,7 @@ fun <T> LargeDropdownMenu(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
// ============== PREVIEWS ==============
|
||||||
fun LargeDropdownMenuItem(
|
|
||||||
text: String,
|
|
||||||
selected: Boolean,
|
|
||||||
enabled: Boolean,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
val contentColor = when {
|
|
||||||
!enabled -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
|
||||||
selected -> MaterialTheme.colorScheme.primary
|
|
||||||
else -> MaterialTheme.colorScheme.onSurface
|
|
||||||
}
|
|
||||||
|
|
||||||
val backgroundColor = if (selected) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.6f) else Color.Transparent
|
|
||||||
val fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal
|
|
||||||
|
|
||||||
CompositionLocalProvider(LocalContentColor provides contentColor) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp) // Outer padding for floating shape
|
|
||||||
.clip(RoundedCornerShape(8.dp))
|
|
||||||
.background(backgroundColor)
|
|
||||||
.clickable(enabled) { onClick() }
|
|
||||||
.padding(horizontal = 16.dp, vertical = 14.dp) // Inner padding
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
style = MaterialTheme.typography.titleSmall.copy(fontWeight = fontWeight),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
@Suppress("HardCodedStringLiteral")
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@@ -354,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")
|
@Suppress("HardCodedStringLiteral")
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -365,53 +735,8 @@ fun LargeDropdownMenuPreview() {
|
|||||||
label = "Select Option",
|
label = "Select Option",
|
||||||
items = options,
|
items = options,
|
||||||
selectedIndex = selectedIndex,
|
selectedIndex = selectedIndex,
|
||||||
onItemSelected = { index, _ ->
|
onItemSelected = { index: Int, _: String ->
|
||||||
selectedIndex = index
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.rotate
|
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.painter.Painter
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -159,14 +159,14 @@ private fun MenuItem(
|
|||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.shadow(elevation = 8.dp, shape = ComponentDefaults.DefaultShape)
|
.glassmorphic(shape = RoundedCornerShape(16.dp), alpha = 0.4f)
|
||||||
.clickable(
|
.clickable(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
indication = null
|
indication = null
|
||||||
),
|
),
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
color = MaterialTheme.colorScheme.surfaceContainer
|
color = Color.Transparent // Allow glassmorphic modifier to handle color
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
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()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 8.dp, horizontal = 8.dp)
|
.padding(vertical = 8.dp, horizontal = 8.dp)
|
||||||
.height(56.dp)
|
.height(56.dp)
|
||||||
.background(
|
// Replace background with glassmorphic extension
|
||||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
.glassmorphic(shape = ComponentDefaults.CardShape, alpha = 0.3f)
|
||||||
shape = ComponentDefaults.CardShape
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
val tabWidth = maxWidth / tabs.size
|
val tabWidth = maxWidth / tabs.size
|
||||||
|
|
||||||
@@ -89,7 +87,7 @@ fun <T : TabItem> AppTabLayout(
|
|||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
.background(
|
.background(
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f), // Glassy indicator
|
||||||
shape = RoundedCornerShape(12.dp)
|
shape = RoundedCornerShape(12.dp)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -41,14 +43,21 @@ fun AppTopAppBar(
|
|||||||
onNavigateBack: (() -> Unit)? = null,
|
onNavigateBack: (() -> Unit)? = null,
|
||||||
navigationIcon: @Composable (() -> Unit)? = null,
|
navigationIcon: @Composable (() -> Unit)? = null,
|
||||||
actions: @Composable RowScope.() -> Unit = {},
|
actions: @Composable RowScope.() -> Unit = {},
|
||||||
colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),
|
colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
scrolledContainerColor = Color.Transparent
|
||||||
|
),
|
||||||
hintContent: Hint? = null
|
hintContent: Hint? = null
|
||||||
) {
|
) {
|
||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
modifier = modifier.glassmorphic(shape = RectangleShape, alpha = 0.2f),
|
||||||
|
color = Color.Transparent
|
||||||
|
) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
modifier = modifier.height(56.dp),
|
modifier = Modifier.height(56.dp),
|
||||||
windowInsets = WindowInsets(0.dp),
|
windowInsets = WindowInsets(0.dp),
|
||||||
colors = colors,
|
colors = colors,
|
||||||
title = {
|
title = {
|
||||||
@@ -104,6 +113,7 @@ fun AppTopAppBar(
|
|||||||
},
|
},
|
||||||
actions = actions
|
actions = actions
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (showBottomSheet) {
|
if (showBottomSheet) {
|
||||||
HintBottomSheet(
|
HintBottomSheet(
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.WindowInsets
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.NavigationBar
|
import androidx.compose.material3.NavigationBar
|
||||||
@@ -28,6 +29,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
@@ -100,24 +102,25 @@ fun BottomNavigationBar(
|
|||||||
targetOffsetY = { it }
|
targetOffsetY = { it }
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val baseHeight = if (showLabels) 80.dp else 56.dp
|
val baseHeight = if (showLabels) 80.dp else 56.dp
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val navBarDp = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() }
|
val navBarDp = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() }
|
||||||
val height = baseHeight + navBarDp
|
val height = baseHeight + navBarDp
|
||||||
|
|
||||||
NavigationBar(
|
NavigationBar(
|
||||||
modifier = modifier.height(height),
|
modifier = modifier
|
||||||
containerColor = MaterialTheme.colorScheme.surface, // Cleaner look than surfaceVariant
|
.height(height)
|
||||||
tonalElevation = 8.dp, // Slight elevation for depth
|
// 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 ->
|
screens.forEach { screen ->
|
||||||
val isSelected = screen == selectedItem
|
val isSelected = screen == selectedItem
|
||||||
val title = stringResource(id = screen.title)
|
val title = stringResource(id = screen.title)
|
||||||
|
|
||||||
// 1. Spring Animation for the Icon Scale
|
|
||||||
val scale by animateFloatAsState(
|
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(
|
animationSpec = spring(
|
||||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||||
stiffness = Spring.StiffnessLow
|
stiffness = Spring.StiffnessLow
|
||||||
@@ -129,7 +132,7 @@ fun BottomNavigationBar(
|
|||||||
selected = isSelected,
|
selected = isSelected,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!isSelected) {
|
if (!isSelected) {
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress) // 2. Tactile feedback
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
onItemSelected(screen)
|
onItemSelected(screen)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -145,17 +148,16 @@ fun BottomNavigationBar(
|
|||||||
}
|
}
|
||||||
} else null,
|
} else null,
|
||||||
icon = {
|
icon = {
|
||||||
// 3. Crossfade between Outlined and Filled icons
|
|
||||||
Crossfade(targetState = isSelected, label = "iconFade") { selected ->
|
Crossfade(targetState = isSelected, label = "iconFade") { selected ->
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (selected) screen.selectedIcon else screen.unselectedIcon,
|
imageVector = if (selected) screen.selectedIcon else screen.unselectedIcon,
|
||||||
contentDescription = title,
|
contentDescription = title,
|
||||||
modifier = Modifier.scale(scale) // Apply the spring scale
|
modifier = Modifier.scale(scale)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = NavigationBarItemDefaults.colors(
|
colors = NavigationBarItemDefaults.colors(
|
||||||
indicatorColor = MaterialTheme.colorScheme.primaryContainer,
|
indicatorColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f), // Glassy indicator
|
||||||
selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
selectedTextColor = MaterialTheme.colorScheme.primary,
|
selectedTextColor = MaterialTheme.colorScheme.primary,
|
||||||
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ package eu.gaudian.translator.view.composable
|
|||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -43,6 +45,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.composed
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.draw.shadow
|
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.R
|
||||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||||
import eu.gaudian.translator.ui.theme.semanticColors
|
import eu.gaudian.translator.ui.theme.semanticColors
|
||||||
import eu.gaudian.translator.view.composable.ComponentDefaults.DefaultElevation
|
|
||||||
|
|
||||||
|
|
||||||
object ComponentDefaults {
|
object ComponentDefaults {
|
||||||
// Sizing
|
|
||||||
val DefaultButtonHeight = 48.dp
|
val DefaultButtonHeight = 48.dp
|
||||||
val CardPadding = 8.dp
|
val CardPadding = 8.dp
|
||||||
|
|
||||||
// Elevation
|
|
||||||
val DefaultElevation = 0.dp
|
val DefaultElevation = 0.dp
|
||||||
val NoElevation = 0.dp
|
val NoElevation = 0.dp
|
||||||
|
|
||||||
// Borders
|
|
||||||
val DefaultBorderWidth = 1.dp
|
val DefaultBorderWidth = 1.dp
|
||||||
|
|
||||||
// Shapes
|
|
||||||
val DefaultCornerRadius = 16.dp
|
val DefaultCornerRadius = 16.dp
|
||||||
val CardClipRadius = 8.dp
|
val CardClipRadius = 16.dp // Increased slightly for softer glass look
|
||||||
val NoRounding = 0.dp
|
val NoRounding = 0.dp
|
||||||
val DefaultShape = RoundedCornerShape(DefaultCornerRadius)
|
val DefaultShape = RoundedCornerShape(DefaultCornerRadius)
|
||||||
val CardClipShape = RoundedCornerShape(CardClipRadius)
|
val CardClipShape = RoundedCornerShape(CardClipRadius)
|
||||||
val CardShape = RoundedCornerShape(DefaultCornerRadius)
|
val CardShape = RoundedCornerShape(DefaultCornerRadius)
|
||||||
val NoShape = RoundedCornerShape(NoRounding)
|
val NoShape = RoundedCornerShape(NoRounding)
|
||||||
|
|
||||||
// Opacity Levels
|
|
||||||
const val ALPHA_HIGH = 0.6f
|
const val ALPHA_HIGH = 0.6f
|
||||||
const val ALPHA_MEDIUM = 0.5f
|
const val ALPHA_MEDIUM = 0.4f
|
||||||
const val ALPHA_LOW = 0.3f
|
const val ALPHA_LOW = 0.2f // Adjusted for glass
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A styled card container for displaying content with a consistent floating look.
|
* Standard Glassmorphism Modifier
|
||||||
*
|
|
||||||
* @param modifier The modifier to be applied to the card.
|
|
||||||
* @param content The content to be displayed inside the card.
|
|
||||||
*/
|
*/
|
||||||
|
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
|
@Composable
|
||||||
fun AppCard(
|
fun AppCard(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
title: String? = null,
|
title: String? = null,
|
||||||
icon: ImageVector? = null, // New optional icon parameter
|
icon: ImageVector? = null,
|
||||||
text: String? = null,
|
text: String? = null,
|
||||||
expandable: Boolean = false,
|
expandable: Boolean = false,
|
||||||
initiallyExpanded: Boolean = false,
|
initiallyExpanded: Boolean = false,
|
||||||
@@ -110,25 +115,17 @@ fun AppCard(
|
|||||||
label = "Chevron Rotation"
|
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
|
val hasHeader = title != null || text != null || expandable || icon != null
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.shadow(
|
.glassmorphic(shape = ComponentDefaults.CardShape, alpha = 0.25f)
|
||||||
DefaultElevation,
|
|
||||||
shape = ComponentDefaults.CardShape
|
|
||||||
)
|
|
||||||
.clip(ComponentDefaults.CardClipShape)
|
|
||||||
// Animate height changes when expanding/collapsing
|
|
||||||
.animateContentSize(),
|
.animateContentSize(),
|
||||||
shape = ComponentDefaults.CardShape,
|
shape = ComponentDefaults.CardShape,
|
||||||
color = MaterialTheme.colorScheme.surfaceContainer
|
color = Color.Transparent // Let glassmorphic handle the background
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
// --- Header Row ---
|
|
||||||
if (hasHeader) {
|
if (hasHeader) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -137,7 +134,6 @@ fun AppCard(
|
|||||||
.padding(ComponentDefaults.CardPadding),
|
.padding(ComponentDefaults.CardPadding),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// 1. Optional Icon on the left
|
|
||||||
if (icon != null) {
|
if (icon != null) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
@@ -148,7 +144,6 @@ fun AppCard(
|
|||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Title and Text Column
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
if (!title.isNullOrBlank()) {
|
if (!title.isNullOrBlank()) {
|
||||||
Text(
|
Text(
|
||||||
@@ -157,12 +152,9 @@ fun AppCard(
|
|||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only show spacer if both title and text exist
|
|
||||||
if (!title.isNullOrBlank() && !text.isNullOrBlank()) {
|
if (!title.isNullOrBlank() && !text.isNullOrBlank()) {
|
||||||
Spacer(Modifier.size(4.dp))
|
Spacer(Modifier.size(4.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!text.isNullOrBlank()) {
|
if (!text.isNullOrBlank()) {
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
@@ -172,7 +164,6 @@ fun AppCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Expand Chevron (Far right)
|
|
||||||
if (expandable) {
|
if (expandable) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = AppIcons.ArrowDropDown,
|
imageVector = AppIcons.ArrowDropDown,
|
||||||
@@ -184,15 +175,12 @@ fun AppCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Content Area ---
|
|
||||||
if (!expandable || isExpanded) {
|
if (!expandable || isExpanded) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(
|
modifier = Modifier.padding(
|
||||||
start = ComponentDefaults.CardPadding,
|
start = ComponentDefaults.CardPadding,
|
||||||
end = ComponentDefaults.CardPadding,
|
end = ComponentDefaults.CardPadding,
|
||||||
bottom = 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
|
top = if (hasHeader) 0.dp else ComponentDefaults.CardPadding
|
||||||
),
|
),
|
||||||
content = content
|
content = content
|
||||||
@@ -304,31 +292,27 @@ fun AppButton(
|
|||||||
modifier: Modifier? = Modifier,
|
modifier: Modifier? = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
shape: Shape? = null,
|
shape: Shape? = null,
|
||||||
colors: ButtonColors = ButtonDefaults.buttonColors(),
|
colors: ButtonColors = ButtonDefaults.buttonColors(
|
||||||
elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
|
containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f) // Glassy primary
|
||||||
|
),
|
||||||
|
elevation: ButtonElevation? = ButtonDefaults.buttonElevation(defaultElevation = 0.dp),
|
||||||
border: BorderStroke? = null,
|
border: BorderStroke? = null,
|
||||||
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
|
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
|
||||||
interactionSource: MutableInteractionSource? = null,
|
interactionSource: MutableInteractionSource? = null,
|
||||||
content: @Composable RowScope.() -> Unit
|
content: @Composable RowScope.() -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val m = modifier ?: Modifier.height(ComponentDefaults.DefaultButtonHeight)
|
val m = modifier ?: Modifier.height(ComponentDefaults.DefaultButtonHeight)
|
||||||
val s = shape ?: ComponentDefaults.DefaultShape
|
val s = shape ?: ComponentDefaults.DefaultShape
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = m,
|
modifier = m.border(1.dp, MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.2f), s),
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
shape = s,
|
shape = s,
|
||||||
colors = colors,
|
colors = colors,
|
||||||
elevation = elevation,
|
elevation = elevation,
|
||||||
border = border,
|
border = border,
|
||||||
contentPadding = PaddingValues(
|
contentPadding = PaddingValues(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 8.dp),
|
||||||
start = 8.dp, // More horizontal padding
|
|
||||||
end = 8.dp,
|
|
||||||
top = 8.dp, // Default vertical padding
|
|
||||||
bottom = 8.dp
|
|
||||||
),
|
|
||||||
interactionSource = interactionSource
|
interactionSource = interactionSource
|
||||||
) {
|
) {
|
||||||
content()
|
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.
|
* The secondary button for less prominent actions.
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("AssignedValueIsNeverRead", "HardCodedStringLiteral")
|
||||||
|
|
||||||
package eu.gaudian.translator.view.composable
|
package eu.gaudian.translator.view.composable
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -6,19 +8,19 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
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.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -33,7 +35,6 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
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.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -82,7 +83,10 @@ fun BaseLanguageDropDown(
|
|||||||
else -> stringResource(R.string.label_language_none)
|
else -> stringResource(R.string.label_language_none)
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(modifier = modifier) {
|
Box(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
contentAlignment = Alignment.TopCenter
|
||||||
|
) {
|
||||||
AppOutlinedButton(
|
AppOutlinedButton(
|
||||||
shape = RoundedCornerShape(8.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
onClick = { expanded = true },
|
onClick = { expanded = true },
|
||||||
@@ -104,12 +108,17 @@ fun BaseLanguageDropDown(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DropdownMenu(modifier = modifier.fillMaxWidth(), expanded = expanded, onDismissRequest = {
|
if (expanded) {
|
||||||
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = {
|
||||||
expanded = false
|
expanded = false
|
||||||
searchText = ""
|
searchText = ""
|
||||||
tempSelection = emptyList() // Also reset temp selection on dismiss
|
tempSelection = emptyList()
|
||||||
}) {
|
},
|
||||||
// Helper composable for a single language row in multiple selection mode
|
sheetState = sheetState
|
||||||
|
) {
|
||||||
@Composable
|
@Composable
|
||||||
fun MultiSelectItem(language: Language) {
|
fun MultiSelectItem(language: Language) {
|
||||||
val isSelected = tempSelection.contains(language)
|
val isSelected = tempSelection.contains(language)
|
||||||
@@ -120,7 +129,6 @@ fun BaseLanguageDropDown(
|
|||||||
checked = isSelected,
|
checked = isSelected,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
tempSelection = if (isSelected) tempSelection - language else tempSelection + language
|
tempSelection = if (isSelected) tempSelection - language else tempSelection + language
|
||||||
@Suppress("AssignedValueIsNeverRead")
|
|
||||||
selectedLanguagesCount = tempSelection.size
|
selectedLanguagesCount = tempSelection.size
|
||||||
onLanguagesSelected(tempSelection)
|
onLanguagesSelected(tempSelection)
|
||||||
}
|
}
|
||||||
@@ -141,13 +149,11 @@ fun BaseLanguageDropDown(
|
|||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
tempSelection = if (isSelected) tempSelection - language else tempSelection + language
|
tempSelection = if (isSelected) tempSelection - language else tempSelection + language
|
||||||
@Suppress("AssignedValueIsNeverRead")
|
|
||||||
selectedLanguagesCount = tempSelection.size
|
selectedLanguagesCount = tempSelection.size
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper composable for a single language row in single selection mode
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SingleSelectItem(language: Language) {
|
fun SingleSelectItem(language: Language) {
|
||||||
val languageNames = languages.map { it.name }
|
val languageNames = languages.map { it.name }
|
||||||
@@ -197,43 +203,22 @@ fun BaseLanguageDropDown(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Main Dropdown Content ---
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth()
|
||||||
.heightIn(max = 900.dp) // Constrain the height
|
|
||||||
) {
|
) {
|
||||||
// Search bar with a back arrow
|
DropdownSearchField(
|
||||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
searchQuery = searchText,
|
||||||
IconButton(onClick = { expanded = false; searchText = "" }) {
|
onSearchQueryChange = { searchText = it },
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.label_close))
|
|
||||||
}
|
|
||||||
TextField(
|
|
||||||
value = searchText,
|
|
||||||
onValueChange = { searchText = it },
|
|
||||||
singleLine = true,
|
|
||||||
placeholder = { Text(stringResource(R.string.text_search_3d)) },
|
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()
|
HorizontalDivider()
|
||||||
|
|
||||||
|
// Replaced height(max = 900.dp) with standard weight logic to allow proper scrolling bounds
|
||||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f, fill = false)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
val isSearching = searchText.isNotBlank()
|
val isSearching = searchText.isNotBlank()
|
||||||
|
|
||||||
if (isSearching) {
|
if (isSearching) {
|
||||||
@@ -255,80 +240,91 @@ fun BaseLanguageDropDown(
|
|||||||
} else if (alternateLanguages.isNotEmpty()) {
|
} else if (alternateLanguages.isNotEmpty()) {
|
||||||
val sortedAlternate = alternateLanguages.sortedBy { it.name }
|
val sortedAlternate = alternateLanguages.sortedBy { it.name }
|
||||||
if (enableMultipleSelection) {
|
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) }
|
sortedAlternate.forEach { language -> MultiSelectItem(language) }
|
||||||
} else {
|
} 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) }
|
sortedAlternate.forEach { language -> SingleSelectItem(language) }
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (enableMultipleSelection) {
|
if (enableMultipleSelection) {
|
||||||
if (favoriteLanguages.isNotEmpty()) {
|
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) }
|
favoriteLanguages.forEach { language -> MultiSelectItem(language) }
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||||
}
|
}
|
||||||
val recentHistoryFiltered = languageHistory.filter { it !in favoriteLanguages }.takeLast(5)
|
val recentHistoryFiltered = languageHistory.filter { it !in favoriteLanguages }.takeLast(5)
|
||||||
if (recentHistoryFiltered.isNotEmpty() && alternateLanguages.isEmpty()) {
|
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) }
|
recentHistoryFiltered.forEach { language -> MultiSelectItem(language) }
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||||
}
|
}
|
||||||
val remainingLanguages = languages.sortedBy { it.name }
|
val remainingLanguages = languages.sortedBy { it.name }
|
||||||
if (remainingLanguages.isNotEmpty()) {
|
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) }
|
remainingLanguages.forEach { language -> MultiSelectItem(language) }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Logic for single selection default view
|
|
||||||
if (showAutoOption) {
|
if (showAutoOption) {
|
||||||
AppDropdownMenuItem(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()
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
if (showNoneOption) {
|
if (showNoneOption) {
|
||||||
AppDropdownMenuItem(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()
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
if (favoriteLanguages.any {
|
if (favoriteLanguages.any {
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
it.code != "none" && it.code != "auto"
|
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 {
|
favoriteLanguages.filter {
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
it.code != "none" && it.code != "auto"
|
it.code != "none" && it.code != "auto"
|
||||||
}.forEach { language -> SingleSelectItem(language) }
|
}.forEach { language -> SingleSelectItem(language) }
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||||
}
|
}
|
||||||
val recentHistoryFiltered = languageHistory.filter {
|
val recentHistoryFiltered = languageHistory.filter {
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
it !in favoriteLanguages && it.code != "none" && it.code != "auto"
|
it !in favoriteLanguages && it.code != "none" && it.code != "auto"
|
||||||
}.takeLast(5)
|
}.takeLast(5)
|
||||||
if (recentHistoryFiltered.isNotEmpty()) {
|
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) }
|
recentHistoryFiltered.forEach { language -> SingleSelectItem(language) }
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||||
}
|
}
|
||||||
val remainingLanguages = languages.filter {
|
val remainingLanguages = languages.filter {
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
it !in favoriteLanguages && it !in recentHistoryFiltered && it.code != "none" && it.code != "auto"
|
it !in favoriteLanguages && it !in recentHistoryFiltered && it.code != "none" && it.code != "auto"
|
||||||
}.sortedBy { it.name }
|
}.sortedBy { it.name }
|
||||||
if (remainingLanguages.isNotEmpty()) {
|
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) }
|
remainingLanguages.forEach { language -> SingleSelectItem(language) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Done button for multiple selection mode
|
|
||||||
if (enableMultipleSelection) {
|
if (enableMultipleSelection) {
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
AppButton(
|
AppButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
onLanguagesSelected(tempSelection)
|
onLanguagesSelected(tempSelection)
|
||||||
@Suppress("AssignedValueIsNeverRead")
|
|
||||||
selectedLanguagesCount = tempSelection.size
|
selectedLanguagesCount = tempSelection.size
|
||||||
expanded = false
|
expanded = false
|
||||||
searchText = ""
|
searchText = ""
|
||||||
@@ -340,6 +336,10 @@ fun BaseLanguageDropDown(
|
|||||||
Text(stringResource(R.string.label_done))
|
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.AppIcons
|
||||||
import eu.gaudian.translator.view.composable.AppOutlinedTextField
|
import eu.gaudian.translator.view.composable.AppOutlinedTextField
|
||||||
import eu.gaudian.translator.view.composable.MultipleLanguageDropdown
|
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.CategoryViewModel
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||||
|
|
||||||
|
|
||||||
enum class DialogCategoryType { TAG, FILTER }
|
enum class DialogCategoryType { TAG, FILTER }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -79,7 +80,7 @@ fun AddCategoryDialog(
|
|||||||
AppDialog(
|
AppDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = { Text(stringResource(R.string.label_add_category)) },
|
title = { Text(stringResource(R.string.label_add_category)) },
|
||||||
hintContent = { CategoryHint() },
|
hintContent = HintDefinition.CATEGORY.hint(),
|
||||||
content = {
|
content = {
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
SingleChoiceSegmentedButtonRow(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.R
|
||||||
import eu.gaudian.translator.model.VocabularyItem
|
import eu.gaudian.translator.model.VocabularyItem
|
||||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||||
|
import eu.gaudian.translator.utils.StatusMessageService
|
||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.LocalConnectionConfigured
|
import eu.gaudian.translator.view.LocalConnectionConfigured
|
||||||
import eu.gaudian.translator.view.composable.AppButton
|
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.SourceLanguageDropdown
|
||||||
import eu.gaudian.translator.view.composable.TargetLanguageDropdown
|
import eu.gaudian.translator.view.composable.TargetLanguageDropdown
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||||
import eu.gaudian.translator.viewmodel.StatusViewModel
|
|
||||||
import eu.gaudian.translator.viewmodel.TranslationViewModel
|
import eu.gaudian.translator.viewmodel.TranslationViewModel
|
||||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -67,12 +67,12 @@ fun AddVocabularyDialog(
|
|||||||
showMultiple: Boolean = true
|
showMultiple: Boolean = true
|
||||||
) {
|
) {
|
||||||
val activity = LocalContext.current.findActivity()
|
val activity = LocalContext.current.findActivity()
|
||||||
val statusViewModel: StatusViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
|
||||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
val vocabularyViewModel = hiltViewModel<VocabularyViewModel>(viewModelStoreOwner = activity)
|
val vocabularyViewModel = hiltViewModel<VocabularyViewModel>(viewModelStoreOwner = activity)
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val translationViewModel: TranslationViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
val translationViewModel: TranslationViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
val connectionConfigured = LocalConnectionConfigured.current
|
val connectionConfigured = LocalConnectionConfigured.current
|
||||||
|
val statusMessageService = StatusMessageService
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ fun AddVocabularyDialog(
|
|||||||
selectedTranslations.clear()
|
selectedTranslations.clear()
|
||||||
}
|
}
|
||||||
.onFailure { exception ->
|
.onFailure { exception ->
|
||||||
statusViewModel.showErrorMessage(
|
statusMessageService.showErrorMessage(
|
||||||
textFailedToGetTranslations + exception.message)
|
textFailedToGetTranslations + exception.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
|
@file:Suppress("AssignedValueIsNeverRead", "HardCodedStringLiteral")
|
||||||
|
|
||||||
package eu.gaudian.translator.view.dialogs
|
package eu.gaudian.translator.view.dialogs
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
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.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@@ -15,29 +14,30 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.model.TagCategory
|
import eu.gaudian.translator.model.TagCategory
|
||||||
import eu.gaudian.translator.model.VocabularyCategory
|
import eu.gaudian.translator.model.VocabularyCategory
|
||||||
import eu.gaudian.translator.model.VocabularyFilter
|
|
||||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||||
import eu.gaudian.translator.view.composable.AppButton
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppCheckbox
|
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.AppDropdownMenuItem
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
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.AppOutlinedTextField
|
||||||
|
import eu.gaudian.translator.view.composable.DropdownHeader
|
||||||
|
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,22 +49,12 @@ data class CategoryDropdownState(
|
|||||||
val selectedCategories: List<VocabularyCategory?> = emptyList(),
|
val selectedCategories: List<VocabularyCategory?> = emptyList(),
|
||||||
val newCategoryName: String = "",
|
val newCategoryName: String = "",
|
||||||
val categories: List<VocabularyCategory> = emptyList(),
|
val categories: List<VocabularyCategory> = emptyList(),
|
||||||
|
val searchQuery: String = "",
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stateless dropdown content composable for category selection.
|
* Stateless dropdown content composable for category selection.
|
||||||
* This component is fully controlled by its parameters and does not maintain any internal state.
|
* This component is fully controlled by its parameters and does not maintain any internal state.
|
||||||
*
|
|
||||||
* @param state The current state of the dropdown
|
|
||||||
* @param onExpand Callback when the dropdown should expand/collapse
|
|
||||||
* @param onCategorySelected Callback when a category is selected
|
|
||||||
* @param onNewCategoryNameChange Callback when the new category name changes
|
|
||||||
* @param onAddCategory Callback when a new category should be added
|
|
||||||
* @param noneSelectable Whether "None" option is selectable
|
|
||||||
* @param multipleSelectable Whether multiple categories can be selected
|
|
||||||
* @param onlyLists Whether to show only list/category types
|
|
||||||
* @param addCategory Whether to show the "Add Category" option
|
|
||||||
* @param modifier Modifier for the composable
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoryDropdownContent(
|
fun CategoryDropdownContent(
|
||||||
@@ -74,10 +64,12 @@ fun CategoryDropdownContent(
|
|||||||
onCategorySelected: (List<VocabularyCategory?>) -> Unit,
|
onCategorySelected: (List<VocabularyCategory?>) -> Unit,
|
||||||
onNewCategoryNameChange: (String) -> Unit,
|
onNewCategoryNameChange: (String) -> Unit,
|
||||||
onAddCategory: (String) -> Unit,
|
onAddCategory: (String) -> Unit,
|
||||||
|
onSearchQueryChange: (String) -> Unit = {},
|
||||||
noneSelectable: Boolean = true,
|
noneSelectable: Boolean = true,
|
||||||
multipleSelectable: Boolean = false,
|
multipleSelectable: Boolean = false,
|
||||||
onlyLists: Boolean = false,
|
onlyLists: Boolean = false,
|
||||||
addCategory: Boolean = false,
|
addCategory: Boolean = false,
|
||||||
|
enableSearch: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val selectableCategories = if (onlyLists) {
|
val selectableCategories = if (onlyLists) {
|
||||||
state.categories.filterIsInstance<TagCategory>()
|
state.categories.filterIsInstance<TagCategory>()
|
||||||
@@ -85,37 +77,34 @@ fun CategoryDropdownContent(
|
|||||||
state.categories
|
state.categories
|
||||||
}
|
}
|
||||||
|
|
||||||
AppOutlinedButton(
|
// Filter categories by search query if search is enabled
|
||||||
shape = RoundedCornerShape(8.dp),
|
val filteredCategories = if (enableSearch && state.searchQuery.isNotBlank()) {
|
||||||
onClick = { onExpand(true) },
|
selectableCategories.filter { category ->
|
||||||
modifier = modifier.fillMaxWidth(),
|
category.name.contains(state.searchQuery, ignoreCase = true)
|
||||||
) {
|
}
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Text(
|
|
||||||
text = when {
|
|
||||||
state.selectedCategories.isEmpty() -> stringResource(R.string.text_select_category)
|
|
||||||
state.selectedCategories.size == 1 -> state.selectedCategories.first()?.name
|
|
||||||
?: stringResource(R.string.text_none)
|
|
||||||
else -> stringResource(R.string.text_2d_categories_selected, state.selectedCategories.size)
|
|
||||||
},
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
Icon(
|
|
||||||
imageVector = if (state.expanded) AppIcons.ArrowDropUp else AppIcons.ArrowDropDown,
|
|
||||||
contentDescription = if (state.expanded) {
|
|
||||||
stringResource(R.string.cd_collapse)
|
|
||||||
} else {
|
} else {
|
||||||
stringResource(R.string.cd_expand)
|
selectableCategories
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DropdownMenu(
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
AppDropdownContainer(
|
||||||
expanded = state.expanded,
|
expanded = state.expanded,
|
||||||
onDismissRequest = { onExpand(false) },
|
onDismissRequest = { onExpand(false) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
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) }
|
||||||
) {
|
) {
|
||||||
if (noneSelectable) {
|
if (noneSelectable) {
|
||||||
val noneSelected = state.selectedCategories.contains(null)
|
val noneSelected = state.selectedCategories.contains(null)
|
||||||
@@ -128,7 +117,7 @@ fun CategoryDropdownContent(
|
|||||||
if (multipleSelectable) {
|
if (multipleSelectable) {
|
||||||
AppCheckbox(
|
AppCheckbox(
|
||||||
checked = noneSelected,
|
checked = noneSelected,
|
||||||
onCheckedChange = { isChecked ->
|
onCheckedChange = { _ ->
|
||||||
val newSelection = if (noneSelected) {
|
val newSelection = if (noneSelected) {
|
||||||
state.selectedCategories.filterNotNull()
|
state.selectedCategories.filterNotNull()
|
||||||
} else {
|
} else {
|
||||||
@@ -139,7 +128,10 @@ fun CategoryDropdownContent(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
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 = {
|
onClick = {
|
||||||
@@ -158,7 +150,7 @@ fun CategoryDropdownContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
selectableCategories.forEach { category ->
|
filteredCategories.forEach { category ->
|
||||||
val isSelected = state.selectedCategories.contains(category)
|
val isSelected = state.selectedCategories.contains(category)
|
||||||
AppDropdownMenuItem(
|
AppDropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
@@ -180,7 +172,10 @@ fun CategoryDropdownContent(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
}
|
}
|
||||||
Text(category.name)
|
Text(
|
||||||
|
text = category.name,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -199,16 +194,24 @@ fun CategoryDropdownContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addCategory) {
|
if (enableSearch && state.searchQuery.isNotBlank() && filteredCategories.isEmpty()) {
|
||||||
HorizontalDivider()
|
|
||||||
|
|
||||||
AppDropdownMenuItem(
|
AppDropdownMenuItem(
|
||||||
text = {
|
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 = {},
|
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(
|
AppDropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
@@ -222,7 +225,7 @@ fun CategoryDropdownContent(
|
|||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (state.newCategoryName.isNotBlank()) {
|
if (state.newCategoryName.isNotBlank()) {
|
||||||
@@ -241,59 +244,38 @@ fun CategoryDropdownContent(
|
|||||||
onClick = {}
|
onClick = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (multipleSelectable) {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
AppButton(
|
|
||||||
onClick = { onExpand(false) },
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.label_done))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stateful wrapper for CategoryDropdown that manages its own state.
|
* Stateful wrapper for CategoryDropdown that manages its own state.
|
||||||
* This is the main composable that should be used in production code.
|
|
||||||
*
|
|
||||||
* @param initialCategoryId The initial category ID to select
|
|
||||||
* @param onCategorySelected Callback when categories are selected
|
|
||||||
* @param noneSelectable Whether "None" option is selectable
|
|
||||||
* @param multipleSelectable Whether multiple categories can be selected
|
|
||||||
* @param onlyLists Whether to show only list/category types
|
|
||||||
* @param addCategory Whether to show the "Add Category" option
|
|
||||||
* @param modifier Modifier for the composable
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoryDropdown(
|
fun CategoryDropdown(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
initialCategoryId: Int? = null,
|
initialCategoryId: Int? = null,
|
||||||
onCategorySelected: (List<VocabularyCategory?>) -> Unit,
|
onCategorySelected: (List<VocabularyCategory?>) -> Unit,
|
||||||
noneSelectable: Boolean? = true,
|
noneSelectable: Boolean? = true,
|
||||||
multipleSelectable: Boolean = false,
|
multipleSelectable: Boolean = false,
|
||||||
onlyLists: Boolean = false,
|
onlyLists: Boolean = false,
|
||||||
addCategory: Boolean = false,
|
addCategory: Boolean = false,
|
||||||
modifier: Modifier = Modifier,
|
enableSearch: Boolean = false,
|
||||||
) {
|
) {
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
var selectedCategories by remember {
|
var selectedCategories by remember {
|
||||||
mutableStateOf<List<VocabularyCategory?>>(emptyList())
|
mutableStateOf<List<VocabularyCategory?>>(emptyList())
|
||||||
}
|
}
|
||||||
var newCategoryName by remember { mutableStateOf("") }
|
var newCategoryName by remember { mutableStateOf("") }
|
||||||
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
|
|
||||||
// For production use, this would come from ViewModel
|
val activity = LocalContext.current.findActivity()
|
||||||
// For preview, we'll use empty list or pass via state
|
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
val categories by remember { mutableStateOf(emptyList<VocabularyCategory>()) }
|
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
||||||
|
|
||||||
// Find initial category
|
|
||||||
val initialCategory = remember(categories, initialCategoryId) {
|
val initialCategory = remember(categories, initialCategoryId) {
|
||||||
categories.find { it.id == initialCategoryId }
|
categories.find { it.id == initialCategoryId }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize selection with initial category if provided
|
|
||||||
remember(initialCategory) {
|
remember(initialCategory) {
|
||||||
if (initialCategory != null && selectedCategories.isEmpty()) {
|
if (initialCategory != null && selectedCategories.isEmpty()) {
|
||||||
selectedCategories = listOf(initialCategory)
|
selectedCategories = listOf(initialCategory)
|
||||||
@@ -307,6 +289,7 @@ fun CategoryDropdown(
|
|||||||
selectedCategories = selectedCategories,
|
selectedCategories = selectedCategories,
|
||||||
newCategoryName = newCategoryName,
|
newCategoryName = newCategoryName,
|
||||||
categories = categories,
|
categories = categories,
|
||||||
|
searchQuery = searchQuery,
|
||||||
),
|
),
|
||||||
onExpand = { isExpanded -> expanded = isExpanded },
|
onExpand = { isExpanded -> expanded = isExpanded },
|
||||||
onCategorySelected = { newSelection ->
|
onCategorySelected = { newSelection ->
|
||||||
@@ -316,115 +299,35 @@ fun CategoryDropdown(
|
|||||||
onNewCategoryNameChange = { newCategoryName = it },
|
onNewCategoryNameChange = { newCategoryName = it },
|
||||||
onAddCategory = { name ->
|
onAddCategory = { name ->
|
||||||
val newCategory = TagCategory(id = 0, name = name)
|
val newCategory = TagCategory(id = 0, name = name)
|
||||||
// In production, this would call ViewModel.createCategory(newCategory)
|
|
||||||
newCategoryName = ""
|
newCategoryName = ""
|
||||||
|
categoryViewModel.createCategory(newCategory)
|
||||||
|
//selectedCategories = selectedCategories + newCategory
|
||||||
if (!multipleSelectable) {
|
if (!multipleSelectable) {
|
||||||
expanded = false
|
expanded = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onSearchQueryChange = { searchQuery = it },
|
||||||
noneSelectable = noneSelectable == true,
|
noneSelectable = noneSelectable == true,
|
||||||
multipleSelectable = multipleSelectable,
|
multipleSelectable = multipleSelectable,
|
||||||
onlyLists = onlyLists,
|
onlyLists = onlyLists,
|
||||||
addCategory = addCategory,
|
addCategory = addCategory,
|
||||||
|
enableSearch = enableSearch,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== PREVIEWS ==============
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preview provider for CategoryDropdownState
|
|
||||||
*/
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
class CategoryDropdownStateProvider : PreviewParameterProvider<CategoryDropdownState> {
|
|
||||||
override val values = sequenceOf(
|
|
||||||
// Collapsed state - nothing selected
|
|
||||||
CategoryDropdownState(
|
|
||||||
expanded = false,
|
|
||||||
selectedCategories = emptyList(),
|
|
||||||
categories = listOf(
|
|
||||||
TagCategory(1, "Animals"),
|
|
||||||
TagCategory(2, "Food"),
|
|
||||||
VocabularyFilter(3, "Filters", languages = listOf(1, 2)),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
// Collapsed state - one category selected
|
|
||||||
CategoryDropdownState(
|
|
||||||
expanded = false,
|
|
||||||
selectedCategories = listOf(TagCategory(1, "Animals")),
|
|
||||||
categories = listOf(
|
|
||||||
TagCategory(1, "Animals"),
|
|
||||||
TagCategory(2, "Food"),
|
|
||||||
TagCategory(3, "Travel"),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
// Collapsed state - multiple categories selected
|
|
||||||
CategoryDropdownState(
|
|
||||||
expanded = false,
|
|
||||||
selectedCategories = listOf(
|
|
||||||
TagCategory(1, "Animals"),
|
|
||||||
TagCategory(3, "Travel"),
|
|
||||||
),
|
|
||||||
categories = listOf(
|
|
||||||
TagCategory(1, "Animals"),
|
|
||||||
TagCategory(2, "Food"),
|
|
||||||
TagCategory(3, "Travel"),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
// Expanded state - nothing selected
|
|
||||||
CategoryDropdownState(
|
|
||||||
expanded = true,
|
|
||||||
selectedCategories = emptyList(),
|
|
||||||
categories = listOf(
|
|
||||||
TagCategory(1, "Animals"),
|
|
||||||
TagCategory(2, "Food"),
|
|
||||||
TagCategory(3, "Travel"),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
// Expanded state - one selected
|
|
||||||
CategoryDropdownState(
|
|
||||||
expanded = true,
|
|
||||||
selectedCategories = listOf(TagCategory(2, "Food")),
|
|
||||||
categories = listOf(
|
|
||||||
TagCategory(1, "Animals"),
|
|
||||||
TagCategory(2, "Food"),
|
|
||||||
TagCategory(3, "Travel"),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
// With "None" option selected
|
|
||||||
CategoryDropdownState(
|
|
||||||
expanded = true,
|
|
||||||
selectedCategories = listOf(null),
|
|
||||||
categories = listOf(
|
|
||||||
TagCategory(1, "Animals"),
|
|
||||||
TagCategory(2, "Food"),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
// With add category option
|
|
||||||
CategoryDropdownState(
|
|
||||||
expanded = true,
|
|
||||||
selectedCategories = emptyList(),
|
|
||||||
newCategoryName = "New Cat",
|
|
||||||
categories = listOf(
|
|
||||||
TagCategory(1, "Animals"),
|
|
||||||
TagCategory(2, "Food"),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ThemePreviews
|
@ThemePreviews
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoryDropdownCollapsedPreview(
|
fun CategoryDropdownCollapsedPreview() {
|
||||||
@PreviewParameter(CategoryDropdownStateProvider::class) state: CategoryDropdownState
|
Surface(modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.background) {
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
color = MaterialTheme.colorScheme.background
|
|
||||||
) {
|
|
||||||
CategoryDropdownContent(
|
CategoryDropdownContent(
|
||||||
state = state.copy(expanded = false),
|
state = CategoryDropdownState(
|
||||||
|
expanded = false,
|
||||||
|
selectedCategories = emptyList(),
|
||||||
|
categories = listOf(TagCategory(1, "Animals"), TagCategory(2, "Food")),
|
||||||
|
),
|
||||||
onExpand = {},
|
onExpand = {},
|
||||||
onCategorySelected = {},
|
onCategorySelected = {},
|
||||||
onNewCategoryNameChange = {},
|
onNewCategoryNameChange = {},
|
||||||
@@ -436,15 +339,14 @@ fun CategoryDropdownCollapsedPreview(
|
|||||||
@ThemePreviews
|
@ThemePreviews
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoryDropdownExpandedPreview(
|
fun CategoryDropdownExpandedPreview() {
|
||||||
@PreviewParameter(CategoryDropdownStateProvider::class) state: CategoryDropdownState
|
Surface(modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.background) {
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
color = MaterialTheme.colorScheme.background
|
|
||||||
) {
|
|
||||||
CategoryDropdownContent(
|
CategoryDropdownContent(
|
||||||
state = state.copy(expanded = true),
|
state = CategoryDropdownState(
|
||||||
|
expanded = true,
|
||||||
|
selectedCategories = listOf(TagCategory(1, "Animals")),
|
||||||
|
categories = listOf(TagCategory(1, "Animals"), TagCategory(2, "Food"), TagCategory(3, "Travel")),
|
||||||
|
),
|
||||||
onExpand = {},
|
onExpand = {},
|
||||||
onCategorySelected = {},
|
onCategorySelected = {},
|
||||||
onNewCategoryNameChange = {},
|
onNewCategoryNameChange = {},
|
||||||
@@ -458,21 +360,10 @@ fun CategoryDropdownExpandedPreview(
|
|||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoryDropdownMultipleSelectionPreview() {
|
fun CategoryDropdownMultipleSelectionPreview() {
|
||||||
val categories = listOf(
|
val categories = listOf(TagCategory(1, "Animals"), TagCategory(2, "Food"), TagCategory(3, "Travel"))
|
||||||
TagCategory(1, "Animals"),
|
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory?>>(listOf(categories[0], categories[2])) }
|
||||||
TagCategory(2, "Food"),
|
|
||||||
TagCategory(3, "Travel"),
|
|
||||||
TagCategory(4, "Business"),
|
|
||||||
TagCategory(5, "Technology"),
|
|
||||||
)
|
|
||||||
var selectedCategories by remember {
|
|
||||||
mutableStateOf<List<VocabularyCategory?>>(listOf(categories[0], categories[2]))
|
|
||||||
}
|
|
||||||
|
|
||||||
Surface(
|
Surface(modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.background) {
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
color = MaterialTheme.colorScheme.background
|
|
||||||
) {
|
|
||||||
CategoryDropdownContent(
|
CategoryDropdownContent(
|
||||||
state = CategoryDropdownState(
|
state = CategoryDropdownState(
|
||||||
expanded = true,
|
expanded = true,
|
||||||
@@ -494,26 +385,17 @@ fun CategoryDropdownMultipleSelectionPreview() {
|
|||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoryDropdownWithAddCategoryPreview() {
|
fun CategoryDropdownWithAddCategoryPreview() {
|
||||||
val categories = listOf(
|
Surface(modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.background) {
|
||||||
TagCategory(1, "Animals"),
|
|
||||||
TagCategory(2, "Food"),
|
|
||||||
)
|
|
||||||
var newCategoryName by remember { mutableStateOf("New Category") }
|
|
||||||
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
color = MaterialTheme.colorScheme.background
|
|
||||||
) {
|
|
||||||
CategoryDropdownContent(
|
CategoryDropdownContent(
|
||||||
state = CategoryDropdownState(
|
state = CategoryDropdownState(
|
||||||
expanded = true,
|
expanded = true,
|
||||||
selectedCategories = emptyList(),
|
selectedCategories = emptyList(),
|
||||||
newCategoryName = newCategoryName,
|
newCategoryName = "New Cat",
|
||||||
categories = categories,
|
categories = listOf(TagCategory(1, "Animals"), TagCategory(2, "Food")),
|
||||||
),
|
),
|
||||||
onExpand = {},
|
onExpand = {},
|
||||||
onCategorySelected = {},
|
onCategorySelected = {},
|
||||||
onNewCategoryNameChange = { newCategoryName = it },
|
onNewCategoryNameChange = {},
|
||||||
onAddCategory = {},
|
onAddCategory = {},
|
||||||
addCategory = true,
|
addCategory = true,
|
||||||
)
|
)
|
||||||
@@ -524,127 +406,8 @@ fun CategoryDropdownWithAddCategoryPreview() {
|
|||||||
@ThemePreviews
|
@ThemePreviews
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoryDropdownOnlyListsPreview() {
|
fun CategoryDropdownWithSearchPreview() {
|
||||||
val categories = listOf(
|
Surface(modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.background) {
|
||||||
TagCategory(1, "Animals"),
|
|
||||||
TagCategory(2, "Food"),
|
|
||||||
VocabularyFilter(3, "Language Pair EN-DE", languages = listOf(1, 2)),
|
|
||||||
TagCategory(4, "Travel"),
|
|
||||||
)
|
|
||||||
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
color = MaterialTheme.colorScheme.background
|
|
||||||
) {
|
|
||||||
CategoryDropdownContent(
|
|
||||||
state = CategoryDropdownState(
|
|
||||||
expanded = true,
|
|
||||||
selectedCategories = emptyList(),
|
|
||||||
categories = categories,
|
|
||||||
),
|
|
||||||
onExpand = {},
|
|
||||||
onCategorySelected = {},
|
|
||||||
onNewCategoryNameChange = {},
|
|
||||||
onAddCategory = {},
|
|
||||||
onlyLists = true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
@ThemePreviews
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
fun CategoryDropdownNoNoneOptionPreview() {
|
|
||||||
val categories = listOf(
|
|
||||||
TagCategory(1, "Animals"),
|
|
||||||
TagCategory(2, "Food"),
|
|
||||||
)
|
|
||||||
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
color = MaterialTheme.colorScheme.background
|
|
||||||
) {
|
|
||||||
CategoryDropdownContent(
|
|
||||||
state = CategoryDropdownState(
|
|
||||||
expanded = true,
|
|
||||||
selectedCategories = emptyList(),
|
|
||||||
categories = categories,
|
|
||||||
),
|
|
||||||
onExpand = {},
|
|
||||||
onCategorySelected = {},
|
|
||||||
onNewCategoryNameChange = {},
|
|
||||||
onAddCategory = {},
|
|
||||||
noneSelectable = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ThemePreviews
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
fun CategoryDropdownEmptyPreview() {
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
color = MaterialTheme.colorScheme.background
|
|
||||||
) {
|
|
||||||
CategoryDropdownContent(
|
|
||||||
state = CategoryDropdownState(
|
|
||||||
expanded = false,
|
|
||||||
selectedCategories = emptyList(),
|
|
||||||
categories = emptyList(),
|
|
||||||
),
|
|
||||||
onExpand = {},
|
|
||||||
onCategorySelected = {},
|
|
||||||
onNewCategoryNameChange = {},
|
|
||||||
onAddCategory = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
@ThemePreviews
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
fun CategoryDropdownStatefulPreview() {
|
|
||||||
var expanded by remember { mutableStateOf(true) }
|
|
||||||
var selectedCategories by remember {
|
|
||||||
mutableStateOf<List<VocabularyCategory?>>(listOf(TagCategory(1, "Animals")))
|
|
||||||
}
|
|
||||||
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
color = MaterialTheme.colorScheme.background
|
|
||||||
) {
|
|
||||||
CategoryDropdownContent(
|
|
||||||
state = CategoryDropdownState(
|
|
||||||
expanded = expanded,
|
|
||||||
selectedCategories = selectedCategories,
|
|
||||||
categories = listOf(
|
|
||||||
TagCategory(1, "Animals"),
|
|
||||||
TagCategory(2, "Food"),
|
|
||||||
TagCategory(3, "Travel"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onExpand = { expanded = it },
|
|
||||||
onCategorySelected = { selectedCategories = it },
|
|
||||||
onNewCategoryNameChange = {},
|
|
||||||
onAddCategory = {},
|
|
||||||
multipleSelectable = true,
|
|
||||||
noneSelectable = true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
@ThemePreviews
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
fun CategoryDropdownFullExpandedPreview() {
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
color = MaterialTheme.colorScheme.background
|
|
||||||
) {
|
|
||||||
CategoryDropdownContent(
|
CategoryDropdownContent(
|
||||||
state = CategoryDropdownState(
|
state = CategoryDropdownState(
|
||||||
expanded = true,
|
expanded = true,
|
||||||
@@ -653,20 +416,16 @@ fun CategoryDropdownFullExpandedPreview() {
|
|||||||
TagCategory(1, "Animals"),
|
TagCategory(1, "Animals"),
|
||||||
TagCategory(2, "Food"),
|
TagCategory(2, "Food"),
|
||||||
TagCategory(3, "Travel"),
|
TagCategory(3, "Travel"),
|
||||||
TagCategory(4, "Business"),
|
TagCategory(4, "Technology"),
|
||||||
TagCategory(5, "Technology"),
|
TagCategory(5, "Sports")
|
||||||
TagCategory(6, "Sports"),
|
|
||||||
TagCategory(7, "Music"),
|
|
||||||
TagCategory(8, "Art"),
|
|
||||||
),
|
),
|
||||||
|
searchQuery = "",
|
||||||
),
|
),
|
||||||
onExpand = {},
|
onExpand = {},
|
||||||
onCategorySelected = {},
|
onCategorySelected = {},
|
||||||
onNewCategoryNameChange = {},
|
onNewCategoryNameChange = {},
|
||||||
onAddCategory = {},
|
onAddCategory = {},
|
||||||
addCategory = true,
|
enableSearch = true,
|
||||||
multipleSelectable = true,
|
|
||||||
noneSelectable = true,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -18,7 +15,6 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.model.TagCategory
|
|
||||||
import eu.gaudian.translator.model.VocabularyCategory
|
import eu.gaudian.translator.model.VocabularyCategory
|
||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppDialog
|
import eu.gaudian.translator.view.composable.AppDialog
|
||||||
@@ -34,9 +30,6 @@ fun CategorySelectionDialog(
|
|||||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
|
|
||||||
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
||||||
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory?>>(emptyList()) }
|
|
||||||
var newCategoryName by remember { mutableStateOf("") }
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
AppDialog(
|
AppDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
@@ -45,21 +38,8 @@ fun CategorySelectionDialog(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
// Dropdown button and menu
|
// Dropdown button and menu
|
||||||
CategoryDropdownContent(
|
CategoryDropdown(
|
||||||
state = CategoryDropdownState(
|
onCategorySelected = onCategorySelected,
|
||||||
expanded = expanded,
|
|
||||||
selectedCategories = selectedCategories,
|
|
||||||
newCategoryName = newCategoryName,
|
|
||||||
categories = categories,
|
|
||||||
),
|
|
||||||
onExpand = { isExpanded -> expanded = isExpanded },
|
|
||||||
onCategorySelected = { selectedCategories = it },
|
|
||||||
onNewCategoryNameChange = { newCategoryName = it },
|
|
||||||
onAddCategory = { name ->
|
|
||||||
val newCategory = TagCategory(id = 0, name = name.trim())
|
|
||||||
categoryViewModel.createCategory(newCategory)
|
|
||||||
newCategoryName = ""
|
|
||||||
},
|
|
||||||
noneSelectable = false,
|
noneSelectable = false,
|
||||||
multipleSelectable = true,
|
multipleSelectable = true,
|
||||||
onlyLists = true,
|
onlyLists = true,
|
||||||
@@ -79,10 +59,11 @@ fun CategorySelectionDialog(
|
|||||||
|
|
||||||
DialogButton(
|
DialogButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
onCategorySelected(selectedCategories)
|
// The selected categories are handled by CategoryDropdown's internal state
|
||||||
|
// and passed to onCategorySelected callback
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},
|
},
|
||||||
enabled = selectedCategories.isNotEmpty()
|
enabled = true // Always enabled since CategoryDropdown handles validation
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.label_confirm))
|
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.InspiringSearchField
|
||||||
import eu.gaudian.translator.view.composable.SourceLanguageDropdown
|
import eu.gaudian.translator.view.composable.SourceLanguageDropdown
|
||||||
import eu.gaudian.translator.view.composable.TargetLanguageDropdown
|
import eu.gaudian.translator.view.composable.TargetLanguageDropdown
|
||||||
import eu.gaudian.translator.view.hints.ImportVocabularyHint
|
import eu.gaudian.translator.view.hints.HintDefinition
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -109,7 +109,7 @@ fun ImportDialogContent(
|
|||||||
AppDialog(
|
AppDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = { Text(descriptionText) },
|
title = { Text(descriptionText) },
|
||||||
hintContent = { ImportVocabularyHint() },
|
hintContent = HintDefinition.IMPORT.hint(),
|
||||||
content = {
|
content = {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -55,9 +55,8 @@ fun StartExerciseDialog(
|
|||||||
// Map displayed Language to its DB id (lid) using position mapping from load
|
// Map displayed Language to its DB id (lid) using position mapping from load
|
||||||
var languageIdMap by remember { mutableStateOf<Map<Language, Int>>(emptyMap()) }
|
var languageIdMap by remember { mutableStateOf<Map<Language, Int>>(emptyMap()) }
|
||||||
var selectedLanguages by remember { mutableStateOf<List<Language>>(emptyList()) }
|
var selectedLanguages by remember { mutableStateOf<List<Language>>(emptyList()) }
|
||||||
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory>>(emptyList()) }
|
|
||||||
var selectedStages by remember { mutableStateOf<List<VocabularyStage>>(emptyList()) }
|
var selectedStages by remember { mutableStateOf<List<VocabularyStage>>(emptyList()) }
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory>>(emptyList()) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
@@ -87,19 +86,10 @@ fun StartExerciseDialog(
|
|||||||
},
|
},
|
||||||
languages
|
languages
|
||||||
)
|
)
|
||||||
CategoryDropdownContent(
|
CategoryDropdown(
|
||||||
state = CategoryDropdownState(
|
|
||||||
expanded = expanded,
|
|
||||||
selectedCategories = selectedCategories,
|
|
||||||
newCategoryName = "",
|
|
||||||
categories = categories,
|
|
||||||
),
|
|
||||||
onExpand = { isExpanded -> expanded = isExpanded },
|
|
||||||
onCategorySelected = { cats ->
|
onCategorySelected = { cats ->
|
||||||
selectedCategories = cats.filterIsInstance<VocabularyCategory>()
|
selectedCategories = cats.filterIsInstance<VocabularyCategory>()
|
||||||
},
|
},
|
||||||
onNewCategoryNameChange = {},
|
|
||||||
onAddCategory = {},
|
|
||||||
multipleSelectable = true,
|
multipleSelectable = true,
|
||||||
onlyLists = false, // Show both filters and lists
|
onlyLists = false, // Show both filters and lists
|
||||||
addCategory = false,
|
addCategory = false,
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.model.TagCategory
|
|
||||||
import eu.gaudian.translator.model.VocabularyCategory
|
import eu.gaudian.translator.model.VocabularyCategory
|
||||||
import eu.gaudian.translator.model.VocabularyItem
|
import eu.gaudian.translator.model.VocabularyItem
|
||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
@@ -35,7 +34,7 @@ import eu.gaudian.translator.view.composable.AppButton
|
|||||||
import eu.gaudian.translator.view.composable.AppCheckbox
|
import eu.gaudian.translator.view.composable.AppCheckbox
|
||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
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.CategoryViewModel
|
||||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||||
|
|
||||||
@@ -54,8 +53,6 @@ fun VocabularyReviewScreen(
|
|||||||
val selectedItems = remember { mutableStateListOf<VocabularyItem>() }
|
val selectedItems = remember { mutableStateListOf<VocabularyItem>() }
|
||||||
val duplicates = remember { mutableStateListOf<Boolean>() }
|
val duplicates = remember { mutableStateListOf<Boolean>() }
|
||||||
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory?>>(emptyList()) }
|
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory?>>(emptyList()) }
|
||||||
var newCategoryName by remember { mutableStateOf("") }
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
LaunchedEffect(generatedItems) {
|
LaunchedEffect(generatedItems) {
|
||||||
val duplicateResults = vocabularyViewModel.findDuplicates(generatedItems)
|
val duplicateResults = vocabularyViewModel.findDuplicates(generatedItems)
|
||||||
@@ -69,7 +66,7 @@ fun VocabularyReviewScreen(
|
|||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.found_items)) },
|
title = { Text(stringResource(R.string.found_items)) },
|
||||||
hintContent = getVocabularyReviewHint()
|
hintContent = HintDefinition.REVIEW.hint()
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
@@ -134,21 +131,8 @@ fun VocabularyReviewScreen(
|
|||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
modifier = Modifier.padding(8.dp)
|
modifier = Modifier.padding(8.dp)
|
||||||
)
|
)
|
||||||
CategoryDropdownContent(
|
CategoryDropdown(
|
||||||
state = CategoryDropdownState(
|
|
||||||
expanded = expanded,
|
|
||||||
selectedCategories = selectedCategories,
|
|
||||||
newCategoryName = newCategoryName,
|
|
||||||
categories = categories,
|
|
||||||
),
|
|
||||||
onExpand = { isExpanded -> expanded = isExpanded },
|
|
||||||
onCategorySelected = { selectedCategories = it },
|
onCategorySelected = { selectedCategories = it },
|
||||||
onNewCategoryNameChange = { newCategoryName = it },
|
|
||||||
onAddCategory = { name ->
|
|
||||||
val newCategory = TagCategory(id = 0, name = name.trim())
|
|
||||||
categoryViewModel.createCategory(newCategory)
|
|
||||||
newCategoryName = ""
|
|
||||||
},
|
|
||||||
noneSelectable = false,
|
noneSelectable = false,
|
||||||
multipleSelectable = true,
|
multipleSelectable = true,
|
||||||
onlyLists = true,
|
onlyLists = true,
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
|
@file:Suppress("AssignedValueIsNeverRead")
|
||||||
|
|
||||||
package eu.gaudian.translator.view.dialogs
|
package eu.gaudian.translator.view.dialogs
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.width
|
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.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -20,16 +16,13 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.model.VocabularyStage
|
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.AppCheckbox
|
||||||
|
import eu.gaudian.translator.view.composable.AppDropdownContainer
|
||||||
import eu.gaudian.translator.view.composable.AppDropdownMenuItem
|
import eu.gaudian.translator.view.composable.AppDropdownMenuItem
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
|
||||||
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -44,46 +37,38 @@ fun VocabularyStageDropDown(
|
|||||||
var selectedStages by remember { mutableStateOf(preselectedStages) }
|
var selectedStages by remember { mutableStateOf(preselectedStages) }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
Box(
|
val buttonText = when {
|
||||||
modifier = modifier,
|
|
||||||
contentAlignment = Alignment.CenterEnd
|
|
||||||
) {
|
|
||||||
AppOutlinedButton(
|
|
||||||
shape = RoundedCornerShape(8.dp),
|
|
||||||
onClick = { expanded = true },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Text(text = when {
|
|
||||||
selectedStages.isEmpty() -> stringResource(R.string.label_select_stage)
|
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)
|
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,
|
expanded = expanded,
|
||||||
onDismissRequest = { expanded = false },
|
onDismissRequest = { expanded = false },
|
||||||
modifier = Modifier.fillMaxWidth()
|
onExpandRequest = { expanded = true },
|
||||||
|
buttonText = buttonText,
|
||||||
|
modifier = modifier,
|
||||||
|
showDoneButton = multipleSelectable,
|
||||||
|
onDoneClick = { expanded = false }
|
||||||
) {
|
) {
|
||||||
if (noneSelectable == true) {
|
if (noneSelectable == true) {
|
||||||
val noneSelected = selectedStages.contains(null)
|
val noneSelected = selectedStages.contains(null)
|
||||||
AppDropdownMenuItem(
|
AppDropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
if (multipleSelectable) {
|
if (multipleSelectable) {
|
||||||
AppCheckbox(
|
AppCheckbox(
|
||||||
checked = noneSelected,
|
checked = noneSelected,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
selectedStages = if (noneSelected) selectedStages.filterNotNull() else selectedStages + listOf(null)
|
selectedStages = if (noneSelected) {
|
||||||
|
selectedStages.filterNotNull()
|
||||||
|
} else {
|
||||||
|
selectedStages + listOf(null)
|
||||||
|
}
|
||||||
onStageSelected(selectedStages)
|
onStageSelected(selectedStages)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -113,12 +98,19 @@ fun VocabularyStageDropDown(
|
|||||||
val isSelected = selectedStages.contains(stage)
|
val isSelected = selectedStages.contains(stage)
|
||||||
AppDropdownMenuItem(
|
AppDropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
if (multipleSelectable) {
|
if (multipleSelectable) {
|
||||||
AppCheckbox(
|
AppCheckbox(
|
||||||
checked = isSelected,
|
checked = isSelected,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
selectedStages = if (isSelected) selectedStages - stage else selectedStages + stage
|
selectedStages = if (isSelected) {
|
||||||
|
selectedStages - stage
|
||||||
|
} else {
|
||||||
|
selectedStages + stage
|
||||||
|
}
|
||||||
onStageSelected(selectedStages)
|
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.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
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.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
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.AppOutlinedButton
|
||||||
import eu.gaudian.translator.view.composable.AppSwitch
|
import eu.gaudian.translator.view.composable.AppSwitch
|
||||||
import eu.gaudian.translator.view.composable.DictionaryLanguageDropDown
|
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.CorrectionViewModel
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -415,6 +417,7 @@ fun CorrectionScreenContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ToneDropdown(
|
private fun ToneDropdown(
|
||||||
selectedTone: CorrectionViewModel.Tone,
|
selectedTone: CorrectionViewModel.Tone,
|
||||||
@@ -447,20 +450,33 @@ private fun ToneDropdown(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DropdownMenu(
|
if (expanded) {
|
||||||
expanded = expanded,
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
|
||||||
|
ModalBottomSheet(
|
||||||
onDismissRequest = { expanded = false },
|
onDismissRequest = { expanded = false },
|
||||||
modifier = Modifier.background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp))
|
sheetState = sheetState,
|
||||||
|
containerColor = DropdownDefaults.containerColor()
|
||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
Column(
|
||||||
text = { Text(text = stringResource(R.string.text_none)) },
|
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 = {
|
onClick = {
|
||||||
onToneSelected(CorrectionViewModel.Tone.NONE)
|
onToneSelected(CorrectionViewModel.Tone.NONE)
|
||||||
expanded = false
|
expanded = false
|
||||||
},
|
}
|
||||||
enabled = enabled
|
|
||||||
)
|
)
|
||||||
HorizontalDivider()
|
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||||
|
|
||||||
val options = listOf(
|
val options = listOf(
|
||||||
CorrectionViewModel.Tone.FORMAL,
|
CorrectionViewModel.Tone.FORMAL,
|
||||||
CorrectionViewModel.Tone.CASUAL,
|
CorrectionViewModel.Tone.CASUAL,
|
||||||
@@ -471,16 +487,23 @@ private fun ToneDropdown(
|
|||||||
CorrectionViewModel.Tone.ACADEMIC,
|
CorrectionViewModel.Tone.ACADEMIC,
|
||||||
CorrectionViewModel.Tone.CREATIVE
|
CorrectionViewModel.Tone.CREATIVE
|
||||||
)
|
)
|
||||||
|
|
||||||
options.forEach { tone ->
|
options.forEach { tone ->
|
||||||
DropdownMenuItem(
|
// Replaced with LargeDropdownMenuItem
|
||||||
text = { Text(text = labelFor(tone)) },
|
LargeDropdownMenuItem(
|
||||||
|
text = labelFor(tone),
|
||||||
|
selected = selectedTone == tone,
|
||||||
|
enabled = enabled,
|
||||||
onClick = {
|
onClick = {
|
||||||
onToneSelected(tone)
|
onToneSelected(tone)
|
||||||
expanded = false
|
expanded = false
|
||||||
},
|
}
|
||||||
enabled = enabled
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.navigationBarsPadding().height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
package eu.gaudian.translator.view.hints
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import eu.gaudian.translator.R
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrated AddModelScanHint using markdown-based format.
|
|
||||||
* Content is loaded from localized assets/hints based on device locale.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun getAddModelScanHint(): Hint {
|
|
||||||
return Hint(
|
|
||||||
titleRes = R.string.hint_scan_hint_title,
|
|
||||||
elements = listOf(
|
|
||||||
HintElement.LocalizedMarkdown("example_hint")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun AddModelScanHintPreview() {
|
|
||||||
getAddModelScanHint().Render()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun AddModelScanHint() {
|
|
||||||
getAddModelScanHint().Render()
|
|
||||||
}
|
|
||||||
@@ -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,33 +0,0 @@
|
|||||||
package eu.gaudian.translator.view.hints
|
|
||||||
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import eu.gaudian.translator.R
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrated API Key hint using markdown-based format.
|
|
||||||
* Content is loaded from localized assets/hints based on device locale.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun getApiKeyHint() = Hint (
|
|
||||||
titleRes = R.string.hint_how_to_connect_to_an_ai,
|
|
||||||
elements = listOf(
|
|
||||||
HintElement.LocalizedMarkdown("api_key_hint")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ApiKeyHint() {
|
|
||||||
getApiKeyHint().Render()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun ApiKeyHintPreview() {
|
|
||||||
MaterialTheme {
|
|
||||||
ApiKeyHint()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package eu.gaudian.translator.view.hints
|
|
||||||
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import eu.gaudian.translator.R
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrated Category hint using markdown-based format.
|
|
||||||
* Content is loaded from localized assets/hints based on device locale.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun getCategoryHint(): Hint {
|
|
||||||
return Hint(
|
|
||||||
titleRes = R.string.category_hint_intro,
|
|
||||||
elements = listOf(
|
|
||||||
HintElement.LocalizedMarkdown("category_hint")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun CategoryHint() {
|
|
||||||
getCategoryHint().Render()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun CategoryHintPreview() {
|
|
||||||
MaterialTheme {
|
|
||||||
CategoryHint()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package eu.gaudian.translator.view.hints
|
|
||||||
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import eu.gaudian.translator.R
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrated Category Hint Screen using markdown-based format.
|
|
||||||
* Content is loaded from localized assets/hints based on device locale.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun getCategoryHintScreen(): Hint {
|
|
||||||
return Hint(
|
|
||||||
titleRes = R.string.category_hint_intro,
|
|
||||||
elements = listOf(
|
|
||||||
HintElement.LocalizedMarkdown("category_hint")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun CategoryHintScreen() {
|
|
||||||
getCategoryHintScreen().Render()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun CategoryHintScreenPreview() {
|
|
||||||
MaterialTheme {
|
|
||||||
CategoryHintScreen()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package eu.gaudian.translator.view.hints
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import eu.gaudian.translator.R
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrated DictionaryOptionsHint using markdown-based format.
|
|
||||||
* Content is loaded from localized assets/hints based on device locale.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun getDictionaryOptionsHint(): Hint {
|
|
||||||
return Hint(
|
|
||||||
titleRes = R.string.label_dictionary_options,
|
|
||||||
elements = listOf(
|
|
||||||
HintElement.LocalizedMarkdown("dictionary_hint")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun DictionaryOptionsHint() {
|
|
||||||
getDictionaryOptionsHint().Render()
|
|
||||||
}
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
package eu.gaudian.translator.view.hints
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import eu.gaudian.translator.R
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Example of a migrated hint using the new markdown-based approach.
|
|
||||||
*
|
|
||||||
* This demonstrates how to migrate from the old LegacyHint format to the new
|
|
||||||
* markdown-based format.
|
|
||||||
*
|
|
||||||
* Benefits:
|
|
||||||
* - Easier to manage and translate (no code changes needed)
|
|
||||||
* - Better separation of concerns
|
|
||||||
* - Consistent styling across all hints
|
|
||||||
* - Support for rich formatting (tables, code blocks, etc.)
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Example 1: Loading markdown content from string (simple).
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun getApiKeyMarkdownHint(): String {
|
|
||||||
return """
|
|
||||||
# How to Connect to an AI Model
|
|
||||||
|
|
||||||
This guide explains how to connect your app to an AI model using an API key.
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
To use AI models in your app, you need to provide a valid API key.
|
|
||||||
|
|
||||||
> **Note:** Keep your API key secure and never share it publicly.
|
|
||||||
|
|
||||||
## Key Status Indicators
|
|
||||||
|
|
||||||
| Status | Icon | Meaning |
|
|
||||||
|--------|------|---------|
|
|
||||||
| Active | ✅ | Key is valid and working |
|
|
||||||
| Missing | ⚠️ | Key is not set |
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
1. Verify the key is correct
|
|
||||||
2. Ensure proper permissions
|
|
||||||
3. Check your quota
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"api_key": "sk-xxxxxxxxxxxx"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ApiKeyMarkdownHint() {
|
|
||||||
val content = getApiKeyMarkdownHint()
|
|
||||||
MarkdownHint(
|
|
||||||
markdownContent = content,
|
|
||||||
title = stringResource(R.string.hint_how_to_connect_to_an_ai)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Example 2: Loading markdown from assets file (recommended for production).
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun loadMarkdownFromAssets(fileName: String): String {
|
|
||||||
val context = LocalContext.current
|
|
||||||
return try {
|
|
||||||
context.assets.open("hints/$fileName").bufferedReader().use { it.readText() }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
"Error loading markdown file: ${e.message}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data class for programmatic hint loading.
|
|
||||||
*/
|
|
||||||
data class MarkdownHintDefinition(
|
|
||||||
val fileName: String,
|
|
||||||
val titleRes: Int
|
|
||||||
) {
|
|
||||||
fun loadContent(context: Context): String {
|
|
||||||
return context.assets.open("hints/$fileName").bufferedReader().use { it.readText() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pre-defined hints ready for migration.
|
|
||||||
*/
|
|
||||||
object MarkdownHints {
|
|
||||||
val API_KEY = MarkdownHintDefinition(
|
|
||||||
fileName = "api_key_hint.md",
|
|
||||||
titleRes = R.string.hint_how_to_connect_to_an_ai
|
|
||||||
)
|
|
||||||
val CATEGORY = MarkdownHintDefinition(
|
|
||||||
fileName = "category_hint.md",
|
|
||||||
titleRes = R.string.category_hint_intro
|
|
||||||
)
|
|
||||||
val LEARNING_STAGES = MarkdownHintDefinition(
|
|
||||||
fileName = "learning_stages_hint.md",
|
|
||||||
titleRes = R.string.learning_stages_title
|
|
||||||
)
|
|
||||||
val SORTING = MarkdownHintDefinition(
|
|
||||||
fileName = "sorting_hint.md",
|
|
||||||
titleRes = R.string.sorting_hint_title
|
|
||||||
)
|
|
||||||
val VOCABULARY_PROGRESS = MarkdownHintDefinition(
|
|
||||||
fileName = "vocabulary_progress_hint.md",
|
|
||||||
titleRes = R.string.hint_vocabulary_progress_hint_title
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preview for the migrated API Key hint.
|
|
||||||
*/
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun ApiKeyMarkdownHintPreview() {
|
|
||||||
MaterialTheme {
|
|
||||||
ApiKeyMarkdownHint()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preview for loading from assets.
|
|
||||||
*/
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun LoadFromAssetsPreview() {
|
|
||||||
MaterialTheme {
|
|
||||||
val content = loadMarkdownFromAssets("example_hint.md")
|
|
||||||
MarkdownHint(
|
|
||||||
markdownContent = content,
|
|
||||||
title = stringResource(R.string.hint_title_hints_overview)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package eu.gaudian.translator.view.hints
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This file is kept for reference only.
|
|
||||||
* All hints are now migrated to markdown-based format.
|
|
||||||
* See individual hint files for implementations.
|
|
||||||
*/
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("HardCodedStringLiteral")
|
||||||
|
|
||||||
package eu.gaudian.translator.view.hints
|
package eu.gaudian.translator.view.hints
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -9,9 +11,9 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.jeziellago.compose.markdowntext.MarkdownText
|
import dev.jeziellago.compose.markdowntext.MarkdownText
|
||||||
|
import eu.gaudian.translator.utils.Log
|
||||||
|
|
||||||
private const val TAG = "MarkdownHint"
|
private const val TAG = "MarkdownHint"
|
||||||
|
|
||||||
@@ -25,7 +27,7 @@ sealed class HintElement {
|
|||||||
data class UIElement(val composable: @Composable () -> Unit) : HintElement()
|
data class UIElement(val composable: @Composable () -> Unit) : HintElement()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A localized markdown file element.
|
* A localized Markdown file element.
|
||||||
* The file is loaded from assets based on the current device locale.
|
* The file is loaded from assets based on the current device locale.
|
||||||
* Follows Android's locale-qualified resource pattern:
|
* Follows Android's locale-qualified resource pattern:
|
||||||
* - assets/hints/ - Default (English)
|
* - assets/hints/ - Default (English)
|
||||||
@@ -56,7 +58,7 @@ fun RenderHintElement(element: HintElement) {
|
|||||||
androidx.compose.foundation.layout.Row(
|
androidx.compose.foundation.layout.Row(
|
||||||
modifier = Modifier.padding(top = 4.dp)
|
modifier = Modifier.padding(top = 4.dp)
|
||||||
) {
|
) {
|
||||||
androidx.compose.material3.Text(
|
Text(
|
||||||
text = "[DEBUG: ${element.fileName}]",
|
text = "[DEBUG: ${element.fileName}]",
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.error
|
color = MaterialTheme.colorScheme.error
|
||||||
@@ -68,7 +70,7 @@ fun RenderHintElement(element: HintElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Composable to render localized markdown content.
|
* Composable to render localized Markdown content.
|
||||||
* Automatically loads the correct locale version based on device settings.
|
* Automatically loads the correct locale version based on device settings.
|
||||||
* Falls back to English default if localized version is not available.
|
* Falls back to English default if localized version is not available.
|
||||||
*/
|
*/
|
||||||
@@ -85,23 +87,23 @@ fun LocalizedMarkdownContent(
|
|||||||
// Try localized version (folder has suffix, filename doesn't)
|
// Try localized version (folder has suffix, filename doesn't)
|
||||||
val localizedPath = "hints$suffix/$fileName.md"
|
val localizedPath = "hints$suffix/$fileName.md"
|
||||||
|
|
||||||
android.util.Log.d(TAG, "Loading hint: $fileName")
|
Log.d(TAG, "Loading hint: $fileName")
|
||||||
android.util.Log.d(TAG, "Device locale: ${locale.language}_${locale.country}")
|
Log.d(TAG, "Device locale: ${locale.language}_${locale.country}")
|
||||||
android.util.Log.d(TAG, "Localized path: $localizedPath")
|
Log.d(TAG, "Localized path: $localizedPath")
|
||||||
|
|
||||||
val localized = MarkdownHintLoader.loadFromAssets(context, localizedPath)
|
val localized = MarkdownHintLoader.loadFromAssets(context, localizedPath)
|
||||||
if (localized != null) {
|
if (localized != null) {
|
||||||
android.util.Log.d(TAG, "Found localized version at: $localizedPath")
|
Log.d(TAG, "Found localized version at: $localizedPath")
|
||||||
localized
|
localized
|
||||||
} else {
|
} else {
|
||||||
// Fall back to English default in hints folder
|
// Fall back to English default in hints folder
|
||||||
val defaultPath = "hints/$fileName.md"
|
val defaultPath = "hints/$fileName.md"
|
||||||
android.util.Log.d(TAG, "Localized not found, trying default: $defaultPath")
|
Log.d(TAG, "Localized not found, trying default: $defaultPath")
|
||||||
val default = MarkdownHintLoader.loadFromAssets(context, defaultPath)
|
val default = MarkdownHintLoader.loadFromAssets(context, defaultPath)
|
||||||
if (default != null) {
|
if (default != null) {
|
||||||
android.util.Log.d(TAG, "Found default version at: $defaultPath")
|
Log.d(TAG, "Found default version at: $defaultPath")
|
||||||
} else {
|
} else {
|
||||||
android.util.Log.e(TAG, "No hint found for: $fileName (tried: $localizedPath, $defaultPath)")
|
Log.e(TAG, "No hint found for: $fileName (tried: $localizedPath, $defaultPath)")
|
||||||
}
|
}
|
||||||
default
|
default
|
||||||
}
|
}
|
||||||
@@ -125,22 +127,3 @@ fun LocalizedMarkdownContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun UIElementPreview() {
|
|
||||||
RenderHintElement(
|
|
||||||
HintElement.UIElement {
|
|
||||||
Text("Custom UI Element")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun LocalizedMarkdownElementPreview() {
|
|
||||||
RenderHintElement(
|
|
||||||
HintElement.LocalizedMarkdown("example_hint")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
package eu.gaudian.translator.view.hints
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import eu.gaudian.translator.R
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper for Category Hint Screen
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun CategoryHintScreenWrapper(navController: NavController) {
|
|
||||||
HintScreen(
|
|
||||||
navController = navController,
|
|
||||||
title = stringResource(R.string.category_hint_intro)
|
|
||||||
) {
|
|
||||||
CategoryHint()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dictionary Hint Screen
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun DictionaryHintScreen(navController: NavController) {
|
|
||||||
HintScreen(
|
|
||||||
navController = navController,
|
|
||||||
title = stringResource(R.string.label_dictionary_options)
|
|
||||||
) {
|
|
||||||
getDictionaryOptionsHint().Render()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import Hint Screen
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun ImportHintScreen(navController: NavController) {
|
|
||||||
HintScreen(
|
|
||||||
navController = navController,
|
|
||||||
title = stringResource(R.string.hint_how_to_generate_vocabulary_with_ai)
|
|
||||||
) {
|
|
||||||
getImportVocabularyHint()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sorting Hint Screen
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun SortingHintScreen(navController: NavController) {
|
|
||||||
HintScreen(
|
|
||||||
navController = navController,
|
|
||||||
title = stringResource(R.string.sorting_hint_title)
|
|
||||||
) {
|
|
||||||
SortingScreenHint()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stages Hint Screen
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun StagesHintScreen(navController: NavController) {
|
|
||||||
HintScreen(
|
|
||||||
navController = navController,
|
|
||||||
title = stringResource(R.string.learning_stages_title)
|
|
||||||
) {
|
|
||||||
LearningStagesHint()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translation Hint Screen
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun TranslationHintScreen(navController: NavController) {
|
|
||||||
HintScreen(
|
|
||||||
navController = navController,
|
|
||||||
title = stringResource(R.string.hint_translate_how_it_works)
|
|
||||||
) {
|
|
||||||
getTranslationScreenHint().Render()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scan Hint Screen
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun ScanHintScreen(navController: NavController) {
|
|
||||||
HintScreen(
|
|
||||||
navController = navController,
|
|
||||||
title = stringResource(R.string.hint_scan_hint_title)
|
|
||||||
) {
|
|
||||||
AddModelScanHint()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* API Hint Screen
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun ApiHintScreen(navController: NavController) {
|
|
||||||
HintScreen(
|
|
||||||
navController = navController,
|
|
||||||
title = stringResource(R.string.hint_how_to_connect_to_an_ai)
|
|
||||||
) {
|
|
||||||
ApiKeyHint()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vocabulary Progress Hint Screen
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun VocabularyProgressHintScreen(navController: NavController) {
|
|
||||||
HintScreen(
|
|
||||||
navController = navController,
|
|
||||||
title = stringResource(R.string.hint_vocabulary_progress_hint_title)
|
|
||||||
) {
|
|
||||||
VocabularyProgressHint()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,9 +18,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.compose.rememberNavController
|
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
|
||||||
import eu.gaudian.translator.view.LocalShowExperimentalFeatures
|
import eu.gaudian.translator.view.LocalShowExperimentalFeatures
|
||||||
import eu.gaudian.translator.view.composable.AppCard
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
import eu.gaudian.translator.view.composable.AppIcons
|
||||||
@@ -42,15 +40,16 @@ fun HintsOverviewScreen(
|
|||||||
val showExperimental = LocalShowExperimentalFeatures.current
|
val showExperimental = LocalShowExperimentalFeatures.current
|
||||||
|
|
||||||
// Get hints using the new function-based approach
|
// Get hints using the new function-based approach
|
||||||
val importHint = getImportVocabularyHint()
|
val importHint = HintDefinition.IMPORT.hint()
|
||||||
val addModelScanHint = getAddModelScanHint()
|
val addModelScanHint = HintDefinition.ADD_MODEL_SCAN.hint()
|
||||||
val dictionaryOptionsHint = getDictionaryOptionsHint()
|
val dictionaryOptionsHint = HintDefinition.DICTIONARY_OPTIONS.hint()
|
||||||
val translationScreenHint = getTranslationScreenHint()
|
val translationScreenHint = HintDefinition.TRANSLATION.hint()
|
||||||
val categoryHint = getCategoryHint()
|
val categoryHint = HintDefinition.CATEGORY.hint()
|
||||||
val learningStagesHint = getLearningStagesHint()
|
val learningStagesHint = HintDefinition.LEARNING_STAGES.hint()
|
||||||
val sortingScreenHint = getSortingScreenHint()
|
val sortingScreenHint = HintDefinition.SORTING.hint()
|
||||||
val vocabularyProgressHint = getVocabularyProgressHint()
|
val vocabularyProgressHint = HintDefinition.VOCABULARY_PROGRESS.hint()
|
||||||
val apiKeyHint = getApiKeyHint()
|
val apiKeyHint = HintDefinition.API_KEY.hint()
|
||||||
|
|
||||||
|
|
||||||
val hintGroups = remember(showExperimental, importHint, addModelScanHint, dictionaryOptionsHint, translationScreenHint) {
|
val hintGroups = remember(showExperimental, importHint, addModelScanHint, dictionaryOptionsHint, translationScreenHint) {
|
||||||
val allGroups = listOf(
|
val allGroups = listOf(
|
||||||
@@ -133,11 +132,7 @@ fun HintsOverviewScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ThemePreviews
|
|
||||||
@Composable
|
|
||||||
fun HintsOverviewScreenPreview() {
|
|
||||||
HintsOverviewScreen(navController = rememberNavController())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun HintHeader(
|
private fun HintHeader(
|
||||||
@@ -176,12 +171,4 @@ private fun HintListItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ThemePreviews
|
|
||||||
@Composable
|
|
||||||
fun HintListItemPreview() {
|
|
||||||
HintListItem(
|
|
||||||
title = stringResource(R.string.category_hint_intro),
|
|
||||||
icon = AppIcons.Category,
|
|
||||||
onClick = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
package eu.gaudian.translator.view.hints
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import eu.gaudian.translator.R
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrated ImportVocabularyHint using markdown-based format.
|
|
||||||
* Content is loaded from localized assets/hints based on device locale.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun getImportVocabularyHint(): Hint {
|
|
||||||
return Hint(
|
|
||||||
titleRes = R.string.hint_how_to_generate_vocabulary_with_ai,
|
|
||||||
elements = listOf(
|
|
||||||
HintElement.LocalizedMarkdown("import_hint")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun ImportVocabularyHint() {
|
|
||||||
getImportVocabularyHint().Render()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrated VocabularyReviewHint using markdown-based format.
|
|
||||||
* Content is loaded from localized assets/hints based on device locale.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun getVocabularyReviewHint(): Hint {
|
|
||||||
return Hint(
|
|
||||||
titleRes = R.string.review_intro,
|
|
||||||
elements = listOf(
|
|
||||||
HintElement.LocalizedMarkdown("review_hint")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun VocabularyReviewHint() {
|
|
||||||
getVocabularyReviewHint()
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package eu.gaudian.translator.view.hints
|
|
||||||
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import eu.gaudian.translator.R
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrated Learning Stages hint using markdown-based format.
|
|
||||||
* Content is loaded from localized assets/hints based on device locale.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun getLearningStagesHint(): Hint {
|
|
||||||
return Hint(
|
|
||||||
titleRes = R.string.learning_stages_title,
|
|
||||||
elements = listOf(
|
|
||||||
HintElement.LocalizedMarkdown("learning_stages_hint")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LearningStagesHint() {
|
|
||||||
getLearningStagesHint().Render()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun LearningStagesHintPreview() {
|
|
||||||
MaterialTheme {
|
|
||||||
LearningStagesHint()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
package eu.gaudian.translator.view.hints
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import dev.jeziellago.compose.markdowntext.MarkdownText
|
|
||||||
import eu.gaudian.translator.R
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Markdown-styled hint content using the jeziellago compose-markdown library.
|
|
||||||
* This provides beautiful, consistent rendering of markdown content with
|
|
||||||
* support for headings, lists, tables, code blocks, and more.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* - Create a .md file in assets/hints/ (e.g., "api_key_hint.md")
|
|
||||||
* - Load it using context.assets.open() or pass raw markdown string
|
|
||||||
* - Use MarkdownHint composable for styled rendering
|
|
||||||
*
|
|
||||||
* Supported markdown:
|
|
||||||
* - Headings (# ## ###)
|
|
||||||
* - Bold (**text**) and italic (*text*)
|
|
||||||
* - Lists (- item, 1. item)
|
|
||||||
* - Tables (| col | col |)
|
|
||||||
* - Code blocks (```code```)
|
|
||||||
* - Blockquotes (> quote)
|
|
||||||
* - Links ([text](url))
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun MarkdownHint(
|
|
||||||
markdownContent: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
title: String? = null
|
|
||||||
) {
|
|
||||||
val scrollState = rememberScrollState()
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.verticalScroll(scrollState)
|
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
|
||||||
// Optional title with icon header
|
|
||||||
title?.let {
|
|
||||||
HeaderWithIcon(
|
|
||||||
title = it,
|
|
||||||
subtitle = null
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render markdown
|
|
||||||
MarkdownText(
|
|
||||||
markdown = markdownContent,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preview for MarkdownHint with sample content.
|
|
||||||
*/
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun MarkdownHintPreview() {
|
|
||||||
val sampleMarkdown = """
|
|
||||||
# Welcome to the Hint System
|
|
||||||
|
|
||||||
This is a **markdown-based** hint that provides _beautiful_ and consistent styling.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **Rich text formatting** - bold, italic, strikethrough
|
|
||||||
- **Headings** - H1 through H6 levels
|
|
||||||
- **Lists** - ordered and unordered
|
|
||||||
- **Code blocks** - with syntax highlighting
|
|
||||||
- **Tables** - for structured data
|
|
||||||
- **Links** - styled clickable references
|
|
||||||
- **Blockquotes** - for highlighted content
|
|
||||||
|
|
||||||
## How to Use
|
|
||||||
|
|
||||||
1. Create a `.md` file in `assets/hints/`
|
|
||||||
2. Load it using `context.assets.open("hints/your_hint.md")`
|
|
||||||
3. Pass the content to `MarkdownHint()`
|
|
||||||
|
|
||||||
> **Tip:** You can also embed UI elements by combining markdown with HintElements!
|
|
||||||
|
|
||||||
## Code Example
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
val markdown = loadMarkdownFromAssets("hints/scan_hint.md")
|
|
||||||
MarkdownHint(
|
|
||||||
markdownContent = markdown,
|
|
||||||
title = "Scan Hint"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Table Example
|
|
||||||
|
|
||||||
| Feature | Status | Priority |
|
|
||||||
|---------|--------|----------|
|
|
||||||
| Headings | ✅ | High |
|
|
||||||
| Lists | ✅ | High |
|
|
||||||
| Tables | ✅ | Medium |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Last updated: 2024-01-15*
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
MaterialTheme {
|
|
||||||
MarkdownHint(
|
|
||||||
markdownContent = sampleMarkdown,
|
|
||||||
title = stringResource(R.string.hint_title_hints_overview)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data class for loading markdown content from assets.
|
|
||||||
*/
|
|
||||||
data class MarkdownHintData(
|
|
||||||
val fileName: String,
|
|
||||||
val titleRes: Int
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* Load and return the markdown content as a string.
|
|
||||||
*/
|
|
||||||
fun loadContent(androidContext: android.content.Context): String {
|
|
||||||
return androidContext.assets.open("hints/$fileName")
|
|
||||||
.bufferedReader()
|
|
||||||
.use { it.readText() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@ import android.content.Context
|
|||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internationalization system for markdown hints.
|
* Internationalization system for Markdown hints.
|
||||||
*
|
*
|
||||||
* This follows Android's locale-qualified resource pattern:
|
* This follows Android's locale-qualified resource pattern:
|
||||||
* - assets/hints/ - Default (English)
|
* - assets/hints/ - Default (English)
|
||||||
@@ -18,131 +18,22 @@ import java.util.Locale
|
|||||||
*/
|
*/
|
||||||
object MarkdownHintLoader {
|
object MarkdownHintLoader {
|
||||||
|
|
||||||
/**
|
|
||||||
* Load markdown content with automatic locale detection.
|
|
||||||
*
|
|
||||||
* @param context The context for accessing assets
|
|
||||||
* @param hintFileName The base filename without locale suffix (e.g., "api_key_hint")
|
|
||||||
* @return The markdown content as a string, or null if not found
|
|
||||||
*/
|
|
||||||
fun loadHint(context: Context, hintFileName: String): String? {
|
|
||||||
val locale = getCurrentLocale(context)
|
|
||||||
val suffix = getLocaleSuffix(locale)
|
|
||||||
|
|
||||||
// Try localized version (folder has suffix, filename doesn't)
|
|
||||||
val localizedPath = "hints$suffix/$hintFileName.md"
|
|
||||||
val localizedContent = loadFromAssets(context, localizedPath)
|
|
||||||
if (localizedContent != null) {
|
|
||||||
return localizedContent
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try with just language code (e.g., hints-pt/ instead of hints-pt-rBR/)
|
|
||||||
val languageSuffix = if (locale.country.isNotEmpty()) "-${locale.language}" else ""
|
|
||||||
val languageOnlyPath = "hints$languageSuffix/$hintFileName.md"
|
|
||||||
val languageContent = loadFromAssets(context, languageOnlyPath)
|
|
||||||
if (languageContent != null) {
|
|
||||||
return languageContent
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to default (English) in hints folder
|
|
||||||
val defaultPath = "hints/$hintFileName.md"
|
|
||||||
val defaultContent = loadFromAssets(context, defaultPath)
|
|
||||||
if (defaultContent != null) {
|
|
||||||
return defaultContent
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the localized file path for a hint.
|
|
||||||
*
|
|
||||||
* @param hintFileName The base filename (e.g., "api_key_hint")
|
|
||||||
* @return The full path including locale folder (e.g., "hints-de-rDE/api_key_hint.md")
|
|
||||||
*/
|
|
||||||
fun getHintPath(hintFileName: String): String {
|
|
||||||
val locale = Locale.getDefault()
|
|
||||||
val localeSuffix = getLocaleSuffix(locale)
|
|
||||||
return "hints$localeSuffix/$hintFileName.md"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the localized file name for a hint.
|
|
||||||
*
|
|
||||||
* @param hintFileName The base filename (e.g., "api_key_hint")
|
|
||||||
* @param locale The target locale
|
|
||||||
* @return The file name with locale suffix (e.g., "api_key_hint-de-rDE.md")
|
|
||||||
*/
|
|
||||||
fun getLocalizedFileName(hintFileName: String, locale: Locale): String {
|
|
||||||
val localeSuffix = getLocaleSuffix(locale)
|
|
||||||
return "$hintFileName$localeSuffix.md"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the file name with language-only suffix.
|
|
||||||
*
|
|
||||||
* @param hintFileName The base filename
|
|
||||||
* @param locale The target locale
|
|
||||||
* @return The file name with language suffix (e.g., "api_key_hint-de.md")
|
|
||||||
*/
|
|
||||||
private fun getLanguageOnlyFileName(hintFileName: String, locale: Locale): String {
|
|
||||||
val languageCode = locale.language
|
|
||||||
return "$hintFileName-$languageCode.md"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun getCurrentLocale(context: Context): Locale {
|
fun getCurrentLocale(context: Context): Locale {
|
||||||
return context.resources.configuration.locale
|
return context.resources.configuration.locale
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all supported locales for hints.
|
|
||||||
*/
|
|
||||||
fun getSupportedLocales(): List<Locale> {
|
|
||||||
return listOf(
|
|
||||||
Locale.ENGLISH, // Default
|
|
||||||
Locale.GERMAN, // de-rDE
|
|
||||||
Locale("pt", "BR"), // pt-rBR
|
|
||||||
Locale.FRENCH, // fr-rFR
|
|
||||||
Locale("es", "ES"), // es-rES
|
|
||||||
Locale.ITALIAN, // it-rIT
|
|
||||||
Locale("nl", "NL"), // nl-rNL
|
|
||||||
Locale("hr", "HR") // hr-rHR
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a localized version exists for the given locale.
|
|
||||||
*/
|
|
||||||
fun localizedVersionExists(context: Context, hintFileName: String, locale: Locale): Boolean {
|
|
||||||
val localizedFileName = getLocalizedFileName(hintFileName, locale)
|
|
||||||
val suffix = getLocaleSuffix(locale)
|
|
||||||
return assetExists(context, "hints$suffix/$localizedFileName")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load content from assets.
|
* Load content from assets.
|
||||||
*/
|
*/
|
||||||
fun loadFromAssets(context: Context, fileName: String): String? {
|
fun loadFromAssets(context: Context, fileName: String): String? {
|
||||||
return try {
|
return try {
|
||||||
context.assets.open(fileName).bufferedReader().use { it.readText() }
|
context.assets.open(fileName).bufferedReader().use { it.readText() }
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if an asset file exists.
|
|
||||||
*/
|
|
||||||
private fun assetExists(context: Context, path: String): Boolean {
|
|
||||||
return try {
|
|
||||||
context.assets.open(path).close()
|
|
||||||
true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the locale suffix string.
|
* Get the locale suffix string.
|
||||||
*/
|
*/
|
||||||
@@ -163,55 +54,3 @@ object MarkdownHintLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extension function to get localized hint content.
|
|
||||||
*/
|
|
||||||
fun Context.loadLocalizedHint(hintFileName: String): String? {
|
|
||||||
return MarkdownHintLoader.loadHint(this, hintFileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data class for localized hint information.
|
|
||||||
*/
|
|
||||||
data class LocalizedHint(
|
|
||||||
val fileName: String,
|
|
||||||
val locale: Locale,
|
|
||||||
val isDefault: Boolean = false
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hint localization manager that tracks available translations.
|
|
||||||
*/
|
|
||||||
object HintLocalizationManager {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all available translations for a hint.
|
|
||||||
*/
|
|
||||||
fun getAvailableTranslations(context: Context, baseFileName: String): List<LocalizedHint> {
|
|
||||||
val available = mutableListOf<LocalizedHint>()
|
|
||||||
val defaultLocale = MarkdownHintLoader.getCurrentLocale(context)
|
|
||||||
|
|
||||||
// Check each supported locale
|
|
||||||
MarkdownHintLoader.getSupportedLocales().forEach { locale ->
|
|
||||||
val fileName = MarkdownHintLoader.getLocalizedFileName(baseFileName, locale)
|
|
||||||
val path = "hints${MarkdownHintLoader.getLocaleSuffix(locale)}/$fileName"
|
|
||||||
|
|
||||||
if (MarkdownHintLoader.loadFromAssets(context, path) != null) {
|
|
||||||
available.add(LocalizedHint(
|
|
||||||
fileName = fileName,
|
|
||||||
locale = locale,
|
|
||||||
isDefault = locale.language == "en" && locale.country.isEmpty()
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return available.sortedBy { it.locale.language }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the best available translation for current locale.
|
|
||||||
*/
|
|
||||||
fun getBestTranslation(context: Context, baseFileName: String): String? {
|
|
||||||
return MarkdownHintLoader.loadHint(context, baseFileName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
package eu.gaudian.translator.view.hints
|
|
||||||
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import eu.gaudian.translator.R
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrated Sorting Screen hint using markdown-based format.
|
|
||||||
* Content is loaded from localized assets/hints based on device locale.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun getSortingScreenHint(): Hint {
|
|
||||||
return Hint(
|
|
||||||
titleRes = R.string.sorting_hint_title,
|
|
||||||
elements = listOf(
|
|
||||||
HintElement.LocalizedMarkdown("sorting_hint")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SortingScreenHint() {
|
|
||||||
getSortingScreenHint()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun SortingScreenHintPreview() {
|
|
||||||
MaterialTheme {
|
|
||||||
SortingScreenHint()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package eu.gaudian.translator.view.hints
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import eu.gaudian.translator.R
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrated TranslationScreenHint using markdown-based format.
|
|
||||||
* Content is loaded from localized assets/hints based on device locale.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun getTranslationScreenHint() = Hint(
|
|
||||||
titleRes = R.string.hint_translate_how_it_works,
|
|
||||||
elements = listOf(
|
|
||||||
HintElement.LocalizedMarkdown("translation_hint")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun TranslationScreenHint() {
|
|
||||||
getTranslationScreenHint().Render()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun TranslationScreenHintPreview() {
|
|
||||||
getTranslationScreenHint().Render()
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package eu.gaudian.translator.view.hints
|
|
||||||
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import eu.gaudian.translator.R
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrated Vocabulary Progress hint using markdown-based format.
|
|
||||||
* Content is loaded from localized assets/hints based on device locale.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun getVocabularyProgressHint(): Hint {
|
|
||||||
return Hint(
|
|
||||||
titleRes = R.string.hint_vocabulary_progress_hint_title,
|
|
||||||
elements = listOf(
|
|
||||||
HintElement.LocalizedMarkdown("vocabulary_progress_hint")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun VocabularyProgressHint() {
|
|
||||||
getVocabularyProgressHint().Render()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun VocabularyProgressHintPreview() {
|
|
||||||
MaterialTheme {
|
|
||||||
VocabularyProgressHint()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -44,6 +44,9 @@ import androidx.navigation.NavController
|
|||||||
import eu.gaudian.translator.BuildConfig
|
import eu.gaudian.translator.BuildConfig
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
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.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppCard
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
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.AppTopAppBar
|
||||||
import eu.gaudian.translator.view.composable.SecondaryButton
|
import eu.gaudian.translator.view.composable.SecondaryButton
|
||||||
import eu.gaudian.translator.viewmodel.SettingsViewModel
|
import eu.gaudian.translator.viewmodel.SettingsViewModel
|
||||||
import eu.gaudian.translator.viewmodel.StatusViewModel
|
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
@@ -371,7 +373,7 @@ private fun DeveloperOptions(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val activity = context.findActivity()
|
val activity = context.findActivity()
|
||||||
val statusViewModel: StatusViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
val statusMessageService = StatusMessageService
|
||||||
val settingsViewModel: SettingsViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
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(
|
SecondaryButton(
|
||||||
onClick = { statusViewModel.showLoadingMessage(loadingText) },
|
onClick = { statusMessageService.showMessageById(StatusMessageId.LOADING_GENERIC) },
|
||||||
text = stringResource(R.string.text_show_loading),
|
text = stringResource(R.string.text_show_loading),
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
SecondaryButton(
|
SecondaryButton(
|
||||||
onClick = { statusViewModel.cancelLoadingOperation() },
|
onClick = { statusMessageService.trigger(StatusAction.CancelLoadingOperation)},
|
||||||
text = stringResource(R.string.text_cancel_loading),
|
text = stringResource(R.string.text_cancel_loading),
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
SecondaryButton(
|
SecondaryButton(
|
||||||
onClick = { statusViewModel.showInfoMessage(infoText) },
|
onClick = { statusMessageService.showInfoById(StatusMessageId.TEST_INFO) },
|
||||||
text = stringResource(R.string.text_show_info_message),
|
text = stringResource(R.string.text_show_info_message),
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
SecondaryButton(
|
SecondaryButton(
|
||||||
onClick = { statusViewModel.showSuccessMessage(successText) },
|
onClick = { statusMessageService.showSuccessById(StatusMessageId.TEST_SUCCESS) },
|
||||||
text = stringResource(R.string.title_show_success_message),
|
text = stringResource(R.string.title_show_success_message),
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
SecondaryButton(
|
SecondaryButton(
|
||||||
onClick = { statusViewModel.showErrorMessage(errorText, 2) },
|
onClick = { statusMessageService.showErrorById(StatusMessageId.TEST_ERROR) },
|
||||||
text = stringResource(R.string.text_show_error_message),
|
text = stringResource(R.string.text_show_error_message),
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
SecondaryButton(
|
SecondaryButton(
|
||||||
onClick = { statusViewModel.showApiKeyMissingMessage() },
|
onClick = { statusMessageService.showErrorById(StatusMessageId.ERROR_API_KEY_MISSING) },
|
||||||
text = stringResource(R.string.show_api_key_missing_message),
|
text = stringResource(R.string.show_api_key_missing_message),
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -58,8 +58,7 @@ import eu.gaudian.translator.view.composable.AppScaffold
|
|||||||
import eu.gaudian.translator.view.composable.AppSwitch
|
import eu.gaudian.translator.view.composable.AppSwitch
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.view.composable.ModelBadges
|
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.view.hints.getAddModelScanHint
|
|
||||||
import eu.gaudian.translator.viewmodel.ApiViewModel
|
import eu.gaudian.translator.viewmodel.ApiViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -141,7 +140,7 @@ fun AddModelScreen(navController: NavController, providerKey: String) {
|
|||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hintContent = getAddModelScanHint()
|
hintContent = HintDefinition.ADD_MODEL_SCAN.hint()
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ import eu.gaudian.translator.view.composable.ClickableText
|
|||||||
import eu.gaudian.translator.view.composable.PrimaryButton
|
import eu.gaudian.translator.view.composable.PrimaryButton
|
||||||
import eu.gaudian.translator.view.composable.SecondaryButton
|
import eu.gaudian.translator.view.composable.SecondaryButton
|
||||||
import eu.gaudian.translator.view.composable.TabItem
|
import eu.gaudian.translator.view.composable.TabItem
|
||||||
import eu.gaudian.translator.view.hints.getApiKeyHint
|
import eu.gaudian.translator.view.hints.HintDefinition
|
||||||
import eu.gaudian.translator.viewmodel.ApiKeyManagementState
|
import eu.gaudian.translator.viewmodel.ApiKeyManagementState
|
||||||
import eu.gaudian.translator.viewmodel.ApiViewModel
|
import eu.gaudian.translator.viewmodel.ApiViewModel
|
||||||
import eu.gaudian.translator.viewmodel.ProviderState
|
import eu.gaudian.translator.viewmodel.ProviderState
|
||||||
@@ -121,7 +121,7 @@ fun ApiKeyScreen(navController: NavController) {
|
|||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hintContent = getApiKeyHint()
|
hintContent = HintDefinition.API_KEY.hint()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { 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.AppTopAppBar
|
||||||
import eu.gaudian.translator.view.composable.OptionItemSwitch
|
import eu.gaudian.translator.view.composable.OptionItemSwitch
|
||||||
import eu.gaudian.translator.view.dictionary.DictionaryManagerContent
|
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.ApiViewModel
|
||||||
import eu.gaudian.translator.viewmodel.DictionaryViewModel
|
import eu.gaudian.translator.viewmodel.DictionaryViewModel
|
||||||
import eu.gaudian.translator.viewmodel.SettingsViewModel
|
import eu.gaudian.translator.viewmodel.SettingsViewModel
|
||||||
@@ -72,7 +72,7 @@ fun DictionaryOptionsScreen(
|
|||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hintContent = getDictionaryOptionsHint()
|
hintContent = HintDefinition.DICTIONARY_OPTIONS.hint()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -70,15 +70,24 @@ fun LanguageOptionsScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
|
item {
|
||||||
AppCard {
|
AppCard {
|
||||||
Column(Modifier.padding(16.dp)) {
|
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(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -109,7 +118,10 @@ fun LanguageOptionsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
onClick = { showAddLanguageDialog = true },
|
onClick = { showAddLanguageDialog = true },
|
||||||
text = stringResource(R.string.text_add_custom_language),
|
text = stringResource(R.string.text_add_custom_language),
|
||||||
@@ -117,9 +129,9 @@ fun LanguageOptionsScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (showAddLanguageDialog) {
|
if (showAddLanguageDialog) {
|
||||||
@Suppress("KotlinConstantConditions")
|
|
||||||
AddCustomLanguageDialog(
|
AddCustomLanguageDialog(
|
||||||
showDialog = showAddLanguageDialog,
|
showDialog = showAddLanguageDialog,
|
||||||
onDismiss = { showAddLanguageDialog = false },
|
onDismiss = { showAddLanguageDialog = false },
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ fun MainSettingsScreen(
|
|||||||
Setting(R.string.settings_title_voice, AppIcons.TextToSpeech, SettingsRoutes.TTS_OPTIONS),
|
Setting(R.string.settings_title_voice, AppIcons.TextToSpeech, SettingsRoutes.TTS_OPTIONS),
|
||||||
Setting(R.string.label_logs, AppIcons.Log, SettingsRoutes.LOGS),
|
Setting(R.string.label_logs, AppIcons.Log, SettingsRoutes.LOGS),
|
||||||
Setting(R.string.label_languages, AppIcons.Language, SettingsRoutes.LANGUAGE_OPTIONS),
|
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(
|
R.string.settings_header_translator to listOf(
|
||||||
|
|||||||
@@ -7,16 +7,9 @@ import androidx.navigation.NavGraphBuilder
|
|||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.navigation
|
import androidx.navigation.navigation
|
||||||
import eu.gaudian.translator.view.composable.Screen
|
import eu.gaudian.translator.view.composable.Screen
|
||||||
import eu.gaudian.translator.view.hints.ApiHintScreen
|
import eu.gaudian.translator.view.hints.HintDefinition
|
||||||
import eu.gaudian.translator.view.hints.CategoryHintScreenWrapper
|
import eu.gaudian.translator.view.hints.HintScreen
|
||||||
import eu.gaudian.translator.view.hints.DictionaryHintScreen
|
|
||||||
import eu.gaudian.translator.view.hints.HintsOverviewScreen
|
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
|
// Defines the routes for the settings graph to avoid using raw strings
|
||||||
object SettingsRoutes {
|
object SettingsRoutes {
|
||||||
@@ -114,31 +107,31 @@ fun NavGraphBuilder.settingsGraph(navController: NavController) {
|
|||||||
HintsOverviewScreen(navController = navController)
|
HintsOverviewScreen(navController = navController)
|
||||||
}
|
}
|
||||||
composable(SettingsRoutes.HINTS_CATEGORIES) {
|
composable(SettingsRoutes.HINTS_CATEGORIES) {
|
||||||
CategoryHintScreenWrapper(navController = navController)
|
HintScreen(navController, HintDefinition.CATEGORY)
|
||||||
}
|
}
|
||||||
composable(SettingsRoutes.HINTS_DICTIONARY) {
|
composable(SettingsRoutes.HINTS_DICTIONARY) {
|
||||||
DictionaryHintScreen(navController = navController)
|
HintScreen(navController, HintDefinition.DICTIONARY_OPTIONS)
|
||||||
}
|
}
|
||||||
composable(SettingsRoutes.HINTS_IMPORT) {
|
composable(SettingsRoutes.HINTS_IMPORT) {
|
||||||
ImportHintScreen(navController = navController)
|
HintScreen(navController, HintDefinition.IMPORT)
|
||||||
}
|
}
|
||||||
composable(SettingsRoutes.HINTS_SORTING) {
|
composable(SettingsRoutes.HINTS_SORTING) {
|
||||||
SortingHintScreen(navController = navController)
|
HintScreen(navController, HintDefinition.SORTING)
|
||||||
}
|
}
|
||||||
composable(SettingsRoutes.HINTS_STAGES) {
|
composable(SettingsRoutes.HINTS_STAGES) {
|
||||||
StagesHintScreen(navController = navController)
|
HintScreen(navController, HintDefinition.LEARNING_STAGES)
|
||||||
}
|
}
|
||||||
composable(SettingsRoutes.HINTS_TRANSLATION) {
|
composable(SettingsRoutes.HINTS_TRANSLATION) {
|
||||||
TranslationHintScreen(navController = navController)
|
HintScreen(navController, HintDefinition.TRANSLATION)
|
||||||
}
|
}
|
||||||
composable(SettingsRoutes.HINTS_SCAN) {
|
composable(SettingsRoutes.HINTS_SCAN) {
|
||||||
ScanHintScreen(navController = navController)
|
HintScreen(navController, HintDefinition.ADD_MODEL_SCAN)
|
||||||
}
|
}
|
||||||
composable(SettingsRoutes.HINTS_API) {
|
composable(SettingsRoutes.HINTS_API) {
|
||||||
ApiHintScreen(navController = navController)
|
HintScreen(navController, HintDefinition.API_KEY)
|
||||||
}
|
}
|
||||||
composable(SettingsRoutes.HINTS_VOCABULARY_PROGRESS) {
|
composable(SettingsRoutes.HINTS_VOCABULARY_PROGRESS) {
|
||||||
VocabularyProgressHintScreen(navController = navController)
|
HintScreen(navController, HintDefinition.VOCABULARY_PROGRESS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,8 +49,7 @@ import eu.gaudian.translator.view.composable.AppIcons
|
|||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppSlider
|
import eu.gaudian.translator.view.composable.AppSlider
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
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.view.hints.getVocabularyProgressHint
|
|
||||||
import eu.gaudian.translator.viewmodel.SettingsViewModel
|
import eu.gaudian.translator.viewmodel.SettingsViewModel
|
||||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||||
import kotlin.math.exp
|
import kotlin.math.exp
|
||||||
@@ -86,7 +85,7 @@ fun VocabularyProgressOptionsScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Here is the new hint content
|
// Here is the new hint content
|
||||||
hintContent = getVocabularyProgressHint()
|
hintContent = HintDefinition.VOCABULARY_PROGRESS.hint()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.model.Language
|
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.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppButton
|
import eu.gaudian.translator.view.composable.AppButton
|
||||||
import eu.gaudian.translator.view.composable.AppCard
|
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.SecondaryButton
|
||||||
import eu.gaudian.translator.view.composable.SingleLanguageDropDown
|
import eu.gaudian.translator.view.composable.SingleLanguageDropDown
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||||
import eu.gaudian.translator.viewmodel.StatusViewModel
|
|
||||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -60,7 +61,7 @@ fun VocabularyRepositoryOptionsScreen(
|
|||||||
|
|
||||||
val activity = LocalContext.current.findActivity()
|
val activity = LocalContext.current.findActivity()
|
||||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
val statusViewModel: StatusViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
val statusMessageService = StatusMessageService
|
||||||
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -73,7 +74,7 @@ fun VocabularyRepositoryOptionsScreen(
|
|||||||
context.contentResolver.openInputStream(it)?.use { inputStream ->
|
context.contentResolver.openInputStream(it)?.use { inputStream ->
|
||||||
val jsonString = inputStream.bufferedReader().use { reader -> reader.readText() }
|
val jsonString = inputStream.bufferedReader().use { reader -> reader.readText() }
|
||||||
vocabularyViewModel.importVocabulary(jsonString)
|
vocabularyViewModel.importVocabulary(jsonString)
|
||||||
statusViewModel.showInfoMessage(repositoryStateImportedFrom + " " +it.path)
|
statusMessageService.showInfoMessage(repositoryStateImportedFrom + " " +it.path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,7 +146,7 @@ fun VocabularyRepositoryOptionsScreen(
|
|||||||
row.map { it.trim().trim('"') }
|
row.map { it.trim().trim('"') }
|
||||||
}.filter { r -> r.any { it.isNotBlank() } }
|
}.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 errorParsingTable = stringResource(R.string.error_parsing_table)
|
||||||
val errorParsingTableWithReason = stringResource(R.string.error_parsing_table_with_reason)
|
val errorParsingTableWithReason = stringResource(R.string.error_parsing_table_with_reason)
|
||||||
val importTableLauncher = rememberLauncherForActivityResult(
|
val importTableLauncher = rememberLauncherForActivityResult(
|
||||||
@@ -159,7 +160,7 @@ fun VocabularyRepositoryOptionsScreen(
|
|||||||
val mime = context.contentResolver.getType(u)
|
val mime = context.contentResolver.getType(u)
|
||||||
val isExcel = mime == "application/vnd.ms-excel" || mime == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
val isExcel = mime == "application/vnd.ms-excel" || mime == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||||
if (isExcel) {
|
if (isExcel) {
|
||||||
statusViewModel.showInfoMessage(textExcelNotSupportedUseCsv)
|
statusMessageService.showErrorById(StatusMessageId.ERROR_EXCEL_NOT_SUPPORTED)
|
||||||
return@let
|
return@let
|
||||||
}
|
}
|
||||||
context.contentResolver.openInputStream(u)?.use { inputStream ->
|
context.contentResolver.openInputStream(u)?.use { inputStream ->
|
||||||
@@ -173,12 +174,12 @@ fun VocabularyRepositoryOptionsScreen(
|
|||||||
parseError = null
|
parseError = null
|
||||||
} else {
|
} else {
|
||||||
parseError = errorParsingTable
|
parseError = errorParsingTable
|
||||||
statusViewModel.showErrorMessage(parseError!!)
|
statusMessageService.showErrorMessage(parseError!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
parseError = e.message
|
parseError = e.message
|
||||||
statusViewModel.showErrorMessage(
|
statusMessageService.showErrorMessage(
|
||||||
(errorParsingTableWithReason + " " + e.message)
|
(errorParsingTableWithReason + " " + e.message)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -394,13 +395,13 @@ fun VocabularyRepositoryOptionsScreen(
|
|||||||
val infoImportedItemsFrom = stringResource(R.string.info_imported_items_from)
|
val infoImportedItemsFrom = stringResource(R.string.info_imported_items_from)
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
if (selectedColFirst == selectedColSecond) {
|
if (selectedColFirst == selectedColSecond) {
|
||||||
statusViewModel.showErrorMessage(errorSelectTwoColumns)
|
statusMessageService.showErrorMessage(errorSelectTwoColumns)
|
||||||
return@TextButton
|
return@TextButton
|
||||||
}
|
}
|
||||||
val langA = selectedLangFirst
|
val langA = selectedLangFirst
|
||||||
val langB = selectedLangSecond
|
val langB = selectedLangSecond
|
||||||
if (langA == null || langB == null) {
|
if (langA == null || langB == null) {
|
||||||
statusViewModel.showErrorMessage(errorSelectLanguages)
|
statusMessageService.showErrorMessage(errorSelectLanguages)
|
||||||
return@TextButton
|
return@TextButton
|
||||||
}
|
}
|
||||||
val startIdx = if (skipHeader) 1 else 0
|
val startIdx = if (skipHeader) 1 else 0
|
||||||
@@ -416,11 +417,11 @@ fun VocabularyRepositoryOptionsScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (items.isEmpty()) {
|
if (items.isEmpty()) {
|
||||||
statusViewModel.showErrorMessage(errorNoRowsToImport)
|
statusMessageService.showErrorMessage(errorNoRowsToImport)
|
||||||
return@TextButton
|
return@TextButton
|
||||||
}
|
}
|
||||||
vocabularyViewModel.addVocabularyItems(items)
|
vocabularyViewModel.addVocabularyItems(items)
|
||||||
statusViewModel.showSuccessMessage(infoImportedItemsFrom + " " +items.size)
|
statusMessageService.showSuccessMessage(infoImportedItemsFrom + " " +items.size)
|
||||||
showTableImportDialog.value = false
|
showTableImportDialog.value = false
|
||||||
}) { Text(stringResource(R.string.label_import)) }
|
}) { 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.AppCard
|
||||||
import eu.gaudian.translator.view.composable.AppOutlinedCard
|
import eu.gaudian.translator.view.composable.AppOutlinedCard
|
||||||
import eu.gaudian.translator.view.dialogs.AddVocabularyDialog
|
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.view.settings.SettingsRoutes
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||||
import eu.gaudian.translator.viewmodel.SettingsViewModel
|
import eu.gaudian.translator.viewmodel.SettingsViewModel
|
||||||
@@ -167,7 +167,7 @@ private fun LoadedTranslationContent(
|
|||||||
TopBarActions(
|
TopBarActions(
|
||||||
languageViewModel = languageViewModel,
|
languageViewModel = languageViewModel,
|
||||||
onSettingsClick = onSettingsClick,
|
onSettingsClick = onSettingsClick,
|
||||||
hintContent = { TranslationScreenHint() }
|
hintContent = { HintDefinition.TRANSLATION.Render() }
|
||||||
)
|
)
|
||||||
|
|
||||||
AppCard(modifier = Modifier.padding(8.dp, end = 8.dp, bottom = 8.dp, top = 0.dp)) {
|
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")
|
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")
|
@Suppress("unused", "HardCodedStringLiteral", "UnusedVariable")
|
||||||
@Composable
|
@Composable
|
||||||
fun Dummy() {
|
fun Dummy() {
|
||||||
@@ -297,7 +295,18 @@ fun MainVocabularyScreen(
|
|||||||
VocabularyTab.entries.find { it.route == currentRoute } ?: VocabularyTab.Dashboard
|
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 =
|
val repoEmpty =
|
||||||
vocabularyViewModel.vocabularyItems.collectAsState(initial = emptyList()).value.isEmpty()
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -391,7 +390,6 @@ private fun ExerciseTypeSelector(
|
|||||||
onTypeSelected: (VocabularyExerciseType) -> Unit,
|
onTypeSelected: (VocabularyExerciseType) -> Unit,
|
||||||
) {
|
) {
|
||||||
// Using FlowRow for a more flexible layout that wraps to the next line if needed
|
// Using FlowRow for a more flexible layout that wraps to the next line if needed
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
|
||||||
FlowRow(
|
FlowRow(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
|
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ fun VocabularyCardHost(
|
|||||||
listOf(currentVocabularyItem),
|
listOf(currentVocabularyItem),
|
||||||
it.mapNotNull { category -> category?.id }
|
it.mapNotNull { category -> category?.id }
|
||||||
)
|
)
|
||||||
showCategoryDialog = false
|
//showCategoryDialog = false
|
||||||
},
|
},
|
||||||
onDismissRequest = { showCategoryDialog = false }
|
onDismissRequest = { showCategoryDialog = false }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -77,7 +77,8 @@ fun VocabularyExerciseHostScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
// Reset exercise state when starting fresh
|
||||||
|
exerciseViewModel.resetExercise()
|
||||||
|
|
||||||
vocabularyViewModel.prepareExercise(
|
vocabularyViewModel.prepareExercise(
|
||||||
categoryIdsAsJson,
|
categoryIdsAsJson,
|
||||||
@@ -249,7 +250,15 @@ private fun ExerciseScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(currentExerciseState, score, wrongAnswers) {
|
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)
|
onFinish(score, wrongAnswers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ private data class VocabularyFilterState(
|
|||||||
val searchQuery: String = "",
|
val searchQuery: String = "",
|
||||||
val selectedStage: VocabularyStage? = null,
|
val selectedStage: VocabularyStage? = null,
|
||||||
val sortOrder: SortOrder = SortOrder.NEWEST_FIRST,
|
val sortOrder: SortOrder = SortOrder.NEWEST_FIRST,
|
||||||
val categoryId: Int? = null,
|
val categoryIds: List<Int> = emptyList(),
|
||||||
val dueTodayOnly: Boolean = false,
|
val dueTodayOnly: Boolean = false,
|
||||||
val selectedLanguageIds: List<Int> = emptyList(),
|
val selectedLanguageIds: List<Int> = emptyList(),
|
||||||
val selectedWordClass: String? = null
|
val selectedWordClass: String? = null
|
||||||
@@ -133,7 +133,7 @@ fun VocabularyListScreen(
|
|||||||
var filterState by rememberSaveable {
|
var filterState by rememberSaveable {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
VocabularyFilterState(
|
VocabularyFilterState(
|
||||||
categoryId = categoryId,
|
categoryIds = categoryId?.let { listOf(it) } ?: emptyList(),
|
||||||
dueTodayOnly = showDueTodayOnly == true,
|
dueTodayOnly = showDueTodayOnly == true,
|
||||||
selectedStage = stage
|
selectedStage = stage
|
||||||
)
|
)
|
||||||
@@ -142,7 +142,7 @@ fun VocabularyListScreen(
|
|||||||
val isFilterActive by remember(filterState) {
|
val isFilterActive by remember(filterState) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
filterState.selectedStage != null ||
|
filterState.selectedStage != null ||
|
||||||
(filterState.categoryId != null && filterState.categoryId != 0) ||
|
filterState.categoryIds.isNotEmpty() ||
|
||||||
filterState.dueTodayOnly ||
|
filterState.dueTodayOnly ||
|
||||||
filterState.selectedLanguageIds.isNotEmpty() ||
|
filterState.selectedLanguageIds.isNotEmpty() ||
|
||||||
!filterState.selectedWordClass.isNullOrBlank()
|
!filterState.selectedWordClass.isNullOrBlank()
|
||||||
@@ -165,7 +165,7 @@ fun VocabularyListScreen(
|
|||||||
vocabularyViewModel.filterVocabularyItems(
|
vocabularyViewModel.filterVocabularyItems(
|
||||||
languages = filterState.selectedLanguageIds,
|
languages = filterState.selectedLanguageIds,
|
||||||
query = filterState.searchQuery.takeIf { it.isNotBlank() },
|
query = filterState.searchQuery.takeIf { it.isNotBlank() },
|
||||||
categoryId = filterState.categoryId,
|
categoryIds = filterState.categoryIds,
|
||||||
stage = filterState.selectedStage,
|
stage = filterState.selectedStage,
|
||||||
wordClass = filterState.selectedWordClass,
|
wordClass = filterState.selectedWordClass,
|
||||||
dueTodayOnly = filterState.dueTodayOnly,
|
dueTodayOnly = filterState.dueTodayOnly,
|
||||||
@@ -179,7 +179,7 @@ fun VocabularyListScreen(
|
|||||||
|
|
||||||
LaunchedEffect(categoryId, showDueTodayOnly, stage) {
|
LaunchedEffect(categoryId, showDueTodayOnly, stage) {
|
||||||
filterState = filterState.copy(
|
filterState = filterState.copy(
|
||||||
categoryId = categoryId,
|
categoryIds = categoryId?.let { listOf(it) } ?: emptyList(),
|
||||||
dueTodayOnly = showDueTodayOnly == true,
|
dueTodayOnly = showDueTodayOnly == true,
|
||||||
selectedStage = stage
|
selectedStage = stage
|
||||||
)
|
)
|
||||||
@@ -382,7 +382,8 @@ fun VocabularyListScreen(
|
|||||||
languageViewModel = languageViewModel,
|
languageViewModel = languageViewModel,
|
||||||
languagesPresent = allLanguages.filter { it.nameResId in languagesPresent },
|
languagesPresent = allLanguages.filter { it.nameResId in languagesPresent },
|
||||||
hideCategory = categoryId != null && categoryId != 0,
|
hideCategory = categoryId != null && categoryId != 0,
|
||||||
hideStage = stage != null
|
hideStage = stage != null,
|
||||||
|
categoryViewModel = categoryViewModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,7 +395,7 @@ fun VocabularyListScreen(
|
|||||||
selectedItems,
|
selectedItems,
|
||||||
it.mapNotNull { category -> category?.id }
|
it.mapNotNull { category -> category?.id }
|
||||||
)
|
)
|
||||||
showCategoryDialog = false
|
//showCategoryDialog = false
|
||||||
},
|
},
|
||||||
onDismissRequest = { showCategoryDialog = false }
|
onDismissRequest = { showCategoryDialog = false }
|
||||||
)
|
)
|
||||||
@@ -807,11 +808,12 @@ private fun FilterSortBottomSheet(
|
|||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onApplyFilters: (VocabularyFilterState) -> Unit,
|
onApplyFilters: (VocabularyFilterState) -> Unit,
|
||||||
hideCategory: Boolean = false,
|
hideCategory: Boolean = false,
|
||||||
hideStage: Boolean = false
|
hideStage: Boolean = false,
|
||||||
|
categoryViewModel: CategoryViewModel
|
||||||
) {
|
) {
|
||||||
var selectedStage by rememberSaveable { mutableStateOf(currentFilterState.selectedStage) }
|
var selectedStage by rememberSaveable { mutableStateOf(currentFilterState.selectedStage) }
|
||||||
var dueTodayOnly by rememberSaveable { mutableStateOf(currentFilterState.dueTodayOnly) }
|
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 selectedLanguageIds by rememberSaveable { mutableStateOf(currentFilterState.selectedLanguageIds) }
|
||||||
var selectedWordClass by rememberSaveable { mutableStateOf(currentFilterState.selectedWordClass) }
|
var selectedWordClass by rememberSaveable { mutableStateOf(currentFilterState.selectedWordClass) }
|
||||||
|
|
||||||
@@ -840,7 +842,7 @@ private fun FilterSortBottomSheet(
|
|||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
if (!hideStage) selectedStage = null
|
if (!hideStage) selectedStage = null
|
||||||
dueTodayOnly = false
|
dueTodayOnly = false
|
||||||
if (!hideCategory) selectedCategoryId = null
|
if (!hideCategory) selectedCategoryIds = emptyList()
|
||||||
selectedLanguageIds = emptyList()
|
selectedLanguageIds = emptyList()
|
||||||
selectedWordClass = null
|
selectedWordClass = null
|
||||||
}) {
|
}) {
|
||||||
@@ -853,7 +855,7 @@ private fun FilterSortBottomSheet(
|
|||||||
currentFilterState.copy(
|
currentFilterState.copy(
|
||||||
selectedStage = selectedStage,
|
selectedStage = selectedStage,
|
||||||
dueTodayOnly = dueTodayOnly,
|
dueTodayOnly = dueTodayOnly,
|
||||||
categoryId = selectedCategoryId,
|
categoryIds = selectedCategoryIds,
|
||||||
selectedLanguageIds = selectedLanguageIds,
|
selectedLanguageIds = selectedLanguageIds,
|
||||||
selectedWordClass = selectedWordClass
|
selectedWordClass = selectedWordClass
|
||||||
)
|
)
|
||||||
@@ -893,16 +895,18 @@ private fun FilterSortBottomSheet(
|
|||||||
Text(stringResource(R.string.label_category), style = MaterialTheme.typography.titleMedium)
|
Text(stringResource(R.string.label_category), style = MaterialTheme.typography.titleMedium)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
CategoryDropdown(
|
CategoryDropdown(
|
||||||
initialCategoryId = selectedCategoryId,
|
initialCategoryId = selectedCategoryIds.firstOrNull(),
|
||||||
onCategorySelected = { categories ->
|
onCategorySelected = { categories ->
|
||||||
selectedCategoryId = categories.firstOrNull()?.id
|
selectedCategoryIds = categories.mapNotNull { it?.id }
|
||||||
}
|
},
|
||||||
|
multipleSelectable = true,
|
||||||
|
noneSelectable = false
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hideStage) {
|
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(
|
FlowRow(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
@@ -955,6 +959,7 @@ private fun FilterSortBottomSheet(
|
|||||||
fun FilterSortBottomSheetPreview() {
|
fun FilterSortBottomSheetPreview() {
|
||||||
val activity = LocalContext.current.findActivity()
|
val activity = LocalContext.current.findActivity()
|
||||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
|
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
FilterSortBottomSheet(
|
FilterSortBottomSheet(
|
||||||
currentFilterState = VocabularyFilterState(),
|
currentFilterState = VocabularyFilterState(),
|
||||||
languageViewModel = languageViewModel,
|
languageViewModel = languageViewModel,
|
||||||
@@ -962,6 +967,7 @@ fun FilterSortBottomSheetPreview() {
|
|||||||
onDismiss = {},
|
onDismiss = {},
|
||||||
onApplyFilters = {},
|
onApplyFilters = {},
|
||||||
hideCategory = false,
|
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.composable.SingleLanguageDropDown
|
||||||
import eu.gaudian.translator.view.dialogs.CategoryDropdown
|
import eu.gaudian.translator.view.dialogs.CategoryDropdown
|
||||||
import eu.gaudian.translator.view.dialogs.CreateCategoryListDialog
|
import eu.gaudian.translator.view.dialogs.CreateCategoryListDialog
|
||||||
import eu.gaudian.translator.view.hints.getSortingScreenHint
|
import eu.gaudian.translator.view.hints.HintDefinition
|
||||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||||
import eu.gaudian.translator.viewmodel.LanguageConfigViewModel
|
import eu.gaudian.translator.viewmodel.LanguageConfigViewModel
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||||
@@ -236,7 +236,7 @@ fun VocabularySortingScreen(
|
|||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hintContent = getSortingScreenHint()
|
hintContent = HintDefinition.SORTING.hint()
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -299,6 +299,7 @@ fun VocabularySortingItem(
|
|||||||
val activity = LocalContext.current.findActivity()
|
val activity = LocalContext.current.findActivity()
|
||||||
val languageConfigViewModel: LanguageConfigViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
val languageConfigViewModel: LanguageConfigViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
|
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
var wordFirst by remember { mutableStateOf(item.wordFirst) }
|
var wordFirst by remember { mutableStateOf(item.wordFirst) }
|
||||||
var wordSecond by remember { mutableStateOf(item.wordSecond) }
|
var wordSecond by remember { mutableStateOf(item.wordSecond) }
|
||||||
var selectedCategories by remember { mutableStateOf<List<Int>>(emptyList()) }
|
var selectedCategories by remember { mutableStateOf<List<Int>>(emptyList()) }
|
||||||
@@ -313,6 +314,7 @@ fun VocabularySortingItem(
|
|||||||
var articlesLangSecond by remember { mutableStateOf(emptySet<String>()) }
|
var articlesLangSecond by remember { mutableStateOf(emptySet<String>()) }
|
||||||
|
|
||||||
var showDuplicateDialog by remember { mutableStateOf(false) }
|
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
|
// NEW: Calculate if the item is valid for the "Done" button in faulty mode
|
||||||
val isItemNowValid by remember(wordFirst, wordSecond, langFirst, langSecond) {
|
val isItemNowValid by remember(wordFirst, wordSecond, langFirst, langSecond) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import eu.gaudian.translator.utils.Log
|
import eu.gaudian.translator.utils.Log
|
||||||
import eu.gaudian.translator.utils.StatusAction
|
import eu.gaudian.translator.utils.StatusAction
|
||||||
|
import eu.gaudian.translator.utils.StatusMessageId
|
||||||
import eu.gaudian.translator.utils.StatusMessageService
|
import eu.gaudian.translator.utils.StatusMessageService
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -45,6 +46,16 @@ enum class MessageDisplayType(val priority: Int) {
|
|||||||
ACTIONABLE_ERROR(5)
|
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
|
@HiltViewModel
|
||||||
class StatusViewModel @Inject constructor(
|
class StatusViewModel @Inject constructor(
|
||||||
application: Application,
|
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) {
|
private fun handleAction(action: StatusAction) {
|
||||||
Log.d("StatusViewModel", "Received action: $action")
|
Log.d("StatusViewModel", "Received action: $action")
|
||||||
when (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.ShowMessage -> showMessageInternal(action.text, action.type, action.timeoutInSeconds)
|
||||||
is StatusAction.ShowActionableMessage -> showPermanentActionableMessageInternal(action.text, action.type, action.action)
|
is StatusAction.ShowActionableMessage -> showPermanentActionableMessageInternal(action.text, action.type, action.action)
|
||||||
is StatusAction.ShowPermanentMessage -> showPermanentMessageInternal(action.text, action.type)
|
is StatusAction.ShowPermanentMessage -> showPermanentMessageInternal(action.text, action.type)
|
||||||
@@ -78,16 +94,66 @@ class StatusViewModel @Inject constructor(
|
|||||||
is StatusAction.CancelPermanentMessage -> cancelPermanentMessageInternal()
|
is StatusAction.CancelPermanentMessage -> cancelPermanentMessageInternal()
|
||||||
is StatusAction.HideMessageBar -> hideMessageBarInternal()
|
is StatusAction.HideMessageBar -> hideMessageBarInternal()
|
||||||
is StatusAction.CancelAllMessages -> cancelAllMessagesInternal()
|
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(
|
* Resolves a StatusMessageId to its actual string text using Android string resources.
|
||||||
text = "API Key is missing or invalid.",
|
*/
|
||||||
type = MessageDisplayType.ACTIONABLE_ERROR,
|
private fun resolveMessageText(messageId: StatusMessageId): String {
|
||||||
action = MessageAction.NAVIGATE_TO_API_KEYS
|
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) {
|
private fun showPermanentActionableMessageInternal(message: String, type: MessageDisplayType, action: MessageAction) {
|
||||||
cancelAllOperations() // Clear any other messages or loaders.
|
cancelAllOperations() // Clear any other messages or loaders.
|
||||||
@@ -99,54 +165,6 @@ class StatusViewModel @Inject constructor(
|
|||||||
_status.value = StatusState.Message(messageIdCounter++, message, type, action = null)
|
_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) {
|
private fun performLoadingOperationInternal(block: suspend () -> Unit) {
|
||||||
cancelAllOperations()
|
cancelAllOperations()
|
||||||
_status.value = StatusState.Loading
|
_status.value = StatusState.Loading
|
||||||
@@ -159,7 +177,10 @@ class StatusViewModel @Inject constructor(
|
|||||||
Log.i("StatusViewModel", "Loading operation was cancelled.")
|
Log.i("StatusViewModel", "Loading operation was cancelled.")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("StatusViewModel", "Loading operation failed.", e)
|
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 {
|
} finally {
|
||||||
if (activeLoadingJob == this.coroutineContext[Job]) {
|
if (activeLoadingJob == this.coroutineContext[Job]) {
|
||||||
if (_status.value == StatusState.Loading) {
|
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) {
|
private fun showMessageInternal(message: String, type: MessageDisplayType, timeoutInSeconds: Int) {
|
||||||
val currentState = _status.value
|
val currentState = _status.value
|
||||||
val currentPriority = (currentState as? StatusState.Message)?.type?.priority ?: -1
|
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() {
|
private fun cancelAllOperations() {
|
||||||
messageQueue.clear()
|
messageQueue.clear()
|
||||||
messageDisplayJob?.cancel()
|
messageDisplayJob?.cancel()
|
||||||
@@ -236,7 +265,9 @@ class StatusViewModel @Inject constructor(
|
|||||||
_status.value = StatusState.Hidden
|
_status.value = StatusState.Hidden
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- REVISED LOGIC ---
|
/**
|
||||||
|
* Processes the next message in the queue.
|
||||||
|
*/
|
||||||
private fun processNextMessageInQueue(timeoutInSeconds: Int = 3) {
|
private fun processNextMessageInQueue(timeoutInSeconds: Int = 3) {
|
||||||
if (activeLoadingJob?.isActive == true) {
|
if (activeLoadingJob?.isActive == true) {
|
||||||
Log.d("StatusViewModel", "Full-screen loading is active, not processing message queue now.")
|
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.loadObjectList
|
||||||
import eu.gaudian.translator.model.repository.saveObjectList
|
import eu.gaudian.translator.model.repository.saveObjectList
|
||||||
import eu.gaudian.translator.utils.Log
|
import eu.gaudian.translator.utils.Log
|
||||||
|
import eu.gaudian.translator.utils.StatusMessageService
|
||||||
import eu.gaudian.translator.utils.TextToSpeechHelper
|
import eu.gaudian.translator.utils.TextToSpeechHelper
|
||||||
import eu.gaudian.translator.utils.TranslationService
|
import eu.gaudian.translator.utils.TranslationService
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -31,6 +32,9 @@ class TranslationViewModel @Inject constructor(
|
|||||||
val languageRepository: LanguageRepository
|
val languageRepository: LanguageRepository
|
||||||
) : AndroidViewModel(application) {
|
) : AndroidViewModel(application) {
|
||||||
|
|
||||||
|
private val statusMessageService = StatusMessageService
|
||||||
|
|
||||||
|
|
||||||
// For back/forward navigation of history in the UI (like editors)
|
// For back/forward navigation of history in the UI (like editors)
|
||||||
private val _historyCursor = MutableStateFlow(-1)
|
private val _historyCursor = MutableStateFlow(-1)
|
||||||
|
|
||||||
@@ -112,11 +116,13 @@ class TranslationViewModel @Inject constructor(
|
|||||||
fun translateSentence(sentence: String) {
|
fun translateSentence(sentence: String) {
|
||||||
val sentenceToTranslate = sentence.ifEmpty { _inputText.value }
|
val sentenceToTranslate = sentence.ifEmpty { _inputText.value }
|
||||||
if (sentenceToTranslate.isBlank()) {
|
if (sentenceToTranslate.isBlank()) {
|
||||||
|
statusMessageService.showSimpleMessage("Please enter a sentence to translate.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedTranslationModel.value == null) {
|
if (selectedTranslationModel.value == null) {
|
||||||
Log.e("TranslationViewModel", "Cannot translate because no model is selected.")
|
Log.e("TranslationViewModel", "Cannot translate because no model is selected.")
|
||||||
|
statusMessageService.showSimpleMessage("Cannot translate because no model is selected.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +157,7 @@ class TranslationViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
.onFailure { exception ->
|
.onFailure { exception ->
|
||||||
Log.e("TranslationViewModel", "Translation failed: ${exception.message}")
|
Log.e("TranslationViewModel", "Translation failed: ${exception.message}")
|
||||||
|
statusMessageService.showErrorMessage("Translation failed: ${exception.message}")
|
||||||
}
|
}
|
||||||
|
|
||||||
_isTranslating.value = false
|
_isTranslating.value = false
|
||||||
|
|||||||
@@ -240,38 +240,12 @@ class VocabularyExerciseViewModel @Inject constructor(
|
|||||||
else -> if (shuffleLanguages) Random.nextBoolean() else false
|
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")
|
@Suppress("HardCodedStringLiteral")
|
||||||
Log.d("ExerciseDebug", "Item: ${itemToUse.wordFirst} (${itemToUse.languageFirstId}) / ${itemToUse.wordSecond} (${itemToUse.languageSecondId}), Switched: $isSwitched")
|
Log.d("ExerciseDebug", "Item: ${itemToUse.wordFirst} (${itemToUse.languageFirstId}) / ${itemToUse.wordSecond} (${itemToUse.languageSecondId}), Switched: $isSwitched")
|
||||||
@Suppress("HardCodedStringLiteral")
|
@Suppress("HardCodedStringLiteral")
|
||||||
Log.d("ExerciseDebug", "Origin Lang: ${config.originLanguageId}, Target Lang: ${config.targetLanguageId}")
|
Log.d("ExerciseDebug", "Origin Lang: ${config.originLanguageId}, Target Lang: ${config.targetLanguageId}")
|
||||||
|
|
||||||
|
// Set the exercise state based on the random type
|
||||||
_exerciseState.value = when (randomType) {
|
_exerciseState.value = when (randomType) {
|
||||||
VocabularyExerciseType.GUESSING -> VocabularyExerciseState.Guessing(
|
VocabularyExerciseType.GUESSING -> VocabularyExerciseState.Guessing(
|
||||||
item = itemToUse,
|
item = itemToUse,
|
||||||
@@ -344,18 +318,16 @@ class VocabularyExerciseViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkAnswer(answer: Any) {
|
private suspend fun checkAnswer(answer: Any) {
|
||||||
viewModelScope.launch {
|
val state = _exerciseState.value ?: return
|
||||||
val state = _exerciseState.value ?: return@launch
|
|
||||||
val correctAnswer = if (state.isSwitched) state.item.wordFirst else state.item.wordSecond
|
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) {
|
val isCorrect = when (state) {
|
||||||
is VocabularyExerciseState.Spelling -> {
|
is VocabularyExerciseState.Spelling -> {
|
||||||
val userAnswer = (answer as String).trim()
|
val userAnswer = (answer as String).trim()
|
||||||
val languageId = if (state.isSwitched) state.item.languageFirstId else state.item.languageSecondId
|
val languageId = if (state.isSwitched) state.item.languageFirstId else state.item.languageSecondId
|
||||||
val language = languageRepository.getLanguageById(languageId ?: 0)
|
val language = languageRepository.getLanguageById(languageId ?: 0)
|
||||||
?: return@launch
|
?: return
|
||||||
|
|
||||||
// Get articles for the language
|
// Get articles for the language
|
||||||
val articles = languageConfigRepository.getArticlesForLanguage(language.code)
|
val articles = languageConfigRepository.getArticlesForLanguage(language.code)
|
||||||
@@ -405,7 +377,6 @@ class VocabularyExerciseViewModel @Inject constructor(
|
|||||||
is VocabularyExerciseState.WordJumble -> state.copy(isCorrect = isCorrect, isRevealed = true)
|
is VocabularyExerciseState.WordJumble -> state.copy(isCorrect = isCorrect, isRevealed = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateJumbledWord(assembledWord: List<Pair<Char, Int>>) {
|
private fun updateJumbledWord(assembledWord: List<Pair<Char, Int>>) {
|
||||||
_exerciseState.value = when (val state = _exerciseState.value) {
|
_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.model.repository.VocabularyRepository
|
||||||
import eu.gaudian.translator.utils.Log
|
import eu.gaudian.translator.utils.Log
|
||||||
import eu.gaudian.translator.utils.StatusAction
|
import eu.gaudian.translator.utils.StatusAction
|
||||||
|
import eu.gaudian.translator.utils.StatusMessageId
|
||||||
import eu.gaudian.translator.utils.StatusMessageService
|
import eu.gaudian.translator.utils.StatusMessageService
|
||||||
import eu.gaudian.translator.utils.StringHelper
|
import eu.gaudian.translator.utils.StringHelper
|
||||||
import eu.gaudian.translator.utils.VocabularyService
|
import eu.gaudian.translator.utils.VocabularyService
|
||||||
@@ -774,7 +775,7 @@ class VocabularyViewModel @Inject constructor(
|
|||||||
statusService.hideMessageBar()
|
statusService.hideMessageBar()
|
||||||
if (_cardSet.value == null) {
|
if (_cardSet.value == null) {
|
||||||
statusService.cancelAllMessages()
|
statusService.cancelAllMessages()
|
||||||
statusService.showErrorMessage("No cards found for the specified filter", 3)
|
statusService.showErrorById(StatusMessageId.ERROR_NO_CARDS_FOUND)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
<item>cz,CZ,49</item>
|
<item>cz,CZ,49</item>
|
||||||
<item>he,IL,50</item>
|
<item>he,IL,50</item>
|
||||||
<item>hr,HR,51</item>
|
<item>hr,HR,51</item>
|
||||||
|
<item>fil,PH,52</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string name="language_1">Englisch</string>
|
<string name="language_1">Englisch</string>
|
||||||
@@ -105,6 +106,7 @@
|
|||||||
<string name="language_49">Tschechisch</string>
|
<string name="language_49">Tschechisch</string>
|
||||||
<string name="language_50">Hebräisch</string>
|
<string name="language_50">Hebräisch</string>
|
||||||
<string name="language_51">Kroatisch</string>
|
<string name="language_51">Kroatisch</string>
|
||||||
|
<string name="language_52">Filipino</string>
|
||||||
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -28,7 +28,6 @@
|
|||||||
<string name="cd_re_generate_definition">Definition neu erstellen</string>
|
<string name="cd_re_generate_definition">Definition neu erstellen</string>
|
||||||
<string name="cd_clear_search">Suche löschen</string>
|
<string name="cd_clear_search">Suche löschen</string>
|
||||||
<string name="cd_translation_history">Übersetzungsverlauf</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="label_reload">Neu laden</string>
|
||||||
<string name="title_single">Einzeln</string>
|
<string name="title_single">Einzeln</string>
|
||||||
<string name="title_widget_streak">Streak</string>
|
<string name="title_widget_streak">Streak</string>
|
||||||
@@ -42,7 +41,7 @@
|
|||||||
<string name="title_multiple">Mehrere</string>
|
<string name="title_multiple">Mehrere</string>
|
||||||
<string name="label_translation_settings">Übersetzung</string>
|
<string name="label_translation_settings">Übersetzung</string>
|
||||||
<string name="reset_to_defaults">Auf Standard zurücksetzen</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">Fehler beim Parsen der Tabelle</string>
|
||||||
<string name="error_parsing_table_with_reason">Fehler beim Parsen der Tabelle: %1$s</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>
|
<string name="label_import_table_csv_excel">Tabelle importieren (CSV)</string>
|
||||||
@@ -210,7 +209,7 @@
|
|||||||
<string name="text_favorites">Favoriten</string>
|
<string name="text_favorites">Favoriten</string>
|
||||||
<string name="text_recent_history">Verlauf</string>
|
<string name="text_recent_history">Verlauf</string>
|
||||||
<string name="text_select_auto_recognition">Automatische Erkennung auswählen</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_language_options">Sprachoptionen</string>
|
||||||
<string name="text_select_all_languages">Alle Sprachen auswählen</string>
|
<string name="text_select_all_languages">Alle Sprachen auswählen</string>
|
||||||
<string name="text_delete_custom_language">Eigene Sprache löschen</string>
|
<string name="text_delete_custom_language">Eigene Sprache löschen</string>
|
||||||
@@ -360,7 +359,7 @@
|
|||||||
<string name="days_2d">%1$d Tage</string>
|
<string name="days_2d">%1$d Tage</string>
|
||||||
<string name="progress_by_category">Fortschritt nach Kategorie</string>
|
<string name="progress_by_category">Fortschritt nach Kategorie</string>
|
||||||
<string name="label_apply_filters">Filter anwenden</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="label_category">Kategorie</string>
|
||||||
<string name="language">Sprache</string>
|
<string name="language">Sprache</string>
|
||||||
<string name="label_clear_all">Alle löschen</string>
|
<string name="label_clear_all">Alle löschen</string>
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
<item>cz,CZ,49</item>
|
<item>cz,CZ,49</item>
|
||||||
<item>he,IL,50</item>
|
<item>he,IL,50</item>
|
||||||
<item>hr,HR,51</item>
|
<item>hr,HR,51</item>
|
||||||
|
<item>fil,PH,52</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string name="language_1">Inglês</string>
|
<string name="language_1">Inglês</string>
|
||||||
@@ -105,6 +106,7 @@
|
|||||||
<string name="language_49">Tcheco</string>
|
<string name="language_49">Tcheco</string>
|
||||||
<string name="language_50">Hebraico</string>
|
<string name="language_50">Hebraico</string>
|
||||||
<string name="language_51">Croata</string>
|
<string name="language_51">Croata</string>
|
||||||
|
<string name="language_52">Filipino</string>
|
||||||
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -28,7 +28,6 @@
|
|||||||
<string name="cd_re_generate_definition">Gerar Definição Novamente</string>
|
<string name="cd_re_generate_definition">Gerar Definição Novamente</string>
|
||||||
<string name="cd_clear_search">Limpar pesquisa</string>
|
<string name="cd_clear_search">Limpar pesquisa</string>
|
||||||
<string name="cd_translation_history">Histórico de Tradução</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="label_reload">Recarregar</string>
|
||||||
<string name="title_single">Único</string>
|
<string name="title_single">Único</string>
|
||||||
<string name="title_widget_streak">Sequência</string>
|
<string name="title_widget_streak">Sequência</string>
|
||||||
@@ -42,7 +41,7 @@
|
|||||||
<string name="title_multiple">Múltiplos</string>
|
<string name="title_multiple">Múltiplos</string>
|
||||||
<string name="label_translation_settings">Configurações de Tradução</string>
|
<string name="label_translation_settings">Configurações de Tradução</string>
|
||||||
<string name="reset_to_defaults">Restaurar Padrões</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">Erro ao analisar tabela</string>
|
||||||
<string name="error_parsing_table_with_reason">Erro ao analisar tabela: %1$s</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>
|
<string name="label_import_table_csv_excel">Importar Tabela (CSV)</string>
|
||||||
@@ -207,7 +206,7 @@
|
|||||||
<string name="text_favorites">Favoritos</string>
|
<string name="text_favorites">Favoritos</string>
|
||||||
<string name="text_recent_history">Histórico</string>
|
<string name="text_recent_history">Histórico</string>
|
||||||
<string name="text_select_auto_recognition">Selecionar Reconhecimento Automático</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_language_options">Opções de Idioma</string>
|
||||||
<string name="text_select_all_languages">Selecionar todos os idiomas</string>
|
<string name="text_select_all_languages">Selecionar todos os idiomas</string>
|
||||||
<string name="text_delete_custom_language">Excluir idioma personalizado</string>
|
<string name="text_delete_custom_language">Excluir idioma personalizado</string>
|
||||||
@@ -358,7 +357,7 @@
|
|||||||
<string name="days_2d">%1$d dias</string>
|
<string name="days_2d">%1$d dias</string>
|
||||||
<string name="progress_by_category">Progresso por Categoria</string>
|
<string name="progress_by_category">Progresso por Categoria</string>
|
||||||
<string name="label_apply_filters">Aplicar Filtros</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="label_category">Categoria</string>
|
||||||
<string name="language">Idioma</string>
|
<string name="language">Idioma</string>
|
||||||
<string name="label_clear_all">Limpar Tudo</string>
|
<string name="label_clear_all">Limpar Tudo</string>
|
||||||
@@ -629,7 +628,7 @@
|
|||||||
<string name="text_paste_or_open_a_">Cole ou abra um link do YouTube para ver as legendas aqui.</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_error_2d">Erro: %1$s</string>
|
||||||
<string name="text_repeat_wrong_guesses">Repetir Respostas Erradas</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_grammar_inflections">Flexões</string>
|
||||||
<string name="label_more">Mais</string>
|
<string name="label_more">Mais</string>
|
||||||
<string name="label_translations">Traduções</string>
|
<string name="label_translations">Traduções</string>
|
||||||
|
|||||||
@@ -61,6 +61,8 @@
|
|||||||
<string-array name="changelog_entries">
|
<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.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.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>
|
</string-array>
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
<item>cz,CZ,49</item>
|
<item>cz,CZ,49</item>
|
||||||
<item>he,IL,50</item>
|
<item>he,IL,50</item>
|
||||||
<item>hr,HR,51</item>
|
<item>hr,HR,51</item>
|
||||||
|
<item>fil,PH,52</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string name="language_1">English</string>
|
<string name="language_1">English</string>
|
||||||
@@ -105,6 +106,7 @@
|
|||||||
<string name="language_49">Czech</string>
|
<string name="language_49">Czech</string>
|
||||||
<string name="language_50">Hebrew</string>
|
<string name="language_50">Hebrew</string>
|
||||||
<string name="language_51">Croatian</string>
|
<string name="language_51">Croatian</string>
|
||||||
|
<string name="language_52">Filipino</string>
|
||||||
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -52,4 +52,5 @@
|
|||||||
<string name="native_language_49" translatable="false">Čeština</string>
|
<string name="native_language_49" translatable="false">Čeština</string>
|
||||||
<string name="native_language_50" translatable="false">עברית</string>
|
<string name="native_language_50" translatable="false">עברית</string>
|
||||||
<string name="native_language_51" translatable="false">Hrvatski</string>
|
<string name="native_language_51" translatable="false">Hrvatski</string>
|
||||||
|
<string name="native_language_52" translatable="false">Filipino</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -140,7 +140,7 @@
|
|||||||
<string name="fetching_grammar_details">Fetching Grammar Details</string>
|
<string name="fetching_grammar_details">Fetching Grammar Details</string>
|
||||||
|
|
||||||
<string name="filter_and_sort">Filter and Sort</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="filter_by_word_type">Filter by Word Type</string>
|
||||||
|
|
||||||
<string name="find_translations">Find Translations</string>
|
<string name="find_translations">Find Translations</string>
|
||||||
@@ -225,6 +225,7 @@
|
|||||||
<string name="label_amount_models">%1$d models</string>
|
<string name="label_amount_models">%1$d models</string>
|
||||||
<string name="label_analyze_grammar">Analyze Grammar</string>
|
<string name="label_analyze_grammar">Analyze Grammar</string>
|
||||||
<string name="label_appearance">Appearance</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_apply_filters">Apply Filters</string>
|
||||||
<string name="label_article">Article</string>
|
<string name="label_article">Article</string>
|
||||||
<string name="label_backup_and_restore">Backup and Restore</string>
|
<string name="label_backup_and_restore">Backup and Restore</string>
|
||||||
@@ -315,7 +316,7 @@
|
|||||||
<string name="label_preview_second">Preview (first 5) for second column: %1$s</string>
|
<string name="label_preview_second">Preview (first 5) for second column: %1$s</string>
|
||||||
<string name="label_pronoun">Pronoun</string>
|
<string name="label_pronoun">Pronoun</string>
|
||||||
<string name="label_providers">Providers</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_quit_exercise_qm">Quit Exercise?</string>
|
||||||
<string name="label_raw_data_2d">Raw Data:</string>
|
<string name="label_raw_data_2d">Raw Data:</string>
|
||||||
<string name="label_related_words">Related Words</string>
|
<string name="label_related_words">Related Words</string>
|
||||||
@@ -787,7 +788,7 @@
|
|||||||
<string name="text_error_generating_questions">Error generating questions: %1$s</string>
|
<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_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_error_saving_entry">Error saving entry: %1$s</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_expand_widget">Expand Widget</string>
|
||||||
<string name="text_explanation">Explanation</string>
|
<string name="text_explanation">Explanation</string>
|
||||||
<string name="text_export_category">Export Category</string>
|
<string name="text_export_category">Export Category</string>
|
||||||
@@ -883,7 +884,7 @@
|
|||||||
<string name="text_select_category">Select Category</string>
|
<string name="text_select_category">Select Category</string>
|
||||||
<string name="text_select_languages">Select Languages</string>
|
<string name="text_select_languages">Select Languages</string>
|
||||||
<string name="text_select_model">Select Model</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_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_select_translations_to_add">Select Translations to Add</string>
|
||||||
<string name="text_selected">Selected</string>
|
<string name="text_selected">Selected</string>
|
||||||
@@ -1038,4 +1039,79 @@
|
|||||||
<string name="duplicate">Duplicate</string>
|
<string name="duplicate">Duplicate</string>
|
||||||
<string name="hint_scan_hint_title">Finding the right AI model</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="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>
|
</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
|
||||||
@@ -43,6 +43,7 @@ truth = "1.4.5"
|
|||||||
zstdJni = "1.5.7-7"
|
zstdJni = "1.5.7-7"
|
||||||
composeMarkdown = "0.5.8"
|
composeMarkdown = "0.5.8"
|
||||||
jitpack = "1.0.10"
|
jitpack = "1.0.10"
|
||||||
|
foundationLayoutVersion = "1.10.3"
|
||||||
|
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
@@ -103,6 +104,7 @@ hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", ve
|
|||||||
hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.3.0" }
|
hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.3.0" }
|
||||||
mockk = { module = "io.mockk:mockk", version = "1.14.9" }
|
mockk = { module = "io.mockk:mockk", version = "1.14.9" }
|
||||||
compose-markdown = { module = "com.github.jeziellago:compose-markdown", version.ref = "composeMarkdown" }
|
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]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
Reference in New Issue
Block a user