Compare commits
7 Commits
5e920c43b3
...
glassmorph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d0bf4cb1c | ||
|
|
2b8b9a84a3 | ||
|
|
59f5f5e668 | ||
|
|
15f7eae068 | ||
|
|
8e610259ca | ||
|
|
7d18f8eb04 | ||
|
|
f4fcffe90a |
2
.idea/deploymentTargetSelector.xml
generated
2
.idea/deploymentTargetSelector.xml
generated
@@ -4,7 +4,7 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2026-02-06T10:01:23.649270100Z">
|
||||
<DropdownSelection timestamp="2026-02-15T19:51:37.987601800Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\jonas\.android\avd\Medium_Phone_28.avd" />
|
||||
|
||||
@@ -130,6 +130,7 @@ dependencies {
|
||||
implementation(libs.androidx.room.runtime) // ADDED: Explicitly add runtime
|
||||
implementation(libs.androidx.room.ktx)
|
||||
implementation(libs.core.ktx)
|
||||
implementation(libs.androidx.compose.foundation.layout)
|
||||
ksp(libs.room.compiler) // CHANGED: Use ksp instead of implementation
|
||||
|
||||
// Networking
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
# How to Scan for AI Models
|
||||
# TODO REWRITE
|
||||
|
||||
This guide explains how to use the **Scan** feature to discover and add AI models to your app.
|
||||
|
||||
## How Scanning Works
|
||||
|
||||
The scan feature searches for available AI models on your device or network.
|
||||
|
||||
> **Note:** Results depend on your API key permissions.
|
||||
|
||||
### Key Points
|
||||
|
||||
- Only public models are shown by default
|
||||
- Private models require additional setup
|
||||
- Try again if no models are found
|
||||
|
||||
## Why Some Models Are Missing
|
||||
|
||||
Some models may not appear in the scan results due to:
|
||||
|
||||
| Reason | Description | Icon |
|
||||
|--------|-------------|------|
|
||||
| Restricted access | Model requires special permissions | 🔒 |
|
||||
| Not suitable | Model type not supported | ⚠️ |
|
||||
| Text only | Only text-based models are supported | ✓ |
|
||||
|
||||
### Model Tiers
|
||||
|
||||
We recommend these tiers for optimal performance:
|
||||
|
||||
- **Nano** - Fastest, for simple tasks
|
||||
- **Mini** - Balanced speed and capability
|
||||
- **Small** - Good for most tasks
|
||||
- **Medium** - More capable, slower
|
||||
- **Large** - Most capable, paid only
|
||||
|
||||
## Tips for Success
|
||||
|
||||
1. **Verify your API key** is active and has correct permissions
|
||||
2. **Select the correct organization** from your account
|
||||
3. **Type model names manually** if scanning doesn't find them
|
||||
4. **Prefer instruct or chat models** for text generation
|
||||
|
||||
```kotlin
|
||||
// Example: Manual model addition
|
||||
val model = Model(
|
||||
name = "llama3.2",
|
||||
type = ModelType.TEXT,
|
||||
provider = "ollama"
|
||||
)
|
||||
```
|
||||
|
||||
## Visual Guide
|
||||
|
||||
### Step 1: Initiate Scan
|
||||
|
||||
Click the scan button to search for available models.
|
||||
|
||||
### Step 2: Select Model Type
|
||||
|
||||
Choose between different model categories:
|
||||
|
||||
- **Text Chat** - For conversational AI
|
||||
- **Instruct** - For direct instructions
|
||||
- **Complete** - For text completion
|
||||
|
||||
### Step 3: Add & Validate
|
||||
|
||||
Add the selected model and validate it works correctly.
|
||||
|
||||
---
|
||||
|
||||
## Can't Find Your Model?
|
||||
|
||||
If your model doesn't appear in the scan results:
|
||||
|
||||
1. Check if the model is running locally or accessible via API
|
||||
2. Verify network connectivity
|
||||
3. Try adding it manually by entering the model details
|
||||
|
||||
> **Pro Tip:** You can always add models manually by clicking the "+" button in the models screen.
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 2024-01-15*
|
||||
*For more help, visit our documentation website.*
|
||||
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,26 +1,23 @@
|
||||
# Import Vocabulary with AI
|
||||
# TODO REWRITE
|
||||
|
||||
Generate vocabulary lists automatically using AI assistance.
|
||||
|
||||
## 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
|
||||
|
||||
Type a topic, theme, or concept for your vocabulary list:
|
||||
- Be specific for better results
|
||||
- Example: "German food and restaurant phrases"
|
||||
- Example: "Business vocabulary for meetings"
|
||||
- Example: "Things to do in Paris"
|
||||
- Example: "Difficult verbs that are confusing"
|
||||
|
||||
### Step 2: Select Languages
|
||||
|
||||
Choose source and target languages:
|
||||
- **Source language** - The language you're learning from
|
||||
- **Target language** - Your native language
|
||||
- **Source language** - The first language of the flashcard
|
||||
- **Target language** - The second language of the flashcard
|
||||
|
||||
### Step 3: Set Amount
|
||||
|
||||
@@ -33,22 +30,17 @@ Choose how many words to generate:
|
||||
|
||||
Tap the generate button:
|
||||
- AI creates the vocabulary list
|
||||
- Review each entry before saving
|
||||
- Edit any translations if needed
|
||||
|
||||
## After Generation
|
||||
|
||||
Once generated, you can:
|
||||
|
||||
- **Review** - Check each word-translation pair
|
||||
- **Edit** - Correct any mistakes
|
||||
- **Delete** - Remove unwanted entries
|
||||
- **Import All** - Add all to your vocabulary
|
||||
- Choose which terms to keep
|
||||
- Optionally, add it to a category
|
||||
|
||||
## Tips
|
||||
|
||||
> **Pro Tip:** Start with 10 words per import to get familiar with the feature.
|
||||
|
||||
---
|
||||
|
||||
*Need help? Check our vocabulary management guide.*
|
||||
- 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
|
||||
@@ -1,11 +1,4 @@
|
||||
# Sorting Vocabulary
|
||||
# TODO REWRITE
|
||||
|
||||
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:
|
||||
After you imported vocabulary, you can sort vocabulary
|
||||
|
||||
- Review each word-translation pair
|
||||
- Decide the next action for each item
|
||||
@@ -13,19 +6,17 @@ When you import vocabulary, you'll see the sorting screen where you can:
|
||||
|
||||
## Actions
|
||||
|
||||
### ✅ Mark as Learned
|
||||
### Mark as Learned
|
||||
|
||||
Move the word directly to Stage 1:
|
||||
- The word enters your learning queue
|
||||
- You'll review it according to the learning schedule
|
||||
If you already know the word, move the word directly to Stage "Learned". This prevents the word from reappearing in your exercises.
|
||||
|
||||
### 🗑️ Delete
|
||||
### Delete
|
||||
|
||||
Remove the word entirely:
|
||||
- Use for duplicates or unwanted entries
|
||||
- This action is permanent
|
||||
|
||||
### 📝 Edit
|
||||
### Edit
|
||||
|
||||
Tap on any word or translation to edit:
|
||||
- Correct typos
|
||||
@@ -34,18 +25,12 @@ Tap on any word or translation to edit:
|
||||
|
||||
## Duplicate Handling
|
||||
|
||||
When duplicates are detected:
|
||||
|
||||
| Icon | Meaning |
|
||||
|------|---------|
|
||||
| ⚠️ | Duplicate detected |
|
||||
| ✅ | Original entry |
|
||||
| ❌ | Duplicate entry |
|
||||
When duplicates are detected, you can choose how to handle them:
|
||||
|
||||
**Options for duplicates:**
|
||||
- Keep only the original
|
||||
- Keep the newer entry
|
||||
- Keep both (merge)
|
||||
- Keep both (merge): the newer entry will get deleted but all its information (categories) will be added the old item.
|
||||
- Delete the duplicate
|
||||
|
||||
## Helper Features
|
||||
@@ -57,17 +42,7 @@ Toggle to automatically strip articles from words:
|
||||
- "the dog" → "dog"
|
||||
- Useful for cleaner vocabulary lists
|
||||
|
||||
### Quick Actions
|
||||
|
||||
Use quick action buttons for bulk operations:
|
||||
- **Skip All** - Review later
|
||||
- **Learn All** - Add all to Stage 1
|
||||
- **Delete Duplicates** - Auto-remove duplicates
|
||||
|
||||
## Tips
|
||||
|
||||
> **Pro Tip:** Review carefully before sorting. Once sorted, you can still edit words in the vocabulary list.
|
||||
You can edit your flashcards at any point in the flashcard itself
|
||||
|
||||
---
|
||||
|
||||
*For more tips, check our vocabulary management guide.*
|
||||
|
||||
@@ -1,79 +1,7 @@
|
||||
# Vocabulary Progress Tracking
|
||||
# TODO REWRITE
|
||||
|
||||
Monitor your vocabulary learning journey with detailed progress statistics.
|
||||
|
||||
## Progress Overview
|
||||
|
||||
Track your learning with these key metrics:
|
||||
|
||||
### Words Learned
|
||||
|
||||
- Total words added to your vocabulary
|
||||
- Words currently in each learning stage
|
||||
- Words marked as fully learned
|
||||
|
||||
### Learning Streak
|
||||
|
||||
- Days since you started learning
|
||||
- Current streak count
|
||||
- Best streak achieved
|
||||
|
||||
### Review Statistics
|
||||
|
||||
- Words reviewed today
|
||||
- Accuracy rate per session
|
||||
- Words due for review
|
||||
|
||||
## Progress Tracking Features
|
||||
|
||||
### 📊 Dashboard
|
||||
|
||||
View your overall progress at a glance:
|
||||
- Total vocabulary count
|
||||
- Mastery percentage
|
||||
- Recent activity summary
|
||||
|
||||
### 📈 Statistics
|
||||
|
||||
Detailed analytics include:
|
||||
- Learning rate over time
|
||||
- Stage distribution
|
||||
- Accuracy trends
|
||||
- Time spent studying
|
||||
|
||||
### 🎯 Goals
|
||||
|
||||
Set and track learning goals:
|
||||
- Daily word targets
|
||||
- Weekly review quotas
|
||||
- Mastery milestones
|
||||
|
||||
## Learning Stages Summary
|
||||
|
||||
| Stage | Count | Percentage |
|
||||
|-------|-------|------------|
|
||||
| New | X | X% |
|
||||
| Learning | X | X% |
|
||||
| Mastered | X | X% |
|
||||
|
||||
## Review System
|
||||
|
||||
The review system helps you:
|
||||
|
||||
1. **Prioritize** - Shows words due for review first
|
||||
2. **Space** - Optimizes review timing for retention
|
||||
3. **Track** - Records your performance over time
|
||||
|
||||
## Customization
|
||||
|
||||
Customize your progress tracking:
|
||||
|
||||
- **Select metrics** to display on dashboard
|
||||
- **Set goals** for personalized targets
|
||||
- **Export data** for external analysis
|
||||
- **Reset progress** if starting fresh
|
||||
|
||||
---
|
||||
|
||||
*Keep practicing consistently to see your progress grow!*
|
||||
TODO Rewrite
|
||||
@@ -21,10 +21,10 @@
|
||||
"description": "Next-gen efficient architecture; outperforms older 70B models."
|
||||
},
|
||||
{
|
||||
"modelId": "deepseek-ai/DeepSeek-V3",
|
||||
"displayName": "DeepSeek V3",
|
||||
"modelId": "deepseek-ai/DeepSeek-V3.1",
|
||||
"displayName": "DeepSeek V3.1",
|
||||
"provider": "together",
|
||||
"description": "Top-tier open-source model specializing in code and logic."
|
||||
"description": "Latest 671B MoE model with hybrid thinking/non-thinking modes."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -37,10 +37,10 @@
|
||||
"isCustom": false,
|
||||
"models": [
|
||||
{
|
||||
"modelId": "ministral-8b-latest",
|
||||
"displayName": "Ministral 8B",
|
||||
"modelId": "mistral-medium-latest",
|
||||
"displayName": "Mistral Medium",
|
||||
"provider": "mistral",
|
||||
"description": "Extremely efficient edge model for low-latency tasks."
|
||||
"description": "Balanced performance and cost for a wide range of tasks."
|
||||
},
|
||||
{
|
||||
"modelId": "mistral-large-latest",
|
||||
@@ -58,17 +58,17 @@
|
||||
"websiteUrl": "https://platform.openai.com/",
|
||||
"isCustom": false,
|
||||
"models": [
|
||||
{
|
||||
"modelId": "gpt-5.2",
|
||||
"displayName": "GPT-5.2",
|
||||
"provider": "openai",
|
||||
"description": "Balanced performance with enhanced reasoning and creativity."
|
||||
},
|
||||
{
|
||||
"modelId": "gpt-5.1-instant",
|
||||
"displayName": "GPT-5.1 Instant",
|
||||
"provider": "openai",
|
||||
"description": "The standard high-speed efficiency model replacing older 'Nano' tiers."
|
||||
},
|
||||
{
|
||||
"modelId": "gpt-5-nano",
|
||||
"displayName": "GPT-5 Nano",
|
||||
"provider": "openai",
|
||||
"description": "Fast and cheap model sufficient for most tasks."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -81,16 +81,16 @@
|
||||
"isCustom": false,
|
||||
"models": [
|
||||
{
|
||||
"modelId": "claude-sonnet-5-20260203",
|
||||
"displayName": "Claude Sonnet 5",
|
||||
"modelId": "claude-opus-4-6",
|
||||
"displayName": "Claude Opus 4.6",
|
||||
"provider": "anthropic",
|
||||
"description": "Latest stable workhorse (Feb 2026), balancing speed and top-tier reasoning."
|
||||
"description": "Most intelligent model for building agents and coding with 1M context."
|
||||
},
|
||||
{
|
||||
"modelId": "claude-4.5-haiku",
|
||||
"displayName": "Claude 4.5 Haiku",
|
||||
"modelId": "claude-sonnet-4-5",
|
||||
"displayName": "Claude Sonnet 4.5",
|
||||
"provider": "anthropic",
|
||||
"description": "Fastest Claude model for pure speed and simple tasks."
|
||||
"description": "Best combination of speed and intelligence with extended thinking."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -110,9 +110,9 @@
|
||||
},
|
||||
{
|
||||
"modelId": "deepseek-chat",
|
||||
"displayName": "DeepSeek V3",
|
||||
"displayName": "DeepSeek V3.1",
|
||||
"provider": "deepseek",
|
||||
"description": "General purpose chat model, specialized in code and reasoning."
|
||||
"description": "Latest 671B MoE with hybrid thinking/non-thinking modes, 128K context."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -120,15 +120,15 @@
|
||||
"key": "gemini",
|
||||
"displayName": "Google Gemini",
|
||||
"baseUrl": "https://generativelanguage.googleapis.com/",
|
||||
"endpoint": "v1beta/models/gemini-3-flash-preview:generateContent",
|
||||
"endpoint": "v1beta/models/gemini-2.5-pro:generateContent",
|
||||
"websiteUrl": "https://ai.google/",
|
||||
"isCustom": false,
|
||||
"models": [
|
||||
{
|
||||
"modelId": "gemini-3-flash-preview",
|
||||
"displayName": "Gemini 3 Flash",
|
||||
"modelId": "gemini-2.5-pro",
|
||||
"displayName": "Gemini 2.5 Pro",
|
||||
"provider": "gemini",
|
||||
"description": "Current default: Massive context, grounded, and extremely fast."
|
||||
"description": "Stable release: State-of-the-art reasoning with 1M context."
|
||||
},
|
||||
{
|
||||
"modelId": "gemini-3-pro-preview",
|
||||
@@ -156,16 +156,10 @@
|
||||
"isCustom": false,
|
||||
"models": [
|
||||
{
|
||||
"modelId": "llama-4-scout-17b",
|
||||
"displayName": "Llama 4 Scout",
|
||||
"modelId": "meta-llama/llama-4-maverick",
|
||||
"displayName": "Llama 4 Maverick",
|
||||
"provider": "groq",
|
||||
"description": "Powerful Llama 4 model running at extreme speed."
|
||||
},
|
||||
{
|
||||
"modelId": "llama-3.3-70b-versatile",
|
||||
"displayName": "Llama 3.3 70B",
|
||||
"provider": "groq",
|
||||
"description": "Previous gen flagship, highly reliable and fast on Groq chips."
|
||||
"description": "400B MoE powerhouse with industry-leading image and text understanding."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -216,10 +210,10 @@
|
||||
"description": "World's fastest inference (2000+ tokens/sec) on Wafer-Scale Engines."
|
||||
},
|
||||
{
|
||||
"modelId": "llama3.1-8b",
|
||||
"displayName": "Llama 3.1 8B",
|
||||
"modelId": "llama-4-scout",
|
||||
"displayName": "Llama 4 Scout",
|
||||
"provider": "cerebras",
|
||||
"description": "Instant speed for simple tasks."
|
||||
"description": "High-quality 17B active param model running at 2,600 tokens/sec."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -238,10 +232,10 @@
|
||||
"description": "Hosted via the Hugging Face serverless router (Free tier limits apply)."
|
||||
},
|
||||
{
|
||||
"modelId": "microsoft/Phi-3.5-mini-instruct",
|
||||
"displayName": "Phi 3.5 Mini",
|
||||
"modelId": "Qwen/Qwen2.5-72B-Instruct",
|
||||
"displayName": "Qwen 2.5 72B",
|
||||
"provider": "huggingface",
|
||||
"description": "Highly capable small model from Microsoft."
|
||||
"description": "High-quality open model with excellent reasoning and multilingual capabilities."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -102,8 +102,8 @@ class SettingsRepository(private val context: Context) {
|
||||
val intervalStage4 = Setting(context.dataStore, PrefKeys.INTERVAL_STAGE_4, 30)
|
||||
val intervalStage5 = Setting(context.dataStore, PrefKeys.INTERVAL_STAGE_5, 60)
|
||||
val intervalLearned = Setting(context.dataStore, PrefKeys.INTERVAL_LEARNED, 90)
|
||||
val criteriaCorrect = Setting(context.dataStore, PrefKeys.CRITERIA_CORRECT, 3)
|
||||
val criteriaWrong = Setting(context.dataStore, PrefKeys.CRITERIA_WRONG, 2)
|
||||
val criteriaCorrect = Setting(context.dataStore, PrefKeys.CRITERIA_CORRECT, 1)
|
||||
val criteriaWrong = Setting(context.dataStore, PrefKeys.CRITERIA_WRONG, 1)
|
||||
val showHints = Setting(context.dataStore, PrefKeys.SHOW_HINTS, true)
|
||||
val experimentalFeatures = Setting(context.dataStore, PrefKeys.EXPERIMENTAL_FEATURES, false)
|
||||
val tryWiktionaryFirst = Setting(context.dataStore, PrefKeys.TRY_WIKTIONARY_FIRST, false)
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package eu.gaudian.translator.utils
|
||||
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.viewmodel.MessageAction
|
||||
import eu.gaudian.translator.viewmodel.MessageDisplayType
|
||||
|
||||
/**
|
||||
* Simplified status message IDs for internationalization.
|
||||
* Each ID stores its metadata directly, reducing repetitive mapping code.
|
||||
*/
|
||||
enum class StatusMessageId(
|
||||
val stringResId: Int,
|
||||
val defaultType: MessageDisplayType,
|
||||
val defaultTimeout: Int,
|
||||
val associatedAction: MessageAction? = null
|
||||
) {
|
||||
// Generic messages
|
||||
SUCCESS_GENERIC(R.string.message_success_generic, MessageDisplayType.SUCCESS, 3),
|
||||
INFO_GENERIC(R.string.message_info_generic, MessageDisplayType.INFO, 3),
|
||||
ERROR_GENERIC(R.string.message_error_generic, MessageDisplayType.ERROR, 5),
|
||||
LOADING_GENERIC(R.string.message_loading_generic, MessageDisplayType.LOADING, 0),
|
||||
TEST_INFO(R.string.message_test_info, MessageDisplayType.INFO, 3),
|
||||
TEST_SUCCESS(R.string.message_test_success, MessageDisplayType.SUCCESS, 3),
|
||||
TEST_ERROR(R.string.message_test_error, MessageDisplayType.ERROR, 5),
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Language related
|
||||
ERROR_LANGUAGE_NOT_SELECTED(R.string.message_error_language_not_selected, MessageDisplayType.ERROR, 5),
|
||||
ERROR_NO_WORDS_FOUND(R.string.message_error_no_words_found, MessageDisplayType.ERROR, 5),
|
||||
SUCCESS_LANGUAGE_REPLACED(R.string.message_success_language_replaced, MessageDisplayType.SUCCESS, 3),
|
||||
|
||||
// Vocabulary related
|
||||
SUCCESS_VOCABULARY_IMPORTED(R.string.message_success_vocabulary_imported, MessageDisplayType.SUCCESS, 3),
|
||||
ERROR_VOCABULARY_IMPORT_FAILED(R.string.message_error_vocabulary_import_failed, MessageDisplayType.ERROR, 5),
|
||||
SUCCESS_ITEMS_MERGED(R.string.message_success_items_merged, MessageDisplayType.SUCCESS, 3),
|
||||
SUCCESS_ITEMS_ADDED(R.string.message_success_items_added, MessageDisplayType.SUCCESS, 3),
|
||||
ERROR_ITEMS_ADD_FAILED(R.string.message_error_items_add_failed, MessageDisplayType.ERROR, 5),
|
||||
SUCCESS_ITEMS_DELETED(R.string.message_success_items_deleted, MessageDisplayType.SUCCESS, 3),
|
||||
ERROR_ITEMS_DELETE_FAILED(R.string.message_error_items_delete_failed, MessageDisplayType.ERROR, 5),
|
||||
ERROR_NO_CARDS_FOUND(R.string.message_error_no_cards_found, MessageDisplayType.ERROR, 5),
|
||||
SUCCESS_CARDS_LOADED(R.string.message_success_cards_loaded, MessageDisplayType.SUCCESS, 3),
|
||||
|
||||
// Grammar related
|
||||
SUCCESS_GRAMMAR_UPDATED(R.string.message_success_grammar_updated, MessageDisplayType.SUCCESS, 3),
|
||||
ERROR_GRAMMAR_FETCH_FAILED(R.string.message_error_grammar_fetch_failed, MessageDisplayType.ERROR, 5),
|
||||
LOADING_GRAMMAR_FETCH(R.string.message_loading_grammar_fetch, MessageDisplayType.LOADING, 0),
|
||||
|
||||
// File operations
|
||||
SUCCESS_FILE_SAVED(R.string.message_success_file_saved, MessageDisplayType.SUCCESS, 3),
|
||||
ERROR_FILE_SAVE_FAILED(R.string.message_error_file_save_failed, MessageDisplayType.ERROR, 5),
|
||||
ERROR_FILE_SAVE_CANCELLED(R.string.message_error_file_save_cancelled, MessageDisplayType.ERROR, 5),
|
||||
ERROR_FILE_PICKER_NOT_INITIALIZED(R.string.message_error_file_picker_not_initialized, MessageDisplayType.ERROR, 5),
|
||||
SUCCESS_CATEGORY_SAVED(R.string.message_success_category_saved, MessageDisplayType.SUCCESS, 3),
|
||||
ERROR_EXCEL_NOT_SUPPORTED(R.string.message_error_excel_not_supported, MessageDisplayType.ERROR, 5),
|
||||
|
||||
|
||||
// API Key related
|
||||
ERROR_API_KEY_MISSING(R.string.message_error_api_key_missing, MessageDisplayType.ERROR, 0, MessageAction.NAVIGATE_TO_API_KEYS),
|
||||
ERROR_API_KEY_INVALID(R.string.message_error_api_key_invalid, MessageDisplayType.ERROR, 0, MessageAction.NAVIGATE_TO_API_KEYS),
|
||||
|
||||
// Translation related
|
||||
LOADING_TRANSLATING(R.string.message_loading_translating, MessageDisplayType.LOADING, 0),
|
||||
SUCCESS_TRANSLATION_COMPLETED(R.string.message_success_translation_completed, MessageDisplayType.SUCCESS, 3),
|
||||
ERROR_TRANSLATION_FAILED(R.string.message_error_translation_failed, MessageDisplayType.ERROR, 5),
|
||||
|
||||
// Repository operations
|
||||
SUCCESS_REPOSITORY_WIPED(R.string.message_success_repository_wiped, MessageDisplayType.SUCCESS, 3),
|
||||
ERROR_REPOSITORY_WIPE_FAILED(R.string.message_error_repository_wipe_failed, MessageDisplayType.ERROR, 5),
|
||||
LOADING_CARD_SET(R.string.message_loading_card_set, MessageDisplayType.LOADING, 0),
|
||||
|
||||
// Stage operations
|
||||
SUCCESS_STAGE_UPDATED(R.string.message_success_stage_updated, MessageDisplayType.SUCCESS, 3),
|
||||
ERROR_STAGE_UPDATE_FAILED(R.string.message_error_stage_update_failed, MessageDisplayType.ERROR, 5),
|
||||
|
||||
// Category operations
|
||||
SUCCESS_CATEGORY_UPDATED(R.string.message_success_category_updated, MessageDisplayType.SUCCESS, 3),
|
||||
ERROR_CATEGORY_UPDATE_FAILED(R.string.message_error_category_update_failed, MessageDisplayType.ERROR, 5),
|
||||
|
||||
// Article removal
|
||||
SUCCESS_ARTICLES_REMOVED(R.string.message_success_articles_removed, MessageDisplayType.SUCCESS, 3),
|
||||
ERROR_ARTICLES_REMOVE_FAILED(R.string.message_error_articles_remove_failed, MessageDisplayType.ERROR, 5),
|
||||
|
||||
// Synonyms
|
||||
SUCCESS_SYNONYMS_GENERATED(R.string.message_success_synonyms_generated, MessageDisplayType.SUCCESS, 3),
|
||||
ERROR_SYNONYMS_GENERATION_FAILED(R.string.message_error_synonyms_generation_failed, MessageDisplayType.ERROR, 5),
|
||||
|
||||
// Operation status
|
||||
ERROR_OPERATION_FAILED(R.string.message_error_operation_failed, MessageDisplayType.ERROR, 5),
|
||||
LOADING_OPERATION_IN_PROGRESS(R.string.message_loading_operation_in_progress, MessageDisplayType.LOADING, 0);
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Convenience function to get the string resource ID from a StatusMessageId.
|
||||
* Kept for backward compatibility with existing code.
|
||||
*/
|
||||
fun StatusMessageId.getStringResId(): Int = this.stringResId
|
||||
|
||||
/**
|
||||
* Convenience function to get the default display type.
|
||||
*/
|
||||
fun StatusMessageId.getDefaultDisplayType(): MessageDisplayType = this.defaultType
|
||||
|
||||
/**
|
||||
* Convenience function to get the default timeout.
|
||||
*/
|
||||
fun StatusMessageId.getDefaultTimeoutSeconds(): Int = this.defaultTimeout
|
||||
|
||||
/**
|
||||
* Convenience function to get the associated action.
|
||||
*/
|
||||
fun StatusMessageId.getAssociatedAction(): MessageAction? = this.associatedAction
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,10 @@ import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* A sealed class representing all possible actions that can be sent to the status system.
|
||||
* Supports both legacy string-based messages and new ID-based messages for internationalization.
|
||||
*/
|
||||
sealed class StatusAction {
|
||||
// Legacy string-based actions (deprecated in favor of ID-based actions)
|
||||
data class ShowMessage(val text: String, val type: MessageDisplayType, val timeoutInSeconds: Int) : StatusAction()
|
||||
data class ShowPermanentMessage(val text: String, val type: MessageDisplayType) : StatusAction()
|
||||
object CancelPermanentMessage : StatusAction()
|
||||
@@ -20,31 +22,59 @@ sealed class StatusAction {
|
||||
object CancelLoadingOperation : StatusAction()
|
||||
object HideMessageBar : StatusAction()
|
||||
object CancelAllMessages : StatusAction()
|
||||
|
||||
data class ShowActionableMessage(val text: String, val type: MessageDisplayType, val action: MessageAction) : StatusAction()
|
||||
|
||||
// New ID-based actions for internationalization
|
||||
data class ShowMessageById(
|
||||
val messageId: StatusMessageId,
|
||||
val type: MessageDisplayType = messageId.defaultType,
|
||||
val timeoutInSeconds: Int = messageId.defaultTimeout
|
||||
) : StatusAction()
|
||||
data class ShowPermanentMessageById(
|
||||
val messageId: StatusMessageId,
|
||||
val type: MessageDisplayType = messageId.defaultType
|
||||
) : StatusAction()
|
||||
data class ShowActionableMessageById(
|
||||
val messageId: StatusMessageId,
|
||||
val type: MessageDisplayType = messageId.defaultType,
|
||||
val action: MessageAction = messageId.associatedAction ?: MessageAction.NAVIGATE_TO_API_KEYS
|
||||
) : StatusAction()
|
||||
}
|
||||
|
||||
/**
|
||||
* A singleton object that acts as a central event bus for status messages.
|
||||
* Any part of the app can trigger an action, and any StatusViewModel listening will receive it.
|
||||
*
|
||||
* NOTE: All message display requests should go through this service.
|
||||
*/
|
||||
object StatusMessageService {
|
||||
private val _actions = MutableSharedFlow<StatusAction>()
|
||||
val actions = _actions.asSharedFlow()
|
||||
private val scope = CoroutineScope(Dispatchers.Default)
|
||||
|
||||
suspend fun trigger(action: StatusAction) {
|
||||
/**
|
||||
* Triggers a status action. This is the primary way to display messages.
|
||||
* Internally launches a coroutine, so this function is not suspend.
|
||||
*/
|
||||
fun trigger(action: StatusAction) {
|
||||
Log.d("StatusMessageService", "Received action: $action")
|
||||
_actions.emit(action)
|
||||
}
|
||||
|
||||
fun triggerNonSuspend(action: StatusAction) {
|
||||
Log.d("StatusMessageService", "Received non-suspend action: $action")
|
||||
scope.launch {
|
||||
_actions.emit(action)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use trigger() instead.
|
||||
*/
|
||||
@Deprecated("Use trigger() instead", ReplaceWith("trigger(action)"))
|
||||
fun triggerNonSuspend(action: StatusAction) {
|
||||
trigger(action)
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use showMessageById() instead for internationalization support.
|
||||
*/
|
||||
@Deprecated("Use showMessageById() for internationalization support", ReplaceWith("showMessageById(messageId)"))
|
||||
@Suppress("unused")
|
||||
fun showSimpleMessage(text: String, type: MessageDisplayType = MessageDisplayType.INFO) {
|
||||
scope.launch {
|
||||
@@ -52,6 +82,10 @@ object StatusMessageService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use showErrorById() instead for internationalization support.
|
||||
*/
|
||||
@Deprecated("Use showErrorById() for internationalization support", ReplaceWith("showErrorById(messageId)"))
|
||||
fun showErrorMessage(text: String, timeoutInSeconds: Int = 5) {
|
||||
scope.launch {
|
||||
_actions.emit(StatusAction.ShowMessage(
|
||||
@@ -62,6 +96,10 @@ object StatusMessageService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use showLoadingById() instead for internationalization support.
|
||||
*/
|
||||
@Deprecated("Use showLoadingById() for internationalization support", ReplaceWith("showLoadingById(messageId)"))
|
||||
fun showLoadingMessage(text: String, timeoutInSeconds: Int = 0) {
|
||||
scope.launch {
|
||||
_actions.emit(StatusAction.ShowMessage(
|
||||
@@ -71,6 +109,10 @@ object StatusMessageService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use showInfoById() instead for internationalization support.
|
||||
*/
|
||||
@Deprecated("Use showInfoById() for internationalization support", ReplaceWith("showInfoById(messageId)"))
|
||||
fun showInfoMessage(text: String, timeoutInSeconds: Int = 3) {
|
||||
scope.launch {
|
||||
_actions.emit(StatusAction.ShowMessage(
|
||||
@@ -80,6 +122,10 @@ object StatusMessageService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use showSuccessById() instead for internationalization support.
|
||||
*/
|
||||
@Deprecated("Use showSuccessById() for internationalization support", ReplaceWith("showSuccessById(messageId)"))
|
||||
fun showSuccessMessage(text: String, timeoutInSeconds: Int = 3) {
|
||||
scope.launch {
|
||||
_actions.emit(StatusAction.ShowMessage(
|
||||
@@ -89,33 +135,102 @@ object StatusMessageService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use showPermanentMessageById() instead for internationalization support.
|
||||
*/
|
||||
@Deprecated("Use showPermanentMessageById() for internationalization support", ReplaceWith("showPermanentMessageById(messageId)"))
|
||||
fun showPermanentMessage(text: String, type: MessageDisplayType) {
|
||||
scope.launch {
|
||||
_actions.emit(StatusAction.ShowPermanentMessage(text, type))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use StatusAction.CancelPermanentMessage via trigger() if needed.
|
||||
*/
|
||||
@Deprecated("Use StatusAction.CancelPermanentMessage via trigger() if needed")
|
||||
fun cancelPermanentMessage() {
|
||||
scope.launch {
|
||||
_actions.emit(StatusAction.CancelPermanentMessage)
|
||||
}
|
||||
trigger(StatusAction.CancelPermanentMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use StatusAction.HideMessageBar via trigger() if needed.
|
||||
*/
|
||||
@Deprecated("Use StatusAction.HideMessageBar via trigger() if needed")
|
||||
fun hideMessageBar() {
|
||||
scope.launch {
|
||||
_actions.emit(StatusAction.HideMessageBar)
|
||||
}
|
||||
trigger(StatusAction.HideMessageBar)
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use StatusAction.CancelAllMessages via trigger() if needed.
|
||||
*/
|
||||
@Deprecated("Use StatusAction.CancelAllMessages via trigger() if needed")
|
||||
fun cancelAllMessages() {
|
||||
scope.launch {
|
||||
_actions.emit(StatusAction.CancelAllMessages)
|
||||
}
|
||||
trigger(StatusAction.CancelAllMessages)
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use showActionableMessageById() instead for internationalization support.
|
||||
*/
|
||||
@Deprecated("Use showActionableMessageById() for internationalization support", ReplaceWith("showActionableMessageById(messageId)"))
|
||||
fun showActionableMessage(text: String, type: MessageDisplayType, action: MessageAction) {
|
||||
scope.launch {
|
||||
_actions.emit(StatusAction.ShowActionableMessage(text, type, action))
|
||||
}
|
||||
}
|
||||
|
||||
// === NEW ID-BASED METHODS (for internationalization) ===
|
||||
|
||||
/**
|
||||
* Shows a message by its ID. The actual text is resolved by StatusViewModel using string resources.
|
||||
* @param messageId The StatusMessageId that maps to a string resource
|
||||
* @param type Optional override for the display type
|
||||
* @param timeoutInSeconds Optional override for the timeout
|
||||
*/
|
||||
fun showMessageById(
|
||||
messageId: StatusMessageId,
|
||||
type: MessageDisplayType = messageId.defaultType,
|
||||
timeoutInSeconds: Int = messageId.defaultTimeout
|
||||
) {
|
||||
trigger(StatusAction.ShowMessageById(messageId, type, timeoutInSeconds))
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a permanent message (until dismissed) by its ID.
|
||||
*/
|
||||
fun showPermanentMessageById(
|
||||
messageId: StatusMessageId,
|
||||
type: MessageDisplayType = messageId.defaultType
|
||||
) {
|
||||
trigger(StatusAction.ShowPermanentMessageById(messageId, type))
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows an actionable message by its ID with an optional action.
|
||||
*/
|
||||
fun showActionableMessageById(
|
||||
messageId: StatusMessageId,
|
||||
type: MessageDisplayType = messageId.defaultType,
|
||||
action: MessageAction = messageId.associatedAction ?: MessageAction.NAVIGATE_TO_API_KEYS
|
||||
) {
|
||||
trigger(StatusAction.ShowActionableMessageById(messageId, type, action))
|
||||
}
|
||||
|
||||
// Convenience methods for common message types
|
||||
|
||||
fun showErrorById(messageId: StatusMessageId, timeoutInSeconds: Int = messageId.defaultTimeout) {
|
||||
showMessageById(messageId, MessageDisplayType.ERROR, timeoutInSeconds)
|
||||
}
|
||||
|
||||
fun showSuccessById(messageId: StatusMessageId, timeoutInSeconds: Int = messageId.defaultTimeout) {
|
||||
showMessageById(messageId, MessageDisplayType.SUCCESS, timeoutInSeconds)
|
||||
}
|
||||
|
||||
fun showInfoById(messageId: StatusMessageId, timeoutInSeconds: Int = messageId.defaultTimeout) {
|
||||
showMessageById(messageId, MessageDisplayType.INFO, timeoutInSeconds)
|
||||
}
|
||||
|
||||
fun showLoadingById(messageId: StatusMessageId, timeoutInSeconds: Int = messageId.defaultTimeout) {
|
||||
showMessageById(messageId, MessageDisplayType.LOADING, timeoutInSeconds)
|
||||
}
|
||||
}
|
||||
@@ -117,6 +117,7 @@ class TranslationService(private val context: Context) {
|
||||
}
|
||||
|
||||
suspend fun translateSentence(sentence: String): Result<TranslationHistoryItem> = withContext(Dispatchers.IO) {
|
||||
val statusMessageService = StatusMessageService
|
||||
val additionalInstructions = settingsRepository.customPromptTranslation.flow.first()
|
||||
val selectedSource = languageRepository.loadSelectedSourceLanguage().first()
|
||||
val sourceLangName = selectedSource?.englishName ?: "Auto"
|
||||
|
||||
@@ -5,11 +5,9 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -42,7 +40,7 @@ import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.PrimaryButton
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
|
||||
@Composable
|
||||
fun IntroNavHost(onIntroFinished: () -> Unit) {
|
||||
val pages = listOf(
|
||||
@@ -55,9 +53,16 @@ fun IntroNavHost(onIntroFinished: () -> Unit) {
|
||||
title = stringResource(R.string.intro_title_ai_assistant),
|
||||
description = stringResource(R.string.intro_desc_ai_assistant),
|
||||
content = {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
IconContent(iconRes = R.drawable.ic_intro_ai_agents)
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), verticalArrangement = Arrangement.spacedBy(2.dp), modifier = Modifier.fillMaxWidth()) {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
SuggestionChip(onClick = { }, label = { Text(stringResource(R.string.text_mistral)) })
|
||||
SuggestionChip(onClick = { }, label = { Text(stringResource(R.string.text_your_own_ai)) })
|
||||
SuggestionChip(onClick = { }, label = { Text(stringResource(R.string.text_openai)) })
|
||||
@@ -89,7 +94,7 @@ fun IntroNavHost(onIntroFinished: () -> Unit) {
|
||||
IntroPageData(
|
||||
title = stringResource(R.string.intro_title_learning_journey),
|
||||
description = stringResource(R.string.intro_desc_learning_journey),
|
||||
content = { IconContent(iconRes = R.drawable.ic_intro_learning_journey)}
|
||||
content = { IconContent(iconRes = R.drawable.ic_intro_learning_journey) }
|
||||
),
|
||||
IntroPageData(
|
||||
title = stringResource(R.string.intro_title_categories),
|
||||
@@ -128,7 +133,6 @@ fun IntroNavHost(onIntroFinished: () -> Unit) {
|
||||
.statusBarsPadding()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
// Full-width Skip intro button aligned to end but sized like primary (fillMaxWidth)
|
||||
eu.gaudian.translator.view.composable.SecondaryButton(
|
||||
onClick = { onIntroFinished() },
|
||||
text = stringResource(R.string.intro_skip),
|
||||
@@ -145,7 +149,9 @@ fun IntroNavHost(onIntroFinished: () -> Unit) {
|
||||
) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.weight(1f)
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
) { pageIndex ->
|
||||
IntroPage(pageData = pages[pageIndex])
|
||||
}
|
||||
@@ -170,7 +176,7 @@ fun IntroNavHost(onIntroFinished: () -> Unit) {
|
||||
}
|
||||
},
|
||||
text = if (pagerState.currentPage < pages.size - 1) stringResource(R.string.next) else stringResource(R.string.get_started),
|
||||
icon = if (pagerState.currentPage < pages.size - 1)AppIcons.ArrowForwardNoChevron else null,
|
||||
icon = if (pagerState.currentPage < pages.size - 1) AppIcons.ArrowForwardNoChevron else null,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
@@ -189,9 +195,9 @@ private fun IntroPage(pageData: IntroPageData) {
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.CenterVertically),
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.fillMaxSize() // Fixed: This was previously fillMaxHeight()
|
||||
.padding(horizontal = 16.dp)
|
||||
.verticalScroll(rememberScrollState()) // Allow scrolling for larger hint content
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
@@ -240,9 +246,8 @@ private fun IconContent(iconRes: Int) {
|
||||
tint = Color.Unspecified,
|
||||
modifier = Modifier.size(250.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun FlashcardTopicsPreview() {
|
||||
|
||||
@@ -62,6 +62,8 @@ import eu.gaudian.translator.ui.theme.AllThemes
|
||||
import eu.gaudian.translator.ui.theme.ProvideSemanticColors
|
||||
import eu.gaudian.translator.ui.theme.buildColorScheme
|
||||
import eu.gaudian.translator.utils.Log
|
||||
import eu.gaudian.translator.utils.StatusAction
|
||||
import eu.gaudian.translator.utils.StatusMessageService
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppAlertDialog
|
||||
import eu.gaudian.translator.view.composable.BottomNavigationBar
|
||||
@@ -153,6 +155,7 @@ fun TranslatorApp(
|
||||
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val statusViewModel: StatusViewModel = hiltViewModel(activity)
|
||||
val statusMessageService = StatusMessageService
|
||||
|
||||
val navController = rememberNavController()
|
||||
val statusState by statusViewModel.status.collectAsStateWithLifecycle()
|
||||
@@ -304,7 +307,7 @@ fun TranslatorApp(
|
||||
StatusMessageSystem(
|
||||
statusState = statusState,
|
||||
navController = navController,
|
||||
onDismiss = { statusViewModel.hideMessageBar() },
|
||||
onDismiss = { statusMessageService.trigger(StatusAction.HideMessageBar) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
)
|
||||
@@ -408,4 +411,6 @@ private fun AppTheme(
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -41,6 +41,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.view.hints.Hint
|
||||
import eu.gaudian.translator.view.hints.HintBottomSheet
|
||||
import eu.gaudian.translator.view.hints.LocalShowHints
|
||||
|
||||
@@ -48,7 +49,7 @@ import eu.gaudian.translator.view.hints.LocalShowHints
|
||||
fun AppDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
title: (@Composable () -> Unit)? = null,
|
||||
hintContent: @Composable (() -> Unit)? = null,
|
||||
hintContent: Hint? = null,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
// 1. Swipe Resistance: Prevent accidental dismissal
|
||||
@@ -98,7 +99,7 @@ fun AppDialog(
|
||||
if (showBottomSheet) {
|
||||
EnhancedHintBottomSheet(
|
||||
onDismissRequest = { showBottomSheet = false },
|
||||
content = hintContent,
|
||||
content = {hintContent?.Render()},
|
||||
parentTitle = title
|
||||
)
|
||||
}
|
||||
@@ -156,7 +157,7 @@ fun AppAlertDialog(
|
||||
@Composable
|
||||
private fun DialogHeader(
|
||||
title: (@Composable () -> Unit)?,
|
||||
hintContent: @Composable (() -> Unit)?,
|
||||
hintContent: Hint? = null,
|
||||
onHintClick: () -> Unit,
|
||||
onCloseClick: () -> Unit
|
||||
) {
|
||||
@@ -327,7 +328,6 @@ fun AppDialogPreview() {
|
||||
AppDialog(
|
||||
onDismissRequest = {},
|
||||
title = { Text("Dialog Title") },
|
||||
hintContent = { Text("This is a hint.") },
|
||||
content = {
|
||||
Column {
|
||||
Text("Content line 1")
|
||||
@@ -378,7 +378,6 @@ fun AppDialogLongContentPreview() {
|
||||
AppDialog(
|
||||
onDismissRequest = {},
|
||||
title = { Text("Long Content Dialog") },
|
||||
hintContent = { Text("Hint for long content dialog") },
|
||||
content = {
|
||||
Column {
|
||||
Text("This is a long content dialog to test scrolling")
|
||||
|
||||
@@ -35,7 +35,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -159,14 +159,14 @@ private fun MenuItem(
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.shadow(elevation = 8.dp, shape = ComponentDefaults.DefaultShape)
|
||||
.glassmorphic(shape = RoundedCornerShape(16.dp), alpha = 0.4f)
|
||||
.clickable(
|
||||
onClick = onClick,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null
|
||||
),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceContainer
|
||||
color = Color.Transparent // Allow glassmorphic modifier to handle color
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
@@ -197,15 +197,3 @@ private fun MenuItem(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun MenuItemPreview() {
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
MenuItem(
|
||||
text = "Menu Item",
|
||||
imageVector = AppIcons.Add,
|
||||
painter = null,
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
@@ -69,10 +69,8 @@ fun <T : TabItem> AppTabLayout(
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp, horizontal = 8.dp)
|
||||
.height(56.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||
shape = ComponentDefaults.CardShape
|
||||
)
|
||||
// Replace background with glassmorphic extension
|
||||
.glassmorphic(shape = ComponentDefaults.CardShape, alpha = 0.3f)
|
||||
) {
|
||||
val tabWidth = maxWidth / tabs.size
|
||||
|
||||
@@ -89,7 +87,7 @@ fun <T : TabItem> AppTabLayout(
|
||||
.fillMaxHeight()
|
||||
.padding(4.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f), // Glassy indicator
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -25,6 +25,8 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -41,14 +43,21 @@ fun AppTopAppBar(
|
||||
onNavigateBack: (() -> Unit)? = null,
|
||||
navigationIcon: @Composable (() -> Unit)? = null,
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),
|
||||
colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color.Transparent,
|
||||
scrolledContainerColor = Color.Transparent
|
||||
),
|
||||
hintContent: Hint? = null
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
var showBottomSheet by remember { mutableStateOf(false) }
|
||||
|
||||
Surface(
|
||||
modifier = modifier.glassmorphic(shape = RectangleShape, alpha = 0.2f),
|
||||
color = Color.Transparent
|
||||
) {
|
||||
TopAppBar(
|
||||
modifier = modifier.height(56.dp),
|
||||
modifier = Modifier.height(56.dp),
|
||||
windowInsets = WindowInsets(0.dp),
|
||||
colors = colors,
|
||||
title = {
|
||||
@@ -104,6 +113,7 @@ fun AppTopAppBar(
|
||||
},
|
||||
actions = actions
|
||||
)
|
||||
}
|
||||
|
||||
if (showBottomSheet) {
|
||||
HintBottomSheet(
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
@@ -28,6 +29,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
@@ -100,24 +102,25 @@ fun BottomNavigationBar(
|
||||
targetOffsetY = { it }
|
||||
)
|
||||
) {
|
||||
|
||||
val baseHeight = if (showLabels) 80.dp else 56.dp
|
||||
val density = LocalDensity.current
|
||||
val navBarDp = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() }
|
||||
val height = baseHeight + navBarDp
|
||||
|
||||
NavigationBar(
|
||||
modifier = modifier.height(height),
|
||||
containerColor = MaterialTheme.colorScheme.surface, // Cleaner look than surfaceVariant
|
||||
tonalElevation = 8.dp, // Slight elevation for depth
|
||||
modifier = modifier
|
||||
.height(height)
|
||||
// Apply glassmorphism on the top corners
|
||||
.glassmorphic(shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), alpha = 0.35f),
|
||||
containerColor = Color.Transparent, // Let the glass shine through
|
||||
tonalElevation = 0.dp,
|
||||
) {
|
||||
screens.forEach { screen ->
|
||||
val isSelected = screen == selectedItem
|
||||
val title = stringResource(id = screen.title)
|
||||
|
||||
// 1. Spring Animation for the Icon Scale
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = if (isSelected) 1.2f else 1.0f, // Subtle pop effect
|
||||
targetValue = if (isSelected) 1.2f else 1.0f,
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
@@ -129,7 +132,7 @@ fun BottomNavigationBar(
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
if (!isSelected) {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress) // 2. Tactile feedback
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onItemSelected(screen)
|
||||
}
|
||||
},
|
||||
@@ -145,17 +148,16 @@ fun BottomNavigationBar(
|
||||
}
|
||||
} else null,
|
||||
icon = {
|
||||
// 3. Crossfade between Outlined and Filled icons
|
||||
Crossfade(targetState = isSelected, label = "iconFade") { selected ->
|
||||
Icon(
|
||||
imageVector = if (selected) screen.selectedIcon else screen.unselectedIcon,
|
||||
contentDescription = title,
|
||||
modifier = Modifier.scale(scale) // Apply the spring scale
|
||||
modifier = Modifier.scale(scale)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
indicatorColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
indicatorColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f), // Glassy indicator
|
||||
selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
selectedTextColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
|
||||
@@ -5,6 +5,8 @@ package eu.gaudian.translator.view.composable
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -43,6 +45,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.draw.shadow
|
||||
@@ -55,49 +58,51 @@ import androidx.compose.ui.unit.dp
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||
import eu.gaudian.translator.ui.theme.semanticColors
|
||||
import eu.gaudian.translator.view.composable.ComponentDefaults.DefaultElevation
|
||||
|
||||
|
||||
object ComponentDefaults {
|
||||
// Sizing
|
||||
val DefaultButtonHeight = 48.dp
|
||||
val CardPadding = 8.dp
|
||||
|
||||
// Elevation
|
||||
val DefaultElevation = 0.dp
|
||||
val NoElevation = 0.dp
|
||||
|
||||
// Borders
|
||||
val DefaultBorderWidth = 1.dp
|
||||
|
||||
// Shapes
|
||||
val DefaultCornerRadius = 16.dp
|
||||
val CardClipRadius = 8.dp
|
||||
val CardClipRadius = 16.dp // Increased slightly for softer glass look
|
||||
val NoRounding = 0.dp
|
||||
val DefaultShape = RoundedCornerShape(DefaultCornerRadius)
|
||||
val CardClipShape = RoundedCornerShape(CardClipRadius)
|
||||
val CardShape = RoundedCornerShape(DefaultCornerRadius)
|
||||
val NoShape = RoundedCornerShape(NoRounding)
|
||||
|
||||
// Opacity Levels
|
||||
const val ALPHA_HIGH = 0.6f
|
||||
const val ALPHA_MEDIUM = 0.5f
|
||||
const val ALPHA_LOW = 0.3f
|
||||
const val ALPHA_MEDIUM = 0.4f
|
||||
const val ALPHA_LOW = 0.2f // Adjusted for glass
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A styled card container for displaying content with a consistent floating look.
|
||||
*
|
||||
* @param modifier The modifier to be applied to the card.
|
||||
* @param content The content to be displayed inside the card.
|
||||
* Standard Glassmorphism Modifier
|
||||
*/
|
||||
fun Modifier.glassmorphic(
|
||||
shape: Shape = ComponentDefaults.DefaultShape,
|
||||
alpha: Float = ComponentDefaults.ALPHA_LOW,
|
||||
borderAlpha: Float = 0.15f
|
||||
): Modifier = composed {
|
||||
this
|
||||
.shadow(elevation = 8.dp, shape = shape, spotColor = Color.Black.copy(alpha = 0.05f))
|
||||
.clip(shape)
|
||||
.background(MaterialTheme.colorScheme.surface.copy(alpha = alpha))
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = borderAlpha),
|
||||
shape = shape
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppCard(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String? = null,
|
||||
icon: ImageVector? = null, // New optional icon parameter
|
||||
icon: ImageVector? = null,
|
||||
text: String? = null,
|
||||
expandable: Boolean = false,
|
||||
initiallyExpanded: Boolean = false,
|
||||
@@ -110,25 +115,17 @@ fun AppCard(
|
||||
label = "Chevron Rotation"
|
||||
)
|
||||
|
||||
// Check if we need to render the header row
|
||||
// Updated to include icon in the check
|
||||
val hasHeader = title != null || text != null || expandable || icon != null
|
||||
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.shadow(
|
||||
DefaultElevation,
|
||||
shape = ComponentDefaults.CardShape
|
||||
)
|
||||
.clip(ComponentDefaults.CardClipShape)
|
||||
// Animate height changes when expanding/collapsing
|
||||
.glassmorphic(shape = ComponentDefaults.CardShape, alpha = 0.25f)
|
||||
.animateContentSize(),
|
||||
shape = ComponentDefaults.CardShape,
|
||||
color = MaterialTheme.colorScheme.surfaceContainer
|
||||
color = Color.Transparent // Let glassmorphic handle the background
|
||||
) {
|
||||
Column {
|
||||
// --- Header Row ---
|
||||
if (hasHeader) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -137,7 +134,6 @@ fun AppCard(
|
||||
.padding(ComponentDefaults.CardPadding),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 1. Optional Icon on the left
|
||||
if (icon != null) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
@@ -148,7 +144,6 @@ fun AppCard(
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
}
|
||||
|
||||
// 2. Title and Text Column
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
if (!title.isNullOrBlank()) {
|
||||
Text(
|
||||
@@ -157,12 +152,9 @@ fun AppCard(
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
|
||||
// Only show spacer if both title and text exist
|
||||
if (!title.isNullOrBlank() && !text.isNullOrBlank()) {
|
||||
Spacer(Modifier.size(4.dp))
|
||||
}
|
||||
|
||||
if (!text.isNullOrBlank()) {
|
||||
Text(
|
||||
text = text,
|
||||
@@ -172,7 +164,6 @@ fun AppCard(
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Expand Chevron (Far right)
|
||||
if (expandable) {
|
||||
Icon(
|
||||
imageVector = AppIcons.ArrowDropDown,
|
||||
@@ -184,15 +175,12 @@ fun AppCard(
|
||||
}
|
||||
}
|
||||
|
||||
// --- Content Area ---
|
||||
if (!expandable || isExpanded) {
|
||||
Column(
|
||||
modifier = Modifier.padding(
|
||||
start = ComponentDefaults.CardPadding,
|
||||
end = ComponentDefaults.CardPadding,
|
||||
bottom = ComponentDefaults.CardPadding,
|
||||
// If we have a header, remove the top padding so content sits closer to the title.
|
||||
// If no header (legacy behavior), keep the top padding.
|
||||
top = if (hasHeader) 0.dp else ComponentDefaults.CardPadding
|
||||
),
|
||||
content = content
|
||||
@@ -304,31 +292,27 @@ fun AppButton(
|
||||
modifier: Modifier? = Modifier,
|
||||
enabled: Boolean = true,
|
||||
shape: Shape? = null,
|
||||
colors: ButtonColors = ButtonDefaults.buttonColors(),
|
||||
elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
|
||||
colors: ButtonColors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f) // Glassy primary
|
||||
),
|
||||
elevation: ButtonElevation? = ButtonDefaults.buttonElevation(defaultElevation = 0.dp),
|
||||
border: BorderStroke? = null,
|
||||
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
content: @Composable RowScope.() -> Unit
|
||||
) {
|
||||
|
||||
val m = modifier ?: Modifier.height(ComponentDefaults.DefaultButtonHeight)
|
||||
val s = shape ?: ComponentDefaults.DefaultShape
|
||||
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = m,
|
||||
modifier = m.border(1.dp, MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.2f), s),
|
||||
enabled = enabled,
|
||||
shape = s,
|
||||
colors = colors,
|
||||
elevation = elevation,
|
||||
border = border,
|
||||
contentPadding = PaddingValues(
|
||||
start = 8.dp, // More horizontal padding
|
||||
end = 8.dp,
|
||||
top = 8.dp, // Default vertical padding
|
||||
bottom = 8.dp
|
||||
),
|
||||
contentPadding = PaddingValues(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 8.dp),
|
||||
interactionSource = interactionSource
|
||||
) {
|
||||
content()
|
||||
@@ -368,11 +352,7 @@ fun AppOutlinedButton(
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PrimaryButtonWithIconPreview() {
|
||||
PrimaryButton(onClick = { }, text = stringResource(R.string.primary_with_icon), icon = AppIcons.Add)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The secondary button for less prominent actions.
|
||||
|
||||
@@ -48,7 +48,7 @@ import eu.gaudian.translator.view.composable.AppDialog
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedTextField
|
||||
import eu.gaudian.translator.view.composable.MultipleLanguageDropdown
|
||||
import eu.gaudian.translator.view.hints.CategoryHint
|
||||
import eu.gaudian.translator.view.hints.HintDefinition
|
||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
|
||||
@@ -80,7 +80,7 @@ fun AddCategoryDialog(
|
||||
AppDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(R.string.label_add_category)) },
|
||||
hintContent = { CategoryHint() },
|
||||
hintContent = HintDefinition.CATEGORY.hint(),
|
||||
content = {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
|
||||
|
||||
@@ -41,6 +41,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.VocabularyItem
|
||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||
import eu.gaudian.translator.utils.StatusMessageService
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.LocalConnectionConfigured
|
||||
import eu.gaudian.translator.view.composable.AppButton
|
||||
@@ -50,7 +51,6 @@ import eu.gaudian.translator.view.composable.AppOutlinedTextField
|
||||
import eu.gaudian.translator.view.composable.SourceLanguageDropdown
|
||||
import eu.gaudian.translator.view.composable.TargetLanguageDropdown
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
import eu.gaudian.translator.viewmodel.StatusViewModel
|
||||
import eu.gaudian.translator.viewmodel.TranslationViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -67,12 +67,12 @@ fun AddVocabularyDialog(
|
||||
showMultiple: Boolean = true
|
||||
) {
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val statusViewModel: StatusViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val vocabularyViewModel = hiltViewModel<VocabularyViewModel>(viewModelStoreOwner = activity)
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val translationViewModel: TranslationViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val connectionConfigured = LocalConnectionConfigured.current
|
||||
val statusMessageService = StatusMessageService
|
||||
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ fun AddVocabularyDialog(
|
||||
selectedTranslations.clear()
|
||||
}
|
||||
.onFailure { exception ->
|
||||
statusViewModel.showErrorMessage(
|
||||
statusMessageService.showErrorMessage(
|
||||
textFailedToGetTranslations + exception.message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ import eu.gaudian.translator.view.composable.DialogButton
|
||||
import eu.gaudian.translator.view.composable.InspiringSearchField
|
||||
import eu.gaudian.translator.view.composable.SourceLanguageDropdown
|
||||
import eu.gaudian.translator.view.composable.TargetLanguageDropdown
|
||||
import eu.gaudian.translator.view.hints.ImportVocabularyHint
|
||||
import eu.gaudian.translator.view.hints.HintDefinition
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -109,7 +109,7 @@ fun ImportDialogContent(
|
||||
AppDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(descriptionText) },
|
||||
hintContent = { ImportVocabularyHint() },
|
||||
hintContent = HintDefinition.IMPORT.hint(),
|
||||
content = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -34,7 +34,7 @@ import eu.gaudian.translator.view.composable.AppButton
|
||||
import eu.gaudian.translator.view.composable.AppCheckbox
|
||||
import eu.gaudian.translator.view.composable.AppScaffold
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.view.hints.getVocabularyReviewHint
|
||||
import eu.gaudian.translator.view.hints.HintDefinition
|
||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
|
||||
@@ -66,7 +66,7 @@ fun VocabularyReviewScreen(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = { Text(stringResource(R.string.found_items)) },
|
||||
hintContent = getVocabularyReviewHint()
|
||||
hintContent = HintDefinition.REVIEW.hint()
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
|
||||
@@ -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
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -9,9 +11,9 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.jeziellago.compose.markdowntext.MarkdownText
|
||||
import eu.gaudian.translator.utils.Log
|
||||
|
||||
private const val TAG = "MarkdownHint"
|
||||
|
||||
@@ -25,7 +27,7 @@ sealed class HintElement {
|
||||
data class UIElement(val composable: @Composable () -> Unit) : HintElement()
|
||||
|
||||
/**
|
||||
* A localized markdown file element.
|
||||
* A localized Markdown file element.
|
||||
* The file is loaded from assets based on the current device locale.
|
||||
* Follows Android's locale-qualified resource pattern:
|
||||
* - assets/hints/ - Default (English)
|
||||
@@ -56,7 +58,7 @@ fun RenderHintElement(element: HintElement) {
|
||||
androidx.compose.foundation.layout.Row(
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
) {
|
||||
androidx.compose.material3.Text(
|
||||
Text(
|
||||
text = "[DEBUG: ${element.fileName}]",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
@@ -68,7 +70,7 @@ fun RenderHintElement(element: HintElement) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable to render localized markdown content.
|
||||
* Composable to render localized Markdown content.
|
||||
* Automatically loads the correct locale version based on device settings.
|
||||
* Falls back to English default if localized version is not available.
|
||||
*/
|
||||
@@ -85,23 +87,23 @@ fun LocalizedMarkdownContent(
|
||||
// Try localized version (folder has suffix, filename doesn't)
|
||||
val localizedPath = "hints$suffix/$fileName.md"
|
||||
|
||||
android.util.Log.d(TAG, "Loading hint: $fileName")
|
||||
android.util.Log.d(TAG, "Device locale: ${locale.language}_${locale.country}")
|
||||
android.util.Log.d(TAG, "Localized path: $localizedPath")
|
||||
Log.d(TAG, "Loading hint: $fileName")
|
||||
Log.d(TAG, "Device locale: ${locale.language}_${locale.country}")
|
||||
Log.d(TAG, "Localized path: $localizedPath")
|
||||
|
||||
val localized = MarkdownHintLoader.loadFromAssets(context, localizedPath)
|
||||
if (localized != null) {
|
||||
android.util.Log.d(TAG, "Found localized version at: $localizedPath")
|
||||
Log.d(TAG, "Found localized version at: $localizedPath")
|
||||
localized
|
||||
} else {
|
||||
// Fall back to English default in hints folder
|
||||
val defaultPath = "hints/$fileName.md"
|
||||
android.util.Log.d(TAG, "Localized not found, trying default: $defaultPath")
|
||||
Log.d(TAG, "Localized not found, trying default: $defaultPath")
|
||||
val default = MarkdownHintLoader.loadFromAssets(context, defaultPath)
|
||||
if (default != null) {
|
||||
android.util.Log.d(TAG, "Found default version at: $defaultPath")
|
||||
Log.d(TAG, "Found default version at: $defaultPath")
|
||||
} else {
|
||||
android.util.Log.e(TAG, "No hint found for: $fileName (tried: $localizedPath, $defaultPath)")
|
||||
Log.e(TAG, "No hint found for: $fileName (tried: $localizedPath, $defaultPath)")
|
||||
}
|
||||
default
|
||||
}
|
||||
@@ -125,22 +127,3 @@ fun LocalizedMarkdownContent(
|
||||
}
|
||||
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview
|
||||
@Composable
|
||||
fun UIElementPreview() {
|
||||
RenderHintElement(
|
||||
HintElement.UIElement {
|
||||
Text("Custom UI Element")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview
|
||||
@Composable
|
||||
fun LocalizedMarkdownElementPreview() {
|
||||
RenderHintElement(
|
||||
HintElement.LocalizedMarkdown("example_hint")
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||
import eu.gaudian.translator.view.LocalShowExperimentalFeatures
|
||||
import eu.gaudian.translator.view.composable.AppCard
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
@@ -42,15 +40,16 @@ fun HintsOverviewScreen(
|
||||
val showExperimental = LocalShowExperimentalFeatures.current
|
||||
|
||||
// Get hints using the new function-based approach
|
||||
val importHint = getImportVocabularyHint()
|
||||
val addModelScanHint = getAddModelScanHint()
|
||||
val dictionaryOptionsHint = getDictionaryOptionsHint()
|
||||
val translationScreenHint = getTranslationScreenHint()
|
||||
val categoryHint = getCategoryHint()
|
||||
val learningStagesHint = getLearningStagesHint()
|
||||
val sortingScreenHint = getSortingScreenHint()
|
||||
val vocabularyProgressHint = getVocabularyProgressHint()
|
||||
val apiKeyHint = getApiKeyHint()
|
||||
val importHint = HintDefinition.IMPORT.hint()
|
||||
val addModelScanHint = HintDefinition.ADD_MODEL_SCAN.hint()
|
||||
val dictionaryOptionsHint = HintDefinition.DICTIONARY_OPTIONS.hint()
|
||||
val translationScreenHint = HintDefinition.TRANSLATION.hint()
|
||||
val categoryHint = HintDefinition.CATEGORY.hint()
|
||||
val learningStagesHint = HintDefinition.LEARNING_STAGES.hint()
|
||||
val sortingScreenHint = HintDefinition.SORTING.hint()
|
||||
val vocabularyProgressHint = HintDefinition.VOCABULARY_PROGRESS.hint()
|
||||
val apiKeyHint = HintDefinition.API_KEY.hint()
|
||||
|
||||
|
||||
val hintGroups = remember(showExperimental, importHint, addModelScanHint, dictionaryOptionsHint, translationScreenHint) {
|
||||
val allGroups = listOf(
|
||||
@@ -133,11 +132,7 @@ fun HintsOverviewScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun HintsOverviewScreenPreview() {
|
||||
HintsOverviewScreen(navController = rememberNavController())
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun HintHeader(
|
||||
@@ -176,12 +171,4 @@ private fun HintListItem(
|
||||
)
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun HintListItemPreview() {
|
||||
HintListItem(
|
||||
title = stringResource(R.string.category_hint_intro),
|
||||
icon = AppIcons.Category,
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
* Internationalization system for markdown hints.
|
||||
* Internationalization system for Markdown hints.
|
||||
*
|
||||
* This follows Android's locale-qualified resource pattern:
|
||||
* - assets/hints/ - Default (English)
|
||||
@@ -18,131 +18,22 @@ import java.util.Locale
|
||||
*/
|
||||
object MarkdownHintLoader {
|
||||
|
||||
/**
|
||||
* Load markdown content with automatic locale detection.
|
||||
*
|
||||
* @param context The context for accessing assets
|
||||
* @param hintFileName The base filename without locale suffix (e.g., "api_key_hint")
|
||||
* @return The markdown content as a string, or null if not found
|
||||
*/
|
||||
fun loadHint(context: Context, hintFileName: String): String? {
|
||||
val locale = getCurrentLocale(context)
|
||||
val suffix = getLocaleSuffix(locale)
|
||||
|
||||
// Try localized version (folder has suffix, filename doesn't)
|
||||
val localizedPath = "hints$suffix/$hintFileName.md"
|
||||
val localizedContent = loadFromAssets(context, localizedPath)
|
||||
if (localizedContent != null) {
|
||||
return localizedContent
|
||||
}
|
||||
|
||||
// Try with just language code (e.g., hints-pt/ instead of hints-pt-rBR/)
|
||||
val languageSuffix = if (locale.country.isNotEmpty()) "-${locale.language}" else ""
|
||||
val languageOnlyPath = "hints$languageSuffix/$hintFileName.md"
|
||||
val languageContent = loadFromAssets(context, languageOnlyPath)
|
||||
if (languageContent != null) {
|
||||
return languageContent
|
||||
}
|
||||
|
||||
// Fall back to default (English) in hints folder
|
||||
val defaultPath = "hints/$hintFileName.md"
|
||||
val defaultContent = loadFromAssets(context, defaultPath)
|
||||
if (defaultContent != null) {
|
||||
return defaultContent
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the localized file path for a hint.
|
||||
*
|
||||
* @param hintFileName The base filename (e.g., "api_key_hint")
|
||||
* @return The full path including locale folder (e.g., "hints-de-rDE/api_key_hint.md")
|
||||
*/
|
||||
fun getHintPath(hintFileName: String): String {
|
||||
val locale = Locale.getDefault()
|
||||
val localeSuffix = getLocaleSuffix(locale)
|
||||
return "hints$localeSuffix/$hintFileName.md"
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the localized file name for a hint.
|
||||
*
|
||||
* @param hintFileName The base filename (e.g., "api_key_hint")
|
||||
* @param locale The target locale
|
||||
* @return The file name with locale suffix (e.g., "api_key_hint-de-rDE.md")
|
||||
*/
|
||||
fun getLocalizedFileName(hintFileName: String, locale: Locale): String {
|
||||
val localeSuffix = getLocaleSuffix(locale)
|
||||
return "$hintFileName$localeSuffix.md"
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file name with language-only suffix.
|
||||
*
|
||||
* @param hintFileName The base filename
|
||||
* @param locale The target locale
|
||||
* @return The file name with language suffix (e.g., "api_key_hint-de.md")
|
||||
*/
|
||||
private fun getLanguageOnlyFileName(hintFileName: String, locale: Locale): String {
|
||||
val languageCode = locale.language
|
||||
return "$hintFileName-$languageCode.md"
|
||||
}
|
||||
|
||||
|
||||
fun getCurrentLocale(context: Context): Locale {
|
||||
return context.resources.configuration.locale
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all supported locales for hints.
|
||||
*/
|
||||
fun getSupportedLocales(): List<Locale> {
|
||||
return listOf(
|
||||
Locale.ENGLISH, // Default
|
||||
Locale.GERMAN, // de-rDE
|
||||
Locale("pt", "BR"), // pt-rBR
|
||||
Locale.FRENCH, // fr-rFR
|
||||
Locale("es", "ES"), // es-rES
|
||||
Locale.ITALIAN, // it-rIT
|
||||
Locale("nl", "NL"), // nl-rNL
|
||||
Locale("hr", "HR") // hr-rHR
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a localized version exists for the given locale.
|
||||
*/
|
||||
fun localizedVersionExists(context: Context, hintFileName: String, locale: Locale): Boolean {
|
||||
val localizedFileName = getLocalizedFileName(hintFileName, locale)
|
||||
val suffix = getLocaleSuffix(locale)
|
||||
return assetExists(context, "hints$suffix/$localizedFileName")
|
||||
}
|
||||
|
||||
/**
|
||||
* Load content from assets.
|
||||
*/
|
||||
fun loadFromAssets(context: Context, fileName: String): String? {
|
||||
return try {
|
||||
context.assets.open(fileName).bufferedReader().use { it.readText() }
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an asset file exists.
|
||||
*/
|
||||
private fun assetExists(context: Context, path: String): Boolean {
|
||||
return try {
|
||||
context.assets.open(path).close()
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the locale suffix string.
|
||||
*/
|
||||
@@ -163,55 +54,3 @@ object MarkdownHintLoader {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension function to get localized hint content.
|
||||
*/
|
||||
fun Context.loadLocalizedHint(hintFileName: String): String? {
|
||||
return MarkdownHintLoader.loadHint(this, hintFileName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class for localized hint information.
|
||||
*/
|
||||
data class LocalizedHint(
|
||||
val fileName: String,
|
||||
val locale: Locale,
|
||||
val isDefault: Boolean = false
|
||||
)
|
||||
|
||||
/**
|
||||
* Hint localization manager that tracks available translations.
|
||||
*/
|
||||
object HintLocalizationManager {
|
||||
|
||||
/**
|
||||
* Get all available translations for a hint.
|
||||
*/
|
||||
fun getAvailableTranslations(context: Context, baseFileName: String): List<LocalizedHint> {
|
||||
val available = mutableListOf<LocalizedHint>()
|
||||
val defaultLocale = MarkdownHintLoader.getCurrentLocale(context)
|
||||
|
||||
// Check each supported locale
|
||||
MarkdownHintLoader.getSupportedLocales().forEach { locale ->
|
||||
val fileName = MarkdownHintLoader.getLocalizedFileName(baseFileName, locale)
|
||||
val path = "hints${MarkdownHintLoader.getLocaleSuffix(locale)}/$fileName"
|
||||
|
||||
if (MarkdownHintLoader.loadFromAssets(context, path) != null) {
|
||||
available.add(LocalizedHint(
|
||||
fileName = fileName,
|
||||
locale = locale,
|
||||
isDefault = locale.language == "en" && locale.country.isEmpty()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
return available.sortedBy { it.locale.language }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the best available translation for current locale.
|
||||
*/
|
||||
fun getBestTranslation(context: Context, baseFileName: String): String? {
|
||||
return MarkdownHintLoader.loadHint(context, baseFileName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.R
|
||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||
import eu.gaudian.translator.utils.StatusAction
|
||||
import eu.gaudian.translator.utils.StatusMessageId
|
||||
import eu.gaudian.translator.utils.StatusMessageService
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppCard
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
@@ -52,7 +55,6 @@ import eu.gaudian.translator.view.composable.AppSwitch
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.view.composable.SecondaryButton
|
||||
import eu.gaudian.translator.viewmodel.SettingsViewModel
|
||||
import eu.gaudian.translator.viewmodel.StatusViewModel
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@@ -371,7 +373,7 @@ private fun DeveloperOptions(
|
||||
val context = LocalContext.current
|
||||
|
||||
val activity = context.findActivity()
|
||||
val statusViewModel: StatusViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val statusMessageService = StatusMessageService
|
||||
val settingsViewModel: SettingsViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
|
||||
|
||||
@@ -420,38 +422,35 @@ private fun DeveloperOptions(
|
||||
)
|
||||
}
|
||||
|
||||
val loadingText = stringResource(R.string.text_loading_3d)
|
||||
val infoText = stringResource(R.string.text_sentence_this_is_an_info_message)
|
||||
val successText = stringResource(R.string.text_success_em)
|
||||
val errorText = stringResource(R.string.text_sentence_oops_something_went_wrong)
|
||||
|
||||
|
||||
SecondaryButton(
|
||||
onClick = { statusViewModel.showLoadingMessage(loadingText) },
|
||||
onClick = { statusMessageService.showMessageById(StatusMessageId.LOADING_GENERIC) },
|
||||
text = stringResource(R.string.text_show_loading),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
SecondaryButton(
|
||||
onClick = { statusViewModel.cancelLoadingOperation() },
|
||||
onClick = { statusMessageService.trigger(StatusAction.CancelLoadingOperation)},
|
||||
text = stringResource(R.string.text_cancel_loading),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
SecondaryButton(
|
||||
onClick = { statusViewModel.showInfoMessage(infoText) },
|
||||
onClick = { statusMessageService.showInfoById(StatusMessageId.TEST_INFO) },
|
||||
text = stringResource(R.string.text_show_info_message),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
SecondaryButton(
|
||||
onClick = { statusViewModel.showSuccessMessage(successText) },
|
||||
onClick = { statusMessageService.showSuccessById(StatusMessageId.TEST_SUCCESS) },
|
||||
text = stringResource(R.string.title_show_success_message),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
SecondaryButton(
|
||||
onClick = { statusViewModel.showErrorMessage(errorText, 2) },
|
||||
onClick = { statusMessageService.showErrorById(StatusMessageId.TEST_ERROR) },
|
||||
text = stringResource(R.string.text_show_error_message),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
SecondaryButton(
|
||||
onClick = { statusViewModel.showApiKeyMissingMessage() },
|
||||
onClick = { statusMessageService.showErrorById(StatusMessageId.ERROR_API_KEY_MISSING) },
|
||||
text = stringResource(R.string.show_api_key_missing_message),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
@@ -58,8 +58,7 @@ import eu.gaudian.translator.view.composable.AppScaffold
|
||||
import eu.gaudian.translator.view.composable.AppSwitch
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.view.composable.ModelBadges
|
||||
import eu.gaudian.translator.view.hints.AddModelScanHint
|
||||
import eu.gaudian.translator.view.hints.getAddModelScanHint
|
||||
import eu.gaudian.translator.view.hints.HintDefinition
|
||||
import eu.gaudian.translator.viewmodel.ApiViewModel
|
||||
|
||||
@Composable
|
||||
@@ -141,7 +140,7 @@ fun AddModelScreen(navController: NavController, providerKey: String) {
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||
}
|
||||
},
|
||||
hintContent = getAddModelScanHint()
|
||||
hintContent = HintDefinition.ADD_MODEL_SCAN.hint()
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
|
||||
@@ -80,7 +80,7 @@ import eu.gaudian.translator.view.composable.ClickableText
|
||||
import eu.gaudian.translator.view.composable.PrimaryButton
|
||||
import eu.gaudian.translator.view.composable.SecondaryButton
|
||||
import eu.gaudian.translator.view.composable.TabItem
|
||||
import eu.gaudian.translator.view.hints.getApiKeyHint
|
||||
import eu.gaudian.translator.view.hints.HintDefinition
|
||||
import eu.gaudian.translator.viewmodel.ApiKeyManagementState
|
||||
import eu.gaudian.translator.viewmodel.ApiViewModel
|
||||
import eu.gaudian.translator.viewmodel.ProviderState
|
||||
@@ -121,7 +121,7 @@ fun ApiKeyScreen(navController: NavController) {
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||
}
|
||||
},
|
||||
hintContent = getApiKeyHint()
|
||||
hintContent = HintDefinition.API_KEY.hint()
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
|
||||
@@ -36,7 +36,7 @@ import eu.gaudian.translator.view.composable.AppScaffold
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.view.composable.OptionItemSwitch
|
||||
import eu.gaudian.translator.view.dictionary.DictionaryManagerContent
|
||||
import eu.gaudian.translator.view.hints.getDictionaryOptionsHint
|
||||
import eu.gaudian.translator.view.hints.HintDefinition
|
||||
import eu.gaudian.translator.viewmodel.ApiViewModel
|
||||
import eu.gaudian.translator.viewmodel.DictionaryViewModel
|
||||
import eu.gaudian.translator.viewmodel.SettingsViewModel
|
||||
@@ -72,7 +72,7 @@ fun DictionaryOptionsScreen(
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||
}
|
||||
},
|
||||
hintContent = getDictionaryOptionsHint()
|
||||
hintContent = HintDefinition.DICTIONARY_OPTIONS.hint()
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
|
||||
@@ -70,15 +70,24 @@ fun LanguageOptionsScreen(
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
item {
|
||||
AppCard {
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
|
||||
AppCard(
|
||||
title = stringResource(R.string.text_select_languages),
|
||||
text = stringResource(R.string.text_language_settings_description),
|
||||
expandable = true,
|
||||
initiallyExpanded = false
|
||||
) {
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -109,7 +118,10 @@ fun LanguageOptionsScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
PrimaryButton(
|
||||
onClick = { showAddLanguageDialog = true },
|
||||
text = stringResource(R.string.text_add_custom_language),
|
||||
@@ -117,9 +129,9 @@ fun LanguageOptionsScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showAddLanguageDialog) {
|
||||
@Suppress("KotlinConstantConditions")
|
||||
AddCustomLanguageDialog(
|
||||
showDialog = showAddLanguageDialog,
|
||||
onDismiss = { showAddLanguageDialog = false },
|
||||
|
||||
@@ -47,7 +47,7 @@ fun MainSettingsScreen(
|
||||
Setting(R.string.settings_title_voice, AppIcons.TextToSpeech, SettingsRoutes.TTS_OPTIONS),
|
||||
Setting(R.string.label_logs, AppIcons.Log, SettingsRoutes.LOGS),
|
||||
Setting(R.string.label_languages, AppIcons.Language, SettingsRoutes.LANGUAGE_OPTIONS),
|
||||
Setting(R.string.hint_settings_title_hints, AppIcons.Info, SettingsRoutes.HINTS_OVERVIEW)
|
||||
//Setting(R.string.hint_settings_title_help, AppIcons.Info, SettingsRoutes.HINTS_OVERVIEW)
|
||||
|
||||
),
|
||||
R.string.settings_header_translator to listOf(
|
||||
|
||||
@@ -7,16 +7,9 @@ import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navigation
|
||||
import eu.gaudian.translator.view.composable.Screen
|
||||
import eu.gaudian.translator.view.hints.ApiHintScreen
|
||||
import eu.gaudian.translator.view.hints.CategoryHintScreenWrapper
|
||||
import eu.gaudian.translator.view.hints.DictionaryHintScreen
|
||||
import eu.gaudian.translator.view.hints.HintDefinition
|
||||
import eu.gaudian.translator.view.hints.HintScreen
|
||||
import eu.gaudian.translator.view.hints.HintsOverviewScreen
|
||||
import eu.gaudian.translator.view.hints.ImportHintScreen
|
||||
import eu.gaudian.translator.view.hints.ScanHintScreen
|
||||
import eu.gaudian.translator.view.hints.SortingHintScreen
|
||||
import eu.gaudian.translator.view.hints.StagesHintScreen
|
||||
import eu.gaudian.translator.view.hints.TranslationHintScreen
|
||||
import eu.gaudian.translator.view.hints.VocabularyProgressHintScreen
|
||||
|
||||
// Defines the routes for the settings graph to avoid using raw strings
|
||||
object SettingsRoutes {
|
||||
@@ -114,31 +107,31 @@ fun NavGraphBuilder.settingsGraph(navController: NavController) {
|
||||
HintsOverviewScreen(navController = navController)
|
||||
}
|
||||
composable(SettingsRoutes.HINTS_CATEGORIES) {
|
||||
CategoryHintScreenWrapper(navController = navController)
|
||||
HintScreen(navController, HintDefinition.CATEGORY)
|
||||
}
|
||||
composable(SettingsRoutes.HINTS_DICTIONARY) {
|
||||
DictionaryHintScreen(navController = navController)
|
||||
HintScreen(navController, HintDefinition.DICTIONARY_OPTIONS)
|
||||
}
|
||||
composable(SettingsRoutes.HINTS_IMPORT) {
|
||||
ImportHintScreen(navController = navController)
|
||||
HintScreen(navController, HintDefinition.IMPORT)
|
||||
}
|
||||
composable(SettingsRoutes.HINTS_SORTING) {
|
||||
SortingHintScreen(navController = navController)
|
||||
HintScreen(navController, HintDefinition.SORTING)
|
||||
}
|
||||
composable(SettingsRoutes.HINTS_STAGES) {
|
||||
StagesHintScreen(navController = navController)
|
||||
HintScreen(navController, HintDefinition.LEARNING_STAGES)
|
||||
}
|
||||
composable(SettingsRoutes.HINTS_TRANSLATION) {
|
||||
TranslationHintScreen(navController = navController)
|
||||
HintScreen(navController, HintDefinition.TRANSLATION)
|
||||
}
|
||||
composable(SettingsRoutes.HINTS_SCAN) {
|
||||
ScanHintScreen(navController = navController)
|
||||
HintScreen(navController, HintDefinition.ADD_MODEL_SCAN)
|
||||
}
|
||||
composable(SettingsRoutes.HINTS_API) {
|
||||
ApiHintScreen(navController = navController)
|
||||
HintScreen(navController, HintDefinition.API_KEY)
|
||||
}
|
||||
composable(SettingsRoutes.HINTS_VOCABULARY_PROGRESS) {
|
||||
VocabularyProgressHintScreen(navController = navController)
|
||||
HintScreen(navController, HintDefinition.VOCABULARY_PROGRESS)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,8 +49,7 @@ import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppScaffold
|
||||
import eu.gaudian.translator.view.composable.AppSlider
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.view.hints.VocabularyProgressHint
|
||||
import eu.gaudian.translator.view.hints.getVocabularyProgressHint
|
||||
import eu.gaudian.translator.view.hints.HintDefinition
|
||||
import eu.gaudian.translator.viewmodel.SettingsViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
import kotlin.math.exp
|
||||
@@ -86,7 +85,7 @@ fun VocabularyProgressOptionsScreen(
|
||||
}
|
||||
},
|
||||
// Here is the new hint content
|
||||
hintContent = getVocabularyProgressHint()
|
||||
hintContent = HintDefinition.VOCABULARY_PROGRESS.hint()
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
|
||||
@@ -39,6 +39,8 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.navigation.NavController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.Language
|
||||
import eu.gaudian.translator.utils.StatusMessageId
|
||||
import eu.gaudian.translator.utils.StatusMessageService
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppButton
|
||||
import eu.gaudian.translator.view.composable.AppCard
|
||||
@@ -50,7 +52,6 @@ import eu.gaudian.translator.view.composable.PrimaryButton
|
||||
import eu.gaudian.translator.view.composable.SecondaryButton
|
||||
import eu.gaudian.translator.view.composable.SingleLanguageDropDown
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
import eu.gaudian.translator.viewmodel.StatusViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
|
||||
@Composable
|
||||
@@ -60,7 +61,7 @@ fun VocabularyRepositoryOptionsScreen(
|
||||
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val statusViewModel: StatusViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val statusMessageService = StatusMessageService
|
||||
|
||||
|
||||
val context = LocalContext.current
|
||||
@@ -73,7 +74,7 @@ fun VocabularyRepositoryOptionsScreen(
|
||||
context.contentResolver.openInputStream(it)?.use { inputStream ->
|
||||
val jsonString = inputStream.bufferedReader().use { reader -> reader.readText() }
|
||||
vocabularyViewModel.importVocabulary(jsonString)
|
||||
statusViewModel.showInfoMessage(repositoryStateImportedFrom + " " +it.path)
|
||||
statusMessageService.showInfoMessage(repositoryStateImportedFrom + " " +it.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,7 +146,7 @@ fun VocabularyRepositoryOptionsScreen(
|
||||
row.map { it.trim().trim('"') }
|
||||
}.filter { r -> r.any { it.isNotBlank() } }
|
||||
}
|
||||
val textExcelNotSupportedUseCsv = stringResource(R.string.text_excel_not_supported_use_csv)
|
||||
|
||||
val errorParsingTable = stringResource(R.string.error_parsing_table)
|
||||
val errorParsingTableWithReason = stringResource(R.string.error_parsing_table_with_reason)
|
||||
val importTableLauncher = rememberLauncherForActivityResult(
|
||||
@@ -159,7 +160,7 @@ fun VocabularyRepositoryOptionsScreen(
|
||||
val mime = context.contentResolver.getType(u)
|
||||
val isExcel = mime == "application/vnd.ms-excel" || mime == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
if (isExcel) {
|
||||
statusViewModel.showInfoMessage(textExcelNotSupportedUseCsv)
|
||||
statusMessageService.showErrorById(StatusMessageId.ERROR_EXCEL_NOT_SUPPORTED)
|
||||
return@let
|
||||
}
|
||||
context.contentResolver.openInputStream(u)?.use { inputStream ->
|
||||
@@ -173,12 +174,12 @@ fun VocabularyRepositoryOptionsScreen(
|
||||
parseError = null
|
||||
} else {
|
||||
parseError = errorParsingTable
|
||||
statusViewModel.showErrorMessage(parseError!!)
|
||||
statusMessageService.showErrorMessage(parseError!!)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
parseError = e.message
|
||||
statusViewModel.showErrorMessage(
|
||||
statusMessageService.showErrorMessage(
|
||||
(errorParsingTableWithReason + " " + e.message)
|
||||
)
|
||||
}
|
||||
@@ -394,13 +395,13 @@ fun VocabularyRepositoryOptionsScreen(
|
||||
val infoImportedItemsFrom = stringResource(R.string.info_imported_items_from)
|
||||
TextButton(onClick = {
|
||||
if (selectedColFirst == selectedColSecond) {
|
||||
statusViewModel.showErrorMessage(errorSelectTwoColumns)
|
||||
statusMessageService.showErrorMessage(errorSelectTwoColumns)
|
||||
return@TextButton
|
||||
}
|
||||
val langA = selectedLangFirst
|
||||
val langB = selectedLangSecond
|
||||
if (langA == null || langB == null) {
|
||||
statusViewModel.showErrorMessage(errorSelectLanguages)
|
||||
statusMessageService.showErrorMessage(errorSelectLanguages)
|
||||
return@TextButton
|
||||
}
|
||||
val startIdx = if (skipHeader) 1 else 0
|
||||
@@ -416,11 +417,11 @@ fun VocabularyRepositoryOptionsScreen(
|
||||
)
|
||||
}
|
||||
if (items.isEmpty()) {
|
||||
statusViewModel.showErrorMessage(errorNoRowsToImport)
|
||||
statusMessageService.showErrorMessage(errorNoRowsToImport)
|
||||
return@TextButton
|
||||
}
|
||||
vocabularyViewModel.addVocabularyItems(items)
|
||||
statusViewModel.showSuccessMessage(infoImportedItemsFrom + " " +items.size)
|
||||
statusMessageService.showSuccessMessage(infoImportedItemsFrom + " " +items.size)
|
||||
showTableImportDialog.value = false
|
||||
}) { Text(stringResource(R.string.label_import)) }
|
||||
},
|
||||
|
||||
@@ -59,7 +59,7 @@ import eu.gaudian.translator.view.NoConnectionScreen
|
||||
import eu.gaudian.translator.view.composable.AppCard
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedCard
|
||||
import eu.gaudian.translator.view.dialogs.AddVocabularyDialog
|
||||
import eu.gaudian.translator.view.hints.TranslationScreenHint
|
||||
import eu.gaudian.translator.view.hints.HintDefinition
|
||||
import eu.gaudian.translator.view.settings.SettingsRoutes
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
import eu.gaudian.translator.viewmodel.SettingsViewModel
|
||||
@@ -167,7 +167,7 @@ private fun LoadedTranslationContent(
|
||||
TopBarActions(
|
||||
languageViewModel = languageViewModel,
|
||||
onSettingsClick = onSettingsClick,
|
||||
hintContent = { TranslationScreenHint() }
|
||||
hintContent = { HintDefinition.TRANSLATION.Render() }
|
||||
)
|
||||
|
||||
AppCard(modifier = Modifier.padding(8.dp, end = 8.dp, bottom = 8.dp, top = 0.dp)) {
|
||||
|
||||
@@ -6,7 +6,6 @@ import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -391,7 +390,6 @@ private fun ExerciseTypeSelector(
|
||||
onTypeSelected: (VocabularyExerciseType) -> Unit,
|
||||
) {
|
||||
// Using FlowRow for a more flexible layout that wraps to the next line if needed
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
FlowRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
|
||||
|
||||
@@ -74,7 +74,7 @@ import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.view.composable.SingleLanguageDropDown
|
||||
import eu.gaudian.translator.view.dialogs.CategoryDropdown
|
||||
import eu.gaudian.translator.view.dialogs.CreateCategoryListDialog
|
||||
import eu.gaudian.translator.view.hints.getSortingScreenHint
|
||||
import eu.gaudian.translator.view.hints.HintDefinition
|
||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||
import eu.gaudian.translator.viewmodel.LanguageConfigViewModel
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
@@ -236,7 +236,7 @@ fun VocabularySortingScreen(
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||
}
|
||||
},
|
||||
hintContent = getSortingScreenHint()
|
||||
hintContent = HintDefinition.SORTING.hint()
|
||||
)
|
||||
},
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import eu.gaudian.translator.utils.Log
|
||||
import eu.gaudian.translator.utils.StatusAction
|
||||
import eu.gaudian.translator.utils.StatusMessageId
|
||||
import eu.gaudian.translator.utils.StatusMessageService
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -45,6 +46,16 @@ enum class MessageDisplayType(val priority: Int) {
|
||||
ACTIONABLE_ERROR(5)
|
||||
}
|
||||
|
||||
/**
|
||||
* StatusViewModel is responsible for:
|
||||
* 1. Collecting status actions from StatusMessageService
|
||||
* 2. Managing the message queue
|
||||
* 3. Resolving StatusMessageId to actual strings
|
||||
* 4. Managing status state
|
||||
*
|
||||
* NOTE: All message display requests should go through StatusMessageService.
|
||||
* This ViewModel should NOT be called directly to display messages.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class StatusViewModel @Inject constructor(
|
||||
application: Application,
|
||||
@@ -67,9 +78,14 @@ class StatusViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles all status actions from StatusMessageService.
|
||||
* This is the main entry point for all status messages.
|
||||
*/
|
||||
private fun handleAction(action: StatusAction) {
|
||||
Log.d("StatusViewModel", "Received action: $action")
|
||||
when (action) {
|
||||
// Legacy string-based actions (deprecated but still supported for backward compatibility)
|
||||
is StatusAction.ShowMessage -> showMessageInternal(action.text, action.type, action.timeoutInSeconds)
|
||||
is StatusAction.ShowActionableMessage -> showPermanentActionableMessageInternal(action.text, action.type, action.action)
|
||||
is StatusAction.ShowPermanentMessage -> showPermanentMessageInternal(action.text, action.type)
|
||||
@@ -78,16 +94,66 @@ class StatusViewModel @Inject constructor(
|
||||
is StatusAction.CancelPermanentMessage -> cancelPermanentMessageInternal()
|
||||
is StatusAction.HideMessageBar -> hideMessageBarInternal()
|
||||
is StatusAction.CancelAllMessages -> cancelAllMessagesInternal()
|
||||
|
||||
// New ID-based actions for internationalization
|
||||
is StatusAction.ShowMessageById -> showMessageByIdInternal(
|
||||
action.messageId,
|
||||
action.type,
|
||||
action.timeoutInSeconds
|
||||
)
|
||||
is StatusAction.ShowPermanentMessageById -> showPermanentMessageByIdInternal(
|
||||
action.messageId,
|
||||
action.type
|
||||
)
|
||||
is StatusAction.ShowActionableMessageById -> showPermanentActionableMessageByIdInternal(
|
||||
action.messageId,
|
||||
action.type,
|
||||
action.action
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showApiKeyMissingMessage() = viewModelScope.launch {
|
||||
statusMessageService.showActionableMessage(
|
||||
text = "API Key is missing or invalid.",
|
||||
type = MessageDisplayType.ACTIONABLE_ERROR,
|
||||
action = MessageAction.NAVIGATE_TO_API_KEYS
|
||||
)
|
||||
/**
|
||||
* Resolves a StatusMessageId to its actual string text using Android string resources.
|
||||
*/
|
||||
private fun resolveMessageText(messageId: StatusMessageId): String {
|
||||
return try {
|
||||
getApplication<Application>().getString(messageId.stringResId)
|
||||
} catch (e: Exception) {
|
||||
Log.e("StatusViewModel", "Failed to resolve message string for ID: $messageId", e)
|
||||
"Message not available"
|
||||
}
|
||||
}
|
||||
|
||||
// --- ID-based internal methods ---
|
||||
|
||||
private fun showMessageByIdInternal(
|
||||
messageId: StatusMessageId,
|
||||
type: MessageDisplayType,
|
||||
timeoutInSeconds: Int
|
||||
) {
|
||||
val text = resolveMessageText(messageId)
|
||||
showMessageInternal(text, type, timeoutInSeconds)
|
||||
}
|
||||
|
||||
private fun showPermanentMessageByIdInternal(
|
||||
messageId: StatusMessageId,
|
||||
type: MessageDisplayType
|
||||
) {
|
||||
val text = resolveMessageText(messageId)
|
||||
showPermanentMessageInternal(text, type)
|
||||
}
|
||||
|
||||
private fun showPermanentActionableMessageByIdInternal(
|
||||
messageId: StatusMessageId,
|
||||
type: MessageDisplayType,
|
||||
action: MessageAction
|
||||
) {
|
||||
val text = resolveMessageText(messageId)
|
||||
showPermanentActionableMessageInternal(text, type, action)
|
||||
}
|
||||
|
||||
// --- Internal message display methods ---
|
||||
|
||||
private fun showPermanentActionableMessageInternal(message: String, type: MessageDisplayType, action: MessageAction) {
|
||||
cancelAllOperations() // Clear any other messages or loaders.
|
||||
@@ -99,54 +165,6 @@ class StatusViewModel @Inject constructor(
|
||||
_status.value = StatusState.Message(messageIdCounter++, message, type, action = null)
|
||||
}
|
||||
|
||||
fun showPermanentMessage(message: String, type: MessageDisplayType) = viewModelScope.launch {
|
||||
statusMessageService.showPermanentMessage(message, type)
|
||||
}
|
||||
|
||||
fun cancelPermanentMessage() = viewModelScope.launch {
|
||||
statusMessageService.cancelPermanentMessage()
|
||||
}
|
||||
|
||||
fun performLoadingOperation(block: suspend () -> Unit) = viewModelScope.launch {
|
||||
statusMessageService.trigger(StatusAction.PerformLoadingOperation(block))
|
||||
}
|
||||
|
||||
fun cancelLoadingOperation() = viewModelScope.launch {
|
||||
statusMessageService.trigger(StatusAction.CancelLoadingOperation)
|
||||
}
|
||||
|
||||
fun showInfoMessage(message: String, timeoutInSeconds: Int = 3) = viewModelScope.launch {
|
||||
statusMessageService.showInfoMessage(message, timeoutInSeconds)
|
||||
}
|
||||
|
||||
fun showLoadingMessage(message: String, timeoutInSeconds: Int = 0) = viewModelScope.launch { // Default timeout 0 for indefinite
|
||||
statusMessageService.showLoadingMessage(message, timeoutInSeconds)
|
||||
}
|
||||
|
||||
fun showErrorMessage(message: String, timeoutInSeconds: Int = 5) = viewModelScope.launch { // Default timeout 5 for errors
|
||||
statusMessageService.showErrorMessage(message, timeoutInSeconds)
|
||||
}
|
||||
|
||||
fun showSuccessMessage(message: String, timeoutInSeconds: Int = 3) = viewModelScope.launch {
|
||||
statusMessageService.showSuccessMessage(message, timeoutInSeconds)
|
||||
}
|
||||
|
||||
fun hideMessageBar() = viewModelScope.launch {
|
||||
statusMessageService.hideMessageBar()
|
||||
}
|
||||
|
||||
fun cancelAllMessages() = viewModelScope.launch {
|
||||
statusMessageService.cancelAllMessages()
|
||||
}
|
||||
|
||||
private fun cancelPermanentMessageInternal() {
|
||||
if (_status.value is StatusState.Message) {
|
||||
// This logic can be simplified or adjusted based on desired behavior for permanent messages
|
||||
_status.value = StatusState.Hidden
|
||||
processNextMessageInQueue()
|
||||
}
|
||||
}
|
||||
|
||||
private fun performLoadingOperationInternal(block: suspend () -> Unit) {
|
||||
cancelAllOperations()
|
||||
_status.value = StatusState.Loading
|
||||
@@ -159,7 +177,10 @@ class StatusViewModel @Inject constructor(
|
||||
Log.i("StatusViewModel", "Loading operation was cancelled.")
|
||||
} catch (e: Exception) {
|
||||
Log.e("StatusViewModel", "Loading operation failed.", e)
|
||||
showErrorMessage("Operation failed: ${e.localizedMessage ?: "Unknown error"}")
|
||||
// Trigger error message through StatusMessageService
|
||||
viewModelScope.launch {
|
||||
statusMessageService.showErrorById(StatusMessageId.ERROR_OPERATION_FAILED)
|
||||
}
|
||||
} finally {
|
||||
if (activeLoadingJob == this.coroutineContext[Job]) {
|
||||
if (_status.value == StatusState.Loading) {
|
||||
@@ -181,7 +202,38 @@ class StatusViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
// --- REVISED LOGIC ---
|
||||
private fun cancelPermanentMessageInternal() {
|
||||
if (_status.value is StatusState.Message) {
|
||||
_status.value = StatusState.Hidden
|
||||
processNextMessageInQueue()
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideMessageBarInternal() {
|
||||
messageDisplayJob?.cancel()
|
||||
messageDisplayJob = null
|
||||
if (_status.value is StatusState.Message) {
|
||||
_status.value = StatusState.Hidden
|
||||
}
|
||||
if (activeLoadingJob?.isActive != true) {
|
||||
processNextMessageInQueue()
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelAllMessagesInternal() {
|
||||
Log.d("StatusViewModel", "Cancelling all messages.")
|
||||
messageQueue.clear()
|
||||
messageDisplayJob?.cancel()
|
||||
messageDisplayJob = null
|
||||
if (_status.value is StatusState.Message) {
|
||||
_status.value = StatusState.Hidden
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a message with priority-based queuing.
|
||||
* High-priority messages interrupt lower-priority ones.
|
||||
*/
|
||||
private fun showMessageInternal(message: String, type: MessageDisplayType, timeoutInSeconds: Int) {
|
||||
val currentState = _status.value
|
||||
val currentPriority = (currentState as? StatusState.Message)?.type?.priority ?: -1
|
||||
@@ -204,29 +256,6 @@ class StatusViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideMessageBarInternal() {
|
||||
messageDisplayJob?.cancel()
|
||||
messageDisplayJob = null
|
||||
if (_status.value is StatusState.Message) {
|
||||
_status.value = StatusState.Hidden
|
||||
}
|
||||
if (activeLoadingJob?.isActive != true) {
|
||||
processNextMessageInQueue()
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelAllMessagesInternal() {
|
||||
Log.d("StatusViewModel", "Cancelling all messages.")
|
||||
messageQueue.clear()
|
||||
messageDisplayJob?.cancel()
|
||||
messageDisplayJob = null
|
||||
// Do not cancel activeLoadingJob here unless that's the desired behavior.
|
||||
// Assuming CancelAllMessages is for the message bar only.
|
||||
if (_status.value is StatusState.Message) {
|
||||
_status.value = StatusState.Hidden
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelAllOperations() {
|
||||
messageQueue.clear()
|
||||
messageDisplayJob?.cancel()
|
||||
@@ -236,7 +265,9 @@ class StatusViewModel @Inject constructor(
|
||||
_status.value = StatusState.Hidden
|
||||
}
|
||||
|
||||
// --- REVISED LOGIC ---
|
||||
/**
|
||||
* Processes the next message in the queue.
|
||||
*/
|
||||
private fun processNextMessageInQueue(timeoutInSeconds: Int = 3) {
|
||||
if (activeLoadingJob?.isActive == true) {
|
||||
Log.d("StatusViewModel", "Full-screen loading is active, not processing message queue now.")
|
||||
|
||||
@@ -15,6 +15,7 @@ import eu.gaudian.translator.model.repository.dataStore
|
||||
import eu.gaudian.translator.model.repository.loadObjectList
|
||||
import eu.gaudian.translator.model.repository.saveObjectList
|
||||
import eu.gaudian.translator.utils.Log
|
||||
import eu.gaudian.translator.utils.StatusMessageService
|
||||
import eu.gaudian.translator.utils.TextToSpeechHelper
|
||||
import eu.gaudian.translator.utils.TranslationService
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -31,6 +32,9 @@ class TranslationViewModel @Inject constructor(
|
||||
val languageRepository: LanguageRepository
|
||||
) : AndroidViewModel(application) {
|
||||
|
||||
private val statusMessageService = StatusMessageService
|
||||
|
||||
|
||||
// For back/forward navigation of history in the UI (like editors)
|
||||
private val _historyCursor = MutableStateFlow(-1)
|
||||
|
||||
@@ -112,11 +116,13 @@ class TranslationViewModel @Inject constructor(
|
||||
fun translateSentence(sentence: String) {
|
||||
val sentenceToTranslate = sentence.ifEmpty { _inputText.value }
|
||||
if (sentenceToTranslate.isBlank()) {
|
||||
statusMessageService.showSimpleMessage("Please enter a sentence to translate.")
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedTranslationModel.value == null) {
|
||||
Log.e("TranslationViewModel", "Cannot translate because no model is selected.")
|
||||
statusMessageService.showSimpleMessage("Cannot translate because no model is selected.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -151,6 +157,7 @@ class TranslationViewModel @Inject constructor(
|
||||
}
|
||||
.onFailure { exception ->
|
||||
Log.e("TranslationViewModel", "Translation failed: ${exception.message}")
|
||||
statusMessageService.showErrorMessage("Translation failed: ${exception.message}")
|
||||
}
|
||||
|
||||
_isTranslating.value = false
|
||||
|
||||
@@ -26,6 +26,7 @@ import eu.gaudian.translator.model.repository.VocabularyFileSaver
|
||||
import eu.gaudian.translator.model.repository.VocabularyRepository
|
||||
import eu.gaudian.translator.utils.Log
|
||||
import eu.gaudian.translator.utils.StatusAction
|
||||
import eu.gaudian.translator.utils.StatusMessageId
|
||||
import eu.gaudian.translator.utils.StatusMessageService
|
||||
import eu.gaudian.translator.utils.StringHelper
|
||||
import eu.gaudian.translator.utils.VocabularyService
|
||||
@@ -774,7 +775,7 @@ class VocabularyViewModel @Inject constructor(
|
||||
statusService.hideMessageBar()
|
||||
if (_cardSet.value == null) {
|
||||
statusService.cancelAllMessages()
|
||||
statusService.showErrorMessage("No cards found for the specified filter", 3)
|
||||
statusService.showErrorById(StatusMessageId.ERROR_NO_CARDS_FOUND)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<string name="title_multiple">Mehrere</string>
|
||||
<string name="label_translation_settings">Übersetzung</string>
|
||||
<string name="reset_to_defaults">Auf Standard zurücksetzen</string>
|
||||
<string name="text_excel_not_supported_use_csv">Excel wird nicht unterstützt. Bitte CSV verwenden.</string>
|
||||
<string name="message_error_excel_not_supported">Excel wird nicht unterstützt. Bitte CSV verwenden.</string>
|
||||
<string name="error_parsing_table">Fehler beim Parsen der Tabelle</string>
|
||||
<string name="error_parsing_table_with_reason">Fehler beim Parsen der Tabelle: %1$s</string>
|
||||
<string name="label_import_table_csv_excel">Tabelle importieren (CSV)</string>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<string name="title_multiple">Múltiplos</string>
|
||||
<string name="label_translation_settings">Configurações de Tradução</string>
|
||||
<string name="reset_to_defaults">Restaurar Padrões</string>
|
||||
<string name="text_excel_not_supported_use_csv">Excel não é suportado. Use CSV.</string>
|
||||
<string name="message_error_excel_not_supported">Excel não é suportado. Use CSV.</string>
|
||||
<string name="error_parsing_table">Erro ao analisar tabela</string>
|
||||
<string name="error_parsing_table_with_reason">Erro ao analisar tabela: %1$s</string>
|
||||
<string name="label_import_table_csv_excel">Importar Tabela (CSV)</string>
|
||||
|
||||
@@ -225,7 +225,7 @@
|
||||
<string name="label_amount_models">%1$d models</string>
|
||||
<string name="label_analyze_grammar">Analyze Grammar</string>
|
||||
<string name="label_appearance">Appearance</string>
|
||||
<string name="hint_settings_title_hints">Help</string>
|
||||
<string name="hint_settings_title_help">Help</string>
|
||||
<string name="label_apply_filters">Apply Filters</string>
|
||||
<string name="label_article">Article</string>
|
||||
<string name="label_backup_and_restore">Backup and Restore</string>
|
||||
@@ -788,7 +788,7 @@
|
||||
<string name="text_error_generating_questions">Error generating questions: %1$s</string>
|
||||
<string name="text_error_loading_stored_values">Error loading stored values: %1$s</string>
|
||||
<string name="text_error_saving_entry">Error saving entry: %1$s</string>
|
||||
<string name="text_excel_not_supported_use_csv">Excel is not supported. Use CSV instead.</string>
|
||||
<string name="message_error_excel_not_supported">Excel is not supported. Use CSV instead.</string>
|
||||
<string name="text_expand_widget">Expand Widget</string>
|
||||
<string name="text_explanation">Explanation</string>
|
||||
<string name="text_export_category">Export Category</string>
|
||||
@@ -1042,4 +1042,76 @@
|
||||
<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>
|
||||
|
||||
@@ -43,6 +43,7 @@ truth = "1.4.5"
|
||||
zstdJni = "1.5.7-7"
|
||||
composeMarkdown = "0.5.8"
|
||||
jitpack = "1.0.10"
|
||||
foundationLayoutVersion = "1.10.3"
|
||||
|
||||
|
||||
[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" }
|
||||
mockk = { module = "io.mockk:mockk", version = "1.14.9" }
|
||||
compose-markdown = { module = "com.github.jeziellago:compose-markdown", version.ref = "composeMarkdown" }
|
||||
androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayoutVersion" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
Reference in New Issue
Block a user