Compare commits

...

36 Commits

Author SHA1 Message Date
jonasgaudian
64dcc5d0d5 localize UI strings in LibraryComponents and expand German and Portuguese translations with motivational phrases and dictionary content options 2026-02-17 17:57:25 +01:00
jonasgaudian
f39375e9df Refactor navigation and cleanup resources across the application 2026-02-17 17:09:25 +01:00
jonasgaudian
db959dab20 Refactor VocabularyListScreen to AllCardsListScreen, introduce NavigationRoutes for centralized route management, and externalize hardcoded strings. 2026-02-17 16:26:30 +01:00
jonasgaudian
02530dafbf Remove the legacy MainVocabularyScreen and its associated components, consolidating vocabulary management into the new LibraryScreen and StatsScreen architectures. 2026-02-17 15:46:56 +01:00
jonasgaudian
85c407481d Refactor hint management by replacing @Composable lambda hint content with a structured Hint type and updating UI components to support it. 2026-02-17 14:57:56 +01:00
jonasgaudian
d14940ed11 implement language direction and shuffling logic in StartExerciseScreen 2026-02-17 13:55:15 +01:00
jonasgaudian
a0b6509367 update LanguageChip icon, enable default shuffling in ExerciseConfig, and refine onClose navigation in VocabularyExerciseHostScreen 2026-02-17 13:30:03 +01:00
jonasgaudian
d249da5f52 add comprehensive logging for exercise setup and state transitions across screens and ViewModels 2026-02-17 13:22:56 +01:00
jonasgaudian
c061e41cc6 Implement the StartExerciseScreen with comprehensive filtering and configuration options. 2026-02-17 13:07:07 +01:00
jonasgaudian
2db2b47c38 add TODO comments for upcoming implementation 2026-02-17 12:26:55 +01:00
jonasgaudian
f779da470f Refactor VocabularyCard into specialized VocabularyDisplayCard and VocabularyExerciseCard components. 2026-02-17 12:12:57 +01:00
jonasgaudian
4855a347b9 Update motivational phrases and deprecate VocabularyCard composable 2026-02-17 11:40:44 +01:00
jonasgaudian
4dd9fe86aa refactor More menu and replace AppDropDownMenu with ModalBottomSheet in `LibraryScreen 2026-02-17 11:27:23 +01:00
jonasgaudian
35080c208b update VocabularyProgressOptionsScreen layout and expand motivational phrases 2026-02-17 11:13:00 +01:00
jonasgaudian
142eb5a31d implement daily goal tracking and integrate dynamic streak data into HomeScreen 2026-02-17 10:57:59 +01:00
jonasgaudian
f50c0c08a5 remove onNavigateBack from ApiKeyScreen and clean up unused imports 2026-02-16 23:44:18 +01:00
jonasgaudian
dc629a54ef update BottomNavigationBar styling, animations, and icons 2026-02-16 23:38:40 +01:00
jonasgaudian
0c54d6f9c5 add motivational phrases and update HomeScreen profile section with a random phrase and app icon 2026-02-16 23:15:49 +01:00
jonasgaudian
059e5d9d3f implement AddCategoryDialog and add a dropdown menu for adding vocabulary or categories in LibraryScreen 2026-02-16 22:49:54 +01:00
jonasgaudian
3e3d6d9cd1 delete NewVocListScreen.kt, update NewWordScreen to display recently added items, and refactor VocabularyCard styling in LibraryComponents.kt. 2026-02-16 22:39:56 +01:00
jonasgaudian
a7c83bb846 implement CSV import for new words and refactor UI components to use AppCard 2026-02-16 22:22:11 +01:00
jonasgaudian
70e416d5e1 implement NewWordScreen and NewWordReviewScreen for AI-assisted and manual vocabulary entry 2026-02-16 21:55:59 +01:00
jonasgaudian
84cad31810 refactor AppTopAppBar navigation icon to use ArrowBackIosNew and update styling properties 2026-02-16 21:21:48 +01:00
jonasgaudian
89ac7cd9eb integrate ProgressViewModel and WeeklyActivityChartWidget into WeeklyProgressSection and implement navigation to vocabulary_heatmap 2026-02-16 21:14:30 +01:00
jonasgaudian
47d7e01f7f implement show/hide header on scroll in LibraryScreen and prevent haptic feedback on re-selecting the current bottom bar item 2026-02-16 17:56:49 +01:00
jonasgaudian
eae37715cd implement statsGraph and refactor StatsScreen with drag-and-drop widget reordering 2026-02-16 17:47:46 +01:00
jonasgaudian
6c669ac310 implement LibraryScreen with advanced filtering and refactor CategoryDetailScreen 2026-02-16 16:11:25 +01:00
jonasgaudian
af78bd316d implement LibraryScreen UI with search, filtering, and segmented view for cards and categories 2026-02-16 15:49:57 +01:00
jonasgaudian
24cebc4b15 implement LibraryScreen UI with search, filtering, and segmented view for cards and categories 2026-02-16 15:19:45 +01:00
jonasgaudian
cd5a53ff5f Redesign top app bar 2026-02-16 15:02:12 +01:00
jonasgaudian
972b2226d0 implement LibraryScreen, migrate Vocabulary to legacy, and refactor StartExerciseScreen UI 2026-02-16 14:28:28 +01:00
jonasgaudian
5ae96d1f5c Add dummy start exercise button and dummy screen 2026-02-16 13:52:02 +01:00
jonasgaudian
ef90df2150 Add dummy stats screen to bottom navigation 2026-02-16 13:20:06 +01:00
jonasgaudian
d2d2f53b59 Change bottom bar navigation and make space for new order 2026-02-16 13:12:15 +01:00
jonasgaudian
7fccda7f77 implement HomeScreen and refactor navigation to include a separate Home and Translation section 2026-02-16 12:48:52 +01:00
jonasgaudian
801b6f6404 cleanup gradle.properties, remove redundant Kotlin Android plugins, and update android.dependency.useConstraints 2026-02-16 11:23:50 +01:00
112 changed files with 6581 additions and 4388 deletions

View File

@@ -4,10 +4,10 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-02-15T19:51:37.987601800Z"> <DropdownSelection timestamp="2026-02-16T10:13:39.492968600Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\jonas\.android\avd\Medium_Phone_28.avd" /> <DeviceId pluginId="PhysicalDevice" identifier="serial=RFCW11ML1WV" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>

View File

@@ -6,7 +6,6 @@ import java.util.Locale
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.compose)
alias(libs.plugins.hilt.android) alias(libs.plugins.hilt.android)
id("kotlin-parcelize") id("kotlin-parcelize")
@@ -62,11 +61,8 @@ android {
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi" "-opt-in=androidx.compose.ui.ExperimentalComposeUiApi"
) )
} }
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
}
}
buildFeatures { buildFeatures {
compose = true compose = true
viewBinding = false viewBinding = false
@@ -130,7 +126,7 @@ dependencies {
implementation(libs.androidx.room.runtime) // ADDED: Explicitly add runtime implementation(libs.androidx.room.runtime) // ADDED: Explicitly add runtime
implementation(libs.androidx.room.ktx) implementation(libs.androidx.room.ktx)
implementation(libs.core.ktx) implementation(libs.core.ktx)
ksp(libs.room.compiler) // CHANGED: Use ksp instead of implementation ksp(libs.room.compiler)
// Networking // Networking
implementation(libs.retrofit) implementation(libs.retrofit)

View File

@@ -6,9 +6,6 @@ object TestConfig {
// REPLACE with your actual API Key for the test // REPLACE with your actual API Key for the test
const val API_KEY = "YOUR_REAL_API_KEY_HERE" const val API_KEY = "YOUR_REAL_API_KEY_HERE"
// Set to true if you want to see full log output in Logcat
const val ENABLE_LOGGING = true
// Optional: If your ApiManager requires a specific provider (e.g., "Mistral", "OpenAI") // Optional: If your ApiManager requires a specific provider (e.g., "Mistral", "OpenAI")
const val PROVIDER_NAME = "Mistral" const val PROVIDER_NAME = "Mistral"

View File

@@ -32,17 +32,6 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".CorrectActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
</application> </application>

View File

@@ -1,45 +0,0 @@
@file:Suppress("HardCodedStringLiteral")
package eu.gaudian.translator
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.Text
import androidx.compose.ui.res.stringResource
import eu.gaudian.translator.utils.Log
class CorrectActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = intent
val action = intent.action
val type = intent.type
if (Intent.ACTION_SEND == action && type == "text/plain") {
val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT)
if (sharedText != null) {
Log.d("EditActivity", "Received text: $sharedText")
setContent {
Text(stringResource(R.string.editing_text, sharedText))
}
} else {
Log.e("EditActivity", getString(R.string.no_text_received))
setContent {
Text(stringResource(R.string.error_no_text_to_edit))
}
}
} else {
Log.d("EditActivity", "Not launched with ACTION_SEND")
setContent {
Text(stringResource(R.string.not_launched_with_text_to_edit))
}
}
}
}

View File

@@ -1,5 +1,3 @@
@file:Suppress("unused", "HardCodedStringLiteral")
package eu.gaudian.translator.di package eu.gaudian.translator.di
import android.app.Application import android.app.Application

View File

@@ -9,7 +9,6 @@ import eu.gaudian.translator.R
sealed class WidgetType(val id: String, @param:StringRes val titleRes: Int) { sealed class WidgetType(val id: String, @param:StringRes val titleRes: Int) {
data object Status : WidgetType("status", R.string.label_status) data object Status : WidgetType("status", R.string.label_status)
data object Streak : WidgetType("streak", R.string.title_widget_streak) data object Streak : WidgetType("streak", R.string.title_widget_streak)
data object StartButtons : WidgetType("start_buttons", R.string.label_start_exercise)
data object AllVocabulary : WidgetType("all_vocabulary", R.string.label_all_vocabulary) data object AllVocabulary : WidgetType("all_vocabulary", R.string.label_all_vocabulary)
data object DueToday : WidgetType("due_today", R.string.title_widget_due_today) data object DueToday : WidgetType("due_today", R.string.title_widget_due_today)
data object CategoryProgress : WidgetType("category_progress", R.string.label_categories) data object CategoryProgress : WidgetType("category_progress", R.string.label_categories)
@@ -23,7 +22,6 @@ sealed class WidgetType(val id: String, @param:StringRes val titleRes: Int) {
val DEFAULT_ORDER = listOf( val DEFAULT_ORDER = listOf(
Status, Status,
Streak, Streak,
StartButtons,
AllVocabulary, AllVocabulary,
DueToday, DueToday,
CategoryProgress , CategoryProgress ,

View File

@@ -56,6 +56,7 @@ object LocalDictionaryMorphologyMapper {
/** /**
* Overload that uses parsed [DictionaryEntryData] instead of raw JSON. * Overload that uses parsed [DictionaryEntryData] instead of raw JSON.
*/ */
@Suppress("unused")
fun parseMorphology( fun parseMorphology(
langCode: String, langCode: String,
pos: String?, pos: String?,

View File

@@ -144,19 +144,6 @@ class ApiRepository(private val context: Context) {
var configurationValid = true var configurationValid = true
// (Helper function to reduce repetition)
fun checkAndFallback(current: LanguageModel?, setter: suspend (LanguageModel) -> Unit) {
val isValid = current != null && availableModels.any { it.modelId == current.modelId && it.providerKey == current.providerKey }
if (!isValid) {
val fallback = findFallbackModel(availableModels)
if (fallback != null) {
// We must use a blocking call or scope here because we can't easily pass a suspend function to a lambda
// But since we are inside a suspend function, we can just call the setter directly if we unroll the loop.
// For simplicity, I'll keep the unrolled logic below.
}
}
}
// Fallback checks // Fallback checks
if (currentTrans == null || !availableModels.any { it.modelId == currentTrans.modelId && it.providerKey == currentTrans.providerKey }) { if (currentTrans == null || !availableModels.any { it.modelId == currentTrans.modelId && it.providerKey == currentTrans.providerKey }) {
findFallbackModel(availableModels)?.let { setTranslationModel(it) } ?: run { configurationValid = false } findFallbackModel(availableModels)?.let { setTranslationModel(it) } ?: run { configurationValid = false }

View File

@@ -76,6 +76,7 @@ class LanguageRepository(private val context: Context) {
} }
} }
@Suppress("unused")
suspend fun wipeHistoryAndFavorites() { suspend fun wipeHistoryAndFavorites() {
clearLanguages(LanguageListType.HISTORY) clearLanguages(LanguageListType.HISTORY)
clearLanguages(LanguageListType.FAVORITE) clearLanguages(LanguageListType.FAVORITE)

View File

@@ -129,25 +129,6 @@ class JsonHelper {
*/ */
class JsonParsingException(message: String, cause: Throwable? = null) : Exception(message, cause) class JsonParsingException(message: String, cause: Throwable? = null) : Exception(message, cause)
/**
* Legacy JsonHelper class for backward compatibility.
* @deprecated Use the enhanced JsonHelper class instead
*/
@Deprecated("Use the enhanced JsonHelper class instead")
class LegacyJsonHelper {
fun cleanJson(json: String): String {
val startIndex = json.indexOf('{')
val endIndex = json.lastIndexOf('}')
if (startIndex == -1 || endIndex == -1 || startIndex >= endIndex) {
throw IllegalArgumentException("Invalid JSON format")
}
return json.substring(startIndex, endIndex + 1).trim()
}
}
object JsonCleanUtil { object JsonCleanUtil {
private val jsonParser = Json { isLenient = true; ignoreUnknownKeys = true } private val jsonParser = Json { isLenient = true; ignoreUnknownKeys = true }

View File

@@ -10,6 +10,7 @@ import timber.log.Timber
* "HardcodedText" lint warning for log messages, which are for * "HardcodedText" lint warning for log messages, which are for
* development purposes only. * development purposes only.
*/ */
@Suppress("unused")
object Log { object Log {
@SuppressLint("HardcodedText") @SuppressLint("HardcodedText")

View File

@@ -55,6 +55,12 @@ enum class StatusMessageId(
ERROR_FILE_PICKER_NOT_INITIALIZED(R.string.message_error_file_picker_not_initialized, 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), 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), ERROR_EXCEL_NOT_SUPPORTED(R.string.message_error_excel_not_supported, MessageDisplayType.ERROR, 5),
ERROR_PARSING_TABLE(R.string.error_parsing_table, MessageDisplayType.ERROR, 5),
ERROR_PARSING_TABLE_WITH_REASON(R.string.error_parsing_table_with_reason, MessageDisplayType.ERROR, 5),
ERROR_SELECT_TWO_COLUMNS(R.string.error_select_two_columns, MessageDisplayType.ERROR, 5),
ERROR_SELECT_LANGUAGES(R.string.error_select_languages, MessageDisplayType.ERROR, 5),
ERROR_NO_ROWS_TO_IMPORT(R.string.error_no_rows_to_import, MessageDisplayType.ERROR, 5),
SUCCESS_ITEMS_IMPORTED(R.string.info_imported_items_from, MessageDisplayType.SUCCESS, 3),
// API Key related // API Key related

View File

@@ -75,7 +75,6 @@ object StatusMessageService {
* @deprecated Use showMessageById() instead for internationalization support. * @deprecated Use showMessageById() instead for internationalization support.
*/ */
@Deprecated("Use showMessageById() for internationalization support", ReplaceWith("showMessageById(messageId)")) @Deprecated("Use showMessageById() for internationalization support", ReplaceWith("showMessageById(messageId)"))
@Suppress("unused")
fun showSimpleMessage(text: String, type: MessageDisplayType = MessageDisplayType.INFO) { fun showSimpleMessage(text: String, type: MessageDisplayType = MessageDisplayType.INFO) {
scope.launch { scope.launch {
_actions.emit(StatusAction.ShowMessage(text, type, 5)) _actions.emit(StatusAction.ShowMessage(text, type, 5))

View File

@@ -117,7 +117,6 @@ class TranslationService(private val context: Context) {
} }
suspend fun translateSentence(sentence: String): Result<TranslationHistoryItem> = withContext(Dispatchers.IO) { suspend fun translateSentence(sentence: String): Result<TranslationHistoryItem> = withContext(Dispatchers.IO) {
val statusMessageService = StatusMessageService
val additionalInstructions = settingsRepository.customPromptTranslation.flow.first() val additionalInstructions = settingsRepository.customPromptTranslation.flow.first()
val selectedSource = languageRepository.loadSelectedSourceLanguage().first() val selectedSource = languageRepository.loadSelectedSourceLanguage().first()
val sourceLangName = selectedSource?.englishName ?: "Auto" val sourceLangName = selectedSource?.englishName ?: "Auto"

View File

@@ -1,11 +0,0 @@
package eu.gaudian.translator.utils.dictionary
import eu.gaudian.translator.model.grammar.Inflection
/**
* Interface for a language-specific inflection parser.
*/
interface InflectionParser {
fun parse(inflections: List<Inflection>): DisplayInflectionData
}

View File

@@ -5,11 +5,6 @@ package eu.gaudian.translator.utils.dictionary
* Either a simple list or a complex, grouped verb conjugation table. * Either a simple list or a complex, grouped verb conjugation table.
*/ */
sealed class DisplayInflectionData { sealed class DisplayInflectionData {
data class VerbConjugation(
val gerund: String? = null,
val participle: String? = null,
val moods: List<DisplayMood>
) : DisplayInflectionData()
} }
data class DisplayMood( data class DisplayMood(

View File

@@ -253,7 +253,19 @@ fun TranslatorApp(
val currentDestination = navBackStackEntry?.destination val currentDestination = navBackStackEntry?.destination
val selectedScreen = Screen.fromDestination(currentDestination) val selectedScreen = Screen.fromDestination(currentDestination)
@Suppress("HardCodedStringLiteral") val isBottomBarHidden = currentDestination?.route?.startsWith("vocabulary_exercise") == true @Suppress("HardCodedStringLiteral") val isBottomBarHidden = currentDestination?.hierarchy?.any { destination ->
destination.route in setOf(
Screen.Translation.route,
Screen.Dictionary.route,
Screen.Exercises.route,
Screen.Settings.route
)
} == true || currentDestination?.route in setOf(
"start_exercise",
"new_word",
"new_word_review",
"vocabulary_detail/{itemId}"
)
val showBottomNavLabels by settingsViewModel.showBottomNavLabels.collectAsStateWithLifecycle(initialValue = false) val showBottomNavLabels by settingsViewModel.showBottomNavLabels.collectAsStateWithLifecycle(initialValue = false)
BottomNavigationBar( BottomNavigationBar(
@@ -262,6 +274,12 @@ fun TranslatorApp(
showLabels = showBottomNavLabels, showLabels = showBottomNavLabels,
onItemSelected = { screen -> onItemSelected = { screen ->
val inSameSection = currentDestination?.hierarchy?.any { it.route == screen.route } == true val inSameSection = currentDestination?.hierarchy?.any { it.route == screen.route } == true
val isMoreSection = screen in setOf(
Screen.Translation,
Screen.Dictionary,
Screen.Settings,
Screen.Exercises
)
// Always reset the selected section to its root and clear back stack between sections // Always reset the selected section to its root and clear back stack between sections
if (inSameSection) { if (inSameSection) {
@@ -274,6 +292,11 @@ fun TranslatorApp(
launchSingleTop = true launchSingleTop = true
restoreState = false restoreState = false
} }
} else if (isMoreSection) {
navController.navigate(screen.route) {
launchSingleTop = true
restoreState = false
}
} else { } else {
// Switching sections: clear entire back stack to start to avoid back navigation results // Switching sections: clear entire back stack to start to avoid back navigation results
navController.navigate(screen.route) { navController.navigate(screen.route) {
@@ -285,6 +308,10 @@ fun TranslatorApp(
restoreState = false restoreState = false
} }
} }
},
onPlayClicked = {
@Suppress("HardCodedStringLiteral")
navController.navigate("start_exercise")
} }
) )
}, },

View File

@@ -26,27 +26,48 @@ import eu.gaudian.translator.view.dictionary.EtymologyResultScreen
import eu.gaudian.translator.view.dictionary.MainDictionaryScreen import eu.gaudian.translator.view.dictionary.MainDictionaryScreen
import eu.gaudian.translator.view.exercises.ExerciseSessionScreen import eu.gaudian.translator.view.exercises.ExerciseSessionScreen
import eu.gaudian.translator.view.exercises.MainExerciseScreen import eu.gaudian.translator.view.exercises.MainExerciseScreen
import eu.gaudian.translator.view.exercises.StartExerciseScreen
import eu.gaudian.translator.view.exercises.YouTubeBrowserScreen import eu.gaudian.translator.view.exercises.YouTubeBrowserScreen
import eu.gaudian.translator.view.exercises.YouTubeExerciseScreen import eu.gaudian.translator.view.exercises.YouTubeExerciseScreen
import eu.gaudian.translator.view.home.HomeScreen
import eu.gaudian.translator.view.library.LibraryScreen
import eu.gaudian.translator.view.settings.DictionaryOptionsScreen import eu.gaudian.translator.view.settings.DictionaryOptionsScreen
import eu.gaudian.translator.view.settings.SettingsRoutes import eu.gaudian.translator.view.settings.SettingsRoutes
import eu.gaudian.translator.view.settings.TranslationSettingsScreen import eu.gaudian.translator.view.settings.TranslationSettingsScreen
import eu.gaudian.translator.view.settings.settingsGraph import eu.gaudian.translator.view.settings.settingsGraph
import eu.gaudian.translator.view.stats.StatsScreen
import eu.gaudian.translator.view.translation.TranslationScreen import eu.gaudian.translator.view.translation.TranslationScreen
import eu.gaudian.translator.view.vocabulary.AllCardsListScreen
import eu.gaudian.translator.view.vocabulary.CategoryDetailScreen import eu.gaudian.translator.view.vocabulary.CategoryDetailScreen
import eu.gaudian.translator.view.vocabulary.CategoryListScreen import eu.gaudian.translator.view.vocabulary.CategoryListScreen
import eu.gaudian.translator.view.vocabulary.LanguageProgressScreen import eu.gaudian.translator.view.vocabulary.LanguageProgressScreen
import eu.gaudian.translator.view.vocabulary.MainVocabularyScreen import eu.gaudian.translator.view.vocabulary.NewWordReviewScreen
import eu.gaudian.translator.view.vocabulary.NewWordScreen
import eu.gaudian.translator.view.vocabulary.NoGrammarItemsScreen import eu.gaudian.translator.view.vocabulary.NoGrammarItemsScreen
import eu.gaudian.translator.view.vocabulary.StageDetailScreen import eu.gaudian.translator.view.vocabulary.StageDetailScreen
import eu.gaudian.translator.view.vocabulary.VocabularyCardHost import eu.gaudian.translator.view.vocabulary.VocabularyCardHost
import eu.gaudian.translator.view.vocabulary.VocabularyExerciseHostScreen import eu.gaudian.translator.view.vocabulary.VocabularyExerciseHostScreen
import eu.gaudian.translator.view.vocabulary.VocabularyHeatmapScreen import eu.gaudian.translator.view.vocabulary.VocabularyHeatmapScreen
import eu.gaudian.translator.view.vocabulary.VocabularyListScreen
import eu.gaudian.translator.view.vocabulary.VocabularySortingScreen import eu.gaudian.translator.view.vocabulary.VocabularySortingScreen
private const val TRANSITION_DURATION = 300 private const val TRANSITION_DURATION = 300
object NavigationRoutes {
const val NEW_WORD = "new_word"
const val NEW_WORD_REVIEW = "new_word_review"
const val VOCABULARY_DETAIL = "vocabulary_detail"
const val START_EXERCISE = "start_exercise"
const val CATEGORY_DETAIL = "category_detail"
const val CATEGORY_LIST = "category_list_screen"
const val STATS_VOCABULARY_HEATMAP = "stats/vocabulary_heatmap"
const val STATS_LANGUAGE_PROGRESS = "stats/language_progress"
const val STATS_CATEGORY_DETAIL = "stats/category_detail"
const val STATS_CATEGORY_LIST = "stats/category_list_screen"
const val STATS_VOCABULARY_SORTING = "stats/vocabulary_sorting"
const val STATS_NO_GRAMMAR_ITEMS = "stats/no_grammar_items"
const val STATS_VOCABULARY_LIST = "stats/vocabulary_list"
}
@Composable @Composable
fun AppNavHost( fun AppNavHost(
navController: NavHostController, navController: NavHostController,
@@ -57,11 +78,12 @@ fun AppNavHost(
// 1. Define your Main Tab "Leaf" Routes (the actual start destinations of your graphs) // 1. Define your Main Tab "Leaf" Routes (the actual start destinations of your graphs)
val mainTabRoutes = setOf( val mainTabRoutes = setOf(
Screen.Home.route, // "home" or "main_translation" depending on which one you actually navigate to Screen.Home.route,
"main_translation", Screen.Library.route,
"main_dictionary", Screen.Stats.route,
"main_vocabulary", Screen.Translation.route,
"main_exercise", Screen.Dictionary.route,
Screen.Exercises.route,
SettingsRoutes.LIST SettingsRoutes.LIST
) )
@@ -121,77 +143,50 @@ fun AppNavHost(
} }
) { ) {
composable(Screen.Home.route) { composable(Screen.Home.route) {
TranslationScreen(navController = navController) HomeScreen(navController = navController)
}
composable(NavigationRoutes.NEW_WORD) {
NewWordScreen(navController = navController)
}
composable(NavigationRoutes.NEW_WORD_REVIEW) {
NewWordReviewScreen(navController = navController)
}
composable(NavigationRoutes.START_EXERCISE) {
StartExerciseScreen(navController = navController)
} }
// Define all other navigation graphs at the same top level. // Define all other navigation graphs at the same top level.
homeGraph(navController)
libraryGraph(navController)
statsGraph(navController)
translationGraph(navController) translationGraph(navController)
dictionaryGraph(navController) dictionaryGraph(navController)
vocabularyGraph(navController)
exerciseGraph(navController) exerciseGraph(navController)
settingsGraph(navController) settingsGraph(navController)
} }
} }
@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
fun NavGraphBuilder.translationGraph(navController: NavHostController) { fun NavGraphBuilder.homeGraph(navController: NavHostController) {
navigation( navigation(
startDestination = "main_translation", startDestination = "main_home",
route = Screen.Home.route route = Screen.Home.route
) { ) {
composable("main_translation") { composable("main_home") {
TranslationScreen(navController = navController) HomeScreen(navController = navController)
}
composable("custom_translation_prompt") {
TranslationSettingsScreen(navController = navController)
} }
} }
} }
@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class) fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
fun NavGraphBuilder.dictionaryGraph(navController: NavHostController) {
navigation( navigation(
startDestination = "main_dictionary", startDestination = "main_library",
route = Screen.Dictionary.route route = Screen.Library.route
) { ) {
composable("main_dictionary") { composable("main_library") {
MainDictionaryScreen(navController = navController) LibraryScreen(navController = navController)
}
composable("dictionary_result/{entryId}") { backStackEntry ->
val entryId = backStackEntry.arguments?.getString("entryId")?.toIntOrNull()
if (entryId != null) {
DictionaryResultScreen(
entryId = entryId,
navController = navController,
)
} else {
Text("Error: Invalid Entry ID")
}
}
composable("dictionary_options") {
DictionaryOptionsScreen(navController = navController)
}
composable("etymology_result/{word}/{languageCode}") { backStackEntry ->
val word = backStackEntry.arguments?.getString("word") ?: ""
val languageCode = backStackEntry.arguments?.getString("languageCode")?.toIntOrNull() ?: 1
EtymologyResultScreen(
navController = navController,
word = word,
languageCode = languageCode
)
}
}
}
fun NavGraphBuilder.vocabularyGraph(
navController: NavHostController,
) {
navigation(
startDestination = "main_vocabulary",
route = Screen.Vocabulary.route
) {
composable("main_vocabulary") {
MainVocabularyScreen(navController = navController)
} }
composable("vocabulary_sorting") { composable("vocabulary_sorting") {
VocabularySortingScreen( VocabularySortingScreen(
@@ -224,7 +219,7 @@ fun NavGraphBuilder.vocabularyGraph(
composable("vocabulary_list/{showDueTodayOnly}/{categoryId}") { backStackEntry -> composable("vocabulary_list/{showDueTodayOnly}/{categoryId}") { backStackEntry ->
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull() val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull()
VocabularyListScreen( AllCardsListScreen(
navController = navController, navController = navController,
showDueTodayOnly = showDueTodayOnly, showDueTodayOnly = showDueTodayOnly,
categoryId = categoryId, categoryId = categoryId,
@@ -241,7 +236,7 @@ fun NavGraphBuilder.vocabularyGraph(
) )
} }
composable("vocabulary_heatmap") { composable(NavigationRoutes.STATS_VOCABULARY_HEATMAP) {
VocabularyHeatmapScreen( VocabularyHeatmapScreen(
navController = navController, navController = navController,
) )
@@ -253,7 +248,7 @@ fun NavGraphBuilder.vocabularyGraph(
if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it) if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it)
} }
VocabularyListScreen( AllCardsListScreen(
navController = navController, navController = navController,
showDueTodayOnly = showDueTodayOnly, showDueTodayOnly = showDueTodayOnly,
stage = stage, stage = stage,
@@ -376,6 +371,159 @@ fun NavGraphBuilder.vocabularyGraph(
} }
} }
fun NavGraphBuilder.statsGraph(
navController: NavHostController,
) {
navigation(
startDestination = "main_stats",
route = Screen.Stats.route
) {
composable("main_stats") {
StatsScreen(navController = navController)
}
composable("stats/vocabulary_sorting") {
VocabularySortingScreen(
navController = navController
)
}
composable("stats/vocabulary_list/{showDueTodayOnly}/{categoryId}") { backStackEntry ->
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull()
AllCardsListScreen(
navController = navController,
showDueTodayOnly = showDueTodayOnly,
categoryId = categoryId,
onNavigateToItem = { item ->
navController.navigate("vocabulary_detail/${item.id}")
},
onNavigateBack = { navController.popBackStack() },
enableNavigationButtons = true
)
}
composable(NavigationRoutes.STATS_LANGUAGE_PROGRESS) {
LanguageProgressScreen(
navController = navController
)
}
composable(NavigationRoutes.STATS_VOCABULARY_HEATMAP) {
VocabularyHeatmapScreen(
navController = navController,
)
}
composable("stats/vocabulary_list/{showDueTodayOnly}/{stage}") { backStackEntry ->
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
val stageString = backStackEntry.arguments?.getString("stage")
val stage = stageString?.let {
if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it)
}
AllCardsListScreen(
navController = navController,
showDueTodayOnly = showDueTodayOnly,
stage = stage,
onNavigateToItem = { item ->
navController.navigate("vocabulary_detail/${item.id}")
},
onNavigateBack = { navController.popBackStack() },
categoryId = 0,
enableNavigationButtons = true
)
}
composable("stats/category_detail/{categoryId}") { backStackEntry ->
val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull()
if (categoryId != null) {
CategoryDetailScreen(
categoryId = categoryId,
onBackClick = { navController.popBackStack() },
onNavigateToItem = { item ->
navController.navigate("vocabulary_detail/${item.id}")
},
navController = navController
)
}
}
composable("stats/category_list_screen") {
CategoryListScreen(
onNavigateBack = { navController.popBackStack() },
onCategoryClicked = { categoryId ->
navController.navigate("stats/category_detail/$categoryId")
}
)
}
composable(
route = "stats/vocabulary_sorting?mode={mode}",
arguments = listOf(
navArgument("mode") {
type = NavType.StringType
nullable = true
}
)
) { backStackEntry ->
VocabularySortingScreen(
navController = navController,
initialFilterMode = backStackEntry.arguments?.getString("mode")
)
}
composable("stats/no_grammar_items") {
NoGrammarItemsScreen(
navController = navController
)
}
}
}
@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
fun NavGraphBuilder.translationGraph(navController: NavHostController) {
navigation(
startDestination = "main_translation",
route = Screen.Translation.route
) {
composable("main_translation") {
TranslationScreen(navController = navController)
}
composable("custom_translation_prompt") {
TranslationSettingsScreen(navController = navController)
}
}
}
@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
fun NavGraphBuilder.dictionaryGraph(navController: NavHostController) {
navigation(
startDestination = "main_dictionary",
route = Screen.Dictionary.route
) {
composable("main_dictionary") {
MainDictionaryScreen(navController = navController)
}
composable("dictionary_result/{entryId}") { backStackEntry ->
val entryId = backStackEntry.arguments?.getString("entryId")?.toIntOrNull()
if (entryId != null) {
DictionaryResultScreen(
entryId = entryId,
navController = navController,
)
} else {
Text("Error: Invalid Entry ID")
}
}
composable("dictionary_options") {
DictionaryOptionsScreen(navController = navController)
}
composable("etymology_result/{word}/{languageCode}") { backStackEntry ->
val word = backStackEntry.arguments?.getString("word") ?: ""
val languageCode = backStackEntry.arguments?.getString("languageCode")?.toIntOrNull() ?: 1
EtymologyResultScreen(
navController = navController,
word = word,
languageCode = languageCode
)
}
}
}
@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class) @OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
fun NavGraphBuilder.exerciseGraph( fun NavGraphBuilder.exerciseGraph(
navController: NavHostController, navController: NavHostController,

View File

@@ -115,7 +115,7 @@ fun AppAlertDialog(
title: @Composable (() -> Unit)? = null, title: @Composable (() -> Unit)? = null,
text: @Composable (() -> Unit)? = null, text: @Composable (() -> Unit)? = null,
properties: DialogProperties = DialogProperties(), properties: DialogProperties = DialogProperties(),
hintContent: @Composable (() -> Unit)? = null, hintContent:Hint? = null,
) { ) {
val sheetState = rememberModalBottomSheetState() val sheetState = rememberModalBottomSheetState()
var showBottomSheet by remember { mutableStateOf(false) } var showBottomSheet by remember { mutableStateOf(false) }
@@ -142,13 +142,15 @@ fun AppAlertDialog(
) )
if (showBottomSheet) { if (showBottomSheet) {
hintContent?.let {
HintBottomSheet( HintBottomSheet(
onDismissRequest = { showBottomSheet = false }, onDismissRequest = { showBottomSheet = false },
sheetState = sheetState, sheetState = sheetState,
content = hintContent content = it
) )
} }
} }
}
/** /**
* Standard Dialog Header for BottomSheets. * Standard Dialog Header for BottomSheets.
@@ -212,7 +214,7 @@ private fun DialogHeader(
@Composable @Composable
private fun DialogTitleWithHint( private fun DialogTitleWithHint(
title: @Composable () -> Unit, title: @Composable () -> Unit,
hintContent: @Composable (() -> Unit)?, hintContent: Hint? = null,
onHintClick: () -> Unit onHintClick: () -> Unit
) { ) {
val showHints = LocalShowHints.current val showHints = LocalShowHints.current
@@ -424,7 +426,6 @@ fun AppAlertDialogPreview() {
}, },
title = { Text("Alert Dialog Title") }, title = { Text("Alert Dialog Title") },
text = { Text("This is the alert dialog text.") }, text = { Text("This is the alert dialog text.") },
hintContent = { Text("This is a hint for the alert dialog.") }
) )
} }
@@ -492,7 +493,6 @@ fun AppAlertDialogLongTextPreview() {
Text("Third paragraph with additional information that users need to be aware of.") Text("Third paragraph with additional information that users need to be aware of.")
} }
}, },
hintContent = { Text("This hint explains the terms in more detail.") }
) )
} }

View File

@@ -25,6 +25,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
@@ -550,7 +551,55 @@ fun AppDropdownMenu(
// ========================================= // =========================================
// LEGACY COMPONENTS (UPDATED TO USE UNIFIED STYLES) // LEGACY COMPONENTS (UPDATED TO USE UNIFIED STYLES)
// ========================================= // =========================================
@Composable
fun BottomSheetMenuItem(
icon: ImageVector,
title: String,
subtitle: String,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(horizontal = 24.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Circular Icon Background
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.surfaceVariant,
modifier = Modifier.size(52.dp)
) {
Box(contentAlignment = Alignment.Center) {
Icon(
imageVector = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(28.dp)
)
}
}
Spacer(modifier = Modifier.width(16.dp))
// Title and Subtitle Column
Column(modifier = Modifier.weight(1f)) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = subtitle,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
@Composable @Composable
fun LargeDropdownMenuItem( fun LargeDropdownMenuItem(
text: String, text: String,

View File

@@ -10,7 +10,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.Icons.Default import androidx.compose.material.icons.Icons.Default
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBackIos
import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.automirrored.filled.DriveFileMove import androidx.compose.material.icons.automirrored.filled.DriveFileMove
import androidx.compose.material.icons.automirrored.filled.ExitToApp import androidx.compose.material.icons.automirrored.filled.ExitToApp
@@ -81,6 +81,7 @@ import androidx.compose.material.icons.filled.MenuBook
import androidx.compose.material.icons.filled.Merge import androidx.compose.material.icons.filled.Merge
import androidx.compose.material.icons.filled.ModelTraining import androidx.compose.material.icons.filled.ModelTraining
import androidx.compose.material.icons.filled.MonitorHeart import androidx.compose.material.icons.filled.MonitorHeart
import androidx.compose.material.icons.filled.MoreHoriz
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.NoteAdd import androidx.compose.material.icons.filled.NoteAdd
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
@@ -135,7 +136,7 @@ object AppIcons {
val AI = Default.AutoAwesome val AI = Default.AutoAwesome
val Appearance = Icons.Filled.ColorLens val Appearance = Icons.Filled.ColorLens
val ApiKey = Default.Key val ApiKey = Default.Key
val ArrowBack = Icons.AutoMirrored.Filled.ArrowBack val ArrowBack = Icons.AutoMirrored.Filled.ArrowBackIos
val ArrowCircleUp = Icons.Filled.ArrowCircleUp val ArrowCircleUp = Icons.Filled.ArrowCircleUp
val ArrowDropDown = Icons.Filled.KeyboardArrowDown val ArrowDropDown = Icons.Filled.KeyboardArrowDown
val ArrowDropUp = Icons.Filled.KeyboardArrowUp val ArrowDropUp = Icons.Filled.KeyboardArrowUp
@@ -202,6 +203,7 @@ object AppIcons {
val Merge = Icons.Filled.Merge val Merge = Icons.Filled.Merge
val ModelTraining = Icons.Filled.ModelTraining val ModelTraining = Icons.Filled.ModelTraining
val More = Default.MoreVert val More = Default.MoreVert
val MoreHorizontal = Icons.Filled.MoreHoriz
val MoreVert = Default.MoreVert val MoreVert = Default.MoreVert
val MoveTo = Icons.AutoMirrored.Filled.DriveFileMove val MoveTo = Icons.AutoMirrored.Filled.DriveFileMove
val Paste = Default.ContentPaste val Paste = Default.ContentPaste

View File

@@ -2,26 +2,15 @@ package eu.gaudian.translator.view.composable
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.FabPosition import androidx.compose.material3.FabPosition
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScaffoldDefaults import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.contentColorFor import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.gaudian.translator.R
@Composable @Composable
fun AppScaffold( fun AppScaffold(
@@ -58,37 +47,3 @@ fun AppScaffold(
} }
@Composable
fun ParrotTopBar() {
val navyBlue = Color(0xFF1A237E) // The color from your mockup
CenterAlignedTopAppBar(
title = {
Text(
text = "ParrotPal",
style = MaterialTheme.typography.titleLarge,
color = Color.White
)
},
navigationIcon = {
// Your new parrot logo icon
Icon(
painter = painterResource(id = R.drawable.ic_level_parrot),
contentDescription = "Logo",
modifier = Modifier.size(32.dp),
tint = Color.Unspecified // Keeps the logo's original colors
)
},
actions = {
IconButton(onClick = { /* Search */ }) {
Icon(Icons.Default.Search, contentDescription = "Search", tint = Color.White)
}
IconButton(onClick = { /* Profile */ }) {
Icon(Icons.Default.AccountCircle, contentDescription = "Profile", tint = Color.White)
}
},
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = navyBlue
)
)
}

View File

@@ -1,5 +1,3 @@
@file:Suppress("HardCodedStringLiteral")
package eu.gaudian.translator.view.composable package eu.gaudian.translator.view.composable
import android.annotation.SuppressLint import android.annotation.SuppressLint
@@ -20,9 +18,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -36,6 +38,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.gaudian.translator.R
import eu.gaudian.translator.ui.theme.ThemePreviews import eu.gaudian.translator.ui.theme.ThemePreviews
/** /**
@@ -46,28 +49,50 @@ interface TabItem {
val title: String val title: String
val icon: ImageVector val icon: ImageVector
} }
@SuppressLint("UnusedBoxWithConstraintsScope", "LocalContextResourcesRead", "DiscouragedApi",
/** "SuspiciousIndentation"
* A generic, reusable tab layout composable. )
* @param T The type of the tab item, which must implement the TabItem interface.
* @param tabs A list of all tab items to display.
* @param selectedTab The currently selected tab item.
* @param onTabSelected A lambda function to be invoked when a tab is clicked.
*/
@SuppressLint("UnusedBoxWithConstraintsScope", "LocalContextResourcesRead", "DiscouragedApi")
@Composable @Composable
fun <T : TabItem> AppTabLayout( fun <T : TabItem> AppTabLayout(
tabs: List<T>, tabs: List<T>,
selectedTab: T, selectedTab: T,
onTabSelected: (T) -> Unit, onTabSelected: (T) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
onNavigateBack: (() -> Unit)? = null
) { ) {
val selectedIndex = tabs.indexOf(selectedTab) val selectedIndex = tabs.indexOf(selectedTab)
BoxWithConstraints( Row(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 8.dp) .padding(vertical = 8.dp, horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (onNavigateBack != null) {
IconButton(
onClick = onNavigateBack,
modifier = Modifier
.padding(end = 8.dp)
.size(40.dp)
.background(
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
shape = CircleShape
),
colors = IconButtonDefaults.iconButtonColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
contentColor = MaterialTheme.colorScheme.primary
)
) {
Icon(
imageVector = AppIcons.ArrowBack,
contentDescription = stringResource(R.string.cd_navigate_back)
)
}
}
BoxWithConstraints(
modifier = Modifier
.weight(1f)
.height(56.dp) .height(56.dp)
.background( .background(
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
@@ -140,7 +165,9 @@ fun <T : TabItem> AppTabLayout(
} }
} }
} }
}
@Suppress("HardCodedStringLiteral")
@ThemePreviews @ThemePreviews
@Composable @Composable
fun ModernTabLayoutPreview() { fun ModernTabLayoutPreview() {

View File

@@ -1,20 +1,25 @@
@file:Suppress("HardCodedStringLiteral")
package eu.gaudian.translator.view.composable package eu.gaudian.translator.view.composable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBackIosNew
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarColors import androidx.compose.material3.TopAppBarColors
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
@@ -25,8 +30,10 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.gaudian.translator.R import eu.gaudian.translator.R
import eu.gaudian.translator.ui.theme.ThemePreviews import eu.gaudian.translator.ui.theme.ThemePreviews
@@ -36,8 +43,8 @@ import eu.gaudian.translator.view.hints.LocalShowHints
@Composable @Composable
fun AppTopAppBar( fun AppTopAppBar(
title: @Composable () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
title: String,
onNavigateBack: (() -> Unit)? = null, onNavigateBack: (() -> Unit)? = null,
navigationIcon: @Composable (() -> Unit)? = null, navigationIcon: @Composable (() -> Unit)? = null,
actions: @Composable RowScope.() -> Unit = {}, actions: @Composable RowScope.() -> Unit = {},
@@ -47,25 +54,26 @@ fun AppTopAppBar(
val sheetState = rememberModalBottomSheetState() val sheetState = rememberModalBottomSheetState()
var showBottomSheet by remember { mutableStateOf(false) } var showBottomSheet by remember { mutableStateOf(false) }
TopAppBar( // Changed to CenterAlignedTopAppBar to perfectly match the design requirements
CenterAlignedTopAppBar(
modifier = modifier.height(56.dp), modifier = modifier.height(56.dp),
windowInsets = WindowInsets(0.dp), windowInsets = WindowInsets(0.dp),
colors = colors, colors = colors,
title = { title = {
Box(
modifier = Modifier.fillMaxHeight(),
contentAlignment = Alignment.Center
) {
val showHints = LocalShowHints.current val showHints = LocalShowHints.current
if (showHints && hintContent != null) { if (showHints && hintContent != null) {
// Simplified row: keeps the title and hint icon neatly centered together
Row( Row(
modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically,
verticalAlignment = Alignment.CenterVertically horizontalArrangement = Arrangement.Center
) { ) {
Box(modifier = Modifier.weight(1f)) { Text(
title() text = title,
} style = MaterialTheme.typography.titleLarge,
Box { fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f),
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
IconButton(onClick = { showBottomSheet = true }) { IconButton(onClick = { showBottomSheet = true }) {
Icon( Icon(
imageVector = AppIcons.Help, imageVector = AppIcons.Help,
@@ -74,62 +82,55 @@ fun AppTopAppBar(
) )
} }
} }
}
} else { } else {
title() Text(
} text = title,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
} }
}, },
navigationIcon = { navigationIcon = {
if (onNavigateBack != null) { if (onNavigateBack != null) {
Box( IconButton(
modifier = Modifier.fillMaxHeight(), onClick = onNavigateBack,
contentAlignment = Alignment.Center modifier = Modifier
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
) { ) {
IconButton(onClick = onNavigateBack) {
Icon( Icon(
AppIcons.ArrowBack, imageVector = Icons.Default.ArrowBackIosNew,
contentDescription = stringResource(R.string.cd_navigate_back), contentDescription = "Back",
tint = LocalContentColor.current modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.primary
) )
} }
}
} else if (navigationIcon != null) { } else if (navigationIcon != null) {
Box(modifier = Modifier.fillMaxHeight(), contentAlignment = Alignment.Center) {
navigationIcon() navigationIcon()
} }
} else {
// No navigation icon
}
}, },
actions = actions actions = actions
) )
if (showBottomSheet) { if (showBottomSheet) {
hintContent?.let {
HintBottomSheet( HintBottomSheet(
onDismissRequest = { onDismissRequest = {
@Suppress("AssignedValueIsNeverRead") @Suppress("AssignedValueIsNeverRead")
showBottomSheet = false showBottomSheet = false
}, },
sheetState = sheetState, sheetState = sheetState,
content = { content = it
hintContent?.Render()
}
) )
} }
} }
}
/** /**
* A composable that acts as a TopAppBar, containing a back navigation icon * A composable that acts as a TopAppBar, containing a back navigation icon
* and an [AppTabLayout]. * and an [AppTabLayout].
*
* @param T The type of the tab item, must implement [TabItem].
* @param tabs The list of tab items to display.
* @param selectedTab The currently selected tab item.
* @param onTabSelected Callback function when a tab is selected.
* @param onNavigateBack Callback function when the back arrow is clicked.
* @param modifier The modifier to be applied to the layout.
*/ */
@Composable @Composable
fun <T : TabItem> TabbedTopAppBar( fun <T : TabItem> TabbedTopAppBar(
@@ -139,7 +140,6 @@ fun <T : TabItem> TabbedTopAppBar(
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
// Use a Surface to provide background color and context for the app bar
Surface( Surface(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.surface color = MaterialTheme.colorScheme.surface
@@ -148,20 +148,21 @@ fun <T : TabItem> TabbedTopAppBar(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// Back navigation icon, similar to its usage in AppTopAppBar // Updated back icon here as well to keep your entire app consistent!
IconButton( IconButton(
onClick = onNavigateBack, onClick = onNavigateBack,
modifier = Modifier.padding(horizontal = 4.dp) modifier = Modifier
.padding(start = 8.dp, end = 4.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
) { ) {
Icon( Icon(
imageVector = AppIcons.ArrowBack, imageVector = AppIcons.ArrowBack,
contentDescription = stringResource(R.string.cd_navigate_back), contentDescription = stringResource(R.string.cd_navigate_back),
tint = MaterialTheme.colorScheme.onSurface tint = MaterialTheme.colorScheme.primary
) )
} }
// The AppTabLayout, taking up the remaining space.
// Its appearance matches the provided image.
AppTabLayout( AppTabLayout(
tabs = tabs, tabs = tabs,
selectedTab = selectedTab, selectedTab = selectedTab,
@@ -172,11 +173,12 @@ fun <T : TabItem> TabbedTopAppBar(
} }
} }
// ... [Previews remain exactly the same below]
@Suppress("HardCodedStringLiteral") @Suppress("HardCodedStringLiteral")
@ThemePreviews @ThemePreviews
@Composable @Composable
fun TabbedTopAppBarPreview() { fun TabbedTopAppBarPreview() {
// Sample data for preview, similar to ModernTabLayoutPreview
data class SampleTabItem(override val title: String, override val icon: ImageVector) : TabItem data class SampleTabItem(override val title: String, override val icon: ImageVector) : TabItem
val tabs = listOf( val tabs = listOf(
@@ -202,7 +204,7 @@ fun TabbedTopAppBarPreview() {
@Composable @Composable
fun AppTopAppBarPreview() { fun AppTopAppBarPreview() {
AppTopAppBar( AppTopAppBar(
title = { Text("Preview Title") } title = "Previwe Title"
) )
} }
@@ -210,7 +212,7 @@ fun AppTopAppBarPreview() {
@Composable @Composable
fun AppTopAppBarWithNavigationIconPreview() { fun AppTopAppBarWithNavigationIconPreview() {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.title_title_preview_title)) }, title = "Preview Title",
onNavigateBack = {} onNavigateBack = {}
) )
} }
@@ -219,13 +221,13 @@ fun AppTopAppBarWithNavigationIconPreview() {
@Composable @Composable
fun AppTopAppBarWithActionsPreview() { fun AppTopAppBarWithActionsPreview() {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.title_title_preview_title)) }, title = "Preview Title",
actions = { actions = {
IconButton(onClick = {}) { IconButton(onClick = {}) {
Icon(AppIcons.Settings, contentDescription = stringResource(R.string.title_settings)) Icon(AppIcons.Settings, contentDescription = stringResource(R.string.title_settings))
} }
IconButton(onClick = {}) { IconButton(onClick = {}) {
AppIcons.ArrowBack Icon(AppIcons.ArrowBack, contentDescription = null)
} }
} }
) )

View File

@@ -1,4 +1,4 @@
@file:Suppress("HardCodedStringLiteral") @file:Suppress("HardCodedStringLiteral", "AssignedValueIsNeverRead")
package eu.gaudian.translator.view.composable package eu.gaudian.translator.view.composable
@@ -11,23 +11,44 @@ import androidx.compose.animation.core.spring
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.material3.NavigationBarItemDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
@@ -41,6 +62,7 @@ import androidx.navigation.NavDestination.Companion.hierarchy
import eu.gaudian.translator.R import eu.gaudian.translator.R
import eu.gaudian.translator.ui.theme.ThemePreviews import eu.gaudian.translator.ui.theme.ThemePreviews
import eu.gaudian.translator.view.LocalShowExperimentalFeatures import eu.gaudian.translator.view.LocalShowExperimentalFeatures
import kotlinx.coroutines.launch
sealed class Screen( sealed class Screen(
val route: String, val route: String,
@@ -48,34 +70,42 @@ sealed class Screen(
val selectedIcon: ImageVector, val selectedIcon: ImageVector,
val unselectedIcon: ImageVector val unselectedIcon: ImageVector
) { ) {
object Home : Screen("home", R.string.label_translation, AppIcons.TranslateFilled, AppIcons.TranslateOutlined) object Home : Screen("home", R.string.label_home, AppIcons.Home, AppIcons.Home)
object Stats : Screen("stats", R.string.label_stats, AppIcons.Statistics, AppIcons.Statistics)
object Translation : Screen("translation", R.string.label_translation, AppIcons.TranslateFilled, AppIcons.TranslateOutlined)
object Library : Screen("library", R.string.label_library, AppIcons.VocabularyFilled, AppIcons.VocabularyOutlined)
object Settings : Screen("settings", R.string.title_settings, AppIcons.SettingsFilled, AppIcons.SettingsOutlined)
object More : Screen("more", R.string.label_more, AppIcons.MoreVert, AppIcons.MoreHorizontal)
object Dictionary : Screen("dictionary", R.string.label_dictionary, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined) object Dictionary : Screen("dictionary", R.string.label_dictionary, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined)
object Exercises : Screen("exercises", R.string.label_exercises, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined) object Exercises : Screen("exercises", R.string.label_exercises, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined)
object Vocabulary : Screen("vocabulary", R.string.label_vocabulary, AppIcons.VocabularyFilled, AppIcons.VocabularyOutlined)
object Settings : Screen("settings", R.string.title_settings, AppIcons.SettingsFilled, AppIcons.SettingsOutlined)
companion object { companion object {
fun getAllScreens(showExperimental: Boolean = false): List<Screen> { fun getAllScreens(showExperimental: Boolean = false): List<Screen> {
val screens = mutableListOf(Home, Dictionary, Vocabulary, Settings) return listOf(Home, Library, Stats)
if (showExperimental) {
screens.add(2, Exercises)
} }
return screens
fun getMoreMenuItems(showExperimental: Boolean = false): List<Screen> {
val items = mutableListOf<Screen>()
items.add(Translation)
items.add(Dictionary)
items.add(Settings)
if (showExperimental) {
items.add(Exercises)
}
return items
} }
@Composable @Composable
fun fromDestination(destination: NavDestination?): Screen { fun fromDestination(destination: NavDestination?): Screen {
val showExperimental = LocalShowExperimentalFeatures.current val showExperimental = LocalShowExperimentalFeatures.current
return getAllScreens(showExperimental).find { screen -> val allScreens = getAllScreens(showExperimental) + getMoreMenuItems(showExperimental) + More
return allScreens.find { screen ->
destination?.hierarchy?.any { it.route == screen.route } == true destination?.hierarchy?.any { it.route == screen.route } == true
} ?: Home } ?: Home
} }
} }
} }
/**
* A modernized Material 3 bottom navigation bar with spring animations and haptic feedback.
*/
@SuppressLint("UnusedBoxWithConstraintsScope") @SuppressLint("UnusedBoxWithConstraintsScope")
@Composable @Composable
fun BottomNavigationBar( fun BottomNavigationBar(
@@ -84,40 +114,87 @@ fun BottomNavigationBar(
showLabels: Boolean, showLabels: Boolean,
onItemSelected: (Screen) -> Unit, onItemSelected: (Screen) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onPlayClicked: () -> Unit = {}
) { ) {
val showExperimental = LocalShowExperimentalFeatures.current val showExperimental = LocalShowExperimentalFeatures.current
val screens = remember(showExperimental) { Screen.getAllScreens(showExperimental) } val screens = remember(showExperimental) { Screen.getAllScreens(showExperimental) }
val moreScreen = remember { Screen.More }
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
var showMoreMenu by remember { mutableStateOf(false) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val scope = rememberCoroutineScope()
// Configuration for the play button
val playButtonSize = 56.dp
val glowPadding = 12.dp // Total extra space for the glow (16dp on each side)
// This dictates how far up the button shifts.
// Setting it to around half the button size centers it on the top border.
val upwardOffset = 16.dp
AnimatedVisibility( AnimatedVisibility(
visible = isVisible, visible = isVisible,
enter = slideInVertically( enter = slideInVertically(
animationSpec = androidx.compose.animation.core.tween(durationMillis = 220), animationSpec = spring(stiffness = Spring.StiffnessHigh),
initialOffsetY = { it } initialOffsetY = { it }
), ),
exit = slideOutVertically( exit = slideOutVertically(
animationSpec = androidx.compose.animation.core.tween(durationMillis = 220), animationSpec = spring(stiffness = Spring.StiffnessHigh),
targetOffsetY = { it } targetOffsetY = { it }
) )
) { ) {
val baseHeight = if (showLabels) 80.dp else 56.dp val baseHeight = if (showLabels) 80.dp else 56.dp
val density = LocalDensity.current val density = LocalDensity.current
val navBarDp = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() } val navBarDp = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() }
val height = baseHeight + navBarDp val height = baseHeight + navBarDp
NavigationBar( // Outer Box height is purely determined by the NavigationBar now
modifier = modifier.height(height), Box(
containerColor = MaterialTheme.colorScheme.surface, // Cleaner look than surfaceVariant modifier = modifier.fillMaxWidth(),
tonalElevation = 8.dp, // Slight elevation for depth contentAlignment = Alignment.TopCenter
) { ) {
screens.forEach { screen ->
val isSelected = screen == selectedItem // The actual Navigation Bar
NavigationBar(
modifier = Modifier.height(height),
containerColor = MaterialTheme.colorScheme.surface,
tonalElevation = 8.dp,
) {
// Create a list of 5 items (2 left, 1 empty spacer, 2 right)
val allNavItems = buildList {
addAll(screens.take(2))
add(null) // Empty spacer for Play Button gap
if (screens.size > 2) {
addAll(screens.drop(2))
}
add(moreScreen)
}
allNavItems.forEach { screen ->
if (screen == null) {
// Dummy item to create the gap
NavigationBarItem(
selected = false,
onClick = {},
enabled = false, // Disables ripples and clicks
icon = { Spacer(modifier = Modifier.size(24.dp)) },
label = if (showLabels) { { Spacer(modifier = Modifier.size(10.dp)) } } else null,
colors = NavigationBarItemDefaults.colors(
disabledIconColor = Color.Transparent,
disabledTextColor = Color.Transparent
)
)
} else {
// Regular or More items
val isSelected = if (screen == Screen.More) {
selectedItem is Screen.More || Screen.getMoreMenuItems(showExperimental).contains(selectedItem)
} else {
screen == selectedItem
}
val title = stringResource(id = screen.title) val title = stringResource(id = screen.title)
// 1. Spring Animation for the Icon Scale
val scale by animateFloatAsState( val scale by animateFloatAsState(
targetValue = if (isSelected) 1.2f else 1.0f, // Subtle pop effect targetValue = if (isSelected) 1.2f else 1.0f,
animationSpec = spring( animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy, dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow stiffness = Spring.StiffnessLow
@@ -129,8 +206,8 @@ fun BottomNavigationBar(
selected = isSelected, selected = isSelected,
onClick = { onClick = {
if (!isSelected) { if (!isSelected) {
haptic.performHapticFeedback(HapticFeedbackType.LongPress) // 2. Tactile feedback haptic.performHapticFeedback(HapticFeedbackType.LongPress)
onItemSelected(screen) if (screen == Screen.More) showMoreMenu = true else onItemSelected(screen)
} }
}, },
label = if (showLabels) { label = if (showLabels) {
@@ -145,12 +222,11 @@ fun BottomNavigationBar(
} }
} else null, } else null,
icon = { icon = {
// 3. Crossfade between Outlined and Filled icons
Crossfade(targetState = isSelected, label = "iconFade") { selected -> Crossfade(targetState = isSelected, label = "iconFade") { selected ->
Icon( Icon(
imageVector = if (selected) screen.selectedIcon else screen.unselectedIcon, imageVector = if (selected) screen.selectedIcon else screen.unselectedIcon,
contentDescription = title, contentDescription = title,
modifier = Modifier.scale(scale) // Apply the spring scale modifier = Modifier.scale(scale)
) )
} }
}, },
@@ -165,8 +241,147 @@ fun BottomNavigationBar(
} }
} }
} }
// The Glowing Play Button
Box(
modifier = Modifier
// This negative offset pulls the button UP out of the bounding box
// without increasing the layout height of the parent Box.
.offset(y = -upwardOffset)
.size(playButtonSize + glowPadding),
contentAlignment = Alignment.Center
) {
// Background radial glow
Box(
modifier = Modifier
.matchParentSize()
.background(
brush = Brush.radialGradient(
colors = listOf(
MaterialTheme.colorScheme.primary.copy(alpha = 0.5f),
Color.Transparent
)
),
shape = CircleShape
)
)
// Actual clickable button
Box(
modifier = Modifier
.size(playButtonSize)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primaryContainer)
.clickable {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
onPlayClicked()
},
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = "Play",
tint = Color.White,
modifier = Modifier.size(32.dp)
)
}
}
}
} }
// Modal Bottom Sheet for More menu (Remains exactly the same)
if (showMoreMenu) {
ModalBottomSheet(
onDismissRequest = { showMoreMenu = false },
sheetState = sheetState
) {
MoreBottomSheetContent(
showExperimental = showExperimental,
onItemSelected = { screen ->
scope.launch {
sheetState.hide()
showMoreMenu = false
onItemSelected(screen)
}
}
)
}
}
}
@Composable
private fun MoreBottomSheetContent(
showExperimental: Boolean,
onItemSelected: (Screen) -> Unit
) {
val moreItems = remember(showExperimental) { Screen.getMoreMenuItems(showExperimental) }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 32.dp)
) {
Text(
text = stringResource(R.string.label_more),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold, // Added bold to match the new style
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
)
// Removed HorizontalDivider() for a cleaner look
moreItems.forEach { screen ->
MoreMenuItem(
screen = screen,
onClick = { onItemSelected(screen) }
)
}
}
}
@Composable
fun MoreMenuItem(
screen: Screen,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(horizontal = 24.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Circular Icon Background
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.surfaceVariant,
modifier = Modifier.size(52.dp)
) {
Box(contentAlignment = Alignment.Center) {
// Adjust this depending on whether your Screen uses ImageVector or Drawable Res
Icon(
imageVector = screen.selectedIcon,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(28.dp)
)
}
}
Spacer(modifier = Modifier.width(16.dp))
// Title
Text(
text = stringResource(id = screen.title), // Adjust to your actual string property
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f)
)
}
}
@ThemePreviews @ThemePreviews
@Composable @Composable
fun BottomNavigationBarPreview() { fun BottomNavigationBarPreview() {

View File

@@ -28,6 +28,7 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedCard
@@ -36,6 +37,7 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -56,6 +58,9 @@ import eu.gaudian.translator.R
import eu.gaudian.translator.ui.theme.ThemePreviews import eu.gaudian.translator.ui.theme.ThemePreviews
import eu.gaudian.translator.ui.theme.semanticColors import eu.gaudian.translator.ui.theme.semanticColors
import eu.gaudian.translator.view.composable.ComponentDefaults.DefaultElevation import eu.gaudian.translator.view.composable.ComponentDefaults.DefaultElevation
import eu.gaudian.translator.view.hints.Hint
import eu.gaudian.translator.view.hints.HintBottomSheet
import eu.gaudian.translator.view.hints.LocalShowHints
object ComponentDefaults { object ComponentDefaults {
@@ -97,13 +102,16 @@ object ComponentDefaults {
fun AppCard( fun AppCard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
title: String? = null, title: String? = null,
icon: ImageVector? = null, // New optional icon parameter icon: ImageVector? = null,
text: String? = null, text: String? = null,
expandable: Boolean = false, expandable: Boolean = false,
initiallyExpanded: Boolean = false, initiallyExpanded: Boolean = false,
onClick: (() -> Unit)? = null,
hintContent : Hint? = null,
content: @Composable ColumnScope.() -> Unit, content: @Composable ColumnScope.() -> Unit,
) { ) {
var isExpanded by remember { mutableStateOf(initiallyExpanded) } var isExpanded by remember { mutableStateOf(initiallyExpanded) }
val showHints = LocalShowHints.current
val rotationState by animateFloatAsState( val rotationState by animateFloatAsState(
targetValue = if (isExpanded) 180f else 0f, targetValue = if (isExpanded) 180f else 0f,
@@ -113,6 +121,21 @@ fun AppCard(
// Check if we need to render the header row // Check if we need to render the header row
// Updated to include icon in the check // Updated to include icon in the check
val hasHeader = title != null || text != null || expandable || icon != null val hasHeader = title != null || text != null || expandable || icon != null
val canClickHeader = expandable || onClick != null
var showBottomSheet by remember { mutableStateOf(false) }
if (showBottomSheet) {
hintContent?.let {
HintBottomSheet(
onDismissRequest = { showBottomSheet = false },
content = it,
sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
)
)
}
}
Surface( Surface(
modifier = modifier modifier = modifier
@@ -125,7 +148,7 @@ fun AppCard(
// Animate height changes when expanding/collapsing // Animate height changes when expanding/collapsing
.animateContentSize(), .animateContentSize(),
shape = ComponentDefaults.CardShape, shape = ComponentDefaults.CardShape,
color = MaterialTheme.colorScheme.surfaceContainer color = MaterialTheme.colorScheme.surfaceContainer,
) { ) {
Column { Column {
// --- Header Row --- // --- Header Row ---
@@ -133,12 +156,18 @@ fun AppCard(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable(enabled = expandable) { isExpanded = !isExpanded } .clickable(enabled = canClickHeader) {
if (expandable) {
isExpanded = !isExpanded
}
onClick?.invoke()
}
.padding(ComponentDefaults.CardPadding), .padding(ComponentDefaults.CardPadding),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// 1. Optional Icon on the left // 1. Optional Icon on the left
if (icon != null) { if (icon != null) {
Spacer(modifier = Modifier.width(8.dp))
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = null, contentDescription = null,
@@ -172,6 +201,16 @@ fun AppCard(
} }
} }
if (showHints && hintContent != null) {
IconButton(onClick = { showBottomSheet = true }) {
Icon(
imageVector = AppIcons.Help,
contentDescription = stringResource(R.string.show_hint),
tint = MaterialTheme.colorScheme.secondary
)
}
}
// 3. Expand Chevron (Far right) // 3. Expand Chevron (Far right)
if (expandable) { if (expandable) {
Icon( Icon(
@@ -182,21 +221,32 @@ fun AppCard(
) )
} }
} }
} }
// --- Content Area --- // --- Content Area ---
if (!expandable || isExpanded) { if (!expandable || isExpanded) {
Column( val contentModifier = Modifier
modifier = Modifier.padding( .padding(
start = ComponentDefaults.CardPadding, start = ComponentDefaults.CardPadding,
end = ComponentDefaults.CardPadding, end = ComponentDefaults.CardPadding,
bottom = ComponentDefaults.CardPadding, bottom = ComponentDefaults.CardPadding,
// If we have a header, remove the top padding so content sits closer to the title. // If we have a header, remove the top padding so content sits closer to the title.
// If no header (legacy behavior), keep the top padding. // If no header (legacy behavior), keep the top padding.
top = if (hasHeader) 0.dp else ComponentDefaults.CardPadding top = if (hasHeader) 0.dp else ComponentDefaults.CardPadding
), )
if (!hasHeader && onClick != null) {
Column(
modifier = contentModifier.clickable { onClick() },
content = content content = content
) )
} else {
Column(
modifier = contentModifier,
content = content
)
}
} }
} }
} }

View File

@@ -56,6 +56,8 @@ fun BaseLanguageDropDown(
enableMultipleSelection: Boolean = false, enableMultipleSelection: Boolean = false,
onLanguagesSelected: (List<Language>) -> Unit = {}, onLanguagesSelected: (List<Language>) -> Unit = {},
alternateLanguages: List<Language> = emptyList(), alternateLanguages: List<Language> = emptyList(),
restrictToAlternateLanguages: Boolean = false,
enabled: Boolean = true,
iconEnabled: Boolean = true, iconEnabled: Boolean = true,
noBorder: Boolean = false, noBorder: Boolean = false,
) { ) {
@@ -68,9 +70,13 @@ fun BaseLanguageDropDown(
var tempSelection by remember { mutableStateOf<List<Language>>(emptyList()) } var tempSelection by remember { mutableStateOf<List<Language>>(emptyList()) }
var selectedLanguagesCount by remember { mutableIntStateOf(if (enableMultipleSelection) tempSelection.size else 0) } var selectedLanguagesCount by remember { mutableIntStateOf(if (enableMultipleSelection) tempSelection.size else 0) }
val languages = remember(alternateLanguages, defaultLanguages) { val languages = remember(alternateLanguages, defaultLanguages, restrictToAlternateLanguages) {
if (restrictToAlternateLanguages) {
alternateLanguages
} else {
alternateLanguages.ifEmpty { defaultLanguages } alternateLanguages.ifEmpty { defaultLanguages }
} }
}
val buttonText = when { val buttonText = when {
enableMultipleSelection && selectedLanguagesCount > 0 -> stringResource( enableMultipleSelection && selectedLanguagesCount > 0 -> stringResource(
@@ -90,6 +96,7 @@ fun BaseLanguageDropDown(
AppOutlinedButton( AppOutlinedButton(
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
onClick = { expanded = true }, onClick = { expanded = true },
enabled = enabled,
contentPadding = if (!iconEnabled) PaddingValues(0.dp) else PaddingValues(horizontal = 16.dp, vertical = 8.dp), contentPadding = if (!iconEnabled) PaddingValues(0.dp) else PaddingValues(horizontal = 16.dp, vertical = 8.dp),
borderColor = if (noBorder) Color.Unspecified else null borderColor = if (noBorder) Color.Unspecified else null
) { ) {
@@ -222,7 +229,12 @@ fun BaseLanguageDropDown(
val isSearching = searchText.isNotBlank() val isSearching = searchText.isNotBlank()
if (isSearching) { if (isSearching) {
val searchResults = (favoriteLanguages + languageHistory + languages) val searchBase = if (restrictToAlternateLanguages) {
alternateLanguages
} else {
favoriteLanguages + languageHistory + languages
}
val searchResults = searchBase
.distinctBy { it.nameResId } .distinctBy { it.nameResId }
.filter { language -> .filter { language ->
val matchesName = language.name.contains(searchText, ignoreCase = true) val matchesName = language.name.contains(searchText, ignoreCase = true)
@@ -237,6 +249,16 @@ fun BaseLanguageDropDown(
searchResults.forEach { language -> SingleSelectItem(language) } searchResults.forEach { language -> SingleSelectItem(language) }
} }
} else if (restrictToAlternateLanguages) {
val sortedAlternate = alternateLanguages.sortedBy { it.name }
if (enableMultipleSelection) {
DropdownHeader(text = stringResource(R.string.text_all_languages))
sortedAlternate.forEach { language -> MultiSelectItem(language) }
} else {
DropdownHeader(text = stringResource(R.string.text_all_languages))
sortedAlternate.forEach { language -> SingleSelectItem(language) }
}
} else if (alternateLanguages.isNotEmpty()) { } else if (alternateLanguages.isNotEmpty()) {
val sortedAlternate = alternateLanguages.sortedBy { it.name } val sortedAlternate = alternateLanguages.sortedBy { it.name }
if (enableMultipleSelection) { if (enableMultipleSelection) {
@@ -458,7 +480,9 @@ fun SingleLanguageDropDown(
onAutoSelected: () -> Unit = {}, onAutoSelected: () -> Unit = {},
showNoneOption: Boolean = false, showNoneOption: Boolean = false,
onNoneSelected: () -> Unit = {}, onNoneSelected: () -> Unit = {},
alternateLanguages: List<Language> = emptyList() alternateLanguages: List<Language> = emptyList(),
restrictToAlternateLanguages: Boolean = false,
enabled: Boolean = true
) { ) {
val languageHistory by languageViewModel.languageHistory.collectAsState() val languageHistory by languageViewModel.languageHistory.collectAsState()
@@ -477,6 +501,10 @@ fun SingleLanguageDropDown(
showNoneOption = showNoneOption, showNoneOption = showNoneOption,
onNoneSelected = onNoneSelected, onNoneSelected = onNoneSelected,
enableMultipleSelection = false, enableMultipleSelection = false,
alternateLanguages = alternateLanguages alternateLanguages = alternateLanguages,
restrictToAlternateLanguages = restrictToAlternateLanguages,
enabled = enabled,
iconEnabled = enabled,
noBorder = !enabled
) )
} }

View File

@@ -6,8 +6,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -29,8 +27,6 @@ fun CategorySelectionDialog(
val activity = LocalContext.current.findActivity() val activity = LocalContext.current.findActivity()
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity) val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
AppDialog( AppDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = { title = {

View File

@@ -1,219 +0,0 @@
package eu.gaudian.translator.view.dialogs
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import eu.gaudian.translator.R
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppDialog
import eu.gaudian.translator.view.composable.AppSlider
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.HintDefinition
import eu.gaudian.translator.viewmodel.LanguageViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel
import kotlinx.coroutines.launch
@Composable
fun ImportVocabularyDialog(
onDismiss: () -> Unit,
languageViewModel: LanguageViewModel,
vocabularyViewModel : VocabularyViewModel,
optionalDescription: String? = null,
optionalSearchTerm: String? = null
) {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "import") {
composable("import") {
ImportDialogContent(
navController = navController,
onDismiss = onDismiss,
languageViewModel = languageViewModel,
optionalDescription = optionalDescription,
optionalSearchTerm = optionalSearchTerm
)
}
@Suppress("HardCodedStringLiteral")
composable("review") {
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
// Full-screen surface to ensure the dialog covers content and stays above the main FAB/menu
Surface(modifier = Modifier.fillMaxSize()) {
VocabularyReviewScreen(
onConfirm = { selectedItems, categoryIds ->
vocabularyViewModel.addVocabularyItems(selectedItems, categoryIds)
onDismiss()
},
onCancel = onDismiss
)
}
}
}
}
}
@Composable
fun ImportDialogContent(
navController: NavController,
onDismiss: () -> Unit,
languageViewModel: LanguageViewModel,
optionalDescription: String? = null,
optionalSearchTerm: String? = null
) {
val activity = LocalContext.current.findActivity()
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
var category by remember { mutableStateOf(optionalSearchTerm ?: "") }
var amount by remember { mutableFloatStateOf(1f) }
val coroutineScope = rememberCoroutineScope()
val descriptionText = optionalDescription ?: stringResource(R.string.text_let_ai_find_vocabulary_for_you)
val isGenerating by vocabularyViewModel.isGenerating.collectAsState()
AppDialog(
onDismissRequest = onDismiss,
title = { Text(descriptionText) },
hintContent = HintDefinition.IMPORT.hint(),
content = {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(4.dp)
) {
if (isGenerating) {
Box(
modifier = Modifier.fillMaxWidth().padding(16.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else {
Text(
text = stringResource(R.string.text_search_term),
style = MaterialTheme.typography.labelLarge,
modifier = Modifier.padding(bottom = 8.dp)
)
// Modern rotating field using XML resource array
InspiringSearchField(
value = category,
hints = stringArrayResource(R.array.vocabulary_hints),
onValueChange = { category = it }
)
// The "Dica" string has been removed to keep the interface clean
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(R.string.text_select_languages),
style = MaterialTheme.typography.labelLarge,
modifier = Modifier.padding(bottom = 8.dp)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(1.dp)
) {
SourceLanguageDropdown(languageViewModel = languageViewModel)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(1.dp)
) {
TargetLanguageDropdown(languageViewModel = languageViewModel)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(R.string.text_select_amount),
style = MaterialTheme.typography.labelLarge,
modifier = Modifier.padding(bottom = 8.dp)
)
AppSlider(
value = amount,
onValueChange = { amount = it },
valueRange = 1f..25f,
steps = 24,
modifier = Modifier.fillMaxWidth()
)
Text(
text = stringResource(R.string.text_amount_2d, amount.toInt()),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.fillMaxWidth().padding(top = 8.dp)
)
Spacer(modifier = Modifier.height(8.dp))
}
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
DialogButton(
onClick = onDismiss,
content = { Text(stringResource(R.string.label_cancel)) }
)
if (category.isNotBlank() && !isGenerating) {
Spacer(modifier = Modifier.width(8.dp))
DialogButton(onClick = {
coroutineScope.launch {
vocabularyViewModel.generateVocabularyItems(category, amount.toInt())
@Suppress("HardCodedStringLiteral")
navController.navigate("review")
}
}) { Text(stringResource(R.string.text_generate)) }
}
}
}
}
)
}
@Suppress("HardCodedStringLiteral")
@Preview
@Composable
fun ImportDialogContentPreview() {
val activity = LocalContext.current.findActivity()
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
ImportDialogContent(
navController = rememberNavController(),
onDismiss = {},
languageViewModel = languageViewModel,
optionalDescription = "Let AI find vocabulary for you",
optionalSearchTerm = "Travel"
)
}

View File

@@ -1,132 +0,0 @@
package eu.gaudian.translator.view.dialogs
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import eu.gaudian.translator.R
import eu.gaudian.translator.model.Language
import eu.gaudian.translator.model.VocabularyCategory
import eu.gaudian.translator.model.VocabularyStage
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppDialog
import eu.gaudian.translator.view.composable.MultipleLanguageDropdown
import eu.gaudian.translator.viewmodel.CategoryViewModel
import eu.gaudian.translator.viewmodel.LanguageViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel
import kotlinx.coroutines.launch
@Composable
fun StartExerciseDialog(
onDismiss: () -> Unit,
onConfirm: (
categories: List<VocabularyCategory>,
stages: List<VocabularyStage>,
languageIds: List<Int>
) -> Unit
) {
val activity = LocalContext.current.findActivity()
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
val coroutineScope = rememberCoroutineScope()
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
var lids by remember { mutableStateOf<List<Int>>(emptyList()) }
var languages by remember { mutableStateOf<List<Language>>(emptyList()) }
// Map displayed Language to its DB id (lid) using position mapping from load
var languageIdMap by remember { mutableStateOf<Map<Language, Int>>(emptyMap()) }
var selectedLanguages by remember { mutableStateOf<List<Language>>(emptyList()) }
var selectedStages by remember { mutableStateOf<List<VocabularyStage>>(emptyList()) }
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory>>(emptyList()) }
LaunchedEffect(Unit) {
coroutineScope.launch {
lids = vocabularyViewModel.getAllLanguagesIdsPresent().filterNotNull().toList()
languages = lids.map { lid ->
languageViewModel.getLanguageById(lid)
}
// build reverse map
languageIdMap = languages.mapIndexed { index, lang -> lang to lids.getOrNull(index) }.filter { it.second != null }.associate { it.first to it.second!! }
}
}
AppDialog(onDismissRequest = onDismiss, title = { Text(stringResource(R.string.label_start_exercise)) }) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
MultipleLanguageDropdown(
modifier = Modifier.fillMaxWidth(),
languageViewModel = languageViewModel,
onLanguagesSelected = { langs ->
selectedLanguages = langs
},
languages
)
CategoryDropdown(
onCategorySelected = { cats ->
selectedCategories = cats.filterIsInstance<VocabularyCategory>()
},
multipleSelectable = true,
onlyLists = false, // Show both filters and lists
addCategory = false,
modifier = Modifier.fillMaxWidth(),
)
VocabularyStageDropDown(
modifier = Modifier.fillMaxWidth(),
preselectedStages = selectedStages,
onStageSelected = { stages ->
@Suppress("FilterIsInstanceResultIsAlwaysEmpty")
selectedStages = stages.filterIsInstance<VocabularyStage>()
},
multipleSelectable = true
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
horizontalArrangement = Arrangement.End
) {
TextButton(
onClick = onDismiss,
) {
Text(stringResource(R.string.label_cancel))
}
TextButton(
onClick = {
run {
val ids = selectedLanguages.mapNotNull { languageIdMap[it] }
onConfirm(selectedCategories, selectedStages, ids)
}
}
) {
Text(stringResource(R.string.label_start_exercise))
}
}
}
}
}

View File

@@ -1,73 +0,0 @@
@file:Suppress("AssignedValueIsNeverRead")
package eu.gaudian.translator.view.dialogs
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import eu.gaudian.translator.R
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppFabMenu
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.FabMenuItem
import eu.gaudian.translator.viewmodel.LanguageViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel
@Composable
fun VocabularyMenu(
modifier: Modifier = Modifier,
showFabText : Boolean = true
) {
val activity = LocalContext.current.findActivity()
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
var showAddVocabularyDialog by remember { mutableStateOf(false) }
var showImportVocabularyDialog by remember { mutableStateOf(false) }
var showAddCategoryDialog by remember { mutableStateOf(false) }
val menuItems = listOf(
FabMenuItem(
text = stringResource(R.string.label_add_vocabulary),
imageVector = AppIcons.Add,
onClick = { showAddVocabularyDialog = true }
),
FabMenuItem(
text = stringResource(R.string.menu_import_vocabulary),
imageVector = AppIcons.AI,
onClick = { showImportVocabularyDialog = true }
),
FabMenuItem(
text = stringResource(R.string.label_add_category),
imageVector = AppIcons.Add,
onClick = { showAddCategoryDialog = true }
)
)
AppFabMenu(items = menuItems, modifier = modifier, title = stringResource(R.string.label_add_vocabulary), showFabText = showFabText)
if (showAddVocabularyDialog) {
AddVocabularyDialog(
onDismissRequest = { showAddVocabularyDialog = false }
)
}
if (showImportVocabularyDialog) {
ImportVocabularyDialog(
languageViewModel = languageViewModel,
vocabularyViewModel = vocabularyViewModel,
onDismiss = { showImportVocabularyDialog = false }
)
}
if (showAddCategoryDialog) {
AddCategoryDialog(
onDismiss = { showAddCategoryDialog = false }
)
}
}

View File

@@ -35,7 +35,6 @@ import eu.gaudian.translator.view.composable.AppCheckbox
import eu.gaudian.translator.view.composable.AppScaffold import eu.gaudian.translator.view.composable.AppScaffold
import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.hints.HintDefinition import eu.gaudian.translator.view.hints.HintDefinition
import eu.gaudian.translator.viewmodel.CategoryViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel import eu.gaudian.translator.viewmodel.VocabularyViewModel
@Composable @Composable
@@ -45,10 +44,8 @@ fun VocabularyReviewScreen(
) { ) {
val activity = LocalContext.current.findActivity() val activity = LocalContext.current.findActivity()
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(activity) val vocabularyViewModel: VocabularyViewModel = hiltViewModel(activity)
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
val generatedItems: List<VocabularyItem> by vocabularyViewModel.generatedVocabularyItems.collectAsState() val generatedItems: List<VocabularyItem> by vocabularyViewModel.generatedVocabularyItems.collectAsState()
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
val selectedItems = remember { mutableStateListOf<VocabularyItem>() } val selectedItems = remember { mutableStateListOf<VocabularyItem>() }
val duplicates = remember { mutableStateListOf<Boolean>() } val duplicates = remember { mutableStateListOf<Boolean>() }
@@ -65,7 +62,7 @@ fun VocabularyReviewScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.found_items)) }, title = stringResource(R.string.found_items),
hintContent = HintDefinition.REVIEW.hint() hintContent = HintDefinition.REVIEW.hint()
) )
}, },

View File

@@ -381,7 +381,7 @@ fun DefinitionPart(part: EntryPart) {
// Fallback for JsonObject or other top-level types // Fallback for JsonObject or other top-level types
else -> contentElement.toString() else -> contentElement.toString()
} }
} catch (e: Exception) { } catch (_: Exception) {
// Ultimate fallback if something else goes wrong during parsing // Ultimate fallback if something else goes wrong during parsing
part.content.toString() part.content.toString()
} }
@@ -466,12 +466,6 @@ fun DefinitionPartPreview() {
DefinitionPart(part = mockPart) DefinitionPart(part = mockPart)
} }
// Data classes for the refactored components
data class EntryData(
val entry: DictionaryEntry,
val language: Language?
)
data class BreadcrumbItem( data class BreadcrumbItem(
val word: String, val word: String,
val entryId: Int val entryId: Int

View File

@@ -10,8 +10,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -28,7 +26,6 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavController import androidx.navigation.NavController
@@ -346,28 +343,8 @@ fun DictionarySimpleTopBar(
onNavigateBack: () -> Unit onNavigateBack: () -> Unit
) { ) {
AppTopAppBar( AppTopAppBar(
title = { title = "TODO",
Column { onNavigateBack = onNavigateBack
Text(
text = word ?: stringResource(R.string.text_loading_3d),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
languageName?.let {
Text(
text = it,
style = MaterialTheme.typography.bodyMedium,
fontStyle = FontStyle.Italic
)
}
}
},
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
actions = {}
) )
} }

View File

@@ -1,3 +1,5 @@
@file:Suppress("SameParameterValue")
package eu.gaudian.translator.view.dictionary package eu.gaudian.translator.view.dictionary
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize

View File

@@ -30,7 +30,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@@ -94,27 +93,8 @@ fun EtymologyResultScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { title = "TODO",
Column { onNavigateBack = { navController.popBackStack() },
Text(
text = word,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
language?.name?.let {
Text(
text = it,
style = MaterialTheme.typography.bodyMedium,
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic
)
}
}
},
navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
actions = { actions = {
etymologyData?.let { data -> etymologyData?.let { data ->
if (isTtsAvailable) { if (isTtsAvailable) {

View File

@@ -21,6 +21,7 @@ import eu.gaudian.translator.view.LocalConnectionConfigured
import eu.gaudian.translator.view.NoConnectionScreen import eu.gaudian.translator.view.NoConnectionScreen
import eu.gaudian.translator.view.composable.AppIcons import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppTabLayout import eu.gaudian.translator.view.composable.AppTabLayout
import eu.gaudian.translator.view.composable.Screen
import eu.gaudian.translator.view.composable.TabItem import eu.gaudian.translator.view.composable.TabItem
import eu.gaudian.translator.view.settings.SettingsRoutes import eu.gaudian.translator.view.settings.SettingsRoutes
import eu.gaudian.translator.viewmodel.CorrectionViewModel import eu.gaudian.translator.viewmodel.CorrectionViewModel
@@ -63,7 +64,15 @@ fun MainDictionaryScreen(
AppTabLayout( AppTabLayout(
tabs = dictionaryTabs, tabs = dictionaryTabs,
selectedTab = selectedTab, selectedTab = selectedTab,
onTabSelected = { selectedTab = it } onTabSelected = { selectedTab = it },
onNavigateBack = {
if (!navController.popBackStack()) {
navController.navigate(Screen.Home.route) {
launchSingleTop = true
restoreState = false
}
}
}
) )
when (selectedTab) { when (selectedTab) {

View File

@@ -15,7 +15,7 @@ import androidx.navigation.NavHostController
import eu.gaudian.translator.R import eu.gaudian.translator.R
import eu.gaudian.translator.view.composable.AppButton import eu.gaudian.translator.view.composable.AppButton
import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.vocabulary.VocabularyListScreen import eu.gaudian.translator.view.vocabulary.AllCardsListScreen
@Composable @Composable
fun ExerciseVocabularyScreen( fun ExerciseVocabularyScreen(
@@ -24,7 +24,7 @@ fun ExerciseVocabularyScreen(
) { ) {
Scaffold( Scaffold(
topBar = { topBar = {
AppTopAppBar(title = { Text(stringResource(R.string.text_new_vocabulary_for_this_exercise)) }) AppTopAppBar(title =stringResource(R.string.text_new_vocabulary_for_this_exercise))
}, },
bottomBar = { bottomBar = {
Surface(shadowElevation = 8.dp) { Surface(shadowElevation = 8.dp) {
@@ -41,7 +41,7 @@ fun ExerciseVocabularyScreen(
) { paddingValues -> ) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) { Box(modifier = Modifier.padding(paddingValues)) {
VocabularyListScreen( AllCardsListScreen(
navController = navController as NavHostController?, navController = navController as NavHostController?,
onNavigateToItem = { item -> onNavigateToItem = { item ->
// Navigate to the detail screen for a specific vocabulary item // Navigate to the detail screen for a specific vocabulary item

View File

@@ -38,6 +38,7 @@ import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppOutlinedCard import eu.gaudian.translator.view.composable.AppOutlinedCard
import eu.gaudian.translator.view.composable.AppTabLayout import eu.gaudian.translator.view.composable.AppTabLayout
import eu.gaudian.translator.view.composable.DialogButton import eu.gaudian.translator.view.composable.DialogButton
import eu.gaudian.translator.view.composable.Screen
import eu.gaudian.translator.view.composable.TabItem import eu.gaudian.translator.view.composable.TabItem
import eu.gaudian.translator.viewmodel.AiGenerationState import eu.gaudian.translator.viewmodel.AiGenerationState
import eu.gaudian.translator.viewmodel.ExerciseViewModel import eu.gaudian.translator.viewmodel.ExerciseViewModel
@@ -76,7 +77,15 @@ fun MainExerciseScreen(
AppTabLayout( AppTabLayout(
tabs = ExerciseTab.entries, tabs = ExerciseTab.entries,
selectedTab = selectedTab, selectedTab = selectedTab,
onTabSelected = { selectedTab = it } onTabSelected = { selectedTab = it },
onNavigateBack = {
if (!navController.popBackStack()) {
navController.navigate(Screen.Home.route) {
launchSingleTop = true
restoreState = false
}
}
}
) )
Box(modifier = Modifier.weight(1f)) { Box(modifier = Modifier.weight(1f)) {

View File

@@ -0,0 +1,932 @@
package eu.gaudian.translator.view.exercises
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBackIosNew
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.outlined.Circle
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavHostController
import eu.gaudian.translator.R
import eu.gaudian.translator.model.Language
import eu.gaudian.translator.model.TagCategory
import eu.gaudian.translator.model.VocabularyCategory
import eu.gaudian.translator.model.VocabularyStage
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppButton
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppOutlinedButton
import eu.gaudian.translator.view.composable.AppSlider
import eu.gaudian.translator.view.composable.OptionItemSwitch
import eu.gaudian.translator.view.dialogs.CategoryDropdown
import eu.gaudian.translator.view.vocabulary.VocabularyExerciseType
import eu.gaudian.translator.viewmodel.CategoryViewModel
import eu.gaudian.translator.viewmodel.LanguageViewModel
import eu.gaudian.translator.viewmodel.VocabularyExerciseViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel
import kotlinx.coroutines.launch
@Composable
fun StartExerciseScreen(
navController: NavHostController,
modifier: Modifier = Modifier
) {
val activity = androidx.compose.ui.platform.LocalContext.current.findActivity()
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
val exerciseViewModel: VocabularyExerciseViewModel = hiltViewModel(viewModelStoreOwner = activity)
val exerciseConfig by exerciseViewModel.pendingExerciseConfig.collectAsState()
var selectedLanguagePairs by remember { mutableStateOf<List<Pair<Language, Language>>>(emptyList()) }
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory>>(emptyList()) }
var selectedStages by remember { mutableStateOf<List<VocabularyStage>>(emptyList()) }
var selectedOriginLanguage by remember { mutableStateOf<Language?>(null) }
var selectedTargetLanguage by remember { mutableStateOf<Language?>(null) }
val isDirectionPreferenceSet = selectedOriginLanguage != null || selectedTargetLanguage != null
val selectedPairsIds = remember(selectedLanguagePairs) {
selectedLanguagePairs.map { it.first.nameResId to it.second.nameResId }
}
val selectedCategoryIds = remember(selectedCategories) {
selectedCategories.map { it.id }
}
val filteredItemsFlow = remember(
selectedPairsIds,
selectedCategoryIds,
selectedStages,
exerciseConfig.dueTodayOnly
) {
vocabularyViewModel.filterVocabularyItemsByPairs(
languagePairs = selectedPairsIds.ifEmpty { null },
query = null,
categoryIds = selectedCategoryIds.ifEmpty { null },
stages = selectedStages.ifEmpty { null },
sortOrder = VocabularyViewModel.SortOrder.NEWEST_FIRST,
dueTodayOnly = exerciseConfig.dueTodayOnly
)
}
val itemsToShow by filteredItemsFlow.collectAsState(initial = emptyList())
val totalItemCount = itemsToShow.size
val availableLanguagesFromItems = remember(itemsToShow, selectedPairsIds) {
val ids = if (selectedPairsIds.isNotEmpty()) {
selectedPairsIds.flatMap { pair -> listOf(pair.first, pair.second) }.toSet()
} else {
itemsToShow.flatMap { listOfNotNull(it.languageFirstId, it.languageSecondId) }.toSet()
}
ids
}
var amount by remember { mutableIntStateOf(0) }
androidx.compose.runtime.LaunchedEffect(totalItemCount) {
amount = totalItemCount
}
val updateConfig: (eu.gaudian.translator.viewmodel.ExerciseConfig) -> Unit = { config ->
exerciseViewModel.updatePendingExerciseConfig(config)
}
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.TopCenter
) {
Column(
modifier = Modifier
.widthIn(max = 700.dp) // Keeps it from over-stretching on tablets
.fillMaxSize()
) {
TopBarSection(
onBackClick = { navController.popBackStack() },
shuffleCards = exerciseConfig.shuffleCards,
onShuffleCardsChanged = { updateConfig(exerciseConfig.copy(shuffleCards = it)) },
shuffleLanguages = exerciseConfig.shuffleLanguages,
onShuffleLanguagesChanged = { updateConfig(exerciseConfig.copy(shuffleLanguages = it)) },
shuffleLanguagesEnabled = !isDirectionPreferenceSet,
trainingMode = exerciseConfig.trainingMode,
onTrainingModeChanged = { updateConfig(exerciseConfig.copy(trainingMode = it)) },
)
LazyColumn(
modifier = Modifier
.weight(1f)
.padding(horizontal = 24.dp),
verticalArrangement = Arrangement.spacedBy(32.dp)
) {
item { Spacer(modifier = Modifier.height(8.dp)) }
item {
LanguagePairSection(
selectedPairs = selectedLanguagePairs,
availableLanguageIds = availableLanguagesFromItems,
onPairsChanged = { updatedPairs ->
val hadPairs = selectedLanguagePairs.isNotEmpty()
selectedLanguagePairs = updatedPairs
if (updatedPairs.isNotEmpty()) {
selectedOriginLanguage = null
selectedTargetLanguage = null
updateConfig(exerciseConfig.copy(originLanguageId = null, targetLanguageId = null))
} else if (hadPairs) {
updateConfig(exerciseConfig.copy(originLanguageId = null, targetLanguageId = null))
}
},
onOriginLanguageSelected = { language ->
if (language?.nameResId == selectedOriginLanguage?.nameResId) {
selectedOriginLanguage = null
updateConfig(exerciseConfig.copy(originLanguageId = null))
} else {
selectedOriginLanguage = language
if (selectedTargetLanguage?.nameResId == language?.nameResId) {
selectedTargetLanguage = null
updateConfig(exerciseConfig.copy(targetLanguageId = null))
}
updateConfig(exerciseConfig.copy(originLanguageId = language?.nameResId))
}
},
onTargetLanguageSelected = { language ->
if (language?.nameResId == selectedTargetLanguage?.nameResId) {
selectedTargetLanguage = null
updateConfig(exerciseConfig.copy(targetLanguageId = null))
} else {
selectedTargetLanguage = language
if (selectedOriginLanguage?.nameResId == language?.nameResId) {
selectedOriginLanguage = null
updateConfig(exerciseConfig.copy(originLanguageId = null))
}
updateConfig(exerciseConfig.copy(targetLanguageId = language?.nameResId))
}
},
languageSelectionEnabled = true,
selectedPairsCount = selectedLanguagePairs.size,
selectedOriginLanguage = selectedOriginLanguage,
selectedTargetLanguage = selectedTargetLanguage
)
}
item {
CategoriesSection(
selectedCategories = selectedCategories,
onCategoriesChanged = { selectedCategories = it }
)
}
item {
DifficultySection(
selectedStages = selectedStages,
onStagesChanged = { selectedStages = it }
)
}
item {
NumberOfCardsSection(
totalAvailable = totalItemCount,
amount = amount,
onAmountChanged = { amount = it },
dueTodayOnly = exerciseConfig.dueTodayOnly,
onDueTodayOnlyChanged = { updateConfig(exerciseConfig.copy(dueTodayOnly = it)) }
)
}
item {
QuestionTypesSection(
selectedTypes = exerciseConfig.selectedExerciseTypes,
onTypeSelected = { type ->
val current = exerciseConfig.selectedExerciseTypes.toMutableSet()
if (type in current) {
if (current.size > 1) current.remove(type)
} else {
current.add(type)
}
updateConfig(exerciseConfig.copy(selectedExerciseTypes = current))
}
)
}
item { Spacer(modifier = Modifier.height(24.dp)) }
}
BottomButtonSection(
enabled = totalItemCount > 0 && amount > 0,
amount = amount,
onStart = {
val finalItems = if (exerciseConfig.shuffleCards) {
itemsToShow.shuffled().take(amount)
} else {
itemsToShow.take(amount)
}
exerciseViewModel.startExerciseWithConfig(
finalItems,
exerciseConfig.copy(
exerciseItemCount = finalItems.size,
originalExerciseItems = finalItems,
originLanguageId = selectedOriginLanguage?.nameResId,
targetLanguageId = selectedTargetLanguage?.nameResId
)
)
@Suppress("HardCodedStringLiteral")
navController.navigate("vocabulary_exercise/false")
}
)
}
}
}
@Composable
fun TopBarSection(
onBackClick: () -> Unit,
shuffleCards: Boolean,
onShuffleCardsChanged: (Boolean) -> Unit,
shuffleLanguages: Boolean,
onShuffleLanguagesChanged: (Boolean) -> Unit,
shuffleLanguagesEnabled: Boolean,
trainingMode: Boolean,
onTrainingModeChanged: (Boolean) -> Unit
) {
var showSettings by remember { mutableStateOf(false) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val scope = rememberCoroutineScope()
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = onBackClick,
modifier = Modifier
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
) {
Icon(
imageVector = Icons.Default.ArrowBackIosNew,
contentDescription = stringResource(R.string.cd_back),
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.primary
)
}
Text(
text = stringResource(R.string.label_start_exercise),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f),
textAlign = TextAlign.Center
)
IconButton(
onClick = { showSettings = true },
modifier = Modifier
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = stringResource(R.string.cd_settings),
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.primary
)
}
}
if (showSettings) {
StartExerciseSettingsBottomSheet(
sheetState = sheetState,
shuffleCards = shuffleCards,
onShuffleCardsChanged = onShuffleCardsChanged,
shuffleLanguages = shuffleLanguages,
onShuffleLanguagesChanged = onShuffleLanguagesChanged,
shuffleLanguagesEnabled = shuffleLanguagesEnabled,
trainingMode = trainingMode,
onTrainingModeChanged = onTrainingModeChanged,
onDismiss = {
scope.launch { sheetState.hide() }.invokeOnCompletion {
if (!sheetState.isVisible) {
@Suppress("AssignedValueIsNeverRead")
showSettings = false
}
}
}
)
}
}
@Composable
fun SectionHeader(title: String, actionText: String? = null, onActionClick: () -> Unit = {}) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = title.uppercase(),
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
if (actionText != null) {
Text(
text = actionText,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold,
modifier = Modifier.clickable { onActionClick() }
)
}
}
}
@Composable
fun LanguagePairSection(
selectedPairs: List<Pair<Language, Language>>,
availableLanguageIds: Set<Int>,
onPairsChanged: (List<Pair<Language, Language>>) -> Unit,
onOriginLanguageSelected: (Language?) -> Unit,
onTargetLanguageSelected: (Language?) -> Unit,
languageSelectionEnabled: Boolean,
selectedPairsCount: Int,
selectedOriginLanguage: Language?,
selectedTargetLanguage: Language?
) {
val activity = androidx.compose.ui.platform.LocalContext.current.findActivity()
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
val allLanguages by languageViewModel.allLanguages.collectAsState(initial = emptyList())
val availableLanguages = remember(availableLanguageIds, allLanguages) {
allLanguages.filter { it.nameResId in availableLanguageIds }
}
val allItems by vocabularyViewModel.vocabularyItems.collectAsState(initial = emptyList())
val pairCounts = remember(allItems) {
allItems.mapNotNull { item ->
val first = item.languageFirstId
val second = item.languageSecondId
if (first != null && second != null) {
val pair = if (first < second) first to second else second to first
pair
} else {
null
}
}.groupingBy { it }.eachCount()
}
val availablePairs = remember(pairCounts, allLanguages) {
pairCounts.entries
.sortedByDescending { it.value }
.mapNotNull { (pairIds, _) ->
val first = allLanguages.find { it.nameResId == pairIds.first }
val second = allLanguages.find { it.nameResId == pairIds.second }
if (first != null && second != null) first to second else null
}
}
Column {
SectionHeader(title = stringResource(R.string.language_pair))
if (availablePairs.isEmpty()) {
Text(
text = stringResource(R.string.text_no_dictionary_language_pairs_found),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
} else {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
availablePairs.forEach { pair ->
val isSelected = selectedPairs.contains(pair)
LanguageChip(
text = "${pair.first.name}${pair.second.name}",
isSelected = isSelected,
modifier = Modifier.widthIn(min = 160.dp),
onClick = {
val updated = if (isSelected) {
selectedPairs - pair
} else {
selectedPairs + pair
}
onPairsChanged(updated)
}
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(R.string.label_language_direction),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Text(
text = stringResource(R.string.text_language_direction_explanation),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
if (!languageSelectionEnabled && selectedPairsCount > 0) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.text_language_direction_disabled_with_pairs),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.height(12.dp))
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = stringResource(R.string.label_origin_language),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 8.dp)
)
eu.gaudian.translator.view.composable.SingleLanguageDropDown(
modifier = Modifier.fillMaxWidth(),
languageViewModel = languageViewModel,
selectedLanguage = selectedOriginLanguage,
onLanguageSelected = { language ->
if (selectedTargetLanguage?.nameResId == language.nameResId) return@SingleLanguageDropDown
onOriginLanguageSelected(language)
},
showNoneOption = true,
onNoneSelected = { onOriginLanguageSelected(null) },
alternateLanguages = availableLanguages,
restrictToAlternateLanguages = true,
enabled = languageSelectionEnabled
)
}
Column(modifier = Modifier.weight(1f)) {
Text(
text = stringResource(R.string.label_target_language),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 8.dp)
)
eu.gaudian.translator.view.composable.SingleLanguageDropDown(
modifier = Modifier.fillMaxWidth(),
languageViewModel = languageViewModel,
selectedLanguage = selectedTargetLanguage,
onLanguageSelected = { language ->
if (selectedOriginLanguage?.nameResId == language.nameResId) return@SingleLanguageDropDown
onTargetLanguageSelected(language)
},
showNoneOption = true,
onNoneSelected = { onTargetLanguageSelected(null) },
alternateLanguages = availableLanguages,
restrictToAlternateLanguages = true,
enabled = languageSelectionEnabled
)
}
}
}
}
@Composable
fun LanguageChip(text: String, isSelected: Boolean, modifier: Modifier = Modifier, onClick: () -> Unit) {
Surface(
modifier = modifier.height(56.dp),
shape = RoundedCornerShape(16.dp),
color = if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) else MaterialTheme.colorScheme.surfaceVariant,
border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null,
onClick = onClick
) {
Row(
modifier = Modifier.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
// Dummy overlapping flags
Box(modifier = Modifier.width(32.dp)) {
Box(modifier = Modifier
.size(20.dp)
.clip(CircleShape)
.background(Color.Red)
.align(Alignment.CenterStart))
Box(modifier = Modifier
.size(20.dp)
.clip(CircleShape)
.background(Color.Blue)
.align(Alignment.CenterEnd))
}
Spacer(modifier = Modifier.width(8.dp))
Text(text = text, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.bodyMedium)
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun CategoriesSection(
selectedCategories: List<VocabularyCategory>,
onCategoriesChanged: (List<VocabularyCategory>) -> Unit
) {
val activity = androidx.compose.ui.platform.LocalContext.current.findActivity()
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
Column {
SectionHeader(title = stringResource(R.string.label_categories))
val tagCategories = categories.filterIsInstance<TagCategory>()
if (tagCategories.size > 15) {
CategoryDropdown(
onCategorySelected = { selections ->
onCategoriesChanged(selections.filterNotNull())
},
multipleSelectable = true,
onlyLists = false,
addCategory = false,
modifier = Modifier.fillMaxWidth(),
enableSearch = true
)
} else {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth()
) {
tagCategories.forEach { category ->
val isSelected = selectedCategories.contains(category)
Surface(
shape = RoundedCornerShape(20.dp),
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
onClick = {
val updated = if (isSelected) {
selectedCategories - category
} else {
selectedCategories + category
}
onCategoriesChanged(updated)
}
) {
Text(
text = category.name,
color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
style = MaterialTheme.typography.bodyMedium,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal
)
}
}
}
}
}
}
//Make it a flow row, as all stages in one row does not work
@Composable
fun DifficultySection(
selectedStages: List<VocabularyStage>,
onStagesChanged: (List<VocabularyStage>) -> Unit
) {
val context = androidx.compose.ui.platform.LocalContext.current
Column {
SectionHeader(title = stringResource(R.string.label_filter_by_stage))
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
VocabularyStage.entries.forEach { stage ->
val isSelected = selectedStages.contains(stage)
Surface(
shape = RoundedCornerShape(20.dp),
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
onClick = {
val updated = if (isSelected) {
selectedStages - stage
} else {
selectedStages + stage
}
onStagesChanged(updated)
}
) {
Text(
text = stage.toString(context),
color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
style = MaterialTheme.typography.bodyMedium,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal
)
}
}
}
}
}
@Composable
fun NumberOfCardsSection(
totalAvailable: Int,
amount: Int,
onAmountChanged: (Int) -> Unit,
dueTodayOnly: Boolean,
onDueTodayOnlyChanged: (Boolean) -> Unit
) {
Column {
OptionItemSwitch(
title = stringResource(R.string.text_due_today_only),
description = stringResource(R.string.text_due_today_only_description),
checked = dueTodayOnly,
onCheckedChange = onDueTodayOnlyChanged
)
Spacer(modifier = Modifier.height(16.dp))
if (totalAvailable == 0) {
Text(
text = stringResource(R.string.no_cards_found_for_the_selected_filters),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error
)
return@Column
}
val maxAvailable = totalAvailable.coerceAtLeast(1)
val coercedAmount = amount.coerceIn(1, maxAvailable)
if (coercedAmount != amount) {
onAmountChanged(coercedAmount)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.text_amount_of_cards).uppercase(),
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Surface(
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.primary,
) {
Text(
text = "$coercedAmount / $totalAvailable",
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
)
}
}
AppSlider(
value = coercedAmount.toFloat(),
onValueChange = { onAmountChanged(it.toInt().coerceIn(1, maxAvailable)) },
valueRange = 1f..maxAvailable.toFloat(),
steps = if (maxAvailable > 1) maxAvailable - 2 else 0,
modifier = Modifier.fillMaxWidth()
)
val quickSelectValues = listOf(10, 25, 50, 100)
val availableQuickSelections = quickSelectValues.filter { it <= maxAvailable }
if (availableQuickSelections.isNotEmpty()) {
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
availableQuickSelections.forEach { value ->
AppOutlinedButton(
onClick = { onAmountChanged(value) },
modifier = Modifier.weight(1f)
) {
Text(value.toString())
}
}
}
}
}
}
//TODO use our four question types here, from StartScreen.kt, user must select at least one
@Composable
fun QuestionTypesSection(
selectedTypes: Set<VocabularyExerciseType>,
onTypeSelected: (VocabularyExerciseType) -> Unit
) {
Column {
SectionHeader(title = stringResource(R.string.text_question_types))
QuestionTypeCard(
title = stringResource(R.string.label_guessing_exercise),
subtitle = stringResource(R.string.flip_card),
icon = AppIcons.Guessing,
isSelected = selectedTypes.contains(VocabularyExerciseType.GUESSING),
onClick = { onTypeSelected(VocabularyExerciseType.GUESSING) }
)
Spacer(modifier = Modifier.height(12.dp))
QuestionTypeCard(
title = stringResource(R.string.label_spelling_exercise),
subtitle = stringResource(R.string.type_the_translation),
icon = AppIcons.SpellCheck,
isSelected = selectedTypes.contains(VocabularyExerciseType.SPELLING),
onClick = { onTypeSelected(VocabularyExerciseType.SPELLING) }
)
Spacer(modifier = Modifier.height(12.dp))
QuestionTypeCard(
title = stringResource(R.string.label_multiple_choice_exercise),
subtitle = stringResource(R.string.label_choose_exercise_types),
icon = AppIcons.CheckList,
isSelected = selectedTypes.contains(VocabularyExerciseType.MULTIPLE_CHOICE),
onClick = { onTypeSelected(VocabularyExerciseType.MULTIPLE_CHOICE) }
)
Spacer(modifier = Modifier.height(12.dp))
QuestionTypeCard(
title = stringResource(R.string.label_word_jumble_exercise),
subtitle = stringResource(R.string.text_assemble_the_word_here),
icon = AppIcons.Extension,
isSelected = selectedTypes.contains(VocabularyExerciseType.WORD_JUMBLE),
onClick = { onTypeSelected(VocabularyExerciseType.WORD_JUMBLE) }
)
}
}
@Composable
fun QuestionTypeCard(title: String, subtitle: String, icon: ImageVector, isSelected: Boolean, onClick: () -> Unit) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
color = if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.05f) else MaterialTheme.colorScheme.surfaceVariant,
border = if (isSelected) BorderStroke(1.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)) else null,
onClick = onClick
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surface),
contentAlignment = Alignment.Center
) {
Icon(imageVector = icon, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
}
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(text = title, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.bodyLarge)
Text(text = subtitle, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
Icon(
imageVector = if (isSelected) Icons.Default.CheckCircle else Icons.Outlined.Circle,
contentDescription = null,
tint = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
@Composable
fun BottomButtonSection(
enabled: Boolean,
amount: Int,
onStart: () -> Unit
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp)
) {
AppButton(
onClick = onStart,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
enabled = enabled,
shape = RoundedCornerShape(28.dp)
) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.label_start_exercise_2d, amount),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.width(8.dp))
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = stringResource(R.string.cd_play),
modifier = Modifier.size(20.dp)
)
}
}
}
}
@Composable
private fun StartExerciseSettingsBottomSheet(
sheetState: SheetState,
shuffleCards: Boolean,
onShuffleCardsChanged: (Boolean) -> Unit,
shuffleLanguages: Boolean,
onShuffleLanguagesChanged: (Boolean) -> Unit,
shuffleLanguagesEnabled: Boolean,
trainingMode: Boolean,
onTrainingModeChanged: (Boolean) -> Unit,
onDismiss: () -> Unit
) {
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = stringResource(R.string.options),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
OptionItemSwitch(
title = stringResource(R.string.shuffle_cards),
description = stringResource(R.string.text_shuffle_card_order_description),
checked = shuffleCards,
onCheckedChange = onShuffleCardsChanged
)
OptionItemSwitch(
title = stringResource(R.string.text_shuffle_languages),
description = stringResource(R.string.text_shuffle_languages_description),
checked = shuffleLanguages && shuffleLanguagesEnabled,
onCheckedChange = { enabled ->
if (shuffleLanguagesEnabled) {
onShuffleLanguagesChanged(enabled)
} else {
onShuffleLanguagesChanged(false)
}
}
)
if (!shuffleLanguagesEnabled) {
Text(
text = stringResource(R.string.text_shuffle_languages_disabled_by_direction),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
OptionItemSwitch(
title = stringResource(R.string.label_training_mode),
description = stringResource(R.string.text_training_mode_description),
checked = trainingMode,
onCheckedChange = onTrainingModeChanged
)
}
}
}

View File

@@ -16,8 +16,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -26,12 +24,10 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import eu.gaudian.translator.R
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppIcons import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppScaffold import eu.gaudian.translator.view.composable.AppScaffold
@@ -61,12 +57,8 @@ fun YouTubeBrowserScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text("YouTube") }, title = "YouTube" ,
navigationIcon = { onNavigateBack = { navController.popBackStack() }
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, stringResource(R.string.cd_back))
}
}
) )
} }
) { padding -> ) { padding ->

View File

@@ -183,14 +183,8 @@ fun YouTubeExerciseScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(title, maxLines = 1) }, title = title,
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(
R.string.cd_back
))
}
},
actions = { actions = {
IconButton( IconButton(
onClick = { onFinishVideo() }, onClick = { onFinishVideo() },

View File

@@ -21,7 +21,7 @@ enum class HintDefinition(
CATEGORY("category_hint", R.string.category_hint_intro), CATEGORY("category_hint", R.string.category_hint_intro),
DICTIONARY_OPTIONS("dictionary_hint", R.string.label_dictionary_options), DICTIONARY_OPTIONS("dictionary_hint", R.string.label_dictionary_options),
EXERCISE("exercise_hint", R.string.label_exercise), EXERCISE("exercise_hint", R.string.label_exercise),
IMPORT("import_hint", R.string.hint_how_to_generate_vocabulary_with_ai), VOCABULARY_GENERATE_AI("import_hint", R.string.hint_how_to_generate_vocabulary_with_ai),
LEARNING_STAGES("learning_stages_hint", R.string.learning_stages_title), LEARNING_STAGES("learning_stages_hint", R.string.learning_stages_title),
REVIEW("review_hint", R.string.review_intro), REVIEW("review_hint", R.string.review_intro),
SORTING("sorting_hint", R.string.sorting_hint_title), SORTING("sorting_hint", R.string.sorting_hint_title),
@@ -40,7 +40,6 @@ enum class HintDefinition(
@Composable @Composable
fun hint(definition: HintDefinition): Hint = definition.hint() fun hint(definition: HintDefinition): Hint = definition.hint()
@Composable fun HintContent(definition: HintDefinition) = definition.Render()
@Composable fun HintScreen(navController: NavController, definition: HintDefinition) = HintScreen( @Composable fun HintScreen(navController: NavController, definition: HintDefinition) = HintScreen(
navController = navController, navController = navController,
title = stringResource(definition.titleRes), title = stringResource(definition.titleRes),

View File

@@ -1,5 +1,3 @@
@file:Suppress("HardCodedStringLiteral")
package eu.gaudian.translator.view.hints package eu.gaudian.translator.view.hints
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement

View File

@@ -32,7 +32,7 @@ import kotlinx.coroutines.launch
fun HintBottomSheet( fun HintBottomSheet(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
sheetState: SheetState, sheetState: SheetState,
content: @Composable (() -> Unit)? content: Hint,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
ModalBottomSheet( ModalBottomSheet(
@@ -50,7 +50,7 @@ fun HintBottomSheet(
.weight(1f, fill = false) .weight(1f, fill = false)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
content?.invoke() content.Render()
} }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))

View File

@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
@@ -16,7 +15,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.gaudian.translator.R import eu.gaudian.translator.R
import eu.gaudian.translator.view.composable.AppIcons import eu.gaudian.translator.view.composable.AppIcons
@@ -39,8 +37,8 @@ val LocalShowHints = compositionLocalOf { false }
*/ */
@Composable @Composable
fun WithHint( fun WithHint(
hintContent: @Composable () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
hintContent: Hint? = null,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val showHints = LocalShowHints.current val showHints = LocalShowHints.current
@@ -69,27 +67,16 @@ fun WithHint(
} }
if (showBottomSheet) { if (showBottomSheet) {
hintContent?.let {
HintBottomSheet( HintBottomSheet(
onDismissRequest = { onDismissRequest = {
@Suppress("AssignedValueIsNeverRead") @Suppress("AssignedValueIsNeverRead")
showBottomSheet = false showBottomSheet = false
}, },
sheetState = sheetState, sheetState = sheetState,
hintContent content = it,
) )
} }
} }
}
@Preview
@Composable
fun WithHintPreview() {
androidx.compose.runtime.CompositionLocalProvider(LocalShowHints provides true) {
WithHint(
hintContent = {
Text(stringResource(R.string.this_is_a_hint))
}
) {
Text(stringResource(R.string.this_is_the_main_content))
}
}
}

View File

@@ -5,15 +5,9 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController import androidx.navigation.NavController
import eu.gaudian.translator.R
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppScaffold import eu.gaudian.translator.view.composable.AppScaffold
import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.AppTopAppBar
@@ -30,12 +24,8 @@ fun HintScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(title) }, title = title,
navigationIcon = { onNavigateBack = { navController.popBackStack() }
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
}
) )
} }
) { paddingValues -> ) { paddingValues ->

View File

@@ -40,7 +40,7 @@ fun HintsOverviewScreen(
val showExperimental = LocalShowExperimentalFeatures.current val showExperimental = LocalShowExperimentalFeatures.current
// Get hints using the new function-based approach // Get hints using the new function-based approach
val importHint = HintDefinition.IMPORT.hint() val importHint = HintDefinition.VOCABULARY_GENERATE_AI.hint()
val addModelScanHint = HintDefinition.ADD_MODEL_SCAN.hint() val addModelScanHint = HintDefinition.ADD_MODEL_SCAN.hint()
val dictionaryOptionsHint = HintDefinition.DICTIONARY_OPTIONS.hint() val dictionaryOptionsHint = HintDefinition.DICTIONARY_OPTIONS.hint()
val translationScreenHint = HintDefinition.TRANSLATION.hint() val translationScreenHint = HintDefinition.TRANSLATION.hint()
@@ -77,7 +77,7 @@ fun HintsOverviewScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.hint_title_hints_overview), style = MaterialTheme.typography.titleLarge) } title = stringResource(R.string.hint_title_hints_overview)
) )
} }
) { paddingValues -> ) { paddingValues ->

View File

@@ -47,6 +47,7 @@ object MarkdownHintLoader {
append(language.lowercase()) append(language.lowercase())
} }
if (country.isNotEmpty()) { if (country.isNotEmpty()) {
@Suppress("HardCodedStringLiteral")
append("-r") append("-r")
append(country.uppercase()) append(country.uppercase())
} }

View File

@@ -0,0 +1,433 @@
package eu.gaudian.translator.view.home
import android.content.Context
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddCircle
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.LocalFireDepartment
import androidx.compose.material.icons.filled.Psychology
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavHostController
import eu.gaudian.translator.R
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.NavigationRoutes
import eu.gaudian.translator.view.composable.AppCard
import eu.gaudian.translator.view.composable.Screen
import eu.gaudian.translator.view.settings.SettingsRoutes
import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget
import eu.gaudian.translator.viewmodel.ProgressViewModel
@Composable
fun HomeScreen(
navController: NavHostController,
modifier: Modifier = Modifier
) {
val activity = LocalContext.current.findActivity()
val viewModel: ProgressViewModel = hiltViewModel(activity)
val streak by viewModel.streak.collectAsState()
val dailyGoal by viewModel.dailyGoal.collectAsState()
val todayCompletedCount by viewModel.todayCompletedCount.collectAsState()
// Calculate daily goal progress
val progress = if (dailyGoal > 0) {
(todayCompletedCount.toFloat() / dailyGoal).coerceIn(0f, 1f)
} else 0f
// A Box with TopCenter alignment keeps the UI centered on wide screens (tablets/foldables)
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.TopCenter
) {
LazyColumn(
modifier = Modifier
.widthIn(max = 700.dp) // Prevents extreme stretching on tablets
.fillMaxSize()
.padding(horizontal = 16.dp, vertical = 0.dp),
verticalArrangement = Arrangement.spacedBy(15.dp)
) {
item { Spacer(modifier = Modifier.height(16.dp)) }
item { TopProfileSection(
navController = navController,
context = LocalContext.current
) }
item {
StreakAndGoalSection(
streak = streak,
progress = progress,
progressTitle = "$todayCompletedCount / $dailyGoal",
onGoalClick = { navController.navigate(SettingsRoutes.VOCABULARY_OPTIONS) },
onStreakClick = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) }
)
}
item {
//TODO replace with actual implementation
@Suppress("HardCodedStringLiteral")
ActionCard(
title = "Daily Review",
subtitle = "42 words need attention",
icon = Icons.Default.Psychology,
contentColor = MaterialTheme.colorScheme.onPrimary
)
}
item {
ActionCard(
title = stringResource(R.string.label_new_words),
subtitle = stringResource(R.string.desc_expand_your_vocabulary),
icon = Icons.Default.AddCircle,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
onClick = { navController.navigate(NavigationRoutes.NEW_WORD) }
)
}
item { WeeklyProgressSection(navController = navController) }
item { BottomStatsSection(navController = navController) }
// Bottom padding for edge-to-edge screens
item { Spacer(modifier = Modifier.height(24.dp)) }
}
}
}
@Composable
fun TopProfileSection(navController: NavHostController, context: Context) {
val motivationalPhrases = remember {
context.resources.getStringArray(R.array.motivational_phrases)
}
val randomPhrase = remember { motivationalPhrases.random() }
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = randomPhrase,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurface
)
}
IconButton(
onClick = { navController.navigate(Screen.Settings.route) },
modifier = Modifier
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant)
) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = stringResource(R.string.label_settings),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
@Composable
fun StreakAndGoalSection(
streak: Int,
progress: Float,
progressTitle: String,
onGoalClick: () -> Unit,
onStreakClick: () -> Unit
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
// Streak Card
StatCard(
modifier = Modifier.weight(1f),
icon = Icons.Default.LocalFireDepartment,
title = stringResource(R.string.label_2d_days, streak),
subtitle = stringResource(R.string.label_current_streak).uppercase(),
onClick = onStreakClick
)
// Goal Card
GoalCard(
modifier = Modifier.weight(1f),
progress = progress,
title = progressTitle,
subtitle = stringResource(R.string.label_daily_goal).uppercase(),
onClick = onGoalClick
)
}
}
@Composable
fun StatCard(
modifier: Modifier = Modifier,
icon: ImageVector,
title: String,
subtitle: String,
onClick: (() -> Unit)? = null
) {
if (onClick != null) {
AppCard(
modifier = modifier,
onClick = onClick
) {
StatCardContent(icon = icon, title = title, subtitle = subtitle)
}
} else {
AppCard(
modifier = modifier,
) {
StatCardContent(icon = icon, title = title, subtitle = subtitle)
}
}
}
@Composable
private fun StatCardContent(
icon: ImageVector,
title: String,
subtitle: String
) {
Column(
modifier = Modifier
.padding(20.dp)
.height(120.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(32.dp)
)
Spacer(modifier = Modifier.height(12.dp))
Text(text = title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(4.dp))
Text(text = subtitle, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))
}
}
@Composable
fun GoalCard(
modifier: Modifier = Modifier,
progress: Float,
title: String,
subtitle: String,
onClick: (() -> Unit)? = null
) {
if (onClick != null) {
AppCard(
modifier = modifier,
onClick = onClick
) {
GoalCardContent(progress = progress, title = title, subtitle = subtitle)
}
} else {
AppCard(
modifier = modifier,
) {
GoalCardContent(progress = progress, title = title, subtitle = subtitle)
}
}
}
@Composable
private fun GoalCardContent(
progress: Float,
title: String,
subtitle: String
) {
Column(
modifier = Modifier
.padding(20.dp)
.height(120.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Box(contentAlignment = Alignment.Center) {
CircularProgressIndicator(
progress = { progress },
modifier = Modifier.size(48.dp),
strokeWidth = 4.dp,
color = MaterialTheme.colorScheme.primary,
trackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
)
Text(text = "${(progress * 100).toInt()}%", style = MaterialTheme.typography.labelSmall, fontWeight = FontWeight.Bold)
}
Spacer(modifier = Modifier.height(12.dp))
Text(text = title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(4.dp))
Text(text = subtitle, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))
}
}
@Composable
fun ActionCard(
title: String,
subtitle: String,
icon: ImageVector,
contentColor: Color,
onClick: (() -> Unit)? = null
) {
val cardContent: @Composable () -> Unit = {
Row(
modifier = Modifier.padding(20.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(40.dp)
)
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(text = title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Text(text = subtitle, style = MaterialTheme.typography.bodyMedium, color = contentColor.copy(alpha = 0.8f))
}
Icon(
imageVector = Icons.Default.ChevronRight,
contentDescription = stringResource(R.string.cd_go),
modifier = Modifier.size(24.dp)
)
}
}
if (onClick != null) {
AppCard(
modifier = Modifier.fillMaxWidth(),
onClick = onClick
) {
cardContent()
}
} else {
AppCard(
modifier = Modifier.fillMaxWidth(),
) {
cardContent()
}
}
}
@Composable
fun WeeklyProgressSection(
navController: NavHostController,
modifier: Modifier = Modifier
) {
val activity = LocalContext.current.findActivity()
val viewModel: ProgressViewModel = hiltViewModel(activity)
val weeklyActivityStats by viewModel.weeklyActivityStats.collectAsState()
Column(modifier = modifier) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(text = stringResource(R.string.label_weekly_progress), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
TextButton(onClick = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) }) {
Text(stringResource(R.string.label_see_history))
}
}
Spacer(modifier = Modifier.height(8.dp))
AppCard(
modifier = Modifier.fillMaxWidth(),
onClick = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) }
) {
if (weeklyActivityStats.isEmpty()) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(R.string.text_desc_no_activity_data_available),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
} else {
WeeklyActivityChartWidget(weeklyStats = weeklyActivityStats)
}
}
}
}
@Composable
fun BottomStatsSection(
navController: NavHostController
) {
val activity = LocalContext.current.findActivity()
val viewModel: ProgressViewModel = hiltViewModel(activity)
val totalWords by viewModel.totalWords.collectAsState()
val learnedWords by viewModel.totalWordsCompleted.collectAsState()
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
// Total Words
AppCard(
modifier = Modifier.weight(1f),
onClick = { navController.navigate(Screen.Library.route) }
) {
Column(modifier = Modifier.padding(20.dp)) {
Text(text = stringResource(R.string.label_total_words).uppercase(), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))
Spacer(modifier = Modifier.height(8.dp))
Text(text = totalWords.toString(), style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(8.dp))
}
}
// Learned
AppCard(
modifier = Modifier.weight(1f),
onClick = { navController.navigate(NavigationRoutes.STATS_LANGUAGE_PROGRESS) }
) {
Column(modifier = Modifier.padding(20.dp)) {
Text(text = stringResource(R.string.label_learned).uppercase(), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))
Spacer(modifier = Modifier.height(8.dp))
Text(text = learnedWords.toString(), style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}

View File

@@ -0,0 +1,730 @@
package eu.gaudian.translator.view.library
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.AddCircleOutline
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.LocalMall
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Tune
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import eu.gaudian.translator.R
import eu.gaudian.translator.model.Language
import eu.gaudian.translator.model.VocabularyCategory
import eu.gaudian.translator.model.VocabularyItem
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.insertBreakOpportunities
/**
* Top bar for the library screen with title and add button
*/
@Composable
fun LibraryTopBar(
onAddClick: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.label_library),
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
IconButton(
onClick = onAddClick,
modifier = Modifier
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant)
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = stringResource(R.string.cd_add),
tint = MaterialTheme.colorScheme.primary
)
}
}
}
/**
* Top bar shown when items are selected for batch operations
*/
@Composable
fun SelectionTopBar(
selectionCount: Int,
onCloseClick: () -> Unit,
onSelectAllClick: () -> Unit,
onDeleteClick: () -> Unit,
onMoveToCategoryClick: () -> Unit,
onMoveToStageClick: () -> Unit,
isRemoveEnabled: Boolean,
onRemoveFromCategoryClick: () -> Unit,
modifier: Modifier = Modifier
) {
var showOverflowMenu by remember { mutableStateOf(false) }
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = onCloseClick) {
Icon(imageVector = AppIcons.Close, contentDescription = stringResource(R.string.label_close_selection_mode))
}
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.d_selected, selectionCount),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
}
Row {
IconButton(onClick = onSelectAllClick) {
Icon(
imageVector = AppIcons.SelectAll,
contentDescription = stringResource(R.string.select_all)
)
}
IconButton(onClick = onDeleteClick) {
Icon(AppIcons.Delete, contentDescription = stringResource(R.string.label_delete))
}
Box {
IconButton(onClick = { showOverflowMenu = true }) {
Icon(imageVector = AppIcons.More, contentDescription = stringResource(R.string.more_actions))
}
DropdownMenu(
expanded = showOverflowMenu,
onDismissRequest = { showOverflowMenu = false }
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.move_to_category)) },
onClick = {
onMoveToCategoryClick()
showOverflowMenu = false
},
leadingIcon = { Icon(AppIcons.Category, contentDescription = null) }
)
if (isRemoveEnabled) {
DropdownMenuItem(
text = { Text(stringResource(R.string.remove_from_category)) },
onClick = {
onRemoveFromCategoryClick()
showOverflowMenu = false
},
leadingIcon = { Icon(AppIcons.Remove, contentDescription = null) }
)
}
DropdownMenuItem(
text = { Text(stringResource(R.string.move_to_stage)) },
onClick = {
onMoveToStageClick()
showOverflowMenu = false
},
leadingIcon = { Icon(AppIcons.Stages, contentDescription = null) }
)
}
}
}
}
}
/**
* Search bar with filter button
*/
@Composable
fun SearchBar(
searchQuery: String,
onQueryChanged: (String) -> Unit,
onFilterClick: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.fillMaxWidth()
.height(56.dp)
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
.padding(start = 16.dp, end = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Search,
contentDescription = stringResource(R.string.cd_search),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.width(12.dp))
androidx.compose.foundation.text.BasicTextField(
value = searchQuery,
onValueChange = onQueryChanged,
modifier = Modifier.weight(1f),
textStyle = MaterialTheme.typography.bodyLarge.copy(
color = MaterialTheme.colorScheme.onSurface
),
singleLine = true,
cursorBrush = androidx.compose.ui.graphics.SolidColor(MaterialTheme.colorScheme.primary),
decorationBox = { innerTextField ->
Box(contentAlignment = Alignment.CenterStart) {
if (searchQuery.isEmpty()) {
Text(
text = stringResource(R.string.label_search_cards),
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
style = MaterialTheme.typography.bodyLarge
)
}
innerTextField()
}
}
)
IconButton(onClick = onFilterClick) {
Icon(
imageVector = Icons.Default.Tune,
contentDescription = stringResource(R.string.cd_filter_options),
tint = MaterialTheme.colorScheme.primary
)
}
}
}
/**
* Segmented control for switching between All Cards and Categories view
*/
@Composable
fun SegmentedControl(
isCategoriesView: Boolean,
onTabSelected: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.fillMaxWidth()
.height(48.dp)
.clip(RoundedCornerShape(12.dp))
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
.padding(4.dp)
) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.clip(RoundedCornerShape(8.dp))
.background(if (!isCategoriesView) MaterialTheme.colorScheme.primary else Color.Transparent)
.clickable { onTabSelected(false) },
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(R.string.label_all_cards),
color = if (!isCategoriesView) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.labelLarge
)
}
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.clip(RoundedCornerShape(8.dp))
.background(if (isCategoriesView) MaterialTheme.colorScheme.primary else Color.Transparent)
.clickable { onTabSelected(true) },
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(R.string.label_categories),
color = if (isCategoriesView) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.labelLarge
)
}
}
}
/**
* List view of all vocabulary cards
*/
@Composable
fun AllCardsView(
vocabularyItems: List<VocabularyItem>,
allLanguages: List<Language>,
selection: Set<Long>,
onItemClick: (VocabularyItem) -> Unit,
onItemLongClick: (VocabularyItem) -> Unit,
onDeleteClick: (VocabularyItem) -> Unit,
listState: LazyListState,
modifier: Modifier = Modifier
) {
if (vocabularyItems.isEmpty()) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Image(
modifier = Modifier.size(200.dp),
painter = painterResource(id = R.drawable.ic_nothing_found),
contentDescription = stringResource(id = R.string.no_vocabulary_items_found_perhaps_try_changing_the_filters)
)
Spacer(modifier = Modifier.size(16.dp))
Text(
text = stringResource(R.string.no_vocabulary_items_found_perhaps_try_changing_the_filters),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
}
} else {
LazyColumn(
state = listState,
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = 100.dp)
) {
items(
items = vocabularyItems,
key = { it.id }
) { item ->
val isSelected = selection.contains(item.id.toLong())
VocabularyCard(
item = item,
allLanguages = allLanguages,
isSelected = isSelected,
onItemClick = { onItemClick(item) },
onItemLongClick = { onItemLongClick(item) },
onDeleteClick = { onDeleteClick(item) }
)
}
}
}
}
/**
* Individual vocabulary card component
*/
@Composable
fun VocabularyCard(
item: VocabularyItem,
allLanguages: List<Language>,
isSelected: Boolean,
onItemClick: () -> Unit,
onItemLongClick: () -> Unit,
onDeleteClick: () -> Unit,
modifier: Modifier = Modifier
) {
val languageMap = remember(allLanguages) { allLanguages.associateBy { it.nameResId } }
val langFirst = item.languageFirstId?.let { languageMap[it]?.name } ?: ""
val langSecond = item.languageSecondId?.let { languageMap[it]?.name } ?: ""
Card(
modifier = modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.combinedClickable(
onClick = onItemClick,
onLongClick = onItemLongClick
),
colors = CardDefaults.cardColors(
containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceContainer,
contentColor = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.surfaceContainer
),
border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
// Top row: First word + Language Pill
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = insertBreakOpportunities(item.wordFirst),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.width(8.dp))
Surface(
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f),
shape = RoundedCornerShape(4.dp)
) {
Text(
text = langFirst,
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Spacer(modifier = Modifier.height(4.dp))
// Bottom row: Second word + Language Pill
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = insertBreakOpportunities(item.wordSecond),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.width(8.dp))
Surface(
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f),
shape = RoundedCornerShape(8.dp)
) {
Text(
text = langSecond,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)
)
}
}
}
if (isSelected) {
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = stringResource(R.string.cd_selected),
tint = MaterialTheme.colorScheme.primary
)
} else {
IconButton(onClick = { /* Options menu could go here */ }) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = stringResource(R.string.cd_options),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
/**
* Grid view of categories
*/
@Composable
fun CategoriesView(
categories: List<VocabularyCategory>,
onCategoryClick: (VocabularyCategory) -> Unit,
onExploreMoreClick: () -> Unit,
modifier: Modifier = Modifier
) {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = 100.dp)
) {
items(categories) { category ->
CategoryCard(
category = category,
onClick = { onCategoryClick(category) }
)
}
item(span = { GridItemSpan(2) }) {
ExploreMoreCard(onClick = onExploreMoreClick)
}
}
}
/**
* Individual category card in grid view
*/
@Composable
fun CategoryCard(
category: VocabularyCategory,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier
.fillMaxWidth()
.height(140.dp)
.clickable(onClick = onClick),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
Box(
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(8.dp))
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.LocalMall,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
}
Column {
Text(
text = category.name,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
LinearProgressIndicator(
progress = { 0.5f },
modifier = Modifier
.fillMaxWidth()
.height(4.dp)
.clip(RoundedCornerShape(2.dp)),
color = MaterialTheme.colorScheme.primary,
trackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
)
}
}
}
}
/**
* Card to explore more categories
*/
@Composable
fun ExploreMoreCard(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val borderColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
Box(
modifier = modifier
.fillMaxWidth()
.padding(top = 8.dp)
.height(80.dp)
.clip(RoundedCornerShape(16.dp))
.clickable(onClick = onClick)
.drawBehind {
val stroke = Stroke(
width = 2.dp.toPx(),
pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 20f), 0f)
)
drawRoundRect(
color = borderColor,
style = stroke,
cornerRadius = CornerRadius(16.dp.toPx(), 16.dp.toPx())
)
},
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
imageVector = Icons.Default.AddCircleOutline,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(R.string.text_explore_more_categories),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
/**
* Crossfade container for switching between views
*/
@Composable
fun LibraryViewContainer(
isCategoriesView: Boolean,
categoriesContent: @Composable () -> Unit,
allCardsContent: @Composable () -> Unit,
modifier: Modifier = Modifier
) {
Crossfade(
targetState = isCategoriesView,
label = "LibraryViewTransition",
modifier = modifier
) { showCategories ->
if (showCategories) {
categoriesContent()
} else {
allCardsContent()
}
}
}
// ==================== PREVIEWS ====================
@Preview(showBackground = true)
@Composable
fun LibraryTopBarPreview() {
MaterialTheme {
LibraryTopBar(onAddClick = {})
}
}
@Preview(showBackground = true)
@Composable
fun SelectionTopBarPreview() {
MaterialTheme {
SelectionTopBar(
selectionCount = 5,
onCloseClick = {},
onSelectAllClick = {},
onDeleteClick = {},
onMoveToCategoryClick = {},
onMoveToStageClick = {},
isRemoveEnabled = true,
onRemoveFromCategoryClick = {}
)
}
}
@Preview(showBackground = true)
@Composable
fun SearchBarPreview() {
MaterialTheme {
SearchBar(
searchQuery = "",
onQueryChanged = {},
onFilterClick = {}
)
}
}
@Preview(showBackground = true)
@Composable
fun SegmentedControlPreview() {
MaterialTheme {
SegmentedControl(
isCategoriesView = false,
onTabSelected = {}
)
}
}
@Suppress("HardCodedStringLiteral")
@Preview(showBackground = true)
@Composable
fun VocabularyCardPreview() {
MaterialTheme {
VocabularyCard(
item = VocabularyItem(
id = 1,
wordFirst = "Hello",
wordSecond = "Hola",
languageFirstId = 1,
languageSecondId = 2,
createdAt = null,
features = null,
zipfFrequencyFirst = null,
zipfFrequencySecond = null
),
allLanguages = emptyList(),
isSelected = false,
onItemClick = {},
onItemLongClick = {},
onDeleteClick = {}
)
}
}
@Suppress("HardCodedStringLiteral")
@Preview(showBackground = true)
@Composable
fun CategoryCardPreview() {
MaterialTheme {
CategoryCard(
category = eu.gaudian.translator.model.TagCategory(
1,
"Travel Phrases"
),
onClick = {}
)
}
}
@Preview(showBackground = true)
@Composable
fun ExploreMoreCardPreview() {
MaterialTheme {
ExploreMoreCard(onClick = {})
}
}

View File

@@ -0,0 +1,600 @@
@file:Suppress("AssignedValueIsNeverRead")
package eu.gaudian.translator.view.library
import android.os.Parcelable
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Folder
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.Button
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import eu.gaudian.translator.R
import eu.gaudian.translator.R.string.text_add_new_word_to_list
import eu.gaudian.translator.model.VocabularyStage
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.NavigationRoutes
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppSwitch
import eu.gaudian.translator.view.composable.BottomSheetMenuItem
import eu.gaudian.translator.view.composable.MultipleLanguageDropdown
import eu.gaudian.translator.view.dialogs.AddCategoryDialog
import eu.gaudian.translator.view.dialogs.CategorySelectionDialog
import eu.gaudian.translator.view.dialogs.StageSelectionDialog
import eu.gaudian.translator.viewmodel.LanguageConfigViewModel
import eu.gaudian.translator.viewmodel.LanguageViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel.SortOrder
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@Parcelize
data class LibraryFilterState(
val searchQuery: String = "",
val selectedStage: VocabularyStage? = null,
val sortOrder: SortOrder = SortOrder.NEWEST_FIRST,
val categoryIds: List<Int> = emptyList(),
val dueTodayOnly: Boolean = false,
val selectedLanguageIds: List<Int> = emptyList(),
val selectedWordClass: String? = null
) : Parcelable
@Composable
fun LibraryScreen(
navController: NavHostController,
modifier: Modifier = Modifier
) {
val scope = rememberCoroutineScope()
val activity = LocalContext.current.findActivity()
val lazyListState = rememberLazyListState()
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
var filterState by rememberSaveable { mutableStateOf(LibraryFilterState()) }
var showFilterSheet by remember { mutableStateOf(false) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
var selection by remember { mutableStateOf<Set<Long>>(emptySet()) }
val isInSelectionMode = selection.isNotEmpty()
var showCategoryDialog by remember { mutableStateOf(false) }
var showStageDialog by remember { mutableStateOf(false) }
var showAddMenu by remember { mutableStateOf(false) }
var showAddCategoryDialog by remember { mutableStateOf(false) }
var isCategoriesView by remember { mutableStateOf(false) }
val allLanguages by languageViewModel.allLanguages.collectAsStateWithLifecycle(initialValue = emptyList())
val languagesPresent by vocabularyViewModel.languagesPresent.collectAsStateWithLifecycle(initialValue = emptySet())
val categories by vocabularyViewModel.categories.collectAsStateWithLifecycle()
val vocabularyItemsFlow = remember(filterState) {
vocabularyViewModel.filterVocabularyItems(
languages = filterState.selectedLanguageIds,
query = filterState.searchQuery.takeIf { it.isNotBlank() },
categoryIds = filterState.categoryIds,
stage = filterState.selectedStage,
wordClass = filterState.selectedWordClass,
dueTodayOnly = filterState.dueTodayOnly,
sortOrder = filterState.sortOrder
)
}
val vocabularyItems by vocabularyItemsFlow.collectAsStateWithLifecycle(initialValue = emptyList())
var isHeaderVisible by remember { mutableStateOf(true) }
var previousIndex by remember { mutableIntStateOf(0) }
var previousScrollOffset by remember { mutableIntStateOf(0) }
// Set navigation context when vocabulary items are loaded
LaunchedEffect(vocabularyItems) {
if (vocabularyItems.isNotEmpty()) {
vocabularyViewModel.setNavigationContext(vocabularyItems, vocabularyItems.first().id)
}
}
LaunchedEffect(isCategoriesView, isInSelectionMode) {
if (isCategoriesView || isInSelectionMode) {
isHeaderVisible = true
}
}
LaunchedEffect(lazyListState, isCategoriesView, isInSelectionMode) {
if (isCategoriesView || isInSelectionMode) return@LaunchedEffect
snapshotFlow { lazyListState.firstVisibleItemIndex to lazyListState.firstVisibleItemScrollOffset }
.collect { (index, offset) ->
val isScrollingDown = index > previousIndex || (index == previousIndex && offset > previousScrollOffset)
val isAtTop = index == 0 && offset <= 4
isHeaderVisible = if (isAtTop) true else !isScrollingDown
previousIndex = index
previousScrollOffset = offset
}
}
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.TopCenter
) {
Column(
modifier = Modifier
.widthIn(max = 700.dp)
.fillMaxSize()
.padding(horizontal = 16.dp),
) {
AnimatedVisibility(
visible = isHeaderVisible,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
Column {
Spacer(modifier = Modifier.height(24.dp))
if (isInSelectionMode) {
SelectionTopBar(
selectionCount = selection.size,
onCloseClick = { selection = emptySet() },
onSelectAllClick = {
selection = if (selection.size == vocabularyItems.size) emptySet()
else vocabularyItems.map { it.id.toLong() }.toSet()
},
onDeleteClick = {
vocabularyViewModel.deleteVocabularyItemsById(selection.map { it.toInt() })
selection = emptySet()
},
onMoveToCategoryClick = { showCategoryDialog = true },
onMoveToStageClick = { showStageDialog = true },
isRemoveEnabled = false,
onRemoveFromCategoryClick = {}
)
} else {
LibraryTopBar(
onAddClick = { showAddMenu = true }
)
}
Spacer(modifier = Modifier.height(24.dp))
SearchBar(
searchQuery = filterState.searchQuery,
onQueryChanged = { filterState = filterState.copy(searchQuery = it) },
onFilterClick = { showFilterSheet = true }
)
Spacer(modifier = Modifier.height(24.dp))
SegmentedControl(
isCategoriesView = isCategoriesView,
onTabSelected = { isCategoriesView = it }
)
Spacer(modifier = Modifier.height(24.dp))
}
}
LibraryViewContainer(
isCategoriesView = isCategoriesView,
categoriesContent = {
CategoriesView(
categories = categories,
onCategoryClick = { category ->
@Suppress("HardCodedStringLiteral")
navController.navigate("${NavigationRoutes.CATEGORY_DETAIL}/${category.id}")
},
onExploreMoreClick = {
navController.navigate(NavigationRoutes.CATEGORY_LIST)
}
)
},
allCardsContent = {
AllCardsView(
vocabularyItems = vocabularyItems,
allLanguages = allLanguages,
selection = selection,
listState = lazyListState,
onItemClick = { item ->
if (isInSelectionMode) {
selection = if (selection.contains(item.id.toLong())) {
selection - item.id.toLong()
} else {
selection + item.id.toLong()
}
} else {
vocabularyViewModel.setNavigationContext(vocabularyItems, item.id)
@Suppress("HardCodedStringLiteral")
navController.navigate("${NavigationRoutes.VOCABULARY_DETAIL}/${item.id}")
}
},
onItemLongClick = { item ->
if (!isInSelectionMode) {
selection = setOf(item.id.toLong())
}
},
onDeleteClick = { item ->
vocabularyViewModel.deleteData(
VocabularyViewModel.DeleteType.VOCABULARY_ITEM,
item = item
)
}
)
},
modifier = Modifier.weight(1f)
)
}
// Floating Action Button for scrolling to top
val showFab by remember {
derivedStateOf { lazyListState.firstVisibleItemIndex > 5 && !isInSelectionMode }
}
AnimatedVisibility(
visible = showFab,
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 16.dp)
) {
FloatingActionButton(
onClick = { scope.launch { lazyListState.animateScrollToItem(0) } },
shape = CircleShape,
modifier = Modifier.size(50.dp),
containerColor = MaterialTheme.colorScheme.surfaceVariant
) {
Icon(AppIcons.ArrowCircleUp, contentDescription = stringResource(R.string.cd_scroll_to_top))
}
}
}
if (showFilterSheet) {
ModalBottomSheet(
onDismissRequest = { showFilterSheet = false },
sheetState = sheetState,
containerColor = MaterialTheme.colorScheme.surface,
dragHandle = { BottomSheetDefaults.DragHandle() }
) {
FilterBottomSheetContent(
currentFilterState = filterState,
languageViewModel = languageViewModel,
languagesPresent = allLanguages.filter { it.nameResId in languagesPresent },
onApplyFilters = { newState ->
filterState = newState
showFilterSheet = false
scope.launch { lazyListState.scrollToItem(0) }
},
onResetClick = {
filterState = LibraryFilterState()
}
)
}
}
if (showCategoryDialog) {
val selectedItems = vocabularyItems.filter { selection.contains(it.id.toLong()) }
CategorySelectionDialog(
onCategorySelected = {
vocabularyViewModel.addVocabularyItemToCategories(
selectedItems,
it.mapNotNull { category -> category?.id }
)
showCategoryDialog = false
selection = emptySet()
},
onDismissRequest = { showCategoryDialog = false }
)
}
if (showAddMenu) {
ModalBottomSheet(
onDismissRequest = { showAddMenu = false },
sheetState = sheetState,
containerColor = MaterialTheme.colorScheme.surface,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 32.dp) // Extra padding for system navigation bar
) {
BottomSheetMenuItem(
icon = Icons.Rounded.Add, // Or any custom vector icon you prefer
title = stringResource(R.string.label_add_vocabulary),
subtitle = stringResource(text_add_new_word_to_list), // Suggest adding this to strings.xml
onClick = {
showAddMenu = false
navController.navigate(NavigationRoutes.NEW_WORD)
}
)
BottomSheetMenuItem(
icon = Icons.Rounded.Folder,
title = stringResource(R.string.label_add_category),
subtitle = stringResource(R.string.text_desc_organize_vocabulary_groups), // Suggest adding this to strings.xml
onClick = {
showAddMenu = false
showAddCategoryDialog = true
}
)
}
}
}
if (showAddCategoryDialog) {
AddCategoryDialog(onDismiss = { showAddCategoryDialog = false })
}
if (showStageDialog) {
val selectedItems = vocabularyItems.filter { selection.contains(it.id.toLong()) }
StageSelectionDialog(
onStageSelected = { selectedStage ->
selectedStage?.let {
vocabularyViewModel.addVocabularyItemToStage(selectedItems, it)
}
showStageDialog = false
selection = emptySet()
},
onDismissRequest = { showStageDialog = false }
)
}
}
@Composable
fun FilterBottomSheetContent(
currentFilterState: LibraryFilterState,
languageViewModel: LanguageViewModel,
languagesPresent: List<eu.gaudian.translator.model.Language>,
onApplyFilters: (LibraryFilterState) -> Unit,
onResetClick: () -> Unit,
modifier: Modifier = Modifier
) {
var selectedStage by rememberSaveable { mutableStateOf(currentFilterState.selectedStage) }
var dueTodayOnly by rememberSaveable { mutableStateOf(currentFilterState.dueTodayOnly) }
var selectedLanguageIds by rememberSaveable { mutableStateOf(currentFilterState.selectedLanguageIds) }
var selectedWordClass by rememberSaveable { mutableStateOf(currentFilterState.selectedWordClass) }
var sortOrder by rememberSaveable { mutableStateOf(currentFilterState.sortOrder) }
val context = LocalContext.current
val activity = LocalContext.current.findActivity()
val languageConfigViewModel: LanguageConfigViewModel = hiltViewModel(activity)
val allWordClasses by languageConfigViewModel.allWordClasses.collectAsStateWithLifecycle()
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.padding(bottom = 32.dp)
.navigationBarsPadding()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.label_filter_cards),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
TextButton(onClick = {
selectedStage = null
dueTodayOnly = false
selectedLanguageIds = emptyList()
selectedWordClass = null
sortOrder = SortOrder.NEWEST_FIRST
onResetClick()
}) {
Text(stringResource(R.string.label_reset))
}
}
HorizontalDivider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f))
Spacer(modifier = Modifier.height(16.dp))
Column(
modifier = Modifier
.weight(1f, fill = false)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
// Sort Order
Column {
Text(
text = stringResource(R.string.label_sort_by).uppercase(),
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 12.dp)
)
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth()
) {
SortOrder.entries.forEach { order ->
FilterChip(
selected = sortOrder == order,
onClick = { sortOrder = order },
label = {
Text(order.name.replace('_', ' ').lowercase()
.replaceFirstChar { it.titlecase() })
}
)
}
}
}
// Due Today
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = stringResource(R.string.text_due_today_only).uppercase(),
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
AppSwitch(checked = dueTodayOnly, onCheckedChange = { dueTodayOnly = it })
}
// Stages
Column {
Text(
text = "STAGES",
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 12.dp)
)
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth()
) {
FilterChip(
selected = selectedStage == null,
onClick = { selectedStage = null },
label = { Text(stringResource(R.string.label_all_stages)) }
)
VocabularyStage.entries.forEach { stage ->
FilterChip(
selected = selectedStage == stage,
onClick = { selectedStage = stage },
label = { Text(stage.toString(context)) }
)
}
}
}
// Languages
Column {
Text(
text = stringResource(R.string.language).uppercase(),
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 12.dp)
)
MultipleLanguageDropdown(
languageViewModel = languageViewModel,
onLanguagesSelected = { languages ->
selectedLanguageIds = languages.map { it.nameResId }
},
alternateLanguages = languagesPresent
)
}
// Word Class
if (allWordClasses.isNotEmpty()) {
Column {
Text(
text = stringResource(R.string.filter_by_word_type).uppercase(),
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 12.dp)
)
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth()
) {
FilterChip(
selected = selectedWordClass == null,
onClick = { selectedWordClass = null },
label = { Text(stringResource(R.string.label_all_types)) }
)
allWordClasses.forEach { wordClass ->
FilterChip(
selected = selectedWordClass == wordClass,
onClick = { selectedWordClass = wordClass },
label = { Text(wordClass.replaceFirstChar { it.titlecase() }) }
)
}
}
}
}
}
Spacer(modifier = Modifier.height(24.dp))
Button(
onClick = {
onApplyFilters(
currentFilterState.copy(
selectedStage = selectedStage,
dueTodayOnly = dueTodayOnly,
selectedLanguageIds = selectedLanguageIds,
selectedWordClass = selectedWordClass,
sortOrder = sortOrder
)
)
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(28.dp)
) {
Text(
text = stringResource(R.string.label_apply_filters),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
}
}

View File

@@ -17,7 +17,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -73,12 +72,8 @@ fun AboutScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.label_about)) }, title = stringResource(R.string.label_about),
navigationIcon = { onNavigateBack = { navController.popBackStack() }
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
}
) )
} }
) { paddingValues -> ) { paddingValues ->

View File

@@ -134,12 +134,8 @@ fun AddModelScreen(navController: NavController, providerKey: String) {
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(providerName) }, title = providerName,
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
hintContent = HintDefinition.ADD_MODEL_SCAN.hint() hintContent = HintDefinition.ADD_MODEL_SCAN.hint()
) )
}, },

View File

@@ -115,12 +115,8 @@ fun ApiKeyScreen(navController: NavController) {
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.label_ai_configuration)) }, title = stringResource(R.string.label_ai_configuration),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
hintContent = HintDefinition.API_KEY.hint() hintContent = HintDefinition.API_KEY.hint()
) )
} }
@@ -137,7 +133,7 @@ fun ApiKeyScreen(navController: NavController) {
AppTabLayout( AppTabLayout(
tabs = apiTabs, tabs = apiTabs,
selectedTab = selectedTab, selectedTab = selectedTab,
onTabSelected = { selectedTab = it } onTabSelected = { selectedTab = it },
) )
// Tab Content // Tab Content

View File

@@ -5,9 +5,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -22,7 +19,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController import androidx.navigation.NavController
import eu.gaudian.translator.R import eu.gaudian.translator.R
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppScaffold import eu.gaudian.translator.view.composable.AppScaffold
import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.viewmodel.ApiViewModel import eu.gaudian.translator.viewmodel.ApiViewModel
@@ -55,12 +51,8 @@ fun CustomVocabularyPromptScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.text_vocabulary_prompt)) }, title = stringResource(R.string.text_vocabulary_prompt),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
hintContent = null //TODO: Add hint hintContent = null //TODO: Add hint
) )

View File

@@ -8,9 +8,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -31,7 +28,6 @@ import eu.gaudian.translator.R
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.LocalShowExperimentalFeatures import eu.gaudian.translator.view.LocalShowExperimentalFeatures
import eu.gaudian.translator.view.composable.AppCard import eu.gaudian.translator.view.composable.AppCard
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppScaffold import eu.gaudian.translator.view.composable.AppScaffold
import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.composable.OptionItemSwitch import eu.gaudian.translator.view.composable.OptionItemSwitch
@@ -66,12 +62,8 @@ fun DictionaryOptionsScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.label_dictionary_options)) }, title = stringResource(R.string.label_dictionary_options),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
hintContent = HintDefinition.DICTIONARY_OPTIONS.hint() hintContent = HintDefinition.DICTIONARY_OPTIONS.hint()
) )
} }

View File

@@ -8,8 +8,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -31,7 +29,6 @@ import eu.gaudian.translator.R
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.ApiModelDropDown import eu.gaudian.translator.view.composable.ApiModelDropDown
import eu.gaudian.translator.view.composable.AppCard import eu.gaudian.translator.view.composable.AppCard
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppOutlinedTextField import eu.gaudian.translator.view.composable.AppOutlinedTextField
import eu.gaudian.translator.view.composable.AppScaffold import eu.gaudian.translator.view.composable.AppScaffold
import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.AppTopAppBar
@@ -71,12 +68,8 @@ fun ExerciseSettingsScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.exercise_settings)) }, title = stringResource(R.string.exercise_settings),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
}
) )
} }
) { paddingValues -> ) { paddingValues ->

View File

@@ -7,8 +7,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -24,7 +22,6 @@ import androidx.navigation.NavController
import eu.gaudian.translator.R import eu.gaudian.translator.R
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppCard import eu.gaudian.translator.view.composable.AppCard
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppScaffold import eu.gaudian.translator.view.composable.AppScaffold
import eu.gaudian.translator.view.composable.AppSwitch import eu.gaudian.translator.view.composable.AppSwitch
import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.AppTopAppBar
@@ -41,12 +38,8 @@ fun GeneralSettingsScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.label_general)) }, title = stringResource(R.string.label_general),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
}
) )
} }
) { paddingValues -> ) { paddingValues ->

View File

@@ -61,12 +61,8 @@ fun LanguageOptionsScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.text_language_options)) }, title = stringResource(R.string.text_language_options),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
}
) )
} }
) { paddingValues -> ) { paddingValues ->
@@ -132,6 +128,7 @@ fun LanguageOptionsScreen(
} }
if (showAddLanguageDialog) { if (showAddLanguageDialog) {
@Suppress("KotlinConstantConditions")
AddCustomLanguageDialog( AddCustomLanguageDialog(
showDialog = showAddLanguageDialog, showDialog = showAddLanguageDialog,
onDismiss = { showAddLanguageDialog = false }, onDismiss = { showAddLanguageDialog = false },

View File

@@ -35,7 +35,6 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
@@ -97,16 +96,11 @@ fun LayoutOptionsScreen(navController: NavController) {
val selectedFontName by settingsViewModel.fontPreference.collectAsStateWithLifecycle() val selectedFontName by settingsViewModel.fontPreference.collectAsStateWithLifecycle()
val showBottomNavLabels by settingsViewModel.showBottomNavLabels.collectAsStateWithLifecycle() val showBottomNavLabels by settingsViewModel.showBottomNavLabels.collectAsStateWithLifecycle()
val cdBack = stringResource(R.string.cd_back)
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.label_appearance)) }, title = stringResource(R.string.label_appearance),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = cdBack)
}
}
) )
} }
) { paddingValues -> ) { paddingValues ->

View File

@@ -101,15 +101,8 @@ fun LogsScreen(navController: NavController) {
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.label_logs)) }, title = stringResource(R.string.label_logs),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(
imageVector = AppIcons.ArrowBack,
contentDescription = stringResource(R.string.cd_back)
)
}
},
actions = { actions = {
TextButton(onClick = { TextButton(onClick = {
settingsViewModel.clearApiLogs() settingsViewModel.clearApiLogs()

View File

@@ -27,6 +27,7 @@ import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppOutlinedCard import eu.gaudian.translator.view.composable.AppOutlinedCard
import eu.gaudian.translator.view.composable.AppScaffold import eu.gaudian.translator.view.composable.AppScaffold
import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.composable.Screen
private data class Setting(val titleRes: Int, val icon: ImageVector, val route: String) private data class Setting(val titleRes: Int, val icon: ImageVector, val route: String)
@@ -84,7 +85,15 @@ fun MainSettingsScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.title_settings), style = MaterialTheme.typography.titleLarge) } title =stringResource(R.string.title_settings),
onNavigateBack = {
if (!navController.popBackStack()) {
navController.navigate(Screen.Home.route) {
launchSingleTop = true
restoreState = false
}
}
}
) )
} }
) { paddingValues -> ) { paddingValues ->
@@ -96,7 +105,7 @@ fun MainSettingsScreen(
} }
item { item {
AppCard( AppCard(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp), modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
) { ) {
Column { Column {
settings.forEachIndexed { index, setting -> settings.forEachIndexed { index, setting ->

View File

@@ -113,7 +113,7 @@ fun NavGraphBuilder.settingsGraph(navController: NavController) {
HintScreen(navController, HintDefinition.DICTIONARY_OPTIONS) HintScreen(navController, HintDefinition.DICTIONARY_OPTIONS)
} }
composable(SettingsRoutes.HINTS_IMPORT) { composable(SettingsRoutes.HINTS_IMPORT) {
HintScreen(navController, HintDefinition.IMPORT) HintScreen(navController, HintDefinition.VOCABULARY_GENERATE_AI)
} }
composable(SettingsRoutes.HINTS_SORTING) { composable(SettingsRoutes.HINTS_SORTING) {
HintScreen(navController, HintDefinition.SORTING) HintScreen(navController, HintDefinition.SORTING)

View File

@@ -15,8 +15,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -86,12 +84,8 @@ fun TextToSpeechSettingsScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.settings_title_voice)) }, title = stringResource(R.string.settings_title_voice),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
}
) )
} }
) { paddingValues -> ) { paddingValues ->

View File

@@ -9,9 +9,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -27,7 +24,6 @@ import androidx.navigation.NavController
import eu.gaudian.translator.R import eu.gaudian.translator.R
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppCard import eu.gaudian.translator.view.composable.AppCard
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppScaffold import eu.gaudian.translator.view.composable.AppScaffold
import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.composable.OptionItemSwitch import eu.gaudian.translator.view.composable.OptionItemSwitch
@@ -64,12 +60,8 @@ fun TranslationSettingsScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.label_translation_settings)) }, title = stringResource(R.string.label_translation_settings),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
hintContent = null //TODO add hint hintContent = null //TODO add hint
) )
} }

View File

@@ -21,7 +21,6 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@@ -78,13 +77,8 @@ fun VocabularyProgressOptionsScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.vocabulary_settings)) }, title = stringResource(R.string.label_vocabulary_settings),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
// Here is the new hint content
hintContent = HintDefinition.VOCABULARY_PROGRESS.hint() hintContent = HintDefinition.VOCABULARY_PROGRESS.hint()
) )
} }
@@ -97,44 +91,27 @@ fun VocabularyProgressOptionsScreen(
.padding(16.dp), .padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
// Interval Settings // Daily Goal Settings
AppCard( AppCard {
expandable = true, val dailyGoal by settingsViewModel.dailyGoal.collectAsStateWithLifecycle()
initiallyExpanded = true,
title = stringResource(R.string.text_interval_settings_in_days),
text = stringResource(R.string.text_customize_the_intervals),
) {
val intervals by settingsViewModel.intervals.collectAsStateWithLifecycle()
Column( Column(
modifier = Modifier modifier = Modifier.padding(16.dp),
.padding(16.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
.animateContentSize(),
verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
IntervalTimeline(intervals = intervals)
intervals.forEach { (stageKey, days) ->
val displayLabel = labelForStage(stageKey)
IntervalSlider(
label = displayLabel,
value = days,
onValueChange = { newValue ->
settingsViewModel.setInterval(stageKey, newValue)
}
)
}
Spacer(Modifier.height(8.dp)) @Suppress("USELESS_ELVIS", "HardCodedStringLiteral")
Row( SettingsSlider(
modifier = Modifier.fillMaxWidth(), label = stringResource(R.string.label_target_correct_answers_per_day),
horizontalArrangement = Arrangement.End value = dailyGoal ?: 10,
) { onValueChange = { settingsViewModel.setDailyGoal(it) },
TextButton(onClick = { settingsViewModel.resetIntervalsToDefaults() }) { valueRange = 10f..100f,
Text(stringResource(R.string.reset_to_defaults)) steps = 17 // Allows snapping in steps of 5
} )
} Text(
text = stringResource(R.string.text_daily_goal_description),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
} }
} }
@@ -169,31 +146,42 @@ fun VocabularyProgressOptionsScreen(
} }
} }
// Daily Goal Settings // Interval Settings
AppCard { AppCard(
val dailyGoal by settingsViewModel.dailyGoal.collectAsStateWithLifecycle() expandable = true,
Column( initiallyExpanded = true,
modifier = Modifier.padding(16.dp), title = stringResource(R.string.label_interval_settings_in_days),
verticalArrangement = Arrangement.spacedBy(8.dp) text = stringResource(R.string.text_customize_the_intervals),
) { ) {
Text( val intervals by settingsViewModel.intervals.collectAsStateWithLifecycle()
text = stringResource(R.string.daily_learning_goal), Column(
style = MaterialTheme.typography.titleMedium modifier = Modifier
) .padding(16.dp)
@Suppress("USELESS_ELVIS", "HardCodedStringLiteral") .animateContentSize(),
SettingsSlider( verticalArrangement = Arrangement.spacedBy(16.dp)
label = stringResource(R.string.target_correct_answers_per_day), ) {
value = dailyGoal ?: 10, IntervalTimeline(intervals = intervals)
onValueChange = { settingsViewModel.setDailyGoal(it) }, intervals.forEach { (stageKey, days) ->
valueRange = 10f..100f, val displayLabel = labelForStage(stageKey)
steps = 17 // Allows snapping in steps of 5 IntervalSlider(
) label = displayLabel,
Text( value = days,
text = stringResource(R.string.text_daily_goal_description), onValueChange = { newValue ->
style = MaterialTheme.typography.bodySmall, settingsViewModel.setInterval(stageKey, newValue)
color = MaterialTheme.colorScheme.onSurfaceVariant }
) )
} }
Spacer(Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { settingsViewModel.resetIntervalsToDefaults() }) {
Text(stringResource(R.string.reset_to_defaults))
}
}
}
} }
} }
} }

View File

@@ -18,8 +18,6 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@@ -44,7 +42,6 @@ import eu.gaudian.translator.utils.StatusMessageService
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppButton import eu.gaudian.translator.view.composable.AppButton
import eu.gaudian.translator.view.composable.AppCard import eu.gaudian.translator.view.composable.AppCard
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppOutlinedButton import eu.gaudian.translator.view.composable.AppOutlinedButton
import eu.gaudian.translator.view.composable.AppScaffold import eu.gaudian.translator.view.composable.AppScaffold
import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.AppTopAppBar
@@ -200,12 +197,8 @@ fun VocabularyRepositoryOptionsScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.vocabulary_repository)) }, title = stringResource(R.string.vocabulary_repository),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
}
) )
} }
) { paddingValues -> ) { paddingValues ->

View File

@@ -0,0 +1,678 @@
@file:Suppress("AssignedValueIsNeverRead")
package eu.gaudian.translator.view.stats
import android.annotation.SuppressLint
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.VisibilityThreshold
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import eu.gaudian.translator.R
import eu.gaudian.translator.model.VocabularyStage
import eu.gaudian.translator.model.WidgetType
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.NavigationRoutes
import eu.gaudian.translator.view.composable.AppCard
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppOutlinedCard
import eu.gaudian.translator.view.dialogs.MissingLanguageDialog
import eu.gaudian.translator.view.vocabulary.widgets.AllVocabularyWidget
import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressWidget
import eu.gaudian.translator.view.vocabulary.widgets.DueTodayWidget
import eu.gaudian.translator.view.vocabulary.widgets.LevelWidget
import eu.gaudian.translator.view.vocabulary.widgets.StatusWidget
import eu.gaudian.translator.view.vocabulary.widgets.StreakWidget
import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget
import eu.gaudian.translator.viewmodel.LanguageViewModel
import eu.gaudian.translator.viewmodel.ProgressViewModel
import eu.gaudian.translator.viewmodel.SettingsViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
@SuppressLint("FrequentlyChangingValue")
@Composable
fun StatsScreen(
modifier: Modifier = Modifier,
navController: NavHostController,
onNavigateToCategoryDetail: ((Int) -> Unit)? = null,
onNavigateToCategoryList: (() -> Unit)? = null,
onScroll: (Boolean) -> Unit = {},
) {
val activity = LocalContext.current.findActivity()
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
val settingsViewModel: SettingsViewModel = hiltViewModel(viewModelStoreOwner = activity)
val progressViewModel: ProgressViewModel = hiltViewModel(viewModelStoreOwner = activity)
var showMissingLanguageDialog by remember { mutableStateOf(false) }
var selectedMissingLanguageId by remember { mutableStateOf<Int?>(null) }
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
val affectedItems by remember(selectedMissingLanguageId) {
selectedMissingLanguageId?.let {
vocabularyViewModel.getItemsForLanguage(it)
} ?: flowOf(emptyList())
}.collectAsState(initial = emptyList())
if (showMissingLanguageDialog && selectedMissingLanguageId != null) {
MissingLanguageDialog(
showDialog = true,
missingLanguageId = selectedMissingLanguageId!!,
affectedItems = affectedItems,
onDismiss = { showMissingLanguageDialog = false },
onDelete = { items ->
vocabularyViewModel.deleteVocabularyItemsById(items.map { it.id })
showMissingLanguageDialog = false
},
onReplace = { oldId, newId ->
vocabularyViewModel.replaceLanguageId(oldId, newId)
showMissingLanguageDialog = false
},
onCreate = { newLanguage ->
languageViewModel.addCustomLanguage(newLanguage)
},
languageViewModel = languageViewModel
)
}
val handleNavigateToCategoryDetail = onNavigateToCategoryDetail ?: { categoryId ->
@Suppress("HardCodedStringLiteral")
navController.navigate("${NavigationRoutes.STATS_CATEGORY_DETAIL}/$categoryId")
}
val handleNavigateToCategoryList = onNavigateToCategoryList ?: {
navController.navigate(NavigationRoutes.STATS_CATEGORY_LIST)
}
AppOutlinedCard(modifier = modifier) {
// We collect the order from DB initially
val initialWidgetOrder by settingsViewModel.widgetOrder.collectAsState(initial = null)
val collapsedWidgetIds by settingsViewModel.collapsedWidgetIds.collectAsState(initial = emptySet())
val dashboardScrollState by settingsViewModel.dashboardScrollState.collectAsState()
val scope = rememberCoroutineScope()
if (initialWidgetOrder == null) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 64.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else {
// BEST PRACTICE: Use a SnapshotStateList for immediate UI updates.
// We only initialize this once, so DB updates don't reset the list while dragging.
val orderedWidgets = remember { mutableStateListOf<WidgetType>() }
// Sync with DB only on first load
LaunchedEffect(initialWidgetOrder) {
if (orderedWidgets.isEmpty() && !initialWidgetOrder.isNullOrEmpty()) {
orderedWidgets.addAll(initialWidgetOrder!!)
} else if (orderedWidgets.isEmpty()) {
orderedWidgets.addAll(WidgetType.DEFAULT_ORDER)
}
}
val lazyListState = rememberLazyListState(
initialFirstVisibleItemIndex = dashboardScrollState.first,
initialFirstVisibleItemScrollOffset = dashboardScrollState.second
)
// Save scroll state
LaunchedEffect(lazyListState.firstVisibleItemIndex, lazyListState.firstVisibleItemScrollOffset) {
settingsViewModel.saveDashboardScrollState(
lazyListState.firstVisibleItemIndex,
lazyListState.firstVisibleItemScrollOffset
)
}
// Detect scroll and notify parent
LaunchedEffect(lazyListState.isScrollInProgress) {
onScroll(lazyListState.isScrollInProgress)
}
DisposableEffect(Unit) {
onDispose {
settingsViewModel.saveDashboardScrollState(
lazyListState.firstVisibleItemIndex,
lazyListState.firstVisibleItemScrollOffset
)
}
}
// --- Robust Drag and Drop State ---
val dragDropState = rememberDragDropState(
lazyListState = lazyListState,
onSwap = { fromIndex, toIndex ->
// Swap data immediately for responsiveness
orderedWidgets.apply {
add(toIndex, removeAt(fromIndex))
}
},
onDragEnd = {
// Persist to DB only when user drops
settingsViewModel.saveWidgetOrder(orderedWidgets.toList())
}
)
LazyColumn(
state = lazyListState,
modifier = Modifier
.fillMaxSize()
.dragContainer(dragDropState),
contentPadding = PaddingValues(bottom = 160.dp)
) {
itemsIndexed(
items = orderedWidgets,
key = { _, widget -> widget.id }
) { index, widgetType ->
val isDragging = index == dragDropState.draggingItemIndex
// Calculate translation: distinct logic for dragged vs. stationary items
val translationY = if (isDragging) {
dragDropState.draggingItemOffset
} else {
0f
}
Box(
modifier = Modifier
.zIndex(if (isDragging) 1f else 0f)
.graphicsLayer {
this.translationY = translationY
this.shadowElevation = if (isDragging) 8.dp.toPx() else 0f
this.scaleX = if (isDragging) 1.02f else 1f
this.scaleY = if (isDragging) 1.02f else 1f
}
// CRITICAL FIX: Only apply animation to items NOT being dragged.
// This prevents the "flicker" by stopping the layout animation
// from fighting your manual drag offset.
.then(
if (!isDragging) {
Modifier.animateItem(
placementSpec = spring(
stiffness = Spring.StiffnessLow,
visibilityThreshold = IntOffset.VisibilityThreshold
)
)
} else {
Modifier
}
)
) {
WidgetContainer(
widgetType = widgetType,
isExpanded = widgetType.id !in collapsedWidgetIds,
onExpandedChange = { newExpandedState ->
scope.launch {
settingsViewModel.setWidgetExpandedState(widgetType.id, newExpandedState)
}
},
onDragStart = { dragDropState.onDragStart(index) },
onDrag = { dragAmount -> dragDropState.onDrag(dragAmount) },
onDragEnd = { dragDropState.onDragEnd() },
onDragCancel = { dragDropState.onDragInterrupted() },
modifier = Modifier.fillMaxWidth()
) {
LazyWidget(
widgetType = widgetType,
navController = navController,
vocabularyViewModel = vocabularyViewModel,
progressViewModel = progressViewModel,
onNavigateToCategoryDetail = handleNavigateToCategoryDetail,
onNavigateToCategoryList = handleNavigateToCategoryList,
onMissingLanguage = { missingId ->
selectedMissingLanguageId = missingId
showMissingLanguageDialog = true
}
)
}
}
}
}
}
}
}
@Composable
private fun WidgetContainer(
widgetType: WidgetType,
isExpanded: Boolean,
onExpandedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
onDragStart: () -> Unit,
onDrag: (Float) -> Unit,
onDragEnd: () -> Unit,
onDragCancel: () -> Unit,
content: @Composable () -> Unit
) {
AppCard(
modifier = modifier.padding(horizontal = 8.dp, vertical = 4.dp),
) {
Column(
modifier = Modifier
.fillMaxWidth()
.animateContentSize(animationSpec = tween(300, easing = FastOutSlowInEasing))
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(widgetType.titleRes),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.weight(1f)
)
IconButton(onClick = { onExpandedChange(!isExpanded) }) {
Icon(
imageVector = if (isExpanded) AppIcons.ArrowDropUp
else AppIcons.ArrowDropDown,
contentDescription = if (isExpanded) stringResource(R.string.text_collapse_widget)
else stringResource(R.string.text_expand_widget)
)
}
// Drag Handle with specific pointer input
Icon(
imageVector = AppIcons.DragHandle,
contentDescription = stringResource(R.string.text_drag_to_reorder),
tint = if (isExpanded) MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
else MaterialTheme.colorScheme.onSurface,
modifier = Modifier
.padding(end = 8.dp, start = 8.dp)
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { _ -> onDragStart() },
onDrag = { change, dragAmount ->
change.consume()
onDrag(dragAmount.y)
},
onDragEnd = { onDragEnd() },
onDragCancel = { onDragCancel() }
)
}
)
}
if (isExpanded) {
content()
}
}
}
}
// --------------------------------------------------------------------------------
// Fixed Drag and Drop Logic
// --------------------------------------------------------------------------------
@Composable
fun rememberDragDropState(
lazyListState: LazyListState,
onSwap: (Int, Int) -> Unit,
onDragEnd: () -> Unit
): DragDropState {
val scope = rememberCoroutineScope()
return remember(lazyListState, scope) {
DragDropState(
state = lazyListState,
onSwap = onSwap,
onDragFinished = onDragEnd,
scope = scope
)
}
}
fun Modifier.dragContainer(dragDropState: DragDropState): Modifier {
return this.pointerInput(dragDropState) {
// Just allows the modifier to exist in the chain, logic is in the handle
}
}
class DragDropState(
private val state: LazyListState,
private val onSwap: (Int, Int) -> Unit,
private val onDragFinished: () -> Unit,
private val scope: CoroutineScope
) {
var draggingItemIndex by mutableIntStateOf(-1)
private set
private val _draggingItemOffset = Animatable(0f)
val draggingItemOffset: Float
get() = _draggingItemOffset.value
private val scrollChannel = Channel<Float>(Channel.CONFLATED)
init {
scope.launch {
for (scrollAmount in scrollChannel) {
if (scrollAmount != 0f) {
state.scrollBy(scrollAmount)
checkSwap()
}
}
}
}
fun onDragStart(index: Int) {
draggingItemIndex = index
scope.launch { _draggingItemOffset.snapTo(0f) }
}
fun onDrag(dragAmount: Float) {
if (draggingItemIndex == -1) return
scope.launch {
_draggingItemOffset.snapTo(_draggingItemOffset.value + dragAmount)
checkSwap()
checkOverscroll()
}
}
private fun checkSwap() {
val draggedIndex = draggingItemIndex
if (draggedIndex == -1) return
val visibleItems = state.layoutInfo.visibleItemsInfo
val draggedItemInfo = visibleItems.find { it.index == draggedIndex } ?: return
// Calculate the visual center of the dragged item
val draggedCenter = draggedItemInfo.offset + (draggedItemInfo.size / 2) + _draggingItemOffset.value
// Find a target to swap with
// FIX: We strictly check if we have crossed the CENTER of the target item.
// This acts as a hysteresis buffer to prevent flickering at the edges.
val targetItem = visibleItems.find { item ->
item.index != draggedIndex &&
draggedCenter > item.offset &&
draggedCenter < (item.offset + item.size)
}
if (targetItem != null) {
// Extra Check: Ensure we have actually crossed the midpoint of the target
val targetCenter = itemCenter(targetItem.offset, targetItem.size)
val isAboveAndMovingDown = draggedIndex < targetItem.index && draggedCenter > targetCenter
val isBelowAndMovingUp = draggedIndex > targetItem.index && draggedCenter < targetCenter
if (isAboveAndMovingDown || isBelowAndMovingUp) {
val targetIndex = targetItem.index
// 1. Swap Data
onSwap(draggedIndex, targetIndex)
// 2. Adjust Offset
// We calculate the physical distance the item moved in the layout (e.g. 150px).
// We subtract this from the current drag offset to keep the item visually stationary under the finger.
val layoutJumpDistance = (targetItem.offset - draggedItemInfo.offset).toFloat()
scope.launch {
_draggingItemOffset.snapTo(_draggingItemOffset.value - layoutJumpDistance)
}
// 3. Update Index
draggingItemIndex = targetIndex
}
}
}
private fun itemCenter(offset: Int, size: Int): Float {
return offset + (size / 2f)
}
private fun checkOverscroll() {
val draggedIndex = draggingItemIndex
if (draggedIndex == -1) {
scrollChannel.trySend(0f)
return
}
val layoutInfo = state.layoutInfo
val visibleItems = layoutInfo.visibleItemsInfo
val draggedItemInfo = visibleItems.find { it.index == draggedIndex } ?: return
val viewportStart = layoutInfo.viewportStartOffset
val viewportEnd = layoutInfo.viewportEndOffset
// Increased threshold slightly for smoother top-edge scrolling
val boundsStart = viewportStart + (viewportEnd * 0.15f)
val boundsEnd = viewportEnd - (viewportEnd * 0.15f)
val itemTop = draggedItemInfo.offset + _draggingItemOffset.value
val itemBottom = itemTop + draggedItemInfo.size
val scrollAmount = when {
itemTop < boundsStart -> -10f // Slower, more controlled scroll speed
itemBottom > boundsEnd -> 10f
else -> 0f
}
scrollChannel.trySend(scrollAmount)
}
fun onDragEnd() {
resetDrag()
onDragFinished()
}
fun onDragInterrupted() {
resetDrag()
}
private fun resetDrag() {
draggingItemIndex = -1
scrollChannel.trySend(0f)
scope.launch { _draggingItemOffset.snapTo(0f) }
}
}
// --------------------------------------------------------------------------------
// Remainder of your existing components
// --------------------------------------------------------------------------------
@Suppress("HardCodedStringLiteral")
@Composable
private fun LazyWidget(
widgetType: WidgetType,
navController: NavController,
vocabularyViewModel: VocabularyViewModel,
progressViewModel: ProgressViewModel,
onNavigateToCategoryDetail: (Int) -> Unit,
onNavigateToCategoryList: () -> Unit,
onMissingLanguage: (Int) -> Unit
) {
when (widgetType) {
WidgetType.Status -> LazyStatusWidget(
vocabularyViewModel = vocabularyViewModel,
onNavigateToNew = { navController.navigate("${NavigationRoutes.STATS_VOCABULARY_SORTING}?mode=NEW") },
onNavigateToDuplicates = { navController.navigate("${NavigationRoutes.STATS_VOCABULARY_SORTING}?mode=DUPLICATES") },
onNavigateToFaulty = { navController.navigate("${NavigationRoutes.STATS_VOCABULARY_SORTING}?mode=FAULTY") },
onNavigateToNoGrammar = { navController.navigate(NavigationRoutes.STATS_NO_GRAMMAR_ITEMS) },
onNavigateToMissingLanguage = onMissingLanguage
)
else -> {
// Regular widgets that load immediately
when (widgetType) {
WidgetType.Streak -> StreakWidget(
streak = progressViewModel.streak.collectAsState(initial = 0).value,
lastSevenDays = progressViewModel.lastSevenDays.collectAsState().value,
dueTodayCount = progressViewModel.dueTodayCount.collectAsState().value,
wordsCompleted = progressViewModel.totalWordsCompleted.collectAsState().value,
onStatisticsClicked = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) }
)
WidgetType.WeeklyActivityChart -> WeeklyActivityChartWidget(
weeklyStats = progressViewModel.weeklyActivityStats.collectAsState().value
)
WidgetType.AllVocabulary -> AllVocabularyWidget(
vocabularyViewModel = vocabularyViewModel,
onOpenAllVocabulary = { navController.navigate("${NavigationRoutes.STATS_VOCABULARY_LIST}/false/null") },
onStageClicked = { stage ->
@Suppress("HardCodedStringLiteral")
navController.navigate("${NavigationRoutes.STATS_VOCABULARY_LIST}/false/$stage")
}
)
WidgetType.DueToday -> DueTodayWidget(
vocabularyViewModel = vocabularyViewModel,
onStageClicked = { stage ->
@Suppress("HardCodedStringLiteral")
navController.navigate("${NavigationRoutes.STATS_VOCABULARY_LIST}/false/$stage")
}
)
WidgetType.CategoryProgress -> CategoryProgressWidget(
onCategoryClicked = { category ->
category?.let { onNavigateToCategoryDetail(it.id) }
},
onViewAllClicked = onNavigateToCategoryList
)
WidgetType.Levels -> LevelWidget(
totalWords = vocabularyViewModel.vocabularyItems.collectAsState().value.size,
learnedWords = vocabularyViewModel.stageStats.collectAsState().value
.firstOrNull { it.stage == VocabularyStage.LEARNED }?.itemCount ?: 0,
onNavigateToProgress = { navController.navigate(NavigationRoutes.STATS_LANGUAGE_PROGRESS) }
)
}
}
}
}
@Composable
private fun LazyStatusWidget(
vocabularyViewModel: VocabularyViewModel,
onNavigateToNew: () -> Unit,
onNavigateToDuplicates: () -> Unit,
onNavigateToFaulty: () -> Unit,
onNavigateToNoGrammar: () -> Unit,
onNavigateToMissingLanguage: (Int) -> Unit
) {
var isLoading by remember { mutableStateOf(true) }
// Collect all flows asynchronously
val newItemsCount by vocabularyViewModel.newItemsCount.collectAsState()
val duplicateCount by vocabularyViewModel.duplicateItemsCount.collectAsState()
val faultyItemsCount by vocabularyViewModel.faultyItemsCount.collectAsState()
val itemsWithoutGrammarCount by vocabularyViewModel.itemsWithoutGrammarCount.collectAsState()
val missingLanguageInfo by vocabularyViewModel.missingLanguageInfo.collectAsState()
LaunchedEffect(
newItemsCount,
duplicateCount,
faultyItemsCount,
itemsWithoutGrammarCount,
missingLanguageInfo
) {
delay(100)
isLoading = false
}
if (isLoading) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 32.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(modifier = Modifier.padding(16.dp))
}
} else {
StatusWidget(
onNavigateToNew = onNavigateToNew,
onNavigateToDuplicates = onNavigateToDuplicates,
onNavigateToFaulty = onNavigateToFaulty,
onNavigateToNoGrammar = onNavigateToNoGrammar,
onNavigateToMissingLanguage = onNavigateToMissingLanguage
)
}
}
@Preview
@Composable
fun StatsScreenPreview() {
val navController = rememberNavController()
StatsScreen(
navController = navController,
onNavigateToCategoryDetail = {},
onNavigateToCategoryList = {},
)
}
@Preview
@Composable
fun WidgetContainerPreview() {
WidgetContainer(
widgetType = WidgetType.Streak,
isExpanded = true,
onExpandedChange = {},
onDragStart = { },
onDrag = { },
onDragEnd = { },
onDragCancel = { }
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
@Suppress("HardCodedStringLiteral")
Text("Preview Content")
}
}
}

View File

@@ -37,6 +37,7 @@ import eu.gaudian.translator.R
import eu.gaudian.translator.view.composable.AppIcons import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.SourceLanguageDropdown import eu.gaudian.translator.view.composable.SourceLanguageDropdown
import eu.gaudian.translator.view.composable.TargetLanguageDropdown import eu.gaudian.translator.view.composable.TargetLanguageDropdown
import eu.gaudian.translator.view.hints.Hint
import eu.gaudian.translator.view.hints.WithHint import eu.gaudian.translator.view.hints.WithHint
import eu.gaudian.translator.viewmodel.LanguageViewModel import eu.gaudian.translator.viewmodel.LanguageViewModel
@@ -67,9 +68,23 @@ fun ActionBar(
} }
@Composable @Composable
fun TopBarActions(languageViewModel: LanguageViewModel, onSettingsClick: () -> Unit, hintContent: (@Composable () -> Unit)? = null) { fun TopBarActions(
languageViewModel: LanguageViewModel,
onSettingsClick: () -> Unit,
onNavigateBack: (() -> Unit)? = null,
hintContent: Hint? = null
) {
ActionBar(modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp)) { ActionBar(modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp)) {
if (onNavigateBack != null) {
IconButton(onClick = onNavigateBack) {
Icon(
imageVector = AppIcons.ArrowBack,
contentDescription = stringResource(R.string.cd_navigate_back)
)
}
}
if (hintContent != null) { if (hintContent != null) {
WithHint(hintContent = hintContent) { WithHint(hintContent = hintContent) {
} }

View File

@@ -106,6 +106,14 @@ fun TranslationScreen(
settingsViewModel = settingsViewModel, settingsViewModel = settingsViewModel,
onHistoryClick = onHistoryClick, onHistoryClick = onHistoryClick,
onSettingsClick = onSettingsClick, onSettingsClick = onSettingsClick,
onNavigateBack = {
if (!navController.popBackStack()) {
navController.navigate(eu.gaudian.translator.view.composable.Screen.Home.route) {
launchSingleTop = true
restoreState = false
}
}
},
context = context context = context
) )
} }
@@ -119,6 +127,7 @@ private fun LoadedTranslationContent(
settingsViewModel: SettingsViewModel, settingsViewModel: SettingsViewModel,
onHistoryClick: () -> Unit, onHistoryClick: () -> Unit,
onSettingsClick: () -> Unit, onSettingsClick: () -> Unit,
onNavigateBack: () -> Unit,
context: Context context: Context
) { ) {
val inputText by translationViewModel.inputText.collectAsState() val inputText by translationViewModel.inputText.collectAsState()
@@ -167,7 +176,8 @@ private fun LoadedTranslationContent(
TopBarActions( TopBarActions(
languageViewModel = languageViewModel, languageViewModel = languageViewModel,
onSettingsClick = onSettingsClick, onSettingsClick = onSettingsClick,
hintContent = { HintDefinition.TRANSLATION.Render() } onNavigateBack = onNavigateBack,
hintContent = HintDefinition.TRANSLATION.hint()
) )
AppCard(modifier = Modifier.padding(8.dp, end = 8.dp, bottom = 8.dp, top = 0.dp)) { AppCard(modifier = Modifier.padding(8.dp, end = 8.dp, bottom = 8.dp, top = 0.dp)) {

View File

@@ -5,14 +5,16 @@ package eu.gaudian.translator.view.vocabulary
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -30,7 +32,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
@@ -43,10 +46,12 @@ import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppScaffold import eu.gaudian.translator.view.composable.AppScaffold
import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.composable.PrimaryButton import eu.gaudian.translator.view.composable.PrimaryButton
import eu.gaudian.translator.view.composable.SecondaryButton
import eu.gaudian.translator.view.dialogs.DeleteCategoryDialog import eu.gaudian.translator.view.dialogs.DeleteCategoryDialog
import eu.gaudian.translator.view.dialogs.DeleteItemsDialog import eu.gaudian.translator.view.dialogs.DeleteItemsDialog
import eu.gaudian.translator.view.dialogs.EditCategoryDialog import eu.gaudian.translator.view.dialogs.EditCategoryDialog
import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressCircle import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressCircle
import eu.gaudian.translator.viewmodel.CategoryProgress
import eu.gaudian.translator.viewmodel.CategoryViewModel import eu.gaudian.translator.viewmodel.CategoryViewModel
import eu.gaudian.translator.viewmodel.LanguageViewModel import eu.gaudian.translator.viewmodel.LanguageViewModel
import eu.gaudian.translator.viewmodel.ProgressViewModel import eu.gaudian.translator.viewmodel.ProgressViewModel
@@ -89,7 +94,6 @@ fun CategoryDetailScreen(
if (!hasLangList && !hasPair && !hasStages) { if (!hasLangList && !hasPair && !hasStages) {
append(stringResource(R.string.text_filter_all_items)) append(stringResource(R.string.text_filter_all_items))
} else { } else {
//append(stringResource(R.string.filter))
append(" ") append(" ")
if (hasPair) { if (hasPair) {
val (a, b) = cat.languagePairs val (a, b) = cat.languagePairs
@@ -118,30 +122,8 @@ fun CategoryDetailScreen(
modifier = Modifier.background(MaterialTheme.colorScheme.surface) modifier = Modifier.background(MaterialTheme.colorScheme.surface)
) { ) {
AppTopAppBar( AppTopAppBar(
title = { title = title,
Column { onNavigateBack = { navController.popBackStack() },
Text(
text = title,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = subtitle,
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
},
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(
AppIcons.ArrowBack,
contentDescription = stringResource(R.string.cd_back)
)
}
},
actions = { actions = {
IconButton(onClick = { showMenu = !showMenu }) { IconButton(onClick = { showMenu = !showMenu }) {
Icon( Icon(
@@ -150,94 +132,58 @@ fun CategoryDetailScreen(
) )
} }
DropdownMenu( DropdownMenu(
expanded = showMenu, expanded = showMenu,
onDismissRequest = { showMenu = false }, onDismissRequest = { showMenu = false },
modifier = Modifier.width(220.dp) modifier = Modifier.width(220.dp)
) { ) {
DropdownMenuItem(
text = { Text(stringResource(R.string.text_edit_category)) },
onClick = {
categoryViewModel.setShowEditCategoryDialog(true, categoryId)
showMenu = false
}
)
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(R.string.text_export_category)) }, text = { Text(stringResource(R.string.text_export_category)) },
onClick = { onClick = {
vocabularyViewModel.saveCategory(categoryId) vocabularyViewModel.saveCategory(categoryId)
showMenu = false showMenu = false
} },
leadingIcon = { Icon(AppIcons.Share, contentDescription = null) }
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(R.string.delete_items_category)) }, text = { Text(stringResource(R.string.delete_items_category)) },
onClick = { onClick = {
categoryViewModel.setShowDeleteItemsDialog(true, categoryId) categoryViewModel.setShowDeleteItemsDialog(true, categoryId)
showMenu = false showMenu = false
} },
) leadingIcon = { Icon(AppIcons.Delete, contentDescription = null) }
DropdownMenuItem(
text = { Text(stringResource(R.string.text_delete_category)) },
onClick = {
categoryViewModel.setShowDeleteCategoryDialog(true, categoryId)
showMenu = false
}
) )
} }
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
// TODO: Review this
containerColor = MaterialTheme.colorScheme.surface containerColor = MaterialTheme.colorScheme.surface
) )
) )
Row( // Category Header Card with Progress and Action Buttons
modifier = Modifier CategoryHeaderCard(
.fillMaxWidth() subtitle = subtitle,
.padding(vertical = 8.dp, horizontal = 16.dp), categoryProgress = categoryProgress,
horizontalArrangement = Arrangement.SpaceEvenly, onStartExerciseClick = {
verticalAlignment = Alignment.CenterVertically
) {
if (categoryProgress != null) {
Box(modifier = Modifier.weight(1f)) {
CategoryProgressCircle(
totalItems = categoryProgress.totalItems,
itemsCompleted = categoryProgress.itemsCompleted,
itemsInStages = categoryProgress.itemsInStages,
newItems = categoryProgress.newItems,
circleSize = 80.dp,
)
}
} else {
Spacer(modifier = Modifier.weight(1f))
}
Box(
modifier = Modifier
.weight(1f)
.padding(start = 8.dp)
) {
PrimaryButton(
text = stringResource(R.string.label_start),
icon = AppIcons.Play,
onClick = {
val categories = listOf(category) val categories = listOf(category)
val categoryIds = categories.joinToString(",") { it?.id.toString() } val categoryIds = categories.joinToString(",") { it?.id.toString() }
navController.navigate("vocabulary_exercise/false?categories=$categoryIds") navController.navigate("vocabulary_exercise/false?categories=$categoryIds")
}, },
modifier = Modifier.heightIn(max = 80.dp) onEditClick = {
categoryViewModel.setShowEditCategoryDialog(true, categoryId)
},
onDeleteClick = {
categoryViewModel.setShowDeleteCategoryDialog(true, categoryId)
}
) )
} }
} }
}
}
) { paddingValues -> ) { paddingValues ->
Column(modifier = Modifier.padding(paddingValues)) { Column(modifier = Modifier.padding(paddingValues)) {
VocabularyListScreen( AllCardsListScreen(
categoryId = categoryId, categoryId = categoryId,
showDueTodayOnly = false, showDueTodayOnly = false,
onNavigateToItem = onNavigateToItem, onNavigateToItem = onNavigateToItem,
navController = navController, // Pass the received navController here navController = navController,
isRemoveFromCategoryEnabled = category is TagCategory, isRemoveFromCategoryEnabled = category is TagCategory,
showTopBar = false, showTopBar = false,
enableNavigationButtons = true enableNavigationButtons = true
@@ -266,3 +212,131 @@ fun CategoryDetailScreen(
} }
} }
} }
@Composable
fun CategoryHeaderCard(
subtitle: String,
categoryProgress: CategoryProgress?,
onStartExerciseClick: () -> Unit,
onEditClick: () -> Unit,
onDeleteClick: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Subtitle
if (subtitle.isNotBlank()) {
Text(
text = subtitle,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.padding(bottom = 16.dp)
)
}
// Progress Circle
if (categoryProgress != null) {
CategoryProgressCircle(
totalItems = categoryProgress.totalItems,
itemsCompleted = categoryProgress.itemsCompleted,
itemsInStages = categoryProgress.itemsInStages,
newItems = categoryProgress.newItems,
circleSize = 120.dp,
)
Spacer(modifier = Modifier.height(24.dp))
}
// Action Buttons
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// Start Exercise Button (Primary)
PrimaryButton(
text = stringResource(R.string.label_start_exercise),
icon = AppIcons.Play,
onClick = onStartExerciseClick,
modifier = Modifier.weight(1f)
)
}
Spacer(modifier = Modifier.height(12.dp))
// Secondary Action Buttons
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// Edit Button
SecondaryButton(
text = stringResource(R.string.label_edit),
icon = AppIcons.Edit,
onClick = onEditClick,
modifier = Modifier.weight(1f)
)
// Delete Button
SecondaryButton(
text = stringResource(R.string.label_delete),
icon = AppIcons.Delete,
onClick = onDeleteClick,
modifier = Modifier.weight(1f)
)
}
}
}
}
// ==================== PREVIEWS ====================
@Preview(showBackground = true)
@Composable
fun CategoryHeaderCardPreview() {
MaterialTheme {
CategoryHeaderCard(
subtitle = "German - English | All Stages",
categoryProgress = null,
onStartExerciseClick = {},
onEditClick = {},
onDeleteClick = {}
)
}
}
@Preview(showBackground = true)
@Composable
fun CategoryHeaderCardWithProgressPreview() {
MaterialTheme {
CategoryHeaderCard(
subtitle = "Travel Vocabulary",
categoryProgress = CategoryProgress(
vocabularyCategory = TagCategory(
1,
"Travel"
),
totalItems = 50,
newItems = 15,
itemsInStages = 25,
itemsCompleted = 10
),
onStartExerciseClick = {},
onEditClick = {},
onDeleteClick = {}
)
}
}

View File

@@ -100,13 +100,7 @@ fun CategoryListScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { title = "TODO",
if (isSelectionMode && selectedCategories.isNotEmpty()) {
Text(stringResource(R.string.text_2d_categories_selected, selectedCategories.size))
} else {
Text(stringResource(R.string.label_categories))
}
},
navigationIcon = { navigationIcon = {
if (isSelectionMode) { if (isSelectionMode) {
IconButton(onClick = { IconButton(onClick = {

View File

@@ -55,7 +55,6 @@ import androidx.navigation.compose.rememberNavController
import eu.gaudian.translator.R import eu.gaudian.translator.R
import eu.gaudian.translator.model.VocabularyStage import eu.gaudian.translator.model.VocabularyStage
import eu.gaudian.translator.model.WidgetType import eu.gaudian.translator.model.WidgetType
import eu.gaudian.translator.utils.Log
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppCard import eu.gaudian.translator.view.composable.AppCard
import eu.gaudian.translator.view.composable.AppIcons import eu.gaudian.translator.view.composable.AppIcons
@@ -65,7 +64,6 @@ import eu.gaudian.translator.view.vocabulary.widgets.AllVocabularyWidget
import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressWidget import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressWidget
import eu.gaudian.translator.view.vocabulary.widgets.DueTodayWidget import eu.gaudian.translator.view.vocabulary.widgets.DueTodayWidget
import eu.gaudian.translator.view.vocabulary.widgets.LevelWidget import eu.gaudian.translator.view.vocabulary.widgets.LevelWidget
import eu.gaudian.translator.view.vocabulary.widgets.ModernStartButtons
import eu.gaudian.translator.view.vocabulary.widgets.StatusWidget import eu.gaudian.translator.view.vocabulary.widgets.StatusWidget
import eu.gaudian.translator.view.vocabulary.widgets.StreakWidget import eu.gaudian.translator.view.vocabulary.widgets.StreakWidget
import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget
@@ -530,17 +528,7 @@ private fun LazyWidget(
onMissingLanguage: (Int) -> Unit onMissingLanguage: (Int) -> Unit
) { ) {
when (widgetType) { when (widgetType) {
WidgetType.StartButtons -> ModernStartButtons(
onCustomClick = onShowCustomExerciseDialog,
onDailyClick = { isSpelling ->
if (isSpelling) {
onShowWordPairExerciseDialog()
} else {
startDailyExercise(true)
Log.d("DailyExercise", "Starting daily exercise")
}
}
)
WidgetType.Status -> LazyStatusWidget( WidgetType.Status -> LazyStatusWidget(
vocabularyViewModel = vocabularyViewModel, vocabularyViewModel = vocabularyViewModel,

View File

@@ -78,7 +78,7 @@ fun LanguageProgressScreen(navController: NavController) {
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.your_language_journey)) }, title = stringResource(R.string.your_language_journey),
onNavigateBack = { navController.popBackStack() } onNavigateBack = { navController.popBackStack() }
) )
}, },

View File

@@ -1,474 +0,0 @@
@file:Suppress("HardCodedStringLiteral", "AssignedValueIsNeverRead")
package eu.gaudian.translator.view.vocabulary
import androidx.activity.ComponentActivity
import androidx.activity.compose.LocalActivity
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import eu.gaudian.translator.R
import eu.gaudian.translator.model.Exercise
import eu.gaudian.translator.model.MatchingPairsQuestion
import eu.gaudian.translator.model.Question
import eu.gaudian.translator.model.VocabularyCategory
import eu.gaudian.translator.model.VocabularyStage
import eu.gaudian.translator.ui.theme.ThemePreviews
import eu.gaudian.translator.utils.Log
import eu.gaudian.translator.view.composable.AppDialog
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppOutlinedCard
import eu.gaudian.translator.view.composable.AppSlider
import eu.gaudian.translator.view.composable.AppTabLayout
import eu.gaudian.translator.view.composable.OptionItemSwitch
import eu.gaudian.translator.view.composable.TabItem
import eu.gaudian.translator.view.dialogs.StartExerciseDialog
import eu.gaudian.translator.view.dialogs.VocabularyMenu
import eu.gaudian.translator.viewmodel.ExerciseViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@Suppress("HardCodedStringLiteral")
enum class VocabularyTab(
override val title: String,
override val icon: ImageVector,
val route: String
) : TabItem {
Dashboard(title = "title_dashboard", icon = AppIcons.Dashboard, route = "dashboard"),
Statistics(title = "label_all_vocabulary", icon = AppIcons.BarChart, route = "statistics")
}
@Suppress("unused", "HardCodedStringLiteral", "UnusedVariable")
@Composable
fun Dummy() {
val dummy = listOf(
stringResource(id = R.string.title_dashboard),
stringResource(id = R.string.label_all_vocabulary),
)
}
@Composable
fun MainVocabularyScreen(
navController: NavController
) {
val activity = LocalActivity.current as ComponentActivity
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
val exerciseViewModel: ExerciseViewModel = hiltViewModel(activity)
val vocabularyNavController = rememberNavController()
val coroutineScope = rememberCoroutineScope()
var showCustomExerciseDialog by remember { mutableStateOf(false) }
var startDailyExercise by remember { mutableStateOf(false) }
var showWordPairExerciseDialog by remember { mutableStateOf(false) }
// Word Pair settings and temporary selections
var showWordPairSettingsDialog by remember { mutableStateOf(false) }
var tempWpCategories by remember { mutableStateOf<List<VocabularyCategory>>(emptyList()) }
var tempWpStages by remember { mutableStateOf<List<VocabularyStage>>(emptyList()) }
var tempWpLanguageIds by remember { mutableStateOf<List<Int>>(emptyList()) }
var wpQuestionCount by remember { mutableIntStateOf(5) }
var wpShuffleQuestions by remember { mutableStateOf(true) }
var wpShuffleWordOrder by remember { mutableStateOf(true) }
var wpTrainingMode by remember { mutableStateOf(false) }
var wpDueTodayOnly by remember { mutableStateOf(false) }
var isScrolling by remember { mutableStateOf(false) }
if (showCustomExerciseDialog) {
StartExerciseDialog(
onDismiss = { showCustomExerciseDialog = false },
onConfirm = { categories, stages, languageIds ->
showCustomExerciseDialog = false
val categoryIds = categories.joinToString(",") { it.id.toString() }
val stageNames = stages.joinToString(",") { it.name }
val languageIdsStr = languageIds.joinToString(",") { it.toString() }
@Suppress("HardCodedStringLiteral")
navController.navigate("vocabulary_exercise/false?categories=$categoryIds&stages=$stageNames&languages=$languageIdsStr")
}
)
}
if (showWordPairExerciseDialog) {
StartExerciseDialog(
onDismiss = { showWordPairExerciseDialog = false },
onConfirm = { categories, stages, languageIds ->
// Store selections and open settings dialog instead of starting immediately
tempWpCategories = categories
tempWpStages = stages
tempWpLanguageIds = languageIds
showWordPairExerciseDialog = false
showWordPairSettingsDialog = true
}
)
}
val textWordPairSettings = stringResource(R.string.text_word_pair_settings)
// Settings dialog for Word Pair Exercise
if (showWordPairSettingsDialog) {
AppDialog(
onDismissRequest = { showWordPairSettingsDialog = false },
title = { Text(textWordPairSettings) }
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
// Amount of questions
Text(
stringResource(
R.string.text_amount_of_questions_2d,
wpQuestionCount
))
AppSlider(
value = wpQuestionCount.toFloat(),
onValueChange = { wpQuestionCount = it.toInt().coerceIn(1, 20) },
valueRange = 1f..20f,
steps = 18
)
// Toggles
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
OptionItemSwitch(
title = stringResource(R.string.text_shuffle_questions),
checked = wpShuffleQuestions,
onCheckedChange = { wpShuffleQuestions = it },
)
}
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
OptionItemSwitch(
title = stringResource(R.string.text_shuffle_card_order),
description = stringResource(R.string.text_swap_sides),
checked = wpShuffleWordOrder,
onCheckedChange = { wpShuffleWordOrder = it },
)
}
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
OptionItemSwitch(
title = stringResource(R.string.tetx_training_mode),
description = stringResource(R.string.text_no_progress),
checked = wpTrainingMode,
onCheckedChange = { wpTrainingMode = it },
)
}
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
OptionItemSwitch(
title = stringResource(R.string.text_due_today_only),
checked = wpDueTodayOnly,
onCheckedChange = { wpDueTodayOnly = it },
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { showWordPairSettingsDialog = false }) {
Text(stringResource(id = R.string.label_cancel))
}
val textMatchThePairs = stringResource(R.string.text_match_the_pairs)
val textWordPairExercise = stringResource(R.string.text_word_pair_exercise)
val textTrainingModeDescription = stringResource(R.string.text_training_mode_description)
val labelTrainingMode = stringResource(R.string.label_training_mode)
TextButton(onClick = {
showWordPairSettingsDialog = false
// Build a Word Pair Exercise using matching pairs from selected vocabulary with options
coroutineScope.launch {
val items = vocabularyViewModel.filterVocabularyItems(
languages = tempWpLanguageIds,
query = null,
categoryIds = tempWpCategories.map { it.id },
stage = tempWpStages.firstOrNull(),
sortOrder = VocabularyViewModel.SortOrder.NEWEST_FIRST,
dueTodayOnly = wpDueTodayOnly
).first()
val maxPairsPerQuestion = 5
var pairsList = items.mapNotNull { item ->
val k = item.wordFirst.trim()
val v = item.wordSecond.trim()
if (k.isNotBlank() && v.isNotBlank()) k to v else null
}
if (wpShuffleWordOrder) {
pairsList = pairsList.map { (a, b) -> if ((0..1).random() == 0) a to b else b to a }
}
if (pairsList.isEmpty()) return@launch
val shuffledPairs = if (wpShuffleQuestions) pairsList.shuffled() else pairsList
val chunked = shuffledPairs.chunked(maxPairsPerQuestion)
val limitedChunks = chunked.take(wpQuestionCount)
val questions = mutableListOf<Question>()
var qId = 1
limitedChunks.forEach { chunk ->
if (chunk.size >= 2) {
questions.add(
MatchingPairsQuestion(
id = qId++,
name = textMatchThePairs,
pairs = chunk.toMap()
)
)
}
}
if (questions.isEmpty()) return@launch
@Suppress("HardCodedStringLiteral") val exercise = Exercise(
id = "wordpair-" + System.currentTimeMillis().toString(),
title = textWordPairExercise,
questions = questions.map { it.id },
contextTitle = if (wpTrainingMode) labelTrainingMode else null,
contextText = if (wpTrainingMode) textTrainingModeDescription else null
)
exerciseViewModel.startAdHocExercise(exercise, questions)
@Suppress("HardCodedStringLiteral")
navController.navigate("exercise_session")
}
}) {
Text(stringResource(id = R.string.label_start_exercise))
}
}
}
}
}
// Use LaunchedEffect to handle the navigation side effect
LaunchedEffect(startDailyExercise) {
if (startDailyExercise) {
@Suppress("HardCodedStringLiteral")
Log.d("DailyExercise", "Starting daily exercise")
@Suppress("HardCodedStringLiteral")
navController.navigate("vocabulary_exercise/false?categories=&stages=&languages=&dailyOnly=true")
startDailyExercise = false
}
}
val navBackStackEntry by vocabularyNavController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
val selectedTab = remember(currentRoute) {
VocabularyTab.entries.find { it.route == currentRoute } ?: VocabularyTab.Dashboard
}
val rawShowFabText = selectedTab == VocabularyTab.Dashboard && !isScrolling
var showFabText by remember { mutableStateOf(rawShowFabText) }
LaunchedEffect(rawShowFabText) {
if (rawShowFabText) {
// Only delay when showing (true), hide immediately
kotlinx.coroutines.delay(2000)
showFabText = true
} else {
showFabText = false
}
}
val repoEmpty =
vocabularyViewModel.vocabularyItems.collectAsState(initial = emptyList()).value.isEmpty()
if (repoEmpty) {
NoVocabularyScreen()
return
}
Box(modifier = Modifier.fillMaxSize()) {
Column(modifier = Modifier.fillMaxSize()) {
AppTabLayout(
tabs = VocabularyTab.entries,
selectedTab = selectedTab,
onTabSelected = { tab ->
vocabularyNavController.navigate(tab.route) {
popUpTo(vocabularyNavController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
NavHost(
navController = vocabularyNavController,
startDestination = VocabularyTab.Dashboard.route,
modifier = Modifier.weight(1f)
) {
composable(VocabularyTab.Dashboard.route) {
DashboardContent(
navController = navController,
onShowCustomExerciseDialog = { showCustomExerciseDialog = true },
onNavigateToCategoryDetail = { categoryId ->
navController.navigate("category_detail/$categoryId")
},
startDailyExercise = { startDailyExercise = true },
onNavigateToCategoryList = {
navController.navigate("category_list_screen")
},
onShowWordPairExerciseDialog = { showWordPairExerciseDialog = true },
onScroll = { isScrolling = it }
)
}
composable(VocabularyTab.Statistics.route) {
StatisticsContent(navController = navController)
}
composable("category_detail/{categoryId}") { backStackEntry ->
val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull()
if (categoryId != null) {
CategoryDetailScreen(
categoryId = categoryId,
onBackClick = { vocabularyNavController.popBackStack() },
onNavigateToItem = { item ->
navController.navigate("vocabulary_detail/${item.id}")
},
navController = navController as NavHostController
)
}
}
composable("vocabulary_exercise/{isSpelling}") { backStackEntry ->
backStackEntry.arguments?.getString("isSpelling")?.toBooleanStrict() ?: false
VocabularyExerciseHostScreen(
categoryIdsAsJson = null,
stageNamesAsJson = null,
languageIdsAsJson = null,
onClose = { navController.popBackStack() },
navController = navController,
dailyOnlyAsJson = null
)
}
composable("vocabulary_exercise/{dailyOnly}") { backStackEntry ->
backStackEntry.arguments?.getString("dailyOnly")?.toBooleanStrict() ?: false
VocabularyExerciseHostScreen(
categoryIdsAsJson = null,
stageNamesAsJson = null,
languageIdsAsJson = null,
onClose = { navController.popBackStack() },
navController = navController,
dailyOnlyAsJson = "{\"dailyOnly\": true}"
)
}
}
}
var menuHeightPx by remember { mutableIntStateOf(0) }
val density = LocalDensity.current
val menuHeightDp = (menuHeightPx / density.density).dp
val animatedBottomPadding by animateDpAsState(targetValue = 16.dp + 8.dp + menuHeightDp, label = "FBottomPadding")
Column(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp),
horizontalAlignment = Alignment.End
) {
VocabularyMenu(modifier = Modifier.onSizeChanged { menuHeightPx = it.height }, showFabText = showFabText)
}
// Place the FAB separately and animate its bottom padding based on the menu height
FloatingActionButton(
onClick = { showCustomExerciseDialog = true },
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(end = 16.dp, bottom = animatedBottomPadding)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.padding(horizontal = 16.dp)
.animateContentSize()
) {
Icon(
imageVector = AppIcons.Quiz,
contentDescription = null
)
if(showFabText) {
Text(
text = stringResource(R.string.label_start_exercise),
style = MaterialTheme.typography.labelLarge
)}
}
}
}
}
@Composable
fun StatisticsContent(
navController: NavController
) {
AppOutlinedCard {
VocabularyListScreen(
categoryId = null,
showDueTodayOnly = false,
onNavigateToItem = { item ->
navController.navigate("vocabulary_detail/${item.id}")
},
onNavigateBack = null,
navController = navController as NavHostController,
enableNavigationButtons = true
)
}
}
@ThemePreviews
@Composable
fun VocabularyDashboardScreenPreview() {
val navController = rememberNavController()
MainVocabularyScreen(navController = navController)
}
@ThemePreviews
@Composable
fun StatisticsContentPreview() {
val navController = rememberNavController()
StatisticsContent(navController = navController)
}

View File

@@ -0,0 +1,289 @@
package eu.gaudian.translator.view.vocabulary
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.WarningAmber
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavHostController
import eu.gaudian.translator.R
import eu.gaudian.translator.model.VocabularyCategory
import eu.gaudian.translator.model.VocabularyItem
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppButton
import eu.gaudian.translator.view.composable.AppCheckbox
import eu.gaudian.translator.view.composable.AppScaffold
import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.dialogs.CategoryDropdown
import eu.gaudian.translator.viewmodel.VocabularyViewModel
@Composable
fun NewWordReviewScreen(
navController: NavHostController,
modifier: Modifier = Modifier
) {
val activity = LocalContext.current.findActivity()
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(activity)
val generatedItems by vocabularyViewModel.generatedVocabularyItems.collectAsState()
val selectedItems = remember { mutableStateListOf<VocabularyItem>() }
val duplicates = remember { mutableStateListOf<Boolean>() }
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory?>>(emptyList()) }
LaunchedEffect(generatedItems) {
val duplicateResults = vocabularyViewModel.findDuplicates(generatedItems)
duplicates.clear()
duplicates.addAll(duplicateResults)
selectedItems.clear()
selectedItems.addAll(generatedItems.filterIndexed { index, _ -> !duplicateResults[index] })
}
AppScaffold(
topBar = {
AppTopAppBar(
title = stringResource(R.string.found_items),
onNavigateBack = { navController.popBackStack() }
)
},
modifier = modifier.fillMaxSize()
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
) {
SummaryHeader(
totalCount = generatedItems.size,
selectedCount = selectedItems.size,
modifier = Modifier.padding(horizontal = 20.dp, vertical = 12.dp)
)
if (generatedItems.isEmpty()) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(R.string.text_no_data_available),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
} else {
ReviewList(
generatedItems = generatedItems,
selectedItems = selectedItems,
duplicates = duplicates,
modifier = Modifier
.weight(1f)
.padding(horizontal = 16.dp)
)
}
Text(
text = stringResource(R.string.select_list_optional),
style = MaterialTheme.typography.labelLarge,
modifier = Modifier.padding(horizontal = 20.dp, vertical = 8.dp)
)
CategoryDropdown(
onCategorySelected = { selectedCategories = it },
noneSelectable = false,
multipleSelectable = true,
onlyLists = true,
addCategory = true,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
)
ActionRow(
selectedCount = selectedItems.size,
onCancel = { navController.popBackStack() },
onConfirm = {
val selectedCategoryIds = selectedCategories.filterNotNull().map { it.id }
vocabularyViewModel.addVocabularyItems(selectedItems.toList(), selectedCategoryIds)
@Suppress("HardCodedStringLiteral")
navController.popBackStack("new_word", inclusive = false)
},
modifier = Modifier.padding(16.dp)
)
}
}
}
@Composable
private fun SummaryHeader(
totalCount: Int,
selectedCount: Int,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f))
) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = stringResource(R.string.found_items),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Text(
text = stringResource(R.string.text_amount_2d, totalCount),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Column(horizontalAlignment = Alignment.End) {
Text(
text = stringResource(R.string.label_add_, selectedCount),
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.primary
)
Text(
text = stringResource(R.string.select_items_to_add),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
@Composable
private fun ReviewList(
generatedItems: List<VocabularyItem>,
selectedItems: MutableList<VocabularyItem>,
duplicates: List<Boolean>,
modifier: Modifier = Modifier
) {
val duplicateLabel = stringResource(R.string.duplicate)
LazyColumn(modifier = modifier) {
itemsIndexed(generatedItems) { index, item ->
val isDuplicate = duplicates.getOrNull(index) == true
val isSelected = selectedItems.contains(item)
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 6.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = if (isDuplicate) {
MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.25f)
} else {
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.25f)
}
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
AppCheckbox(
checked = isSelected,
onCheckedChange = { checked ->
if (checked) {
selectedItems.add(item)
} else {
selectedItems.remove(item)
}
}
)
Spacer(modifier = Modifier.size(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(text = item.wordFirst, style = MaterialTheme.typography.titleMedium)
Text(text = item.wordSecond, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
if (isDuplicate) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(12.dp))
.background(MaterialTheme.colorScheme.error.copy(alpha = 0.15f))
.padding(horizontal = 8.dp, vertical = 4.dp)
) {
Icon(
imageVector = Icons.Default.WarningAmber,
contentDescription = null,
tint = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.size(4.dp))
Text(
text = duplicateLabel,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.error
)
}
}
}
}
}
}
}
@Composable
private fun ActionRow(
selectedCount: Int,
onCancel: () -> Unit,
onConfirm: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
TextButton(onClick = onCancel) {
Text(stringResource(R.string.label_cancel))
}
AppButton(
onClick = onConfirm,
enabled = selectedCount > 0
) {
Text(stringResource(R.string.label_add_, selectedCount))
}
}
}

View File

@@ -0,0 +1,780 @@
package eu.gaudian.translator.view.vocabulary
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AutoAwesome
import androidx.compose.material.icons.filled.DriveFolderUpload
import androidx.compose.material.icons.filled.EditNote
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavHostController
import eu.gaudian.translator.R
import eu.gaudian.translator.model.Language
import eu.gaudian.translator.model.VocabularyItem
import eu.gaudian.translator.utils.StatusMessageId
import eu.gaudian.translator.utils.StatusMessageService
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.NavigationRoutes
import eu.gaudian.translator.view.composable.AppButton
import eu.gaudian.translator.view.composable.AppCard
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppOutlinedButton
import eu.gaudian.translator.view.composable.AppSlider
import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.composable.InspiringSearchField
import eu.gaudian.translator.view.composable.Screen
import eu.gaudian.translator.view.composable.SourceLanguageDropdown
import eu.gaudian.translator.view.composable.TargetLanguageDropdown
import eu.gaudian.translator.view.hints.HintDefinition
import eu.gaudian.translator.view.library.VocabularyCard
import eu.gaudian.translator.viewmodel.LanguageViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel
import kotlinx.coroutines.launch
@Composable
fun NewWordScreen(
navController: NavHostController,
modifier: Modifier = Modifier
) {
val activity = LocalContext.current.findActivity()
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
val isGenerating by vocabularyViewModel.isGenerating.collectAsState()
val generatedItems by vocabularyViewModel.generatedVocabularyItems.collectAsState()
val allLanguages by languageViewModel.allLanguages.collectAsState()
val recentItems by vocabularyViewModel.vocabularyItems.collectAsState()
val coroutineScope = rememberCoroutineScope()
var category by remember { mutableStateOf("") }
var amount by remember { mutableFloatStateOf(8f) }
var navigateToReview by remember { mutableStateOf(false) }
LaunchedEffect(isGenerating, generatedItems, navigateToReview) {
if (navigateToReview && !isGenerating) {
if (generatedItems.isNotEmpty()) {
navController.navigate(NavigationRoutes.NEW_WORD_REVIEW)
}
navigateToReview = false
}
}
val statusMessageService = StatusMessageService
val context = LocalContext.current
val showTableImportDialog = remember { mutableStateOf(false) }
var parsedTable by remember { mutableStateOf<List<List<String>>>(emptyList()) }
var selectedColFirst by remember { mutableIntStateOf(0) }
var selectedColSecond by remember { mutableIntStateOf(1) }
var skipHeader by remember { mutableStateOf(true) }
var selectedLangFirst by remember { mutableStateOf<Language?>(null) }
var selectedLangSecond by remember { mutableStateOf<Language?>(null) }
val recentlyAdded = remember(recentItems) {
recentItems.sortedByDescending { it.id }.take(4)
}
fun parseCsv(text: String): List<List<String>> {
if (text.isBlank()) return emptyList()
val candidates = listOf(',', ';', '\t')
val sampleLine = text.lineSequence().firstOrNull { it.isNotBlank() } ?: return emptyList()
val delimiter = candidates.maxBy { c -> sampleLine.count { it == c } }
val rows = mutableListOf<List<String>>()
var current = StringBuilder()
var inQuotes = false
val currentRow = mutableListOf<String>()
var i = 0
while (i < text.length) {
when (val ch = text[i]) {
'"' -> {
if (inQuotes && i + 1 < text.length && text[i + 1] == '"') {
current.append('"')
i++
} else {
inQuotes = !inQuotes
}
}
'\r' -> {
// ignore
}
'\n' -> {
val field = current.toString()
current = StringBuilder()
currentRow.add(field)
rows.add(currentRow.toList())
currentRow.clear()
inQuotes = false
}
else -> {
if (ch == delimiter && !inQuotes) {
val field = current.toString()
currentRow.add(field)
current = StringBuilder()
} else {
current.append(ch)
}
}
}
i++
}
if (current.isNotEmpty() || currentRow.isNotEmpty()) {
currentRow.add(current.toString())
rows.add(currentRow.toList())
}
return rows.map { row ->
row.map { it.trim().trim('"') }
}.filter { r -> r.any { it.isNotBlank() } }
}
val importTableLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument(),
onResult = { uri ->
uri?.let { u ->
try {
context.contentResolver.takePersistableUriPermission(u, android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION)
} catch (_: Exception) {}
try {
val mime = context.contentResolver.getType(u)
val isExcel = mime == "application/vnd.ms-excel" ||
mime == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
if (isExcel) {
statusMessageService.showErrorById(StatusMessageId.ERROR_EXCEL_NOT_SUPPORTED)
return@let
}
context.contentResolver.openInputStream(u)?.use { inputStream ->
val text = inputStream.bufferedReader().use { it.readText() }
val rows = parseCsv(text)
if (rows.isNotEmpty() && rows.maxOf { it.size } >= 2) {
parsedTable = rows
selectedColFirst = 0
selectedColSecond = 1.coerceAtMost(rows.first().size - 1)
showTableImportDialog.value = true
} else {
statusMessageService.showErrorById(StatusMessageId.ERROR_PARSING_TABLE)
}
}
} catch (_: Exception) {
statusMessageService.showErrorById(StatusMessageId.ERROR_PARSING_TABLE_WITH_REASON)
}
}
}
)
Box(
modifier = modifier
.fillMaxSize()
.padding(16.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
modifier = Modifier
.widthIn(max = 700.dp) // Perfect scaling for tablets/foldables
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(0.dp)
) {
AppTopAppBar(
title = stringResource(R.string.label_new_words),
onNavigateBack = { navController.popBackStack() }
)
Spacer(modifier = Modifier.height(16.dp))
AIGeneratorCard(
category = category,
onCategoryChange = { category = it },
amount = amount,
onAmountChange = { amount = it },
languageViewModel = languageViewModel,
isGenerating = isGenerating,
onGenerate = {
if (category.isNotBlank() && !isGenerating) {
coroutineScope.launch {
vocabularyViewModel.generateVocabularyItems(category.trim(), amount.toInt())
navigateToReview = true
}
}
},
)
Spacer(modifier = Modifier.height(24.dp))
AddManuallyCard(
languageViewModel = languageViewModel,
vocabularyViewModel = vocabularyViewModel,
)
Spacer(modifier = Modifier.height(24.dp))
BottomActionCardsRow(
onImportCsvClick = {
@Suppress("HardCodedStringLiteral")
importTableLauncher.launch(
arrayOf(
"text/csv",
"text/comma-separated-values",
"text/tab-separated-values",
"text/plain",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)
)
}
)
if (recentlyAdded.isNotEmpty()) {
Spacer(modifier = Modifier.height(32.dp))
Column {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.label_recently_added),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
TextButton(onClick = { navController.navigate(Screen.Library.route) }) {
Text(stringResource(R.string.label_view_all))
}
}
Spacer(modifier = Modifier.height(12.dp))
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
recentlyAdded.forEach { item ->
VocabularyCard(
item = item,
allLanguages = allLanguages,
isSelected = false,
onItemClick = {
@Suppress("HardCodedStringLiteral")
navController.navigate("${NavigationRoutes.VOCABULARY_DETAIL}/${item.id}")
},
onItemLongClick = {},
onDeleteClick = {}
)
}
}
}
}
// Extra padding at the bottom for scroll clearance
Spacer(modifier = Modifier.height(100.dp))
}
}
if (showTableImportDialog.value) {
AlertDialog(
onDismissRequest = { showTableImportDialog.value = false },
title = { Text(stringResource(R.string.label_import_table_csv_excel)) },
text = {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
val columnCount = parsedTable.maxOfOrNull { it.size } ?: 0
Row(verticalAlignment = Alignment.CenterVertically) {
Text(stringResource(R.string.label_first_column), modifier = Modifier.weight(1f))
var menu1Expanded by remember { mutableStateOf(false) }
AppOutlinedButton(onClick = { menu1Expanded = true }) {
Text(stringResource(R.string.label_column_n, selectedColFirst + 1))
}
DropdownMenu(expanded = menu1Expanded, onDismissRequest = { menu1Expanded = false }) {
(0 until columnCount).forEach { idx ->
val header = parsedTable.firstOrNull()?.getOrNull(idx).orEmpty()
DropdownMenuItem(
text = { Text("#${idx + 1}$header") },
onClick = { selectedColFirst = idx; menu1Expanded = false }
)
}
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text(stringResource(R.string.label_second_column), modifier = Modifier.weight(1f))
var menu2Expanded by remember { mutableStateOf(false) }
AppOutlinedButton(onClick = { menu2Expanded = true }) {
Text(stringResource(R.string.label_column_n, selectedColSecond + 1))
}
DropdownMenu(expanded = menu2Expanded, onDismissRequest = { menu2Expanded = false }) {
(0 until columnCount).forEach { idx ->
val header = parsedTable.firstOrNull()?.getOrNull(idx).orEmpty()
DropdownMenuItem(
text = { Text("#${idx + 1}$header") },
onClick = { selectedColSecond = idx; menu2Expanded = false }
)
}
}
}
Text(stringResource(R.string.label_languages))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
Column(modifier = Modifier.weight(1f)) {
Text(stringResource(R.string.label_first_language))
eu.gaudian.translator.view.composable.SingleLanguageDropDown(
languageViewModel = languageViewModel,
selectedLanguage = selectedLangFirst,
onLanguageSelected = { selectedLangFirst = it }
)
}
Column(modifier = Modifier.weight(1f)) {
Text(stringResource(R.string.label_second_language))
eu.gaudian.translator.view.composable.SingleLanguageDropDown(
languageViewModel = languageViewModel,
selectedLanguage = selectedLangSecond,
onLanguageSelected = { selectedLangSecond = it }
)
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
androidx.compose.material3.Checkbox(checked = skipHeader, onCheckedChange = { skipHeader = it })
Spacer(modifier = Modifier.width(4.dp))
Text(stringResource(R.string.label_header_row))
}
val startIdx = if (skipHeader) 1 else 0
val previewA = parsedTable.drop(startIdx).take(5).mapNotNull { it.getOrNull(selectedColFirst) }.joinToString(", ")
val previewB = parsedTable.drop(startIdx).take(5).mapNotNull { it.getOrNull(selectedColSecond) }.joinToString(", ")
Text(stringResource(R.string.label_preview_first, previewA))
Text(stringResource(R.string.label_preview_second, previewB))
val totalRows = parsedTable.drop(startIdx).count { row ->
val a = row.getOrNull(selectedColFirst).orEmpty().isNotBlank()
val b = row.getOrNull(selectedColSecond).orEmpty().isNotBlank()
a || b
}
Text(stringResource(R.string.text_rows_to_import_1d, totalRows))
}
},
confirmButton = {
TextButton(onClick = {
if (selectedColFirst == selectedColSecond) {
statusMessageService.showErrorById(StatusMessageId.ERROR_SELECT_TWO_COLUMNS)
return@TextButton
}
val langA = selectedLangFirst
val langB = selectedLangSecond
if (langA == null || langB == null) {
statusMessageService.showErrorById(StatusMessageId.ERROR_SELECT_LANGUAGES)
return@TextButton
}
val startIdx = if (skipHeader) 1 else 0
val items = parsedTable.drop(startIdx).mapNotNull { row ->
val a = row.getOrNull(selectedColFirst)?.trim().orEmpty()
val b = row.getOrNull(selectedColSecond)?.trim().orEmpty()
if (a.isBlank() && b.isBlank()) null else VocabularyItem(
id = 0,
languageFirstId = langA.nameResId,
languageSecondId = langB.nameResId,
wordFirst = a,
wordSecond = b
)
}
if (items.isEmpty()) {
statusMessageService.showErrorById(StatusMessageId.ERROR_NO_ROWS_TO_IMPORT)
return@TextButton
}
vocabularyViewModel.addVocabularyItems(items)
statusMessageService.showSuccessById(StatusMessageId.SUCCESS_ITEMS_IMPORTED)
showTableImportDialog.value = false
}) { Text(stringResource(R.string.label_import)) }
},
dismissButton = {
TextButton(onClick = { showTableImportDialog.value = false }) {
Text(stringResource(R.string.label_cancel))
}
}
)
}
}
// --- AI GENERATOR CARD (From previous implementation) ---
@Composable
fun AIGeneratorCard(
category: String,
onCategoryChange: (String) -> Unit,
amount: Float,
onAmountChange: (Float) -> Unit,
languageViewModel: LanguageViewModel,
isGenerating: Boolean,
onGenerate: () -> Unit,
modifier: Modifier = Modifier
) {
val icon = Icons.Default.AutoAwesome
val hints = stringArrayResource(R.array.vocabulary_hints)
AppCard(
modifier = modifier.fillMaxWidth(),
title = stringResource(R.string.label_ai_generator),
icon = icon,
hintContent = HintDefinition.VOCABULARY_GENERATE_AI.hint(),
) {
Column(modifier = Modifier.padding(8.dp)) {
Text(
text = stringResource(R.string.text_search_term),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(8.dp))
InspiringSearchField(
value = category,
hints = hints,
onValueChange = onCategoryChange
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.text_select_languages),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
SourceLanguageDropdown(
modifier = Modifier.weight(1f),
languageViewModel = languageViewModel,
iconEnabled = false,
noBorder = true
)
TargetLanguageDropdown(
modifier = Modifier.weight(1f),
languageViewModel = languageViewModel,
iconEnabled = false,
noBorder = true
)
}
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.text_select_amount),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(8.dp))
AppSlider(
value = amount,
onValueChange = onAmountChange,
valueRange = 1f..25f,
steps = 24,
modifier = Modifier.fillMaxWidth()
)
Text(
text = stringResource(R.string.text_amount_2d, amount.toInt()),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(top = 8.dp)
)
Spacer(modifier = Modifier.height(32.dp))
if (isGenerating) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
CircularProgressIndicator()
}
Spacer(modifier = Modifier.height(16.dp))
}
AppButton(
onClick = onGenerate,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
enabled = category.isNotBlank() && !isGenerating
) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Icon(imageVector = Icons.Default.AutoAwesome, contentDescription = null, modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.text_generate),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
}
}
}
}
// --- NEW COMPONENTS START HERE ---
@Composable
fun AddManuallyCard(
languageViewModel: LanguageViewModel,
vocabularyViewModel: VocabularyViewModel,
modifier: Modifier = Modifier
) {
var wordText by remember { mutableStateOf("") }
var translationText by remember { mutableStateOf("") }
val selectedSourceLanguage by languageViewModel.selectedSourceLanguage.collectAsState()
val selectedTargetLanguage by languageViewModel.selectedTargetLanguage.collectAsState()
val canAdd = wordText.isNotBlank() && translationText.isNotBlank() &&
selectedSourceLanguage != null && selectedTargetLanguage != null
AppCard(
modifier = modifier.fillMaxWidth(),
) {
Column(modifier = Modifier.padding(24.dp)) {
// Header Row
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(12.dp))
.background(MaterialTheme.colorScheme.surfaceVariant),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.EditNote,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
}
Spacer(modifier = Modifier.width(16.dp))
Text(
text = stringResource(R.string.label_add_vocabulary),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
}
}
Spacer(modifier = Modifier.height(24.dp))
// Input Fields
TextField(
value = wordText,
onValueChange = { wordText = it },
placeholder = { Text(stringResource(R.string.text_label_word), color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)) },
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = TextFieldDefaults.colors(
focusedContainerColor = MaterialTheme.colorScheme.surface, // Very dark background
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
),
singleLine = true
)
Spacer(modifier = Modifier.height(16.dp))
TextField(
value = translationText,
onValueChange = { translationText = it },
placeholder = { Text(stringResource(R.string.text_translation), color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)) },
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = TextFieldDefaults.colors(
focusedContainerColor = MaterialTheme.colorScheme.surface,
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
),
singleLine = true
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(R.string.text_select_languages),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f))
.padding(12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
SourceLanguageDropdown(
modifier = Modifier.weight(1f),
languageViewModel = languageViewModel,
iconEnabled = false,
noBorder = true
)
TargetLanguageDropdown(
modifier = Modifier.weight(1f),
languageViewModel = languageViewModel,
iconEnabled = false,
noBorder = true
)
}
Spacer(modifier = Modifier.height(24.dp))
// Add to List Button (Darker variant)
AppButton(
onClick = {
val newItem = VocabularyItem(
languageFirstId = selectedSourceLanguage?.nameResId,
languageSecondId = selectedTargetLanguage?.nameResId,
wordFirst = wordText.trim(),
wordSecond = translationText.trim(),
id = 0
)
vocabularyViewModel.addVocabularyItems(listOf(newItem))
wordText = ""
translationText = ""
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
enabled = canAdd
) {
Text(
text = stringResource(R.string.label_add),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
}
}
}
@Composable
fun BottomActionCardsRow(
modifier: Modifier = Modifier,
onImportCsvClick: () -> Unit
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
//TODO Explore Packs Card
AppCard(
modifier = Modifier
.weight(1f)
.height(120.dp),
) {
Column(
modifier = Modifier
.fillMaxSize()
.alpha(0.6f),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = AppIcons.Vocabulary,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
}
Spacer(modifier = Modifier.height(12.dp))
@Suppress("HardCodedStringLiteral")
Text(
text = "Explore Packs",
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(6.dp))
@Suppress("HardCodedStringLiteral")
Text(
text = "Coming soon",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
)
}
}
// Import CSV Card
AppCard(
modifier = Modifier
.weight(1f)
.height(120.dp),
onClick = onImportCsvClick
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.DriveFolderUpload,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
}
Spacer(modifier = Modifier.height(12.dp))
Text(
text = stringResource(R.string.label_import_csv),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Bold
)
}
}
}
}

View File

@@ -14,8 +14,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -39,7 +37,6 @@ import androidx.navigation.NavController
import eu.gaudian.translator.R import eu.gaudian.translator.R
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppButton import eu.gaudian.translator.view.composable.AppButton
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppSlider import eu.gaudian.translator.view.composable.AppSlider
import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.viewmodel.VocabularyViewModel import eu.gaudian.translator.viewmodel.VocabularyViewModel
@@ -57,7 +54,7 @@ fun NoGrammarItemsScreen(
var showFetchGrammarDialog by remember { mutableStateOf(false) } var showFetchGrammarDialog by remember { mutableStateOf(false) }
@Suppress("UnusedVariable", "unused", "HardCodedStringLiteral") val onClose = { navController.popBackStack() } @Suppress("UnusedVariable") val onClose = { navController.popBackStack() }
if (itemsWithoutGrammar.isEmpty() && !isGenerating) { if (itemsWithoutGrammar.isEmpty() && !isGenerating) {
Column( Column(
@@ -66,12 +63,8 @@ fun NoGrammarItemsScreen(
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.title_items_without_grammar)) }, title = stringResource(R.string.title_items_without_grammar),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
) )
Box( Box(
modifier = Modifier modifier = Modifier
@@ -87,8 +80,8 @@ fun NoGrammarItemsScreen(
} }
} }
} else { } else {
// Use the generic VocabularyListScreen to display the items // Use the generic AllCardsListScreen to display the items
VocabularyListScreen( AllCardsListScreen(
itemsToShow = itemsWithoutGrammar, itemsToShow = itemsWithoutGrammar,
onNavigateToItem = { item -> onNavigateToItem = { item ->
@Suppress("HardCodedStringLiteral") @Suppress("HardCodedStringLiteral")

View File

@@ -1,98 +0,0 @@
@file:Suppress("AssignedValueIsNeverRead")
package eu.gaudian.translator.view.vocabulary
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import eu.gaudian.translator.R
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.LocalConnectionConfigured
import eu.gaudian.translator.view.composable.AppButton
import eu.gaudian.translator.view.dialogs.AddVocabularyDialog
import eu.gaudian.translator.view.dialogs.ImportVocabularyDialog
import eu.gaudian.translator.viewmodel.LanguageViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel
@Composable
fun NoVocabularyScreen(){
val activity = LocalContext.current.findActivity()
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
var showAddVocabularyDialog by remember { mutableStateOf(false) }
var showImportVocabularyDialog by remember { mutableStateOf(false) }
val connectionConfigured = LocalConnectionConfigured.current
if (showAddVocabularyDialog) {
AddVocabularyDialog(
onDismissRequest = { showAddVocabularyDialog = false }
)
}
if (showImportVocabularyDialog) {
ImportVocabularyDialog(
languageViewModel = languageViewModel,
vocabularyViewModel = vocabularyViewModel,
onDismiss = { showImportVocabularyDialog = false }
)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
modifier = Modifier.size(200.dp),
painter = painterResource(id = R.drawable.ic_empty),
contentDescription = stringResource(id = R.string.text_vocab_empty)
)
Spacer(modifier = Modifier.size(16.dp))
Text(
text = stringResource(id = R.string.text_vocab_empty),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onBackground,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.size(16.dp))
AppButton(modifier = Modifier.fillMaxWidth(), onClick = { showAddVocabularyDialog = true }) {
Text(stringResource(R.string.label_add_vocabulary))
}
if(connectionConfigured) {
AppButton(modifier = Modifier.fillMaxWidth(), onClick = { showImportVocabularyDialog = true }) {
Text(stringResource(R.string.label_create_vocabulary_with_ai))
}
}
}
}

View File

@@ -2,9 +2,6 @@ package eu.gaudian.translator.view.vocabulary
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -17,7 +14,6 @@ import androidx.navigation.NavHostController
import eu.gaudian.translator.R import eu.gaudian.translator.R
import eu.gaudian.translator.model.VocabularyStage import eu.gaudian.translator.model.VocabularyStage
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppScaffold import eu.gaudian.translator.view.composable.AppScaffold
import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.vocabulary.widgets.DetailedStageProgressBar import eu.gaudian.translator.view.vocabulary.widgets.DetailedStageProgressBar
@@ -40,15 +36,8 @@ fun StageDetailScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(text = stringResource(R.string.due_today_, stage.toString())) }, title = stringResource(R.string.due_today_, stage.toString()),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(
AppIcons.ArrowBack,
contentDescription =stringResource(R.string.cd_back)
)
}
}
) )
} }
) { paddingValues -> ) { paddingValues ->
@@ -60,7 +49,7 @@ fun StageDetailScreen(
onStageTapped = {}, onStageTapped = {},
) )
VocabularyListScreen( AllCardsListScreen(
categoryId = null, categoryId = null,
showDueTodayOnly = true, showDueTodayOnly = true,
stage = stage, stage = stage,

View File

@@ -1,467 +0,0 @@
package eu.gaudian.translator.view.vocabulary
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
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.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import eu.gaudian.translator.R
import eu.gaudian.translator.model.CardSet
import eu.gaudian.translator.model.Language
import eu.gaudian.translator.model.VocabularyItem
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppButton
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppOutlinedButton
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.composable.OptionItemSwitch
import eu.gaudian.translator.view.composable.SingleLanguageDropDown
import eu.gaudian.translator.viewmodel.LanguageViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel
@Composable
fun StartScreen(
cardSet: CardSet?,
onStartClicked: (List<VocabularyItem>) -> Unit,
onClose: () -> Unit,
shuffleCards: Boolean,
onShuffleCardsChanged: (Boolean) -> Unit,
shuffleLanguages: Boolean,
onShuffleLanguagesChanged: (Boolean) -> Unit,
trainingMode: Boolean,
onTrainingModeChanged: (Boolean) -> Unit,
dueTodayOnly: Boolean,
onDueTodayOnlyChanged: (Boolean) -> Unit,
selectedExerciseTypes: Set<VocabularyExerciseType>,
onExerciseTypeSelected: (VocabularyExerciseType) -> Unit,
hideTodayOnlySwitch: Boolean = false,
selectedOriginLanguage: Language?,
onOriginLanguageChanged: (Language?) -> Unit,
selectedTargetLanguage: Language?,
onTargetLanguageChanged: (Language?) -> Unit,
) {
val activity = LocalContext.current.findActivity()
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
val dueTodayItems by vocabularyViewModel.dueTodayItems.collectAsState(initial = emptyList())
val allItems = cardSet?.cards ?: emptyList()
var amount by remember(allItems) { mutableIntStateOf(allItems.size) }
val itemsToShow = if (dueTodayOnly) {
allItems.filter { card -> dueTodayItems.any { it.id == card.id } }
} else {
allItems
}
if (amount > itemsToShow.size) {
amount = itemsToShow.size
}
StartScreenContent(
vocabularyItemsCount = itemsToShow.size,
shuffleCards = shuffleCards,
onShuffleCardsChanged = onShuffleCardsChanged,
shuffleLanguages = shuffleLanguages,
onShuffleLanguagesChanged = onShuffleLanguagesChanged,
trainingMode = trainingMode,
onTrainingModeChanged = onTrainingModeChanged,
dueTodayOnly = dueTodayOnly,
onDueTodayOnlyChanged = onDueTodayOnlyChanged,
amount = amount,
onAmountChanged = {
@Suppress("AssignedValueIsNeverRead")
amount = it
},
onStartClicked = {
val finalItems = if (shuffleCards) {
itemsToShow.shuffled().take(amount)
} else {
itemsToShow.take(amount)
}
onStartClicked(finalItems)
},
onClose = onClose,
selectedExerciseTypes = selectedExerciseTypes,
onExerciseTypeSelected = onExerciseTypeSelected,
hideTodayOnlySwitch = hideTodayOnlySwitch,
selectedOriginLanguage = selectedOriginLanguage,
onOriginLanguageChanged = onOriginLanguageChanged,
selectedTargetLanguage = selectedTargetLanguage,
onTargetLanguageChanged = onTargetLanguageChanged,
allItems = allItems
)
}
@Composable
private fun StartScreenContent(
vocabularyItemsCount: Int,
shuffleCards: Boolean,
onShuffleCardsChanged: (Boolean) -> Unit,
shuffleLanguages: Boolean,
onShuffleLanguagesChanged: (Boolean) -> Unit,
trainingMode: Boolean,
onTrainingModeChanged: (Boolean) -> Unit,
dueTodayOnly: Boolean,
onDueTodayOnlyChanged: (Boolean) -> Unit,
amount: Int,
onAmountChanged: (Int) -> Unit,
onStartClicked: () -> Unit,
onClose: () -> Unit,
selectedExerciseTypes: Set<VocabularyExerciseType>,
onExerciseTypeSelected: (VocabularyExerciseType) -> Unit,
hideTodayOnlySwitch: Boolean = false,
selectedOriginLanguage: Language?,
onOriginLanguageChanged: (Language?) -> Unit,
selectedTargetLanguage: Language?,
onTargetLanguageChanged: (Language?) -> Unit,
allItems: List<VocabularyItem>,
) {
AppScaffold(
topBar = {
AppTopAppBar(
title = { Text(stringResource(R.string.prepare_exercise)) },
navigationIcon = {
IconButton(onClick = onClose) {
Icon(
AppIcons.Close,
contentDescription = stringResource(R.string.label_close)
)
}
}
)
},
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
Box(
modifier = Modifier.weight(1f)
) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(Modifier.height(16.dp))
if (vocabularyItemsCount > 0) {
Text(stringResource(R.string.number_of_cards, amount, vocabularyItemsCount))
AppSlider(
value = amount.toFloat(),
onValueChange = { onAmountChanged(it.toInt()) },
valueRange = 1f..vocabularyItemsCount.toFloat(),
steps = if (vocabularyItemsCount > 1) vocabularyItemsCount - 2 else 0
)
// Quick selection buttons
val quickSelectValues = listOf(10, 25, 50, 100)
val availableValues =
quickSelectValues.filter { it <= vocabularyItemsCount }
if (availableValues.isNotEmpty()) {
Spacer(Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(
8.dp,
Alignment.CenterHorizontally
)
) {
availableValues.forEach { value ->
AppOutlinedButton(
onClick = { onAmountChanged(value) },
modifier = Modifier.weight(1f),
enabled = value <= vocabularyItemsCount
) {
Text(value.toString())
}
}
}
}
} else {
Text(
stringResource(R.string.no_cards_found_for_the_selected_filters),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.error
)
}
HorizontalDivider(
modifier = Modifier.padding(vertical = 24.dp),
thickness = DividerDefaults.Thickness,
color = DividerDefaults.color
)
// Language Selection Section
Text(
stringResource(R.string.label_language_direction),
style = MaterialTheme.typography.titleLarge
)
Text(
stringResource(R.string.text_language_direction_explanation),
style = MaterialTheme.typography.bodyMedium
)
Spacer(Modifier.height(16.dp))
val activity = LocalContext.current.findActivity()
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
// Get available languages from the card set
val availableLanguages = remember(allItems) {
allItems.flatMap { listOfNotNull(it.languageFirstId, it.languageSecondId) }
.distinct()
.mapNotNull { languageId ->
languageViewModel.allLanguages.value.find { it.nameResId == languageId }
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
// Origin Language Dropdown
Column(modifier = Modifier.weight(1f)) {
Text(
stringResource(R.string.label_origin_language),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 8.dp)
)
SingleLanguageDropDown(
modifier = Modifier.fillMaxWidth(),
languageViewModel = languageViewModel,
selectedLanguage = selectedOriginLanguage,
onLanguageSelected = { language ->
onOriginLanguageChanged(language)
// Clear target language if it's the same as origin
if (selectedTargetLanguage?.nameResId == language.nameResId) {
onTargetLanguageChanged(null)
}
},
showNoneOption = true,
alternateLanguages = availableLanguages
)
}
// Target Language Dropdown
Column(modifier = Modifier.weight(1f)) {
Text(
stringResource(R.string.label_target_language),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 8.dp)
)
SingleLanguageDropDown(
modifier = Modifier.fillMaxWidth(),
languageViewModel = languageViewModel,
selectedLanguage = selectedTargetLanguage,
onLanguageSelected = { language ->
onTargetLanguageChanged(language)
// Clear origin language if it's the same as target
if (selectedOriginLanguage?.nameResId == language.nameResId) {
onOriginLanguageChanged(null)
}
},
alternateLanguages = availableLanguages,
showNoneOption = true,
)
}
}
Spacer(Modifier.height(16.dp))
HorizontalDivider(
modifier = Modifier.padding(vertical = 24.dp),
thickness = DividerDefaults.Thickness,
color = DividerDefaults.color
)
Text(
stringResource(R.string.label_choose_exercise_types),
style = MaterialTheme.typography.titleLarge
)
Spacer(Modifier.height(16.dp))
ExerciseTypeSelector(
selectedTypes = selectedExerciseTypes,
onTypeSelected = onExerciseTypeSelected
)
HorizontalDivider(
modifier = Modifier.padding(vertical = 24.dp),
thickness = DividerDefaults.Thickness,
color = DividerDefaults.color
)
Text(
stringResource(R.string.options),
style = MaterialTheme.typography.titleLarge
)
Spacer(Modifier.height(16.dp))
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
OptionItemSwitch(
title = stringResource(R.string.shuffle_cards),
description = stringResource(R.string.text_shuffle_card_order_description),
checked = shuffleCards,
onCheckedChange = onShuffleCardsChanged
)
OptionItemSwitch(
title = stringResource(R.string.text_shuffle_languages),
description = stringResource(R.string.text_shuffle_languages_description),
checked = shuffleLanguages,
onCheckedChange = onShuffleLanguagesChanged
)
OptionItemSwitch(
title = stringResource(R.string.label_training_mode),
description = stringResource(R.string.text_training_mode_description),
checked = trainingMode,
onCheckedChange = onTrainingModeChanged
)
if (!hideTodayOnlySwitch) {
OptionItemSwitch(
title = stringResource(R.string.text_due_today_only),
description = stringResource(R.string.text_due_today_only_description),
checked = dueTodayOnly,
onCheckedChange = onDueTodayOnlyChanged
)
}
}
Spacer(Modifier.height(16.dp))
}
}
AppButton(
onClick = onStartClicked,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.height(50.dp),
enabled = vocabularyItemsCount > 0 && amount > 0
) {
Text(stringResource(R.string.label_start_exercise_2d, amount))
}
}
}
}
@Composable
private fun ExerciseTypeSelector(
selectedTypes: Set<VocabularyExerciseType>,
onTypeSelected: (VocabularyExerciseType) -> Unit,
) {
// Using FlowRow for a more flexible layout that wraps to the next line if needed
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
ExerciseTypeCard(
icon = AppIcons.Guessing,
isSelected = VocabularyExerciseType.GUESSING in selectedTypes,
onClick = { onTypeSelected(VocabularyExerciseType.GUESSING) },
text = stringResource(R.string.label_guessing_exercise),
)
ExerciseTypeCard(
icon = AppIcons.SpellCheck,
isSelected = VocabularyExerciseType.SPELLING in selectedTypes,
onClick = { onTypeSelected(VocabularyExerciseType.SPELLING) },
text = stringResource(R.string.label_spelling_exercise),
)
ExerciseTypeCard(
icon = AppIcons.CheckList,
isSelected = VocabularyExerciseType.MULTIPLE_CHOICE in selectedTypes,
onClick = { onTypeSelected(VocabularyExerciseType.MULTIPLE_CHOICE) },
text = stringResource(R.string.label_multiple_choice_exercise),
)
ExerciseTypeCard(
icon = AppIcons.Extension,
isSelected = VocabularyExerciseType.WORD_JUMBLE in selectedTypes,
onClick = { onTypeSelected(VocabularyExerciseType.WORD_JUMBLE) },
text = stringResource(R.string.label_word_jumble_exercise),
)
}
}
@Composable
private fun ExerciseTypeCard(
text: String,
icon: ImageVector,
isSelected: Boolean,
onClick: () -> Unit,
) {
val borderColor by animateColorAsState(
targetValue = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline.copy(
alpha = 0.5f
),
label = "borderColorAnimation",
animationSpec = tween(300)
)
val containerColor by animateColorAsState(
targetValue = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant,
animationSpec = tween(300)
)
Card(
onClick = onClick,
modifier = Modifier.size(width = 120.dp, height = 100.dp), // Made the cards smaller
shape = RoundedCornerShape(12.dp),
border = BorderStroke(2.dp, borderColor),
colors = CardDefaults.cardColors(containerColor = containerColor),
elevation = CardDefaults.cardElevation(if (isSelected) 4.dp else 1.dp)
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(icon, contentDescription = null, modifier = Modifier.size(32.dp)) // Smaller icon
Spacer(Modifier.height(8.dp))
Text(
text = text,
style = MaterialTheme.typography.bodyLarge, // Smaller text
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center
)
}
}
}

View File

@@ -1,14 +1,23 @@
package eu.gaudian.translator.view.vocabulary package eu.gaudian.translator.view.vocabulary
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -39,10 +48,9 @@ import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppScaffold import eu.gaudian.translator.view.composable.AppScaffold
import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.dialogs.CategorySelectionDialog import eu.gaudian.translator.view.dialogs.CategorySelectionDialog
import eu.gaudian.translator.view.dialogs.ImportVocabularyDialog
import eu.gaudian.translator.view.dialogs.StageSelectionDialog import eu.gaudian.translator.view.dialogs.StageSelectionDialog
import eu.gaudian.translator.view.vocabulary.card.VocabularyCard import eu.gaudian.translator.view.vocabulary.card.VocabularyDisplayCard
import eu.gaudian.translator.viewmodel.LanguageViewModel import eu.gaudian.translator.view.vocabulary.card.VocabularyExerciseCard
import eu.gaudian.translator.viewmodel.VocabularyViewModel import eu.gaudian.translator.viewmodel.VocabularyViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
@@ -59,7 +67,6 @@ fun VocabularyCardHost(
) { ) {
val activity = LocalContext.current.findActivity() val activity = LocalContext.current.findActivity()
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity) val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val navigationItems by vocabularyViewModel.currentNavigationItems.collectAsState() val navigationItems by vocabularyViewModel.currentNavigationItems.collectAsState()
@@ -71,26 +78,18 @@ fun VocabularyCardHost(
vocabularyItem = vocabularyViewModel.getVocabularyItemById(itemId) vocabularyItem = vocabularyViewModel.getVocabularyItemById(itemId)
} }
var isEditing by remember { mutableStateOf(false) }
var onSaveEdit by remember { mutableStateOf<(() -> Unit)?>(null) }
var onCancelEdit by remember { mutableStateOf<(() -> Unit)?>(null) }
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
modifier = Modifier.height(56.dp), modifier = Modifier.height(56.dp),
title = { title = stringResource(R.string.item_details),
if (navigationItems.isNotEmpty()) { onNavigateBack = { navController.popBackStack() },
Text(stringResource(R.string.label_card_with_position, navigationPosition + 1, navigationItems.size))
} else {
Text(stringResource(R.string.item_details))
}
},
navigationIcon = {
IconButton(onClick = { onBackPressed?.invoke() }) {
Icon(
AppIcons.ArrowBack,
contentDescription = stringResource(R.string.cd_back)
)
}
},
actions = { actions = {
if (!isEditing) {
// Previous button // Previous button
if (navigationPosition > 0) { if (navigationPosition > 0) {
IconButton(onClick = { IconButton(onClick = {
@@ -130,6 +129,7 @@ fun VocabularyCardHost(
} }
} }
} }
}
) )
} }
) { paddingValues -> ) { paddingValues ->
@@ -146,8 +146,12 @@ fun VocabularyCardHost(
var showStatisticsDialog by remember { mutableStateOf(false) } var showStatisticsDialog by remember { mutableStateOf(false) }
var showCategoryDialog by remember { mutableStateOf(false) } var showCategoryDialog by remember { mutableStateOf(false) }
var showStageDialog by remember { mutableStateOf(false) } var showStageDialog by remember { mutableStateOf(false) }
var showImportDialog by remember { mutableStateOf(false) }
var showDeleteDialog by remember { mutableStateOf(false) } var showDeleteDialog by remember { mutableStateOf(false) }
LaunchedEffect(currentVocabularyItem.id) {
isEditing = false
onSaveEdit = null
onCancelEdit = null
}
val lifecycleOwner = LocalLifecycleOwner.current val lifecycleOwner = LocalLifecycleOwner.current
val stats by vocabularyViewModel val stats by vocabularyViewModel
@@ -199,9 +203,45 @@ fun VocabularyCardHost(
} }
// Main content // Main content
VocabularyCard( Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
if (!exerciseMode && isEditing) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
OutlinedButton(
onClick = { onCancelEdit?.invoke() },
shape = RoundedCornerShape(16.dp)
) {
Text(text = stringResource(R.string.label_cancel))
}
Spacer(modifier = Modifier.width(8.dp))
Button(
onClick = { onSaveEdit?.invoke() },
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
)
) {
Text(text = stringResource(R.string.label_save))
}
}
}
if (exerciseMode) {
VocabularyExerciseCard(
vocabularyItem = currentVocabularyItem,
switchOrder = switchOrder == true,
isFlipped = isFlipped,
navController = navController,
)
} else {
VocabularyDisplayCard(
vocabularyItem = currentVocabularyItem, vocabularyItem = currentVocabularyItem,
exerciseMode = exerciseMode,
switchOrder = switchOrder == true, switchOrder = switchOrder == true,
isFlipped = isFlipped, isFlipped = isFlipped,
onStatisticsClick = { showStatisticsDialog = true }, onStatisticsClick = { showStatisticsDialog = true },
@@ -209,8 +249,20 @@ fun VocabularyCardHost(
onMoveToStageClick = { showStageDialog = true }, onMoveToStageClick = { showStageDialog = true },
onDeleteClick = { showDeleteDialog = true }, onDeleteClick = { showDeleteDialog = true },
navController = navController, navController = navController,
isUserSpellingCorrect = false, onEditStateChange = { editing ->
isEditing = editing
if (!editing) {
onSaveEdit = null
onCancelEdit = null
}
},
onEditActionHandlersReady = { onSave, onCancel ->
onSaveEdit = onSave
onCancelEdit = onCancel
},
) )
}
}
// Dialogs are unaffected by the layout change // Dialogs are unaffected by the layout change
if (showQuitDialog) { if (showQuitDialog) {
@@ -259,16 +311,6 @@ fun VocabularyCardHost(
) )
} }
if (showImportDialog) {
ImportVocabularyDialog(
onDismiss = { showImportDialog = false },
languageViewModel = languageViewModel,
optionalDescription = stringResource(R.string.generate_related_vocabulary_items),
optionalSearchTerm = currentVocabularyItem.wordFirst,
vocabularyViewModel = vocabularyViewModel
)
}
LaunchedEffect(spellingMode) { LaunchedEffect(spellingMode) {
@Suppress("ControlFlowWithEmptyBody") @Suppress("ControlFlowWithEmptyBody")
if (spellingMode) { if (spellingMode) {

View File

@@ -48,7 +48,7 @@ import eu.gaudian.translator.ui.theme.semanticColors
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppButton import eu.gaudian.translator.view.composable.AppButton
import eu.gaudian.translator.view.composable.ComponentDefaults import eu.gaudian.translator.view.composable.ComponentDefaults
import eu.gaudian.translator.view.vocabulary.card.VocabularyCard import eu.gaudian.translator.view.vocabulary.card.VocabularyExerciseCard
import eu.gaudian.translator.viewmodel.LanguageViewModel import eu.gaudian.translator.viewmodel.LanguageViewModel
/** /**
@@ -141,11 +141,10 @@ fun GuessingExercise(
navController: NavController, navController: NavController,
) { ) {
VocabularyCard( VocabularyExerciseCard(
vocabularyItem = state.item, vocabularyItem = state.item,
isFlipped = state.isRevealed, isFlipped = state.isRevealed,
navController = navController, navController = navController,
exerciseMode = true,
switchOrder = state.isSwitched, switchOrder = state.isSwitched,
) )
} }
@@ -158,13 +157,12 @@ fun SpellingExercise(
navController: NavController, navController: NavController,
) { ) {
VocabularyCard( VocabularyExerciseCard(
vocabularyItem = state.item, vocabularyItem = state.item,
isFlipped = state.isRevealed, isFlipped = state.isRevealed,
userSpellingAnswer = state.userAnswer, userSpellingAnswer = state.userAnswer,
isUserSpellingCorrect = state.isCorrect, isUserSpellingCorrect = state.isCorrect,
navController = navController, navController = navController,
exerciseMode = true,
switchOrder = state.isSwitched, switchOrder = state.isSwitched,
) )
} }

View File

@@ -1,3 +1,5 @@
@file:Suppress("HardCodedStringLiteral")
package eu.gaudian.translator.view.vocabulary package eu.gaudian.translator.view.vocabulary
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
@@ -19,7 +21,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -28,10 +29,10 @@ import androidx.compose.ui.res.stringResource
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import eu.gaudian.translator.R import eu.gaudian.translator.R
import eu.gaudian.translator.model.Language import eu.gaudian.translator.utils.Log
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppAlertDialog import eu.gaudian.translator.view.composable.AppAlertDialog
import eu.gaudian.translator.viewmodel.ExerciseConfig import eu.gaudian.translator.view.composable.Screen
import eu.gaudian.translator.viewmodel.ScreenState import eu.gaudian.translator.viewmodel.ScreenState
import eu.gaudian.translator.viewmodel.VocabularyExerciseViewModel import eu.gaudian.translator.viewmodel.VocabularyExerciseViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel import eu.gaudian.translator.viewmodel.VocabularyViewModel
@@ -57,14 +58,7 @@ fun VocabularyExerciseHostScreen(
val cardSet by vocabularyViewModel.cardSet.collectAsState() val cardSet by vocabularyViewModel.cardSet.collectAsState()
val screenState by exerciseViewModel.screenState.collectAsState() val screenState by exerciseViewModel.screenState.collectAsState()
val pendingConfig by exerciseViewModel.pendingExerciseConfig.collectAsState()
var shuffleCards by rememberSaveable { mutableStateOf(false) }
var shuffleLanguages by remember { mutableStateOf(false) }
var trainingMode by remember { mutableStateOf(false) }
var dueTodayOnly by remember { mutableStateOf(false) }
var selectedExerciseTypes by remember { mutableStateOf(setOf(VocabularyExerciseType.GUESSING)) }
var selectedOriginLanguage by remember { mutableStateOf<Language?>(null) }
var selectedTargetLanguage by remember { mutableStateOf<Language?>(null) }
var finalScore by remember { mutableIntStateOf(0) } var finalScore by remember { mutableIntStateOf(0) }
var finalWrongAnswers by remember { mutableIntStateOf(0) } var finalWrongAnswers by remember { mutableIntStateOf(0) }
@@ -76,89 +70,59 @@ fun VocabularyExerciseHostScreen(
false false
} }
LaunchedEffect(Unit) { LaunchedEffect(categoryIdsAsJson, stageNamesAsJson, languageIdsAsJson, dailyOnly) {
// Reset exercise state when starting fresh Log.d("ExerciseHost", "LaunchedEffect filters: categories=$categoryIdsAsJson, stages=$stageNamesAsJson, languages=$languageIdsAsJson, dailyOnly=$dailyOnly")
// Only reset and prepare if the host is opened via explicit filters.
if (!categoryIdsAsJson.isNullOrBlank() || !stageNamesAsJson.isNullOrBlank() || !languageIdsAsJson.isNullOrBlank() || dailyOnly) {
Log.d("ExerciseHost", "Preparing exercise from filters")
exerciseViewModel.resetExercise() exerciseViewModel.resetExercise()
vocabularyViewModel.prepareExercise( vocabularyViewModel.prepareExercise(
categoryIdsAsJson, categoryIdsAsJson,
stageNamesAsJson, stageNamesAsJson,
languageIdsAsJson, languageIdsAsJson,
dailyOnly = dailyOnly, dailyOnly = dailyOnly,
) )
} else {
Log.d("ExerciseHost", "No filters provided; skipping prepareExercise")
}
} }
if (cardSet == null && screenState != ScreenState.START) { LaunchedEffect(cardSet, screenState, pendingConfig) {
Log.d("ExerciseHost", "State update: screenState=$screenState, cardSet=${cardSet?.cards?.size ?: 0}, pendingCount=${pendingConfig.exerciseItemCount}")
if (cardSet != null && screenState == ScreenState.START) {
val items = cardSet?.cards.orEmpty()
if (items.isNotEmpty()) {
val selectedCount = pendingConfig.exerciseItemCount
.takeIf { it > 0 }
?: items.size
val finalItems = if (pendingConfig.shuffleCards) {
items.shuffled().take(selectedCount)
} else {
items.take(selectedCount)
}
Log.d("ExerciseHost", "Auto-starting exercise with ${finalItems.size} items")
exerciseViewModel.startExerciseWithConfig(
finalItems,
pendingConfig.copy(
exerciseItemCount = finalItems.size,
originalExerciseItems = finalItems
)
)
} else {
Log.d("ExerciseHost", "CardSet present but empty")
}
}
}
when (screenState) {
ScreenState.START -> {
Log.d("ExerciseHost", "Rendering START screen")
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator() CircularProgressIndicator()
} }
} else {
when (screenState) {
ScreenState.START -> {
StartScreen(
cardSet = cardSet,
onStartClicked = { finalItems ->
exerciseViewModel.startExerciseWithConfig(
finalItems,
ExerciseConfig(
shuffleCards = shuffleCards,
shuffleLanguages = shuffleLanguages,
trainingMode = trainingMode,
dueTodayOnly = dueTodayOnly,
selectedExerciseTypes = selectedExerciseTypes,
exerciseItemCount = finalItems.size,
originalExerciseItems = finalItems,
originLanguageId = selectedOriginLanguage?.nameResId,
targetLanguageId = selectedTargetLanguage?.nameResId
)
)
},
onClose = onClose,
shuffleCards = shuffleCards,
onShuffleCardsChanged = {
@Suppress("AssignedValueIsNeverRead")
shuffleCards = it
},
shuffleLanguages = shuffleLanguages,
onShuffleLanguagesChanged = {
@Suppress("AssignedValueIsNeverRead")
shuffleLanguages = it
},
trainingMode = trainingMode,
onTrainingModeChanged = {
@Suppress("AssignedValueIsNeverRead")
trainingMode = it
exerciseViewModel.onTrainingModeChanged(it)
},
hideTodayOnlySwitch = dailyOnly,
dueTodayOnly = dueTodayOnly,
onDueTodayOnlyChanged = {
@Suppress("AssignedValueIsNeverRead")
dueTodayOnly = it
},
selectedExerciseTypes = selectedExerciseTypes,
onExerciseTypeSelected = { type ->
val currentTypes = selectedExerciseTypes.toMutableSet()
if (type in currentTypes) {
if (currentTypes.size > 1) currentTypes.remove(type)
} else {
currentTypes.add(type)
}
selectedExerciseTypes = currentTypes
},
selectedOriginLanguage = selectedOriginLanguage,
onOriginLanguageChanged = {
@Suppress("AssignedValueIsNeverRead")
selectedOriginLanguage = it
},
selectedTargetLanguage = selectedTargetLanguage,
onTargetLanguageChanged = {
@Suppress("AssignedValueIsNeverRead")
selectedTargetLanguage = it
}
)
} }
ScreenState.EXERCISE -> { ScreenState.EXERCISE -> {
Log.d("ExerciseHost", "Rendering EXERCISE screen")
ExerciseScreen( ExerciseScreen(
viewModel = exerciseViewModel, viewModel = exerciseViewModel,
onClose = onClose, onClose = onClose,
@@ -173,6 +137,7 @@ fun VocabularyExerciseHostScreen(
) )
} }
ScreenState.RESULT -> { ScreenState.RESULT -> {
Log.d("ExerciseHost", "Rendering RESULT screen")
val totalItems by exerciseViewModel.totalItems.collectAsState() val totalItems by exerciseViewModel.totalItems.collectAsState()
val originalItems by exerciseViewModel.originalItems.collectAsState() val originalItems by exerciseViewModel.originalItems.collectAsState()
ResultScreen( ResultScreen(
@@ -186,10 +151,14 @@ fun VocabularyExerciseHostScreen(
onRetryWrong = { _ -> onRetryWrong = { _ ->
exerciseViewModel.retryWrongAnswers(originalItems) exerciseViewModel.retryWrongAnswers(originalItems)
}, },
onClose = onClose onClose = {
) navController.navigate(Screen.Home.route) {
popUpTo(Screen.Home.route) { inclusive = true }
launchSingleTop = true
} }
} }
)
}
} }
} }

View File

@@ -22,8 +22,6 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
@@ -53,6 +51,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import eu.gaudian.translator.R import eu.gaudian.translator.R
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppCard
import eu.gaudian.translator.view.composable.AppIcons import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppScaffold import eu.gaudian.translator.view.composable.AppScaffold
import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.AppTopAppBar
@@ -68,6 +67,7 @@ import kotlinx.datetime.plus
import kotlinx.datetime.todayIn import kotlinx.datetime.todayIn
import java.time.YearMonth import java.time.YearMonth
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.Locale.getDefault
import kotlin.math.log2 import kotlin.math.log2
import kotlin.time.Clock import kotlin.time.Clock
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
@@ -95,7 +95,7 @@ fun VocabularyHeatmapScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.label_vocabulary_activity)) }, title = stringResource(R.string.label_vocabulary_activity),
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },
) )
}, },
@@ -263,7 +263,8 @@ private fun MonthHeader(
Icon(AppIcons.ArrowLeft, contentDescription = stringResource(R.string.previous_month)) Icon(AppIcons.ArrowLeft, contentDescription = stringResource(R.string.previous_month))
} }
Text( Text(
text = month.format(formatter), text = month.format(formatter)
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(getDefault()) else it.toString() },
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 16.dp) modifier = Modifier.padding(horizontal = 16.dp)
@@ -283,7 +284,7 @@ private fun MonthGrid(
) { ) {
Column(modifier = modifier) { Column(modifier = modifier) {
Row(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.fillMaxWidth()) {
val locale = java.util.Locale.getDefault() val locale = getDefault()
// Generate localized short weekday labels for Monday to Sunday. // Generate localized short weekday labels for Monday to Sunday.
val dayFormatter = remember(locale) { val dayFormatter = remember(locale) {
DateTimeFormatter.ofPattern("EEEEE", locale) DateTimeFormatter.ofPattern("EEEEE", locale)
@@ -385,7 +386,7 @@ private fun Legend(modifier: Modifier = Modifier) {
Row( Row(
modifier = modifier, modifier = modifier,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End horizontalArrangement = Arrangement.Center
) { ) {
Text( Text(
text = stringResource(R.string.less), text = stringResource(R.string.less),
@@ -467,12 +468,11 @@ fun StatsOverview(
@Composable @Composable
private fun StatCard(title: String, value: String, modifier: Modifier = Modifier) { private fun StatCard(title: String, value: String, modifier: Modifier = Modifier) {
Card( AppCard(
modifier = modifier, modifier = modifier.padding(0.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) { ) {
Column( Column(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {

View File

@@ -5,19 +5,13 @@ package eu.gaudian.translator.view.vocabulary
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@@ -25,14 +19,10 @@ import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FabPosition import androidx.compose.material3.FabPosition
@@ -43,7 +33,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
@@ -59,20 +48,15 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import eu.gaudian.translator.R import eu.gaudian.translator.R
import eu.gaudian.translator.model.Language import eu.gaudian.translator.model.Language
import eu.gaudian.translator.model.VocabularyItem import eu.gaudian.translator.model.VocabularyItem
@@ -84,10 +68,10 @@ import eu.gaudian.translator.view.composable.AppScaffold
import eu.gaudian.translator.view.composable.AppSwitch import eu.gaudian.translator.view.composable.AppSwitch
import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.composable.MultipleLanguageDropdown import eu.gaudian.translator.view.composable.MultipleLanguageDropdown
import eu.gaudian.translator.view.composable.insertBreakOpportunities
import eu.gaudian.translator.view.dialogs.CategoryDropdown import eu.gaudian.translator.view.dialogs.CategoryDropdown
import eu.gaudian.translator.view.dialogs.CategorySelectionDialog import eu.gaudian.translator.view.dialogs.CategorySelectionDialog
import eu.gaudian.translator.view.dialogs.StageSelectionDialog import eu.gaudian.translator.view.dialogs.StageSelectionDialog
import eu.gaudian.translator.view.library.AllCardsView
import eu.gaudian.translator.viewmodel.CategoryViewModel import eu.gaudian.translator.viewmodel.CategoryViewModel
import eu.gaudian.translator.viewmodel.LanguageConfigViewModel import eu.gaudian.translator.viewmodel.LanguageConfigViewModel
import eu.gaudian.translator.viewmodel.LanguageViewModel import eu.gaudian.translator.viewmodel.LanguageViewModel
@@ -110,7 +94,7 @@ private data class VocabularyFilterState(
) : Parcelable ) : Parcelable
@Composable @Composable
fun VocabularyListScreen( fun AllCardsListScreen(
categoryId: Int? = null, categoryId: Int? = null,
showDueTodayOnly: Boolean? = null, showDueTodayOnly: Boolean? = null,
stage: VocabularyStage? = null, stage: VocabularyStage? = null,
@@ -245,10 +229,6 @@ fun VocabularyListScreen(
) )
"search" -> SearchTopAppBar( "search" -> SearchTopAppBar(
searchQuery = filterState.searchQuery,
onQueryChanged = { newQuery ->
filterState = filterState.copy(searchQuery = newQuery)
},
onCloseSearch = { onCloseSearch = {
isSearchActive = false isSearchActive = false
filterState = filterState.copy(searchQuery = "") filterState = filterState.copy(searchQuery = "")
@@ -295,50 +275,16 @@ fun VocabularyListScreen(
floatingActionButtonPosition = FabPosition.Center floatingActionButtonPosition = FabPosition.Center
) { paddingValues -> ) { paddingValues ->
Column(modifier = Modifier.padding(paddingValues).padding(bottom = 0.dp)) { Column(modifier = Modifier.padding(paddingValues).padding(bottom = 0.dp)) {
if (vocabularyItems.isEmpty()) { AllCardsView(
Column( vocabularyItems = vocabularyItems,
modifier = Modifier allLanguages = allLanguages,
.fillMaxSize() selection = selection,
.padding(16.dp), listState = lazyListState,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
modifier = Modifier.size(200.dp),
painter = painterResource(id = R.drawable.ic_nothing_found),
contentDescription = stringResource(id = R.string.no_vocabulary_items_found_perhaps_try_changing_the_filters)
)
Spacer(modifier = Modifier.size(16.dp))
Box(modifier = Modifier
.fillMaxSize()
.padding(8.dp), contentAlignment = Alignment.Center) {
Text(
text = stringResource(R.string.no_vocabulary_items_found_perhaps_try_changing_the_filters),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
}
}
} else {
LazyColumn(
state = lazyListState,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(horizontal = 8.dp), .padding(horizontal = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp), onItemClick = { item ->
contentPadding = PaddingValues(vertical = 0.dp)
) {
items(
items = vocabularyItems,
key = { it.id }
) { item ->
val isSelected = selection.contains(item.id.toLong()) val isSelected = selection.contains(item.id.toLong())
VocabularyListItem(
item = item,
allLanguages = allLanguages,
isSelected = isSelected,
onItemClick = {
if (isInSelectionMode) { if (isInSelectionMode) {
selection = if (isSelected) { selection = if (isSelected) {
selection - item.id.toLong() selection - item.id.toLong()
@@ -354,20 +300,16 @@ fun VocabularyListScreen(
} }
} }
}, },
onItemLongClick = { onItemLongClick = { item ->
if (!isInSelectionMode) { if (!isInSelectionMode) {
selection = setOf(item.id.toLong()) selection = setOf(item.id.toLong())
} }
}, },
onDeleteClick = { onDeleteClick = { item ->
vocabularyViewModel.deleteData(VocabularyViewModel.DeleteType.VOCABULARY_ITEM, item = item) vocabularyViewModel.deleteData(VocabularyViewModel.DeleteType.VOCABULARY_ITEM, item = item)
}, }
modifier = Modifier.animateItem()
) )
} }
}
}
}
if (showFilterSheet) { if (showFilterSheet) {
@@ -382,8 +324,7 @@ fun VocabularyListScreen(
languageViewModel = languageViewModel, languageViewModel = languageViewModel,
languagesPresent = allLanguages.filter { it.nameResId in languagesPresent }, languagesPresent = allLanguages.filter { it.nameResId in languagesPresent },
hideCategory = categoryId != null && categoryId != 0, hideCategory = categoryId != null && categoryId != 0,
hideStage = stage != null, hideStage = stage != null
categoryViewModel = categoryViewModel
) )
} }
@@ -417,21 +358,34 @@ fun VocabularyListScreen(
} }
} }
@ThemePreviews @Deprecated("Use AllCardsListScreen which renders AllCardsView")
@Composable @Composable
fun VocabularyListScreenPreview() { fun VocabularyListScreen(
val navController = rememberNavController() categoryId: Int? = null,
VocabularyListScreen( showDueTodayOnly: Boolean? = null,
categoryId = 1, stage: VocabularyStage? = null,
showDueTodayOnly = false, onNavigateToItem: (VocabularyItem) -> Unit?,
stage = VocabularyStage.NEW, onNavigateBack: (() -> Unit)? = null,
onNavigateToItem = {}, navController: NavHostController? = null,
onNavigateBack = {}, itemsToShow: List<VocabularyItem> = emptyList(),
navController = navController isRemoveFromCategoryEnabled: Boolean = false,
showTopBar: Boolean = true,
enableNavigationButtons: Boolean = false
) {
AllCardsListScreen(
categoryId = categoryId,
showDueTodayOnly = showDueTodayOnly,
stage = stage,
onNavigateToItem = onNavigateToItem,
onNavigateBack = onNavigateBack,
navController = navController,
itemsToShow = itemsToShow,
isRemoveFromCategoryEnabled = isRemoveFromCategoryEnabled,
showTopBar = showTopBar,
enableNavigationButtons = enableNavigationButtons
) )
} }
@Composable @Composable
private fun DefaultTopAppBar( private fun DefaultTopAppBar(
title: String, title: String,
@@ -446,25 +400,8 @@ private fun DefaultTopAppBar(
var showSortMenu by remember { mutableStateOf(false) } var showSortMenu by remember { mutableStateOf(false) }
AppTopAppBar( AppTopAppBar(
modifier = Modifier.height(56.dp), modifier = Modifier.height(56.dp),
title = { title = title,
onNavigateBack = onNavigateBack,
Box(
modifier = Modifier.fillMaxHeight(),
contentAlignment = Alignment.Center
) {
Text(title)
}
},
navigationIcon = {
onNavigateBack?.let {
IconButton(onClick = it) {
Icon(
AppIcons.ArrowBack,
contentDescription = "stringResource(R.string.navigate_back)"
)
}
}
},
actions = { actions = {
IconButton(onClick = onSearchClick) { IconButton(onClick = onSearchClick) {
Icon( Icon(
@@ -522,8 +459,6 @@ private fun DefaultTopAppBar(
@Composable @Composable
private fun SearchTopAppBar( private fun SearchTopAppBar(
searchQuery: String,
onQueryChanged: (String) -> Unit,
onCloseSearch: () -> Unit onCloseSearch: () -> Unit
) { ) {
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
@@ -534,37 +469,7 @@ private fun SearchTopAppBar(
AppTopAppBar( AppTopAppBar(
modifier = Modifier.height(56.dp), modifier = Modifier.height(56.dp),
title = { title = "TODO",
Box(
modifier = Modifier.fillMaxHeight(),
contentAlignment = Alignment.Center
) {
BasicTextField(
value = searchQuery,
onValueChange = onQueryChanged,
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
textStyle = MaterialTheme.typography.bodyLarge.copy(
color = MaterialTheme.colorScheme.onSurface
),
singleLine = true,
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
decorationBox = { innerTextField ->
Box(contentAlignment = Alignment.CenterStart) {
if (searchQuery.isEmpty()) {
Text(
text = stringResource(R.string.search_vocabulary),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
innerTextField()
}
}
)
}
},
navigationIcon = { navigationIcon = {
IconButton(onClick = onCloseSearch) { IconButton(onClick = onCloseSearch) {
Icon( Icon(
@@ -582,8 +487,6 @@ private fun SearchTopAppBar(
@Composable @Composable
fun SearchTopAppBarPreview() { fun SearchTopAppBarPreview() {
SearchTopAppBar( SearchTopAppBar(
searchQuery = stringResource(R.string.search_query),
onQueryChanged = {},
onCloseSearch = {} onCloseSearch = {}
) )
} }
@@ -605,14 +508,7 @@ private fun ContextualTopAppBar(
AppTopAppBar( AppTopAppBar(
modifier = modifier.height(56.dp), modifier = modifier.height(56.dp),
title = { title = stringResource(R.string.d_selected, selectionCount),
Box(
modifier = Modifier.fillMaxHeight(),
contentAlignment = Alignment.Center
) {
Text(stringResource(R.string.d_selected, selectionCount))
}
},
navigationIcon = { navigationIcon = {
IconButton(onClick = onCloseClick) { IconButton(onClick = onCloseClick) {
Icon(imageVector = AppIcons.Close, contentDescription = stringResource(R.string.label_close_selection_mode)) Icon(imageVector = AppIcons.Close, contentDescription = stringResource(R.string.label_close_selection_mode))
@@ -693,112 +589,6 @@ fun ContextualTopAppBarPreview() {
) )
} }
@Composable
private fun VocabularyListItem(
item: VocabularyItem,
allLanguages: List<Language>,
isSelected: Boolean,
onItemClick: () -> Unit,
onItemLongClick: () -> Unit,
onDeleteClick: () -> Unit,
modifier: Modifier = Modifier
) {
val languageMap = remember(allLanguages) { allLanguages.associateBy { it.nameResId } }
val langFirst = item.languageFirstId?.let { languageMap[it]?.name } ?: ""
val langSecond = item.languageSecondId?.let { languageMap[it]?.name } ?: ""
OutlinedCard(
modifier = modifier
.fillMaxWidth()
.clip(CardDefaults.shape)
.combinedClickable(
onClick = onItemClick,
onLongClick = onItemLongClick
)
.animateContentSize(),
elevation = CardDefaults.cardElevation(defaultElevation = if (isSelected) 4.dp else 0.dp),
colors = CardDefaults.cardColors(
containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer
else MaterialTheme.colorScheme.surface
),
border = BorderStroke(
width = if (isSelected) 1.5.dp else 1.dp,
color = if (isSelected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)
)
) {
Row(
modifier = Modifier.padding(horizontal = 4.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
LanguageRow(word = item.wordFirst, language = langFirst)
HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f))
LanguageRow(word = item.wordSecond, language = langSecond)
}
Box(
modifier = Modifier.padding(4.dp),
contentAlignment = Alignment.Center
) {
Crossfade(targetState = isSelected, label = "action-icon-fade") { selected ->
if (selected) {
Icon(
imageVector = AppIcons.Check,
contentDescription = "Selected",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
} else {
IconButton(onClick = onDeleteClick) {
Icon(
imageVector = AppIcons.Delete,
contentDescription = stringResource(id = R.string.label_delete),
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
)
}
}
}
}
}
}
}
@Composable
private fun LanguageRow(word: String, language: String) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 2.dp)) {
Text(
text = insertBreakOpportunities(word),
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(0.7f)
)
Spacer(Modifier.width(8.dp))
Text(
text = language,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
modifier = Modifier.weight(0.3f)
)
}
}
@ThemePreviews
@Composable
fun LanguageRowPreview() {
LanguageRow(
word = "Hello",
language = "English"
)
}
@Composable @Composable
private fun FilterSortBottomSheet( private fun FilterSortBottomSheet(
@@ -808,8 +598,7 @@ private fun FilterSortBottomSheet(
onDismiss: () -> Unit, onDismiss: () -> Unit,
onApplyFilters: (VocabularyFilterState) -> Unit, onApplyFilters: (VocabularyFilterState) -> Unit,
hideCategory: Boolean = false, hideCategory: Boolean = false,
hideStage: Boolean = false, hideStage: Boolean = false
categoryViewModel: CategoryViewModel
) { ) {
var selectedStage by rememberSaveable { mutableStateOf(currentFilterState.selectedStage) } var selectedStage by rememberSaveable { mutableStateOf(currentFilterState.selectedStage) }
var dueTodayOnly by rememberSaveable { mutableStateOf(currentFilterState.dueTodayOnly) } var dueTodayOnly by rememberSaveable { mutableStateOf(currentFilterState.dueTodayOnly) }
@@ -954,20 +743,3 @@ private fun FilterSortBottomSheet(
} }
} }
@ThemePreviews
@Composable
fun FilterSortBottomSheetPreview() {
val activity = LocalContext.current.findActivity()
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
FilterSortBottomSheet(
currentFilterState = VocabularyFilterState(),
languageViewModel = languageViewModel,
languagesPresent = emptyList(),
onDismiss = {},
onApplyFilters = {},
hideCategory = false,
hideStage = false,
categoryViewModel = categoryViewModel
)
}

View File

@@ -155,7 +155,7 @@ fun VocabularySortingScreen(
var showFilterMenu by remember { mutableStateOf(false) } var showFilterMenu by remember { mutableStateOf(false) }
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.sort_new_vocabulary)) }, title = stringResource(R.string.sort_new_vocabulary),
actions = { actions = {
Box { Box {
IconButton(onClick = { showFilterMenu = true }) { IconButton(onClick = { showFilterMenu = true }) {
@@ -231,11 +231,7 @@ fun VocabularySortingScreen(
} }
} }
}, },
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
hintContent = HintDefinition.SORTING.hint() hintContent = HintDefinition.SORTING.hint()
) )
}, },
@@ -299,7 +295,6 @@ fun VocabularySortingItem(
val activity = LocalContext.current.findActivity() val activity = LocalContext.current.findActivity()
val languageConfigViewModel: LanguageConfigViewModel = hiltViewModel(viewModelStoreOwner = activity) val languageConfigViewModel: LanguageConfigViewModel = hiltViewModel(viewModelStoreOwner = activity)
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity) val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
var wordFirst by remember { mutableStateOf(item.wordFirst) } var wordFirst by remember { mutableStateOf(item.wordFirst) }
var wordSecond by remember { mutableStateOf(item.wordSecond) } var wordSecond by remember { mutableStateOf(item.wordSecond) }
var selectedCategories by remember { mutableStateOf<List<Int>>(emptyList()) } var selectedCategories by remember { mutableStateOf<List<Int>>(emptyList()) }
@@ -314,7 +309,6 @@ fun VocabularySortingItem(
var articlesLangSecond by remember { mutableStateOf(emptySet<String>()) } var articlesLangSecond by remember { mutableStateOf(emptySet<String>()) }
var showDuplicateDialog by remember { mutableStateOf(false) } var showDuplicateDialog by remember { mutableStateOf(false) }
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
// NEW: Calculate if the item is valid for the "Done" button in faulty mode // NEW: Calculate if the item is valid for the "Done" button in faulty mode
val isItemNowValid by remember(wordFirst, wordSecond, langFirst, langSecond) { val isItemNowValid by remember(wordFirst, wordSecond, langFirst, langSecond) {

View File

@@ -66,8 +66,6 @@ internal fun DraggableActionPanel(
onDismiss: () -> Unit, onDismiss: () -> Unit,
isEditing: Boolean, isEditing: Boolean,
onEditClick: () -> Unit, onEditClick: () -> Unit,
onSaveClick: () -> Unit,
onCancelClick: () -> Unit,
onStatisticsClick: () -> Unit, onStatisticsClick: () -> Unit,
onMoveToCategoryClick: () -> Unit, onMoveToCategoryClick: () -> Unit,
onMoveToStageClick: () -> Unit, onMoveToStageClick: () -> Unit,
@@ -175,13 +173,8 @@ internal fun DraggableActionPanel(
} }
} }
if (isEditing) {
ActionItem(icon = AppIcons.Check, label = stringResource(R.string.label_save), isExtended = isExtended, onClick = actionClickHandler(onSaveClick))
ActionItem(icon = AppIcons.Close, stringResource(R.string.label_cancel), isExtended = isExtended, onClick = actionClickHandler(onCancelClick))
} else {
ActionItem(icon = AppIcons.Edit, label = stringResource(R.string.edit), isExtended = isExtended, onClick = actionClickHandler(onEditClick))
}
if (!isEditing) { if (!isEditing) {
ActionItem(icon = AppIcons.Edit, label = stringResource(R.string.edit), isExtended = isExtended, onClick = actionClickHandler(onEditClick))
if (showAnalyzeGrammarButton) { if (showAnalyzeGrammarButton) {
ActionItem( ActionItem(
@@ -252,8 +245,6 @@ fun DraggableActionPanelPreview() {
onDismiss = {}, onDismiss = {},
isEditing = false, isEditing = false,
onEditClick = {}, onEditClick = {},
onSaveClick = {},
onCancelClick = {},
onStatisticsClick = {}, onStatisticsClick = {},
onMoveToCategoryClick = {}, onMoveToCategoryClick = {},

View File

@@ -87,10 +87,58 @@ import kotlinx.coroutines.launch
@Composable @Composable
fun VocabularyCard( fun VocabularyDisplayCard(
vocabularyItem: VocabularyItem, vocabularyItem: VocabularyItem,
navController: NavController, navController: NavController,
exerciseMode: Boolean, isFlipped: Boolean,
switchOrder: Boolean,
onStatisticsClick: () -> Unit = {},
onMoveToCategoryClick: () -> Unit = {},
onMoveToStageClick: () -> Unit = {},
onDeleteClick: () -> Unit = {},
onEditStateChange: ((Boolean) -> Unit)? = null,
onEditActionHandlersReady: ((onSave: () -> Unit, onCancel: () -> Unit) -> Unit)? = null,
) {
VocabularyCardContent(
vocabularyItem = vocabularyItem,
navController = navController,
isExerciseMode = false,
isFlipped = isFlipped,
switchOrder = switchOrder,
onStatisticsClick = onStatisticsClick,
onMoveToCategoryClick = onMoveToCategoryClick,
onMoveToStageClick = onMoveToStageClick,
onDeleteClick = onDeleteClick,
onEditStateChange = onEditStateChange,
onEditActionHandlersReady = onEditActionHandlersReady,
)
}
@Composable
fun VocabularyExerciseCard(
vocabularyItem: VocabularyItem,
navController: NavController,
isFlipped: Boolean,
switchOrder: Boolean,
userSpellingAnswer: String? = null,
isUserSpellingCorrect: Boolean? = null,
) {
VocabularyCardContent(
vocabularyItem = vocabularyItem,
navController = navController,
isExerciseMode = true,
isFlipped = isFlipped,
switchOrder = switchOrder,
userSpellingAnswer = userSpellingAnswer,
isUserSpellingCorrect = isUserSpellingCorrect,
)
}
@Composable
private fun VocabularyCardContent(
vocabularyItem: VocabularyItem,
navController: NavController,
isExerciseMode: Boolean,
isFlipped: Boolean, isFlipped: Boolean,
switchOrder: Boolean, switchOrder: Boolean,
onStatisticsClick: () -> Unit = {}, onStatisticsClick: () -> Unit = {},
@@ -99,6 +147,8 @@ fun VocabularyCard(
onDeleteClick: () -> Unit = {}, onDeleteClick: () -> Unit = {},
userSpellingAnswer: String? = null, userSpellingAnswer: String? = null,
isUserSpellingCorrect: Boolean? = null, isUserSpellingCorrect: Boolean? = null,
onEditStateChange: ((Boolean) -> Unit)? = null,
onEditActionHandlersReady: ((onSave: () -> Unit, onCancel: () -> Unit) -> Unit)? = null,
) { ) {
val activity = LocalContext.current.findActivity() val activity = LocalContext.current.findActivity()
@@ -201,6 +251,7 @@ fun VocabularyCard(
) )
vocabularyViewModel.editVocabularyItem(updatedItem) vocabularyViewModel.editVocabularyItem(updatedItem)
isEditing = false isEditing = false
onEditStateChange?.invoke(false)
} }
} }
} }
@@ -213,6 +264,7 @@ fun VocabularyCard(
editedLangSecondId = item.languageSecondId editedLangSecondId = item.languageSecondId
editedFeatures = item.features?.let { jsonParser.decodeFromString<VocabularyFeatures>(it) } ?: VocabularyFeatures() editedFeatures = item.features?.let { jsonParser.decodeFromString<VocabularyFeatures>(it) } ?: VocabularyFeatures()
isEditing = false isEditing = false
onEditStateChange?.invoke(false)
} }
} }
@@ -286,13 +338,13 @@ fun VocabularyCard(
onWordChange = { if (!switchOrder) editedWordFirst = it else editedWordSecond = it }, onWordChange = { if (!switchOrder) editedWordFirst = it else editedWordSecond = it },
language = if (!switchOrder) languageFirst else languageSecond, language = if (!switchOrder) languageFirst else languageSecond,
onLanguageIdChange = { if (!switchOrder) editedLangFirstId = it else editedLangSecondId = it }, onLanguageIdChange = { if (!switchOrder) editedLangFirstId = it else editedLangSecondId = it },
isRevealed = isFrontFace || exerciseMode, isRevealed = isFrontFace || isExerciseMode,
userSpellingAnswer = userSpellingAnswer, userSpellingAnswer = userSpellingAnswer,
isUserSpellingCorrect = isUserSpellingCorrect, isUserSpellingCorrect = isUserSpellingCorrect,
correctWord = if (switchOrder) item.wordFirst else item.wordSecond, correctWord = if (switchOrder) item.wordFirst else item.wordSecond,
wordDetails = if (!switchOrder) editedFeatures.first else editedFeatures.second, wordDetails = if (!switchOrder) editedFeatures.first else editedFeatures.second,
onEditGrammarClick = { showGrammarDialogFor = "first" }, onEditGrammarClick = { showGrammarDialogFor = "first" },
isExerciseMode = exerciseMode, isExerciseMode = isExerciseMode,
vocabularyItem = item, vocabularyItem = item,
onMoreClick = { onMoreClick = {
@Suppress("HardCodedStringLiteral") @Suppress("HardCodedStringLiteral")
@@ -317,7 +369,7 @@ fun VocabularyCard(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
) )
if (!exerciseMode && !isFlipped) { if (!isExerciseMode && !isEditing && !isFlipped) {
IconButton(onClick = { showActionPanel = true }) { IconButton(onClick = { showActionPanel = true }) {
Icon( Icon(
imageVector = AppIcons.MoreVert, imageVector = AppIcons.MoreVert,
@@ -339,7 +391,7 @@ fun VocabularyCard(
onWordChange = { if (switchOrder) editedWordFirst = it else editedWordSecond = it }, onWordChange = { if (switchOrder) editedWordFirst = it else editedWordSecond = it },
language = if (switchOrder) languageFirst else languageSecond, language = if (switchOrder) languageFirst else languageSecond,
onLanguageIdChange = { if (switchOrder) editedLangFirstId = it else editedLangSecondId = it }, onLanguageIdChange = { if (switchOrder) editedLangFirstId = it else editedLangSecondId = it },
isRevealed = !(!isFlipped && exerciseMode), isRevealed = !(!isFlipped && isExerciseMode),
userSpellingAnswer = userSpellingAnswer, userSpellingAnswer = userSpellingAnswer,
isUserSpellingCorrect = isUserSpellingCorrect, isUserSpellingCorrect = isUserSpellingCorrect,
correctWord = if (switchOrder) item.wordFirst else item.wordSecond, correctWord = if (switchOrder) item.wordFirst else item.wordSecond,
@@ -348,7 +400,7 @@ fun VocabularyCard(
@Suppress("HardCodedStringLiteral") @Suppress("HardCodedStringLiteral")
showGrammarDialogFor = "second" showGrammarDialogFor = "second"
}, },
isExerciseMode = exerciseMode, isExerciseMode = isExerciseMode,
vocabularyItem = item, vocabularyItem = item,
onMoreClick = { onMoreClick = {
@Suppress("HardCodedStringLiteral") @Suppress("HardCodedStringLiteral")
@@ -361,7 +413,7 @@ fun VocabularyCard(
!switchOrder !switchOrder
if(isFlipped || !exerciseMode) if(isFlipped || !isExerciseMode)
DraggableActionPanel( DraggableActionPanel(
modifier = Modifier modifier = Modifier
.align(Alignment.CenterEnd) .align(Alignment.CenterEnd)
@@ -369,9 +421,14 @@ fun VocabularyCard(
isOpen = showActionPanel, isOpen = showActionPanel,
onDismiss = { showActionPanel = false }, onDismiss = { showActionPanel = false },
isEditing = isEditing, isEditing = isEditing,
onEditClick = { isEditing = true }, onEditClick = {
onSaveClick = { handleSave() }, isEditing = true
onCancelClick = handleCancel, onEditStateChange?.invoke(true)
onEditActionHandlersReady?.invoke(
{ handleSave() },
{ handleCancel() }
)
},
onStatisticsClick = onStatisticsClick, onStatisticsClick = onStatisticsClick,
onMoveToCategoryClick = onMoveToCategoryClick, onMoveToCategoryClick = onMoveToCategoryClick,
@@ -438,18 +495,15 @@ fun VocabularyCardPreview() {
languageSecondId = R.string.language_2 languageSecondId = R.string.language_2
) )
val navController = NavController(LocalContext.current) val navController = NavController(LocalContext.current)
VocabularyCard( VocabularyDisplayCard(
vocabularyItem = item, vocabularyItem = item,
navController = navController, navController = navController,
exerciseMode = false,
isFlipped = false, isFlipped = false,
switchOrder = false, switchOrder = false,
onStatisticsClick = {}, onStatisticsClick = {},
onMoveToCategoryClick = {}, onMoveToCategoryClick = {},
onMoveToStageClick = {}, onMoveToStageClick = {},
onDeleteClick = {}, onDeleteClick = {},
userSpellingAnswer = null,
isUserSpellingCorrect = null
) )
} }
@@ -475,7 +529,7 @@ private fun FrequencyPill(zipfFrequency: Float?) {
Column( Column(
modifier = Modifier modifier = Modifier
.padding(horizontal = 4.dp) .padding(horizontal = 4.dp)
.width(80.dp), .width(100.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Surface( Surface(

View File

@@ -1,200 +0,0 @@
package eu.gaudian.translator.view.vocabulary.widgets
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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.Brush
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.gaudian.translator.R
import eu.gaudian.translator.ui.theme.ThemePreviews
import eu.gaudian.translator.view.composable.AppIcons
/**
* A modern, visually appealing set of start buttons for exercises.
* The public signature is identical to the original for drop-in replacement.
*
* @param onCustomClick Lambda for the primary custom exercise action.
* @param onDailyClick Lambda for daily exercises. It's called with `false` for a
* normal daily exercise and `true` for a daily spelling exercise.
*/
@Composable
fun ModernStartButtons(
onCustomClick: () -> Unit,
onDailyClick: (isSpelling: Boolean) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 16.dp),
horizontalArrangement = Arrangement.spacedBy(20.dp),
verticalAlignment = Alignment.CenterVertically
) {
// A large, prominent "feature button" for the main call to action.
FeatureButton(
text = stringResource(R.string.text_custom_exercise),
icon = AppIcons.PlayCircleFilled,
onClick = onCustomClick,
modifier = Modifier.weight(1f)
)
// A column for the two secondary "daily" actions.
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
SecondaryButton(
text = stringResource(R.string.text_daily_exercise),
icon = AppIcons.Today,
onClick = { onDailyClick(false) }
)
SecondaryButton(
text = stringResource(R.string.quick_word_pairs),
icon = AppIcons.SwapHoriz,
onClick = { onDailyClick(true) }
)
}
}
}
/**
* A visually rich feature button with a gradient background and a subtle
* press animation. Designed to be the primary call to action.
*/
@Composable
private fun FeatureButton(
text: String,
icon: ImageVector,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
@Suppress("HardCodedStringLiteral") val scale by animateFloatAsState(targetValue = if (isPressed) 0.95f else 1f, label = "label_scale"
)
Card(
modifier = modifier
.aspectRatio(1f)
.scale(scale)
.clickable(
interactionSource = interactionSource,
indication = null,
onClick = onClick
),
shape = MaterialTheme.shapes.extraLarge,
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.linearGradient(
colors = listOf(
MaterialTheme.colorScheme.primaryContainer,
MaterialTheme.colorScheme.primary
)
)
),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.padding(16.dp)
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.onPrimary
)
Spacer(modifier = Modifier.height(6.dp))
Text(
text = text,
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold,
fontSize = 14.sp
),
color = MaterialTheme.colorScheme.onPrimary,
textAlign = TextAlign.Center
)
}
}
}
}
/**
* A clean and simple OutlinedButton for secondary actions, with an icon and text.
*/
@Composable
private fun SecondaryButton(
text: String,
icon: ImageVector,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
OutlinedButton(
onClick = onClick,
modifier = modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.large,
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.5f))
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = text,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.weight(1f)
)
}
}
@ThemePreviews
@Composable
private fun ModernStartButtonsPreview() {
ModernStartButtons(
onCustomClick = {},
onDailyClick = {}
)
}

View File

@@ -995,7 +995,7 @@ class DictionaryViewModel @Inject constructor(
* Returns true if data is still loading (null). * Returns true if data is still loading (null).
*/ */
fun getStructuredDictionaryDataLoading(entry: DictionaryWordEntry): StateFlow<Boolean> { fun getStructuredDictionaryDataLoading(entry: DictionaryWordEntry): StateFlow<Boolean> {
val key = entry.word + "_" + entry.langCode entry.word + "_" + entry.langCode
// Create a derived flow that emits true when data is null // Create a derived flow that emits true when data is null
val dataFlow = getStructuredDictionaryDataState(entry) val dataFlow = getStructuredDictionaryDataState(entry)
val loadingFlow = MutableStateFlow(true) val loadingFlow = MutableStateFlow(true)

View File

@@ -209,13 +209,6 @@ class ExerciseViewModel @Inject constructor(
} }
} }
fun startAdHocExercise(exercise: Exercise, questions: List<Question>) {
_exerciseSessionState.value = ExerciseSessionState(
exercise = exercise,
questions = questions
)
}
fun startExercise(exercise: Exercise) { fun startExercise(exercise: Exercise) {
viewModelScope.launch { viewModelScope.launch {
val allQuestions = exerciseRepository.getAllQuestionsFlow().first() val allQuestions = exerciseRepository.getAllQuestionsFlow().first()

View File

@@ -90,12 +90,20 @@ class ProgressViewModel @Inject constructor(
private val _totalWordsInProgress = MutableStateFlow(0) private val _totalWordsInProgress = MutableStateFlow(0)
val totalWordsInProgress: StateFlow<Int> = _totalWordsInProgress.asStateFlow() val totalWordsInProgress: StateFlow<Int> = _totalWordsInProgress.asStateFlow()
private val _totalWords = MutableStateFlow(0)
val totalWords: StateFlow<Int> = _totalWords.asStateFlow()
private val _weeklyActivityStats = MutableStateFlow<List<WeeklyActivityStat>>(emptyList()) private val _weeklyActivityStats = MutableStateFlow<List<WeeklyActivityStat>>(emptyList())
val weeklyActivityStats: StateFlow<List<WeeklyActivityStat>> = _weeklyActivityStats.asStateFlow() val weeklyActivityStats: StateFlow<List<WeeklyActivityStat>> = _weeklyActivityStats.asStateFlow()
private val _dailyVocabularyStats = MutableStateFlow<Map<LocalDate, Int>>(emptyMap()) private val _dailyVocabularyStats = MutableStateFlow<Map<LocalDate, Int>>(emptyMap())
val dailyVocabularyStats: StateFlow<Map<LocalDate, Int>> = _dailyVocabularyStats.asStateFlow() val dailyVocabularyStats: StateFlow<Map<LocalDate, Int>> = _dailyVocabularyStats.asStateFlow()
private val _dailyGoal = MutableStateFlow(10)
val dailyGoal: StateFlow<Int> = _dailyGoal.asStateFlow()
private val _todayCompletedCount = MutableStateFlow(0)
val todayCompletedCount: StateFlow<Int> = _todayCompletedCount.asStateFlow()
init { init {
viewModelScope.launch { viewModelScope.launch {
@@ -255,6 +263,15 @@ class ProgressViewModel @Inject constructor(
try { try {
loadSelectedCategories() loadSelectedCategories()
try { try {
// Load daily goal setting
val dailyGoalValue = settingsRepository.dailyGoal.flow.first()
_dailyGoal.value = dailyGoalValue
// Get today's completed count
val today = kotlin.time.Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
val todayCompleted = vocabularyRepository.getCorrectAnswerCountForDate(today)
_todayCompletedCount.value = todayCompleted
val progressDeferred = viewModelScope.async { vocabularyRepository.calculateCategoryProgress() } val progressDeferred = viewModelScope.async { vocabularyRepository.calculateCategoryProgress() }
val lastSevenDaysDeferred = viewModelScope.async { getLastSevenDays() } val lastSevenDaysDeferred = viewModelScope.async { getLastSevenDays() }
val streakDeferred = viewModelScope.async { calculateDailyStreak() } val streakDeferred = viewModelScope.async { calculateDailyStreak() }
@@ -270,6 +287,8 @@ class ProgressViewModel @Inject constructor(
.filter { it.stage != VocabularyStage.LEARNED && it.stage != VocabularyStage.NEW } .filter { it.stage != VocabularyStage.LEARNED && it.stage != VocabularyStage.NEW }
.sumOf { it.itemCount } .sumOf { it.itemCount }
_totalWords.value = stageList.sumOf { it.itemCount }
if (_selectedCategories.value.isEmpty() && progressList.isNotEmpty()) { if (_selectedCategories.value.isEmpty() && progressList.isNotEmpty()) {
val initialCategory = setOf(progressList.first().vocabularyCategory.id) val initialCategory = setOf(progressList.first().vocabularyCategory.id)
_selectedCategories.value = initialCategory _selectedCategories.value = initialCategory

View File

@@ -1,3 +1,5 @@
@file:Suppress("HardCodedStringLiteral")
package eu.gaudian.translator.viewmodel package eu.gaudian.translator.viewmodel
import android.app.Application import android.app.Application
@@ -31,8 +33,8 @@ enum class ScreenState {
} }
data class ExerciseConfig( data class ExerciseConfig(
val shuffleCards: Boolean = false, val shuffleCards: Boolean = true,
val shuffleLanguages: Boolean = false, val shuffleLanguages: Boolean = true,
val trainingMode: Boolean = false, val trainingMode: Boolean = false,
val dueTodayOnly: Boolean = false, val dueTodayOnly: Boolean = false,
val selectedExerciseTypes: Set<VocabularyExerciseType> = setOf(VocabularyExerciseType.GUESSING), val selectedExerciseTypes: Set<VocabularyExerciseType> = setOf(VocabularyExerciseType.GUESSING),
@@ -90,6 +92,9 @@ class VocabularyExerciseViewModel @Inject constructor(
// Exercise configuration state // Exercise configuration state
private val _exerciseConfig = MutableStateFlow(ExerciseConfig()) private val _exerciseConfig = MutableStateFlow(ExerciseConfig())
private val _pendingExerciseConfig = MutableStateFlow(ExerciseConfig())
val pendingExerciseConfig: StateFlow<ExerciseConfig> = _pendingExerciseConfig.asStateFlow()
// Exercise results state // Exercise results state
private val _exerciseResults = MutableStateFlow(ExerciseResults()) private val _exerciseResults = MutableStateFlow(ExerciseResults())
@@ -106,6 +111,7 @@ class VocabularyExerciseViewModel @Inject constructor(
types: Set<VocabularyExerciseType>, types: Set<VocabularyExerciseType>,
shuffleLanguages: Boolean shuffleLanguages: Boolean
) { ) {
Log.d("ExerciseVM", "startExercise called: items=${items.size}, types=$types, shuffleLanguages=$shuffleLanguages")
// Reset counters for the new exercise session // Reset counters for the new exercise session
_correctAnswers.value = 0 _correctAnswers.value = 0
_wrongAnswers.value = 0 _wrongAnswers.value = 0
@@ -158,6 +164,7 @@ class VocabularyExerciseViewModel @Inject constructor(
} }
private fun loadExercise() { private fun loadExercise() {
Log.d("ExerciseVM", "loadExercise: index=$currentIndex, total=${currentItems.size}")
if (currentIndex < currentItems.size) { if (currentIndex < currentItems.size) {
// Ensure item categories align with exercise type by attempting a swap instead of replacement // Ensure item categories align with exercise type by attempting a swap instead of replacement
val randomType = exerciseTypes.random() val randomType = exerciseTypes.random()
@@ -273,8 +280,10 @@ class VocabularyExerciseViewModel @Inject constructor(
) )
} }
} }
Log.d("ExerciseVM", "exerciseState set: type=$randomType, itemId=${itemToUse.id}")
} else { } else {
_exerciseState.value = null // End of exercise _exerciseState.value = null // End of exercise
Log.d("ExerciseVM", "loadExercise: end of exercise, state cleared")
} }
} }
@@ -390,27 +399,33 @@ class VocabularyExerciseViewModel @Inject constructor(
loadExercise() loadExercise()
} }
fun onTrainingModeChanged(value: Boolean) {
_trainingMode.value = value
}
fun startExerciseWithConfig( fun startExerciseWithConfig(
items: List<VocabularyItem>, items: List<VocabularyItem>,
config: ExerciseConfig config: ExerciseConfig
) { ) {
Log.d("ExerciseVM", "startExerciseWithConfig called: items=${items.size}, configCount=${config.exerciseItemCount}, shuffleCards=${config.shuffleCards}, shuffleLanguages=${config.shuffleLanguages}, trainingMode=${config.trainingMode}, dueTodayOnly=${config.dueTodayOnly}, types=${config.selectedExerciseTypes}")
_exerciseConfig.value = config _exerciseConfig.value = config
_pendingExerciseConfig.value = config
_totalItems.value = items.size _totalItems.value = items.size
_originalItems.value = items _originalItems.value = items
startExercise(items, config.selectedExerciseTypes, config.shuffleLanguages) startExercise(items, config.selectedExerciseTypes, config.shuffleLanguages)
_screenState.value = ScreenState.EXERCISE _screenState.value = ScreenState.EXERCISE
Log.d("ExerciseVM", "screenState set to EXERCISE; totalItems=${_totalItems.value}")
} }
fun updatePendingExerciseConfig(config: ExerciseConfig) {
Log.d("ExerciseVM", "updatePendingExerciseConfig: count=${config.exerciseItemCount}, shuffleCards=${config.shuffleCards}, shuffleLanguages=${config.shuffleLanguages}, trainingMode=${config.trainingMode}, dueTodayOnly=${config.dueTodayOnly}, types=${config.selectedExerciseTypes}")
_pendingExerciseConfig.value = config
}
fun finishExercise(score: Int, wrongAnswers: Int) { fun finishExercise(score: Int, wrongAnswers: Int) {
_exerciseResults.value = ExerciseResults(score, wrongAnswers) _exerciseResults.value = ExerciseResults(score, wrongAnswers)
_screenState.value = ScreenState.RESULT _screenState.value = ScreenState.RESULT
} }
fun resetExercise() { fun resetExercise() {
Log.d("ExerciseVM", "resetExercise called")
_screenState.value = ScreenState.START _screenState.value = ScreenState.START
_exerciseConfig.value = ExerciseConfig() _exerciseConfig.value = ExerciseConfig()
_exerciseResults.value = ExerciseResults() _exerciseResults.value = ExerciseResults()
@@ -420,6 +435,7 @@ class VocabularyExerciseViewModel @Inject constructor(
_exerciseState.value = null _exerciseState.value = null
_totalItems.value = 0 _totalItems.value = 0
_originalItems.value = emptyList() _originalItems.value = emptyList()
Log.d("ExerciseVM", "resetExercise completed; screenState=START")
} }
fun retryWrongAnswers(originalItems: List<VocabularyItem>) { fun retryWrongAnswers(originalItems: List<VocabularyItem>) {

View File

@@ -617,6 +617,56 @@ class VocabularyViewModel @Inject constructor(
} }
} }
fun filterVocabularyItemsByPairs(
languagePairs: List<Pair<Int, Int>>?,
query: String?,
categoryIds: List<Int>?,
stages: List<VocabularyStage>?,
wordClass: String? = null,
dueTodayOnly: Boolean = false,
sortOrder: SortOrder
): Flow<List<VocabularyItem>> {
val baseFlow = filterVocabularyItems(
languages = null,
query = query,
categoryIds = categoryIds,
stage = null,
wordClass = wordClass,
dueTodayOnly = dueTodayOnly,
sortOrder = sortOrder
)
val normalizedPairs = languagePairs
?.map { pair -> if (pair.first < pair.second) pair else pair.second to pair.first }
?.toSet()
return combine(baseFlow, stageMapping) { items, stageMap ->
var filteredItems = items
if (!normalizedPairs.isNullOrEmpty()) {
filteredItems = filteredItems.filter { item ->
val firstId = item.languageFirstId
val secondId = item.languageSecondId
if (firstId == null || secondId == null) {
false
} else {
val normalizedPair = if (firstId < secondId) firstId to secondId else secondId to firstId
normalizedPairs.contains(normalizedPair)
}
}
}
if (!stages.isNullOrEmpty()) {
filteredItems = filteredItems.filter { item ->
val stage = stageMap[item.id] ?: VocabularyStage.NEW
stage in stages
}
}
filteredItems
}
}
suspend fun generateVocabularyItems(category: String, amount: Int) { suspend fun generateVocabularyItems(category: String, amount: Int) {
val selectedSourceLanguage = languageRepository.loadSelectedSourceLanguage().first() val selectedSourceLanguage = languageRepository.loadSelectedSourceLanguage().first()
val selectedTargetLanguage = languageRepository.loadSelectedTargetLanguage().first() val selectedTargetLanguage = languageRepository.loadSelectedTargetLanguage().first()
@@ -664,6 +714,7 @@ class VocabularyViewModel @Inject constructor(
languageIdsAsJson: String?, languageIdsAsJson: String?,
dailyOnly: Boolean = false dailyOnly: Boolean = false
) { ) {
Log.d("VocabularyVM", "prepareExercise: categories=$categoryIdsAsJson, stages=$stageNamesAsJson, languages=$languageIdsAsJson, dailyOnly=$dailyOnly")
viewModelScope.launch { viewModelScope.launch {
val categoryList = categoryIdsAsJson?.takeIf { it.isNotBlank() } val categoryList = categoryIdsAsJson?.takeIf { it.isNotBlank() }
?.split(",") ?.split(",")
@@ -682,6 +733,7 @@ class VocabularyViewModel @Inject constructor(
allLangs.filter { it.nameResId in ids } allLangs.filter { it.nameResId in ids }
} ?: emptyList() } ?: emptyList()
Log.d("VocabularyVM", "prepareExercise parsed: categories=${categoryList.size}, stages=${stageList.size}, languages=${languageList.size}")
loadCardSet(categoryList, stageList, languageList, dailyOnly) loadCardSet(categoryList, stageList, languageList, dailyOnly)
} }
} }
@@ -693,6 +745,7 @@ class VocabularyViewModel @Inject constructor(
languages: List<Language>? = null, languages: List<Language>? = null,
dailyOnly: Boolean = false dailyOnly: Boolean = false
) { ) {
Log.d(TAG, "loadCardSet invoked: categories=${categories?.size ?: 0}, stages=${stages?.size ?: 0}, languages=${languages?.map { it.nameResId }}, dailyOnly=$dailyOnly")
Log.d(TAG, "Loading card set with languages: $languages, categories: ${categories?.size}, stages: ${stages?.size}") Log.d(TAG, "Loading card set with languages: $languages, categories: ${categories?.size}, stages: ${stages?.size}")
viewModelScope.launch { viewModelScope.launch {
statusService.showLoadingMessage("Loading card set") statusService.showLoadingMessage("Loading card set")
@@ -752,6 +805,8 @@ class VocabularyViewModel @Inject constructor(
dueTodayOnly = dailyOnly dueTodayOnly = dailyOnly
).first() ).first()
Log.d(TAG, "loadCardSet: filterVocabularyItems returned ${filteredItems.size} items")
Log.d(TAG, "loadCardSet: Filtering completed, found ${filteredItems.size} items") Log.d(TAG, "loadCardSet: Filtering completed, found ${filteredItems.size} items")
if (filteredItems.isNotEmpty()) { if (filteredItems.isNotEmpty()) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -1,10 +1,23 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<resources> <resources>
<string-array name="exercise_example_prompts">Exercise Example Prompts</string-array>
<string-array name="changelog_entries"> <string-array name="changelog_entries">
<item>Version 0.3.0 \n• CSV-Import für Vokabeln aktiviert\n• Option, für einige unterstützte Sprachen einen Übersetzungsserver statt KI-Modelle zu nutzen\n• UI-Fehlerbehebungen \n• Anzeige der Worthäufigkeit \n• Leistungsoptimierungen \n• Verbesserte Übersetzungen (Deutsch und Portugiesisch)</item> <item>Version 0.3.0 \n• CSV-Import für Vokabeln aktiviert\n• Option, für einige unterstützte Sprachen einen Übersetzungsserver, statt KI-Modelle zu nutzen\n• UI-Fehlerbehebungen \n• Anzeige der Worthäufigkeit \n• Leistungsoptimierungen \n• Verbesserte Übersetzungen (Deutsch und Portugiesisch)</item>
<item>Version 0.4.0 \n• Wörterbuch-Download hinzugefügt (Beta) \n• Verbesserungen der Benutzeroberfläche \n• Fehlerbehebungen \n• Neugestaltete Vokabelkarte mit verbesserter UI \n• Mehr vorkonfigurierte Anbieter \n• Verbesserte Leistung</item> <item>Version 0.4.0 \n• Wörterbuch-Download hinzugefügt (Beta) \n• Verbesserungen der Benutzeroberfläche \n• Fehlerbehebungen \n• Neugestaltete Vokabelkarte mit verbesserter UI \n• Mehr vorkonfigurierte Anbieter \n• Verbesserte Leistung</item>
</string-array> </string-array>
<string-array name="dictionary_content">
<item>Wortart und Genus (bei Nomen)</item>
<item>Deklination (bei Nomen)</item>
<item>Aussprache (IPA) und Worttrennung</item>
<item>Definition</item>
<item>Herkunft</item>
<item>Synonyme</item>
<item>Antonyme</item>
<item>Beispiele</item>
<item>Konjugation (bei Verben)</item>
<item>Redewendungen</item>
<item>Grammatikalische Merkmale (bei Präpositionen)</item>
</string-array>
<!-- Stable, non-localized keys for dictionary content options. Order must match dictionary_content. --> <!-- Stable, non-localized keys for dictionary content options. Order must match dictionary_content. -->
<string-array name="dictionary_content_keys"> <string-array name="dictionary_content_keys">
<item>word_class_gender</item> <item>word_class_gender</item>
@@ -19,35 +32,87 @@
<item>idioms</item> <item>idioms</item>
<item>grammatical_features_prepositions</item> <item>grammatical_features_prepositions</item>
</string-array> </string-array>
<string-array name="dictionary_content">
<item>Wortart und Genus (bei Nomen)</item> <string-array name="example_prompts">
<item>Deklination (bei Nomen)</item> <item>Alles übersetzen, ohne etwas hinzuzufügen.</item>
<item>Aussprache (IPA) und Worttrennung</item>
<item>Definition</item>
<item>Herkunft</item>
<item>Synonyme</item>
<item>Antonyme</item>
<item>Beispiele</item>
<item>Konjugation (bei Verben)</item>
<item>Redewendungen</item>
<item>Grammatikalische Merkmale (bei Präpositionen)</item>
</string-array>
<string-array name="example_prompts"><item>Alles übersetzen, ohne etwas hinzuzufügen.</item>
<item>Ersetze höfliche Pronomen Sie (formell) durch "du".</item> <item>Ersetze höfliche Pronomen Sie (formell) durch "du".</item>
<item>Mach es sehr formell.</item> <item>Mach es sehr formell.</item>
<item>Mach es informell und füge ein Emoji hinzu.</item> <item>Mach es informell und füge ein Emoji hinzu.</item>
</string-array> </string-array>
<string-array name="vocabulary_hints"><item>Grundlegende Begrüßungen</item>
<item>Unregelmäßige Verben</item> <string-array name="exercise_example_prompts">Exercise Example Prompts</string-array>
<item>Vokabular am Flughafen</item>
<item>Wie man einen Kaffee bestellt</item> <string-array name="motivational_phrases">
<item>Idiomatische Ausdrücke</item> <item>Die Grenzen deiner Sprache sind die Grenzen deiner Welt. Lass uns den Käfig etwas größer machen.</item>
<item>Du bist im Grunde ein Fleisch-Computer, der eine Sprachsimulation am Laufen hat. Zeit für ein Firmware-Upgrade.</item>
<item>Eine andere Sprache zu sprechen heißt, eine zweite Seele zu besitzen. Oder zumindest einen ziemlich guten Party-Trick.</item>
<item>Irgendwann wirst du deine existenzielle Angst in einem völlig anderen Dialekt formulieren.</item>
<item>Neuroplastizität klingt nach einer schicken OP, ist aber nur dein Gehirn, das mal endlich schweres Heben macht.</item>
<item>Jedes neue Wort, das du lernst, ist ein Konzept, das du dir aus der Leere zurückholst.</item>
<item>Red weniger, sag mehr. Stopp, du lernst Vokabeln rede schlecht, aber rede oft.</item>
<item>Deine heutige Wortanzahl ist im Kosmos statistisch irrelevant, aber für dein Ego monumental.</item>
<item>Descartes Dualismus meint, Geist und Körper seien getrennt. Lass uns sicherstellen, dass der Geist heute auch mal was reißt.</item>
<item>Fließend sein ist nur eine Reihe gut getarnter Fehler. Leg los und mach ein paar.</item>
<item>Wissen ist das Einzige, was die Leere wirklich stillt. Fütter sie.</item>
<item>Dein Kurzzeitgedächtnis ist ein Sieb. Wir werden diese Wörter durch reine Wiederholung in den Tresor des Langzeitgedächtnisses prügeln.</item>
<item>Wahre Freiheit ist, genau das richtige, hochspezifische Adjektiv zu kennen, wenn du jemanden stumm verurteilst.</item>
<item>Du bist ein Künstler und dein Medium sind leicht falsch ausgesprochene Nomen.</item>
<item>Ein hungriger Magen knurrt. Ein hungriger Geist scrollt nur dumm durchs Internet. Lern lieber ein Wort.</item>
<item>Spaced Repetition: weil dein Gehirn aggressiv versucht, alles zu löschen, was du nicht aktiv nutzt.</item>
<item>Kuratiere deinen Wortschatz wie ein Museum, in dem die Hauptattraktion deine eigene Überlegenheit ist.</item>
<item>Solipsismus ist der Glaube, dass nur dein Geist existiert. Wenn das so ist, solltest du ihm vielleicht einen besseren Wortschatz verpassen.</item>
<item>Ein Mensch ohne Sprache ist ein Mensch ohne Land. Oder einfach ein sehr stiller Tourist.</item>
<item>Baue ein Vokabel-Imperium auf, eine aggressiv missverstandene Redewendung nach der anderen.</item>
<item>Die Eule der Minerva beginnt ihren Flug erst in der Dämmerung. Und nach einer richtigen Lerneinheit.</item>
<item>Um dem Gravitationsfeld der Einsprachigkeit zu entkommen, braucht es ordentlich Schubkraft. Bleib am Ball.</item>
<item>Sprache ist das Betriebssystem des Bewusstseins. Deins installiert gerade ein Update.</item>
<item>Wir sind alle in der Gosse, aber einige von uns schauen in fremdsprachige Wörterbücher.</item>
<item>Eine Sache zu benennen heißt, sie zu besitzen. Es wird Zeit, dass du zum absoluten Großgrundbesitzer der Realität wirst.</item>
<item>Grammatik ist die Grundstruktur der Realität. Ignorier sie auf eigene ontologische Gefahr.</item>
<item>Sogar Sokrates wusste, dass er nichts wusste. Aber er wusste wenigstens das Wort für \'nichts\' auf Griechisch.</item>
<item>Die Reise von tausend Meilen beginnt mit einer unbeholfenen Kaffee-Bestellung in der falschen Zeitform.</item>
<item>Dein Gehirn verbraucht etwa zwanzig Prozent der Energie deines Körpers. Lass es sich seinen Unterhalt verdienen.</item>
<item>Motivation ist eine flüchtige Emotion. Gewohnheit ist eine unerbittliche Maschine. Sei die Maschine.</item>
<item>Wenn Worte Waffen sind, ist dein Arsenal gerade eine Sammlung stumpfer Löffel. Lass uns sie schärfen.</item>
<item>Lernen bedeutet, sich freiwillig vorübergehender Inkompetenz zu unterziehen. Leb mit der Peinlichkeit.</item>
<item>Sprachenlernen ist der Mythos des Sisyphos, aber manchmal rollt der Stein den Berg runter und du verstehst tatsächlich einen Podcast.</item>
<item>Dein zukünftiges Ich beurteilt gerade deine heutige Arbeitsmoral. Beweis ihm das Gegenteil.</item>
<item>Entropie besagt, dass das Universum zum Chaos neigt. Vokabeln zu lernen ist deine tägliche Revolte gegen die Physik.</item>
<item>10 Wörter am Tag sind 3.650 im Jahr. Immer noch nicht genug, um die menschliche Existenz zu erklären, aber ein ordentlicher Anfang.</item>
<item>Zinseszins gilt auch für kognitive Werte. Also los, investier in ein paar Verben.</item>
<item>Du bist die Summe deiner Erinnerungen. Lass uns sicherstellen, dass die nicht nur aus Popsongs und Peinlichkeiten bestehen.</item>
<item>Englisch sind einfach drei verschiedene Sprachen, die in einem Mantel aufeinanders Schultern stehen. Wenn das so tun kann, kannst du das auch.</item>
<item>Das französische Wort für 99 heißt wörtlich \'vier-zwanzig-zehn-neun\'. Deine aktuellen Lernprobleme sind nichts gegen deren Mathe.</item>
<item>Im Mandarin macht der falsche Ton aus \'Mutter\' ein \'Pferd\'. Vermeiden wir heute mal ödipale Reitunfälle.</item>
<item>Deutsch lässt dich Nomen zusammenknallen, bis du ein Wort hast, das lang genug ist, um deine ganz spezielle Form des Elends auszudrücken. Streb nach dieser Macht.</item>
<item>Die Beherrschung des grammatikalischen Geschlechts bedeutet zu akzeptieren, dass das Universum willkürlich entschieden hat, dass ein Tisch weiblich und eine Brücke männlich ist. Füg dich dem Absurden.</item>
<item>Japanisch hat spezielle Zählwörter, je nachdem, ob ein Objekt flach, zylindrisch oder ein kleines Tier ist. Präzision ist eine Tugend. Fang an zu üben.</item>
<item>Spanischsprachige rasseln etwa 25% schneller Silben runter als Englischsprachige. Du musst nicht schlauer sein, du musst nur schneller denken.</item>
<item>Finnisch hat 15 Fälle. Plötzlich ist Vokabeln lernen gar nicht mehr so schlimm, oder?</item>
<item>Eine Sprache zu lernen bedeutet zu erkennen, dass Redewendungen nur kulturell sanktionierte, kollektive Halluzinationen sind. Lass uns halluzinieren.</item>
<item>Geschriebenes Walisisch sieht aus, als wäre jemand auf einer vokalfaulen Tastatur eingeschlafen, aber es birgt eine dunkle, uralte Poesie. Find die Poesie in deiner Zielsprache.</item>
<item>Ein robuster Wortschatz ist der sozial akzeptabelste Weg, ein herablassender Snob zu sein. Verdien dir deine Arroganz.</item>
<item>Du kämpfst gerade gegen die Ebbinghaussche Vergessenskurve. Sie ist eine gnadenlose, unverzeihliche mathematische Realität. Wehr dich.</item>
<item>Latein zu lernen ist nutzlos, es sei denn, du willst mit mittelalterlichen Mönchen diskutieren oder versehentlich irgendwas beschwören. So oder so, es ist eine gute Vorbereitung.</item>
<item>Ohne Vokabeln ist Grammatik nur eine wunderschön strukturierte Stille. Gib ihr etwas Lärm.</item>
<item>Irisch hat keine Wörter für \'ja\' oder \'nein\'. Du musst das Verb wiederholen. Bei Sprachen geht es darum, voll und ganz dabei zu sein.</item>
<item>Das hawaiianische Alphabet hat nur 13 Buchstaben. Die Sprache, die du lernst, hat mehr. Hör auf zu jammern und lerne sie.</item>
<item>Wortwörtlich zu übersetzen ist das reinste Hamsterrad. Jede Sprache bildet die Realität anders ab. Zeit, eine neue Karte zu lernen.</item>
<item>Irgendwann kannst du einen fremdsprachigen Film gucken, ohne nur auf das untere Drittel des Bildschirms zu starren. Bleib dran.</item>
</string-array> </string-array>
<string-array name="vocabulary_example_prompts"><item>Verwende lateinamerikanisches Spanisch</item>
<string-array name="vocabulary_example_prompts">
<item>Verwende lateinamerikanisches Spanisch</item>
<item>Vermeide lange Wörter</item> <item>Vermeide lange Wörter</item>
<item>Vermeide Sätze</item> <item>Vermeide Sätze</item>
<item>Enthält viele Verben und Adjektive</item> <item>Enthält viele Verben und Adjektive</item>
<item>Verwende informelle Sprache</item> <item>Verwende informelle Sprache</item>
</string-array> </string-array>
<string-array name="vocabulary_hints">
<item>Grundlegende Begrüßungen</item>
<item>Unregelmäßige Verben</item>
<item>Vokabular am Flughafen</item>
<item>Wie man einen Kaffee bestellt</item>
<item>Idiomatische Ausdrücke</item>
</string-array>
</resources> </resources>

View File

@@ -36,7 +36,6 @@
<string name="title_show_success_message">Erfolgsmeldung anzeigen</string> <string name="title_show_success_message">Erfolgsmeldung anzeigen</string>
<string name="label_add_category">Kategorie hinzufügen</string> <string name="label_add_category">Kategorie hinzufügen</string>
<string name="title_settings">Einstellungen</string> <string name="title_settings">Einstellungen</string>
<string name="title_dashboard">Dashboard</string>
<string name="title_developer_options">Entwickleroptionen</string> <string name="title_developer_options">Entwickleroptionen</string>
<string name="title_multiple">Mehrere</string> <string name="title_multiple">Mehrere</string>
<string name="label_translation_settings">Übersetzung</string> <string name="label_translation_settings">Übersetzung</string>
@@ -61,7 +60,6 @@
<string name="error_no_rows_to_import">Keine Zeilen zum Importieren. Bitte Spalten und Kopfzeile prüfen.</string> <string name="error_no_rows_to_import">Keine Zeilen zum Importieren. Bitte Spalten und Kopfzeile prüfen.</string>
<string name="info_imported_items_from">%1$d Vokabeln importiert.</string> <string name="info_imported_items_from">%1$d Vokabeln importiert.</string>
<string name="label_import">Importieren</string> <string name="label_import">Importieren</string>
<string name="menu_import_vocabulary">Vokabular mit KI erstellen</string>
<string name="menu_create_youtube_exercise">YouTube-Übung erstellen</string> <string name="menu_create_youtube_exercise">YouTube-Übung erstellen</string>
<string name="text_youtube_link">YouTube-Link</string> <string name="text_youtube_link">YouTube-Link</string>
<string name="text_customize_the_intervals">Passe die Intervalle und Kriterien für das Verschieben von Vokabeln an. Karten in niedrigeren Stufen werden öfter abgefragt.</string> <string name="text_customize_the_intervals">Passe die Intervalle und Kriterien für das Verschieben von Vokabeln an. Karten in niedrigeren Stufen werden öfter abgefragt.</string>
@@ -83,7 +81,6 @@
<string name="text_shuffle_languages">Sprachen mischen</string> <string name="text_shuffle_languages">Sprachen mischen</string>
<string name="text_training_mode">Trainingsmodus</string> <string name="text_training_mode">Trainingsmodus</string>
<string name="text_amount_of_cards">Anzahl der Karten</string> <string name="text_amount_of_cards">Anzahl der Karten</string>
<string name="text_interval_settings_in_days">Intervall-Einstellungen (in Tagen)</string>
<string name="text_label_word">Gib ein Wort ein</string> <string name="text_label_word">Gib ein Wort ein</string>
<string name="text_translation">Gib die Übersetzung ein</string> <string name="text_translation">Gib die Übersetzung ein</string>
<string name="text_due_today_only">Nur heute fällige</string> <string name="text_due_today_only">Nur heute fällige</string>
@@ -95,10 +92,7 @@
<string name="text_loading_3d">Laden…</string> <string name="text_loading_3d">Laden…</string>
<string name="text_show_loading">Laden anzeigen</string> <string name="text_show_loading">Laden anzeigen</string>
<string name="text_cancel_loading">Laden abbrechen</string> <string name="text_cancel_loading">Laden abbrechen</string>
<string name="text_sentence_this_is_an_info_message">Dies ist eine Info-Nachricht.</string>
<string name="text_show_info_message">Info-Nachricht anzeigen</string> <string name="text_show_info_message">Info-Nachricht anzeigen</string>
<string name="text_success_em">Erfolg!</string>
<string name="text_sentence_oops_something_went_wrong">Hoppla! Etwas ist schiefgegangen.</string>
<string name="text_show_error_message">Fehlermeldung anzeigen</string> <string name="text_show_error_message">Fehlermeldung anzeigen</string>
<string name="text_reset_intro">Intro zurücksetzen</string> <string name="text_reset_intro">Intro zurücksetzen</string>
<string name="text_sentenc_version_information_not_available">Versionsinformation nicht verfügbar.</string> <string name="text_sentenc_version_information_not_available">Versionsinformation nicht verfügbar.</string>
@@ -128,7 +122,6 @@
<string name="text_enter_api_key">API-Schlüssel eingeben</string> <string name="text_enter_api_key">API-Schlüssel eingeben</string>
<string name="text_save_key">Schlüssel speichern</string> <string name="text_save_key">Schlüssel speichern</string>
<string name="text_select_model">Modell auswählen</string> <string name="text_select_model">Modell auswählen</string>
<string name="title_title_preview_title">Vorschau-Titel</string>
<string name="text_none">Keine</string> <string name="text_none">Keine</string>
<string name="text_manual_vocabulary_list">Manuelle Vokabelliste</string> <string name="text_manual_vocabulary_list">Manuelle Vokabelliste</string>
<string name="text_filter_all_items">Filter: Alle Einträge</string> <string name="text_filter_all_items">Filter: Alle Einträge</string>
@@ -194,7 +187,6 @@
<string name="text_difficulty_2d">Schwierigkeit: %1$s</string> <string name="text_difficulty_2d">Schwierigkeit: %1$s</string>
<string name="text_amount_2d_questions">Anzahl: %1$d Fragen</string> <string name="text_amount_2d_questions">Anzahl: %1$d Fragen</string>
<string name="text_generate">Erstellen</string> <string name="text_generate">Erstellen</string>
<string name="text_let_ai_find_vocabulary_for_you">Lass die KI Vokabeln für dich finden!</string>
<string name="text_search_term">Suchbegriff</string> <string name="text_search_term">Suchbegriff</string>
<string name="text_hint">Tipp</string> <string name="text_hint">Tipp</string>
<string name="text_select_languages">Sprachen auswählen</string> <string name="text_select_languages">Sprachen auswählen</string>
@@ -226,19 +218,14 @@
<string name="cd_target_met">Ziel erreicht</string> <string name="cd_target_met">Ziel erreicht</string>
<string name="text_no_vocabulary_due_today">Heute keine Vokabeln fällig</string> <string name="text_no_vocabulary_due_today">Heute keine Vokabeln fällig</string>
<string name="text_view_all">Alle ansehen</string> <string name="text_view_all">Alle ansehen</string>
<string name="text_custom_exercise">Eigene Übung</string>
<string name="text_daily_exercise">Tägliche Übung</string>
<string name="label_total_words">Wörter gesamt</string> <string name="label_total_words">Wörter gesamt</string>
<string name="label_learned">Gelernt</string> <string name="label_learned">Gelernt</string>
<string name="remaining">Übrig</string> <string name="remaining">Übrig</string>
<string name="label_ai_model_and_prompt"><![CDATA[KI-Modell & Prompt]]></string> <string name="label_ai_model_and_prompt"><![CDATA[KI-Modell & Prompt]]></string>
<string name="examples">Beispiele</string> <string name="examples">Beispiele</string>
<string name="vocabulary_settings">Vokabular-Einstellungen</string>
<string name="label_learning_criteria">Lernkriterien</string> <string name="label_learning_criteria">Lernkriterien</string>
<string name="min_correct_to_advance">Min. richtig zum Aufsteigen</string> <string name="min_correct_to_advance">Min. richtig zum Aufsteigen</string>
<string name="max_wrong_to_demote">Max. falsch zum Absteigen</string> <string name="max_wrong_to_demote">Max. falsch zum Absteigen</string>
<string name="daily_learning_goal">Tägliches Lernziel</string>
<string name="target_correct_answers_per_day">Ziel: Richtige Antworten pro Tag</string>
<string name="label_backup_and_restore">Sicherung &amp; Wiederherstellung</string> <string name="label_backup_and_restore">Sicherung &amp; Wiederherstellung</string>
<string name="export_vocabulary_data">Vokabeldaten exportieren</string> <string name="export_vocabulary_data">Vokabeldaten exportieren</string>
<string name="import_vocabulary_data">Vokabeldaten importieren</string> <string name="import_vocabulary_data">Vokabeldaten importieren</string>
@@ -295,7 +282,7 @@
<string name="label_start_exercise_2d">Übung starten (%1$d)</string> <string name="label_start_exercise_2d">Übung starten (%1$d)</string>
<string name="number_of_cards">Anzahl der Karten: %1$d / %2$d</string> <string name="number_of_cards">Anzahl der Karten: %1$d / %2$d</string>
<string name="no_cards_found_for_the_selected_filters">Keine Karten für die gewählten Filter gefunden.</string> <string name="no_cards_found_for_the_selected_filters">Keine Karten für die gewählten Filter gefunden.</string>
<string name="label_choose_exercise_types">Übungstypen wählen</string> <string name="label_choose_exercise_types">Die richtige Antwort wählen</string>
<string name="options">Optionen</string> <string name="options">Optionen</string>
<string name="shuffle_cards">Karten mischen</string> <string name="shuffle_cards">Karten mischen</string>
<string name="quit">Beenden</string> <string name="quit">Beenden</string>
@@ -336,12 +323,11 @@
<string name="last_incorrect">Zuletzt falsch: %1$s</string> <string name="last_incorrect">Zuletzt falsch: %1$s</string>
<string name="correct_answers_">Richtige Antworten: %1$d</string> <string name="correct_answers_">Richtige Antworten: %1$d</string>
<string name="incorrect_answers">Falsche Antworten: %1$d</string> <string name="incorrect_answers">Falsche Antworten: %1$d</string>
<string name="label_card_with_position">Karte (%1$d/%2$d)</string>
<string name="item_id">Eintrags-ID: %1$d</string> <string name="item_id">Eintrags-ID: %1$d</string>
<string name="statistics_are_loading">Statistiken werden geladen…</string> <string name="statistics_are_loading">Statistiken werden geladen…</string>
<string name="to_d">nach %1$s</string> <string name="to_d">nach %1$s</string>
<string name="label_translate_from_2d">Übersetze von %1$s</string> <string name="label_translate_from_2d">Übersetze von %1$s</string>
<string name="text_assemble_the_word_here">Bilde das Wort hier</string> <string name="text_assemble_the_word_here">Bringe die Buchstaben in Reihenfolge</string>
<string name="correct_answer">Richtige Antwort: %1$s</string> <string name="correct_answer">Richtige Antwort: %1$s</string>
<string name="label_quit_exercise_qm">Übung beenden?</string> <string name="label_quit_exercise_qm">Übung beenden?</string>
<string name="text_are_you_sure_you_want_to_quit_your_progress_in_this_session_will_be_lost">Möchtest du wirklich beenden? Dein Fortschritt in dieser Sitzung geht verloren.</string> <string name="text_are_you_sure_you_want_to_quit_your_progress_in_this_session_will_be_lost">Möchtest du wirklich beenden? Dein Fortschritt in dieser Sitzung geht verloren.</string>
@@ -367,7 +353,6 @@
<string name="more_actions">Mehr Aktionen</string> <string name="more_actions">Mehr Aktionen</string>
<string name="select_all">Alle auswählen</string> <string name="select_all">Alle auswählen</string>
<string name="deselect_all">Auswahl aufheben</string> <string name="deselect_all">Auswahl aufheben</string>
<string name="search_vocabulary">Vokabular suchen…</string>
<string name="no_vocabulary_items_found_perhaps_try_changing_the_filters">Keine Vokabeln gefunden. Vielleicht die Filter ändern?</string> <string name="no_vocabulary_items_found_perhaps_try_changing_the_filters">Keine Vokabeln gefunden. Vielleicht die Filter ändern?</string>
<string name="label_category_2d">Kategorie: %1$s</string> <string name="label_category_2d">Kategorie: %1$s</string>
<string name="repository_state_imported_from">Repository-Status importiert von %1$s</string> <string name="repository_state_imported_from">Repository-Status importiert von %1$s</string>
@@ -472,8 +457,6 @@
<string name="text_assign_these_items_2d">Ordne diese Elemente zu:</string> <string name="text_assign_these_items_2d">Ordne diese Elemente zu:</string>
<string name="translate_the_following_d">Übersetze Folgendes (%1$s):</string> <string name="translate_the_following_d">Übersetze Folgendes (%1$s):</string>
<string name="label_your_translation">Deine Übersetzung</string> <string name="label_your_translation">Deine Übersetzung</string>
<string name="this_is_a_hint">Dies ist ein Hinweis.</string>
<string name="this_is_the_main_content">Dies ist der Hauptinhalt.</string>
<string name="this_is_the_content_inside_the_card">Dies ist der Inhalt in der Karte.</string> <string name="this_is_the_content_inside_the_card">Dies ist der Inhalt in der Karte.</string>
<string name="primary_button">Primärer Button</string> <string name="primary_button">Primärer Button</string>
<string name="primary_with_icon">Primär mit Icon</string> <string name="primary_with_icon">Primär mit Icon</string>
@@ -491,15 +474,12 @@
<string name="text_base_url_and_example">Basis-URL (z.B. \'http://192.168.0.99:1234/\')</string> <string name="text_base_url_and_example">Basis-URL (z.B. \'http://192.168.0.99:1234/\')</string>
<string name="label_close_selection_mode">Auswahlmodus schließen</string> <string name="label_close_selection_mode">Auswahlmodus schließen</string>
<string name="d_selected">%1$d ausgewählt</string> <string name="d_selected">%1$d ausgewählt</string>
<string name="search_query">Suchanfrage</string>
<string name="label_close_search">Suche schließen</string> <string name="label_close_search">Suche schließen</string>
<string name="generate_related_vocabulary_items">Verwandte Vokabeln generieren</string>
<string name="dismiss">Verwerfen</string> <string name="dismiss">Verwerfen</string>
<string name="edit_features_for">Merkmale für \'%1$s\' bearbeiten</string> <string name="edit_features_for">Merkmale für \'%1$s\' bearbeiten</string>
<string name="no_grammar_configuration_found_for_this_language">Keine Grammatikkonfiguration für diese Sprache gefunden.</string> <string name="no_grammar_configuration_found_for_this_language">Keine Grammatikkonfiguration für diese Sprache gefunden.</string>
<string name="word_type">Wortart</string> <string name="word_type">Wortart</string>
<string name="levels">Level</string> <string name="levels">Level</string>
<string name="quick_word_pairs">Schnelle Wortpaare</string>
<string name="stage_filter">Stufenfilter</string> <string name="stage_filter">Stufenfilter</string>
<string name="language_pair">Sprachpaar</string> <string name="language_pair">Sprachpaar</string>
<string name="language_filter">Sprachfilter</string> <string name="language_filter">Sprachfilter</string>
@@ -549,10 +529,6 @@
<string name="friendly">Freundlich</string> <string name="friendly">Freundlich</string>
<string name="label_academic">Akademisch</string> <string name="label_academic">Akademisch</string>
<string name="creative">Kreativ</string> <string name="creative">Kreativ</string>
<string name="editing_text">Text bearbeiten: %1$s</string>
<string name="no_text_received">Kein Text empfangen!</string>
<string name="error_no_text_to_edit">Fehler: Kein Text zum Bearbeiten</string>
<string name="not_launched_with_text_to_edit">Nicht mit zu bearbeitendem Text gestartet</string>
<string name="text_a_simple_list_to">Eine einfache Liste, um deine Vokabeln manuell zu sortieren</string> <string name="text_a_simple_list_to">Eine einfache Liste, um deine Vokabeln manuell zu sortieren</string>
<string name="settings_title_voice">Stimme</string> <string name="settings_title_voice">Stimme</string>
<string name="default_value">Standard</string> <string name="default_value">Standard</string>
@@ -593,21 +569,11 @@
<string name="intro_if_you_need_help_you">Wenn du Hilfe brauchst, findest du Hinweise in allen Bereichen der App.</string> <string name="intro_if_you_need_help_you">Wenn du Hilfe brauchst, findest du Hinweise in allen Bereichen der App.</string>
<string name="text_navigation_bar_labels">Navigationsleisten-Beschriftungen</string> <string name="text_navigation_bar_labels">Navigationsleisten-Beschriftungen</string>
<string name="text_show_text_labels_on_the_main_navigation_bar">Textbeschriftungen in der Hauptnavigationsleiste anzeigen.</string> <string name="text_show_text_labels_on_the_main_navigation_bar">Textbeschriftungen in der Hauptnavigationsleiste anzeigen.</string>
<string name="text_word_pair_settings">Wortpaar-Einstellungen</string>
<string name="text_amount_of_questions_2d">Anzahl der Fragen: %1$d</string>
<string name="text_shuffle_questions">Fragen mischen</string>
<string name="tetx_training_mode">Trainingsmodus</string>
<string name="text_match_the_pairs">Bilde die Paare</string>
<string name="text_word_pair_exercise">Wortpaar-Übung</string>
<string name="text_training_mode_description">Trainingsmodus ist aktiv: Antworten beeinflussen den Fortschritt nicht.</string> <string name="text_training_mode_description">Trainingsmodus ist aktiv: Antworten beeinflussen den Fortschritt nicht.</string>
<string name="text_days">" Tage"</string> <string name="text_days">" Tage"</string>
<string name="label_add_vocabulary">Vokabel hinzufügen</string> <string name="label_add_vocabulary">Vokabel hinzufügen</string>
<string name="label_create_vocabulary_with_ai">Vokabular mit KI erstellen</string>
<string name="text_vocab_empty">Keine Vokabeln gefunden. Jetzt hinzufügen?</string>
<string name="text_this_will_remove_all">Dadurch werden alle konfigurierten API-Anbieter, Modelle und gespeicherten API-Schlüssel entfernt. Diese Aktion kann nicht rückgängig gemacht werden.</string> <string name="text_this_will_remove_all">Dadurch werden alle konfigurierten API-Anbieter, Modelle und gespeicherten API-Schlüssel entfernt. Diese Aktion kann nicht rückgängig gemacht werden.</string>
<string name="text_delete_all_providers_and_models_qm">Alle Anbieter und Modelle löschen?</string> <string name="text_delete_all_providers_and_models_qm">Alle Anbieter und Modelle löschen?</string>
<string name="text_swap_sides">Seiten tauschen</string>
<string name="text_no_progress">Kein Fortschritt</string>
<string name="text_theme_preview">Theme-Vorschau</string> <string name="text_theme_preview">Theme-Vorschau</string>
<string name="text_sample_word">Beispielwort</string> <string name="text_sample_word">Beispielwort</string>
<string name="toggle_use_libretranslate">Übersetungs-Server verwenden</string> <string name="toggle_use_libretranslate">Übersetungs-Server verwenden</string>
@@ -634,7 +600,6 @@
<string name="label_language_none">Keine</string> <string name="label_language_none">Keine</string>
<string name="text_no_data_available">Keine Daten verfügbar</string> <string name="text_no_data_available">Keine Daten verfügbar</string>
<string name="label_grammar_inflections">Flexionen</string> <string name="label_grammar_inflections">Flexionen</string>
<string name="label_more">Weniger</string>
<string name="label_translations">Übersetzungen</string> <string name="label_translations">Übersetzungen</string>
<string name="label_show_examples">Beispiele anzeigen</string> <string name="label_show_examples">Beispiele anzeigen</string>
<string name="label_grammar_hyphenation">Silbentrennung</string> <string name="label_grammar_hyphenation">Silbentrennung</string>
@@ -684,13 +649,15 @@
<string name="label_language_direction">Sprachenrichtung <string name="label_language_direction">Sprachenrichtung
</string> </string>
<string name="text_language_direction_explanation">Du kannst eine optionale Einstellung vornehmen, welche Sprache zuerst oder zweit kommen soll.</string> <string name="text_language_direction_explanation">Du kannst eine optionale Einstellung vornehmen, welche Sprache zuerst oder zweit kommen soll.</string>
<string name="label_guessing_exercise">Vermutung</string> <string name="text_language_direction_disabled_with_pairs">Entferne die Sprachpaar-Auswahl, um eine Richtung zu wählen.</string>
<string name="label_guessing_exercise">Raten</string>
<string name="label_spelling_exercise">Rechtschreibung</string> <string name="label_spelling_exercise">Rechtschreibung</string>
<string name="label_multiple_choice_exercise">Multiple Choice</string> <string name="label_multiple_choice_exercise">Multiple Choice</string>
<string name="label_word_jumble_exercise">Wortwirrwarr</string> <string name="label_word_jumble_exercise">Wortwirrwarr</string>
<string name="text_due_today_only_description">Nur Karten anzeigen, die heute fällig sind.</string> <string name="text_due_today_only_description">Nur Karten anzeigen, die heute fällig sind.</string>
<string name="text_shuffle_card_order_description">Kartenmischung</string> <string name="text_shuffle_card_order_description">Kartenmischung</string>
<string name="text_shuffle_languages_description">Mische die Reihenfolge der Sprachen. Beeinflusst nicht deine Sprachrichtungs-Einstellungen.</string> <string name="text_shuffle_languages_description">Mische die Reihenfolge der Sprachen. Beeinflusst nicht deine Sprachrichtungs-Einstellungen.</string>
<string name="text_shuffle_languages_disabled_by_direction">Deaktiviere die Sprachrichtungs-Einstellung, um das Mischen zu aktivieren.</string>
<string name="label_conjugation">Konjugation: %1$s</string> <string name="label_conjugation">Konjugation: %1$s</string>
<string name="label_collapse">Einklappen</string> <string name="label_collapse">Einklappen</string>
<string name="label_expand">Ausklappen</string> <string name="label_expand">Ausklappen</string>
@@ -862,5 +829,92 @@
<string name="duplicate">Duplikat</string> <string name="duplicate">Duplikat</string>
<string name="hint_scan_hint_title">Das richtige AI-Modell finden</string> <string name="hint_scan_hint_title">Das richtige AI-Modell finden</string>
<string name="hint_translate_how_it_works">Wie Übersetzung funktioniert</string> <string name="hint_translate_how_it_works">Wie Übersetzung funktioniert</string>
<string name="label_home">Startseite</string>
<string name="label_more">Mehr</string>
<string name="label_quit_app">App beenden</string>
<string name="label_target_correct_answers_per_day">Ziel für richtige Antworten pro Tag</string>
<string name="label_interval_settings_in_days">Intervall-Einstellungen</string>
<string name="label_vocabulary_settings">Fortschritts-Einstellungen</string>
<string name="label_no_category">Keine</string>
<string name="text_search">Suche</string>
<string name="text_language_settings_description">Stelle ein, welche Sprachen du in der App verwenden möchtest. Sprachen, die nicht aktiviert sind, werden in dieser App nicht angezeigt. Du kannst auch deine eigene Sprache zur Liste hinzufügen oder eine vorhandene Sprache (Region/Locale) ändern.</string>
<string name="message_success_generic">Erfolg!</string>
<string name="message_info_generic">Info</string>
<string name="message_error_generic">Ein Fehler ist aufgetreten</string>
<string name="message_loading_generic">Lädt…</string>
<string name="message_error_language_not_selected">Quell- und Zielsprachen müssen ausgewählt sein.</string>
<string name="message_error_no_words_found">Keine Wörter im bereitgestellten Text gefunden.</string>
<string name="message_success_language_replaced">Sprach-ID für %1$d Elemente aktualisiert.</string>
<string name="message_success_vocabulary_imported">Vokabeln erfolgreich importiert.</string>
<string name="message_error_vocabulary_import_failed">Fehler beim Importieren der Vokabeln: %1$s</string>
<string name="message_success_items_merged">Einträge zusammengeführt!</string>
<string name="message_success_items_added">%1$d neue Vokabeln erfolgreich hinzugefügt.</string>
<string name="message_error_items_add_failed">Fehler beim Hinzufügen der Einträge: %1$s</string>
<string name="message_success_items_deleted">Vokabeln erfolgreich gelöscht.</string>
<string name="message_error_items_delete_failed">Fehler beim Löschen: %1$s</string>
<string name="message_error_no_cards_found">Keine Karten für den angegebenen Filter gefunden.</string>
<string name="message_success_cards_loaded">Kartensatz erfolgreich geladen.</string>
<string name="message_success_grammar_updated">Grammatikdetails aktualisiert!</string>
<string name="message_error_grammar_fetch_failed">Konnte Grammatikdetails nicht abrufen.</string>
<string name="message_loading_grammar_fetch">Grammatik wird für %1$d Elemente abgerufen…</string>
<string name="message_success_file_saved">Datei unter %1$s gespeichert</string>
<string name="message_error_file_save_failed">Fehler beim Speichern der Datei: %1$s</string>
<string name="message_error_file_save_cancelled">Speichern der Datei abgebrochen oder fehlgeschlagen.</string>
<string name="message_error_file_picker_not_initialized">Save File Launcher nicht initialisiert.</string>
<string name="message_success_category_saved">Kategorie in %1$s gespeichert.</string>
<string name="message_error_api_key_missing">Dein API-Schlüssel fehlt oder ist ungültig.</string>
<string name="message_error_api_key_invalid">Dein API-Schlüssel fehlt oder ist ungültig.</string>
<string name="message_loading_translating">Übersetze %1$d Wörter…</string>
<string name="message_success_translation_completed">Übersetzung abgeschlossen.</string>
<string name="message_error_translation_failed">Übersetzung fehlgeschlagen: %1$s</string>
<string name="message_success_repository_wiped">Alle Repository-Daten gelöscht.</string>
<string name="message_error_repository_wipe_failed">Fehler beim Löschen des Repositorys: %1$s</string>
<string name="message_loading_card_set">Lade Kartensatz</string>
<string name="message_success_stage_updated">Stufe erfolgreich aktualisiert.</string>
<string name="message_error_stage_update_failed">Fehler beim Aktualisieren des Stages: %1$s</string>
<string name="message_success_category_updated">Kategorie erfolgreich aktualisiert.</string>
<string name="message_error_category_update_failed">Fehler beim Aktualisieren der Kategorie: %1$s</string>
<string name="message_success_articles_removed">Artikel erfolgreich entfernt.</string>
<string name="message_error_articles_remove_failed">Fehler beim Entfernen der Artikel: %1$s</string>
<string name="message_success_synonyms_generated">Synonyme erfolgreich generiert.</string>
<string name="message_error_synonyms_generation_failed">Fehler beim Generieren von Synonymen: %1$s</string>
<string name="message_error_operation_failed">Operation fehlgeschlagen: %1$s</string>
<string name="message_loading_operation_in_progress">Operation läuft…</string>
<string name="message_test_info">Das ist eine allgemeine Infomeldung.</string>
<string name="message_test_success">Das ist eine Erfolgsmeldung für den Test!</string>
<string name="message_test_error">Hoppla, da ist etwas schiefgelaufen :(</string>
<string name="label_stats">Statistiken</string>
<string name="label_library">Sammlung</string>
<string name="label_edit">Bearbeiten</string>
<string name="label_new_words">Neue Wörter</string>
<string name="desc_expand_your_vocabulary">Erweitere deinen Wortschatz</string>
<string name="label_settings">Einstellungen</string>
<string name="label_2d_days">%1$d Tage</string>
<string name="label_current_streak">Aktuelle Streak</string>
<string name="label_daily_goal">Tägliches Ziel</string>
<string name="text_desc_no_activity_data_available">Keine Aktivitätsdaten verfügbar</string>
<string name="label_see_history">Verlauf anzeigen</string>
<string name="label_weekly_progress">Wöchentlicher Fortschritt</string>
<string name="cd_go">Los</string>
<string name="label_sort_by">Sortieren nach</string>
<string name="label_reset">Zurücksetzen</string>
<string name="label_filter_cards">Filter Cards</string>
<string name="text_desc_organize_vocabulary_groups">Organisiere deinen Wortschatz in Gruppen</string>
<string name="text_add_new_word_to_list">Extrahiere ein neues Wort in deine Liste</string>
<string name="cd_scroll_to_top">Nach oben scrollen</string>
<string name="cd_settings">Einstellungen</string>
<string name="label_import_csv">CSV importieren</string>
<string name="label_ai_generator">KI-Generator</string>
<string name="label_new_wordss">Neue Wörter</string>
<string name="label_recently_added">Kürzlich hinzugefügt</string>
<string name="label_view_all">Alle anzeigen</string>
<string name="text_explore_more_categories">Entdecke weitere Kategorien</string>
<string name="cd_options">Optionen</string>
<string name="cd_selected">Ausgewählt</string>
<string name="label_all_cards">Alle Karten</string>
<string name="cd_filter_options">Filteroptionen</string>
<string name="cd_add">Hinzufügen</string>
<string name="cd_searchh">Suche</string>
<string name="label_learnedd">gelernt</string>
</resources> </resources>

Some files were not shown because too many files have changed in this diff Show More