Compare commits

..

14 Commits

Author SHA1 Message Date
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
83 changed files with 1914 additions and 4206 deletions

View File

@@ -126,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

@@ -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,15 +253,19 @@ fun TranslatorApp(
val currentDestination = navBackStackEntry?.destination val currentDestination = navBackStackEntry?.destination
val selectedScreen = Screen.fromDestination(currentDestination) val selectedScreen = Screen.fromDestination(currentDestination)
val isBottomBarHidden = currentDestination?.hierarchy?.any { destination -> @Suppress("HardCodedStringLiteral") val isBottomBarHidden = currentDestination?.hierarchy?.any { destination ->
destination.route in setOf( destination.route in setOf(
Screen.Translation.route, Screen.Translation.route,
Screen.Vocabulary.route,
Screen.Dictionary.route, Screen.Dictionary.route,
Screen.Exercises.route, Screen.Exercises.route,
Screen.Settings.route Screen.Settings.route
) )
} == true || currentDestination?.route == "start_exercise" } == 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(
@@ -272,7 +276,6 @@ fun TranslatorApp(
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( val isMoreSection = screen in setOf(
Screen.Translation, Screen.Translation,
Screen.Vocabulary,
Screen.Dictionary, Screen.Dictionary,
Screen.Settings, Screen.Settings,
Screen.Exercises Screen.Exercises
@@ -307,6 +310,7 @@ fun TranslatorApp(
} }
}, },
onPlayClicked = { onPlayClicked = {
@Suppress("HardCodedStringLiteral")
navController.navigate("start_exercise") navController.navigate("start_exercise")
} }
) )

View File

@@ -37,10 +37,10 @@ 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.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.NewWordReviewScreen
import eu.gaudian.translator.view.vocabulary.NewWordScreen import eu.gaudian.translator.view.vocabulary.NewWordScreen
import eu.gaudian.translator.view.vocabulary.NoGrammarItemsScreen import eu.gaudian.translator.view.vocabulary.NoGrammarItemsScreen
@@ -48,11 +48,26 @@ 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,
@@ -67,7 +82,6 @@ fun AppNavHost(
Screen.Library.route, Screen.Library.route,
Screen.Stats.route, Screen.Stats.route,
Screen.Translation.route, Screen.Translation.route,
Screen.Vocabulary.route,
Screen.Dictionary.route, Screen.Dictionary.route,
Screen.Exercises.route, Screen.Exercises.route,
SettingsRoutes.LIST SettingsRoutes.LIST
@@ -132,20 +146,24 @@ fun AppNavHost(
HomeScreen(navController = navController) HomeScreen(navController = navController)
} }
composable(Screen.Library.route) { composable(NavigationRoutes.NEW_WORD) {
LibraryScreen(navController = navController) NewWordScreen(navController = navController)
} }
composable("start_exercise") { composable(NavigationRoutes.NEW_WORD_REVIEW) {
NewWordReviewScreen(navController = navController)
}
composable(NavigationRoutes.START_EXERCISE) {
StartExerciseScreen(navController = navController) 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) homeGraph(navController)
libraryGraph(navController)
statsGraph(navController) statsGraph(navController)
translationGraph(navController) translationGraph(navController)
dictionaryGraph(navController) dictionaryGraph(navController)
vocabularyGraph(navController)
exerciseGraph(navController) exerciseGraph(navController)
settingsGraph(navController) settingsGraph(navController)
} }
@@ -159,177 +177,16 @@ fun NavGraphBuilder.homeGraph(navController: NavHostController) {
composable("main_home") { composable("main_home") {
HomeScreen(navController = navController) HomeScreen(navController = navController)
} }
composable("new_word") {
NewWordScreen(navController = navController)
}
composable("new_word_review") {
NewWordReviewScreen(navController = navController)
}
} }
} }
fun NavGraphBuilder.statsGraph( fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
navController: NavHostController,
) {
navigation( navigation(
startDestination = "main_stats", startDestination = "main_library",
route = Screen.Stats.route route = Screen.Library.route
) { ) {
composable("main_stats") { composable("main_library") {
StatsScreen(navController = navController) LibraryScreen(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()
VocabularyListScreen(
navController = navController,
showDueTodayOnly = showDueTodayOnly,
categoryId = categoryId,
onNavigateToItem = { item ->
navController.navigate("vocabulary_detail/${item.id}")
},
onNavigateBack = { navController.popBackStack() },
enableNavigationButtons = true
)
}
composable("stats/language_progress") {
LanguageProgressScreen(
navController = navController
)
}
composable("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)
}
VocabularyListScreen(
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
)
}
}
}
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(
@@ -362,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,
@@ -379,7 +236,7 @@ fun NavGraphBuilder.vocabularyGraph(
) )
} }
composable("vocabulary_heatmap") { composable(NavigationRoutes.STATS_VOCABULARY_HEATMAP) {
VocabularyHeatmapScreen( VocabularyHeatmapScreen(
navController = navController, navController = navController,
) )
@@ -391,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,
@@ -514,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,12 +142,14 @@ fun AppAlertDialog(
) )
if (showBottomSheet) { if (showBottomSheet) {
hintContent?.let {
HintBottomSheet( HintBottomSheet(
onDismissRequest = { showBottomSheet = false }, onDismissRequest = { showBottomSheet = false },
sheetState = sheetState, sheetState = sheetState,
content = hintContent content = it
) )
} }
}
} }
/** /**
@@ -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

@@ -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
@@ -40,8 +38,8 @@ 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.ui.theme.ThemePreviews
import eu.gaudian.translator.R import eu.gaudian.translator.R
import eu.gaudian.translator.ui.theme.ThemePreviews
/** /**
* An interface that defines the required properties for any item * An interface that defines the required properties for any item
@@ -51,15 +49,9 @@ 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>,
@@ -175,6 +167,7 @@ fun <T : TabItem> AppTabLayout(
} }
} }
@Suppress("HardCodedStringLiteral")
@ThemePreviews @ThemePreviews
@Composable @Composable
fun ModernTabLayoutPreview() { fun ModernTabLayoutPreview() {

View File

@@ -43,9 +43,8 @@ import eu.gaudian.translator.view.hints.LocalShowHints
@Composable @Composable
fun AppTopAppBar( fun AppTopAppBar(
title: String,
additionalContent: @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 = {},
@@ -115,17 +114,18 @@ fun AppTopAppBar(
) )
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()
}
) )
} }
}
} }
/** /**

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
@@ -28,13 +28,13 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.HorizontalDivider
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.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.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -74,7 +74,6 @@ sealed class Screen(
object Stats : Screen("stats", R.string.label_stats, AppIcons.Statistics, AppIcons.Statistics) 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 Translation : Screen("translation", R.string.label_translation, AppIcons.TranslateFilled, AppIcons.TranslateOutlined)
object Library : Screen("library", R.string.label_library, AppIcons.VocabularyFilled, AppIcons.VocabularyOutlined) object Library : Screen("library", R.string.label_library, AppIcons.VocabularyFilled, AppIcons.VocabularyOutlined)
object Vocabulary : Screen("vocabulary", R.string.label_legacy_vocabulary, AppIcons.VocabularyFilled, AppIcons.VocabularyOutlined)
object Settings : Screen("settings", R.string.title_settings, AppIcons.SettingsFilled, AppIcons.SettingsOutlined) 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 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)
@@ -88,7 +87,6 @@ sealed class Screen(
fun getMoreMenuItems(showExperimental: Boolean = false): List<Screen> { fun getMoreMenuItems(showExperimental: Boolean = false): List<Screen> {
val items = mutableListOf<Screen>() val items = mutableListOf<Screen>()
items.add(Translation) items.add(Translation)
items.add(Vocabulary)
items.add(Dictionary) items.add(Dictionary)
items.add(Settings) items.add(Settings)
if (showExperimental) { if (showExperimental) {
@@ -326,10 +324,11 @@ private fun MoreBottomSheetContent(
Text( Text(
text = stringResource(R.string.label_more), text = stringResource(R.string.label_more),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold, // Added bold to match the new style
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
) )
HorizontalDivider() // Removed HorizontalDivider() for a cleaner look
moreItems.forEach { screen -> moreItems.forEach { screen ->
MoreMenuItem( MoreMenuItem(
@@ -340,31 +339,49 @@ private fun MoreBottomSheetContent(
} }
} }
@Composable @Composable
private fun MoreMenuItem( fun MoreMenuItem(
screen: Screen, screen: Screen,
onClick: () -> Unit onClick: () -> Unit
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable(onClick = onClick) .clickable { onClick() }
.padding(horizontal = 24.dp, vertical = 16.dp), .padding(horizontal = 24.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically 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( Icon(
imageVector = screen.selectedIcon, imageVector = screen.selectedIcon,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(28.dp)
) )
}
}
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
// Title
Text( Text(
text = stringResource(screen.title), text = stringResource(id = screen.title), // Adjust to your actual string property
style = MaterialTheme.typography.bodyLarge 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,14 +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, 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,
@@ -116,6 +123,20 @@ fun AppCard(
val hasHeader = title != null || text != null || expandable || icon != null val hasHeader = title != null || text != null || expandable || icon != null
val canClickHeader = expandable || onClick != 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
.fillMaxWidth() .fillMaxWidth()
@@ -146,6 +167,7 @@ fun AppCard(
) { ) {
// 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,
@@ -179,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(
@@ -189,6 +221,7 @@ fun AppCard(
) )
} }
} }
} }
// --- Content Area --- // --- Content Area ---

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

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

@@ -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

@@ -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(
@@ -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

@@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@@ -24,40 +23,117 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBackIosNew import androidx.compose.material.icons.filled.ArrowBackIosNew
import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Hearing
import androidx.compose.material.icons.filled.List
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.outlined.Circle import androidx.compose.material.icons.outlined.Circle
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
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.Slider import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.Surface 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.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf 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.rememberCoroutineScope
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.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector 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.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavHostController 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 @Composable
fun StartExerciseScreen( fun StartExerciseScreen(
navController: NavHostController, navController: NavHostController,
modifier: Modifier = Modifier 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( Box(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.TopCenter contentAlignment = Alignment.TopCenter
@@ -67,7 +143,16 @@ fun StartExerciseScreen(
.widthIn(max = 700.dp) // Keeps it from over-stretching on tablets .widthIn(max = 700.dp) // Keeps it from over-stretching on tablets
.fillMaxSize() .fillMaxSize()
) { ) {
TopBarSection(onBackClick = { navController.popBackStack() }) 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( LazyColumn(
modifier = Modifier modifier = Modifier
@@ -76,21 +161,135 @@ fun StartExerciseScreen(
verticalArrangement = Arrangement.spacedBy(32.dp) verticalArrangement = Arrangement.spacedBy(32.dp)
) { ) {
item { Spacer(modifier = Modifier.height(8.dp)) } item { Spacer(modifier = Modifier.height(8.dp)) }
item { LanguagePairSection() } item {
item { CategoriesSection() } LanguagePairSection(
item { DifficultySection() } selectedPairs = selectedLanguagePairs,
item { NumberOfCardsSection() } availableLanguageIds = availableLanguagesFromItems,
item { QuestionTypesSection() } 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)) } item { Spacer(modifier = Modifier.height(24.dp)) }
} }
BottomButtonSection() 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 @Composable
fun TopBarSection(onBackClick: () -> Unit) { 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( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -105,29 +304,63 @@ fun TopBarSection(onBackClick: () -> Unit) {
) { ) {
Icon( Icon(
imageVector = Icons.Default.ArrowBackIosNew, imageVector = Icons.Default.ArrowBackIosNew,
contentDescription = "Back", contentDescription = stringResource(R.string.cd_back),
modifier = Modifier.size(18.dp), modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
} }
Text( Text(
text = "Start Exercise", text = stringResource(R.string.label_start_exercise),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
textAlign = androidx.compose.ui.text.style.TextAlign.Center textAlign = TextAlign.Center
) )
// Spacer to balance the back button for centering IconButton(
Spacer(modifier = Modifier.size(48.dp)) 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 @Composable
fun SectionHeader(title: String, actionText: String? = null, onActionClick: () -> Unit = {}) { fun SectionHeader(title: String, actionText: String? = null, onActionClick: () -> Unit = {}) {
Row( Row(
modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp), modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -151,28 +384,154 @@ fun SectionHeader(title: String, actionText: String? = null, onActionClick: () -
} }
@Composable @Composable
fun LanguagePairSection() { fun LanguagePairSection(
var selectedPair by remember { mutableStateOf(0) } 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 { Column {
SectionHeader(title = "Language Pair", actionText = "Change") SectionHeader(title = stringResource(R.string.language_pair))
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
LanguageChip( if (availablePairs.isEmpty()) {
text = "EN → ES", Text(
isSelected = selectedPair == 0, text = stringResource(R.string.text_no_dictionary_language_pairs_found),
modifier = Modifier.weight(1f), style = MaterialTheme.typography.bodyMedium,
onClick = { selectedPair = 0 } 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( LanguageChip(
text = "EN → FR", text = "${pair.first.name}${pair.second.name}",
isSelected = selectedPair == 1, isSelected = isSelected,
modifier = Modifier.weight(1f), modifier = Modifier.widthIn(min = 160.dp),
onClick = { selectedPair = 1 } 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 @Composable
fun LanguageChip(text: String, isSelected: Boolean, modifier: Modifier = Modifier, onClick: () -> Unit) { fun LanguageChip(text: String, isSelected: Boolean, modifier: Modifier = Modifier, onClick: () -> Unit) {
Surface( Surface(
@@ -189,8 +548,16 @@ fun LanguageChip(text: String, isSelected: Boolean, modifier: Modifier = Modifie
) { ) {
// Dummy overlapping flags // Dummy overlapping flags
Box(modifier = Modifier.width(32.dp)) { Box(modifier = Modifier.width(32.dp)) {
Box(modifier = Modifier.size(20.dp).clip(CircleShape).background(Color.Red).align(Alignment.CenterStart)) Box(modifier = Modifier
Box(modifier = Modifier.size(20.dp).clip(CircleShape).background(Color.Blue).align(Alignment.CenterEnd)) .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)) Spacer(modifier = Modifier.width(8.dp))
Text(text = text, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.bodyMedium) Text(text = text, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.bodyMedium)
@@ -200,28 +567,91 @@ fun LanguageChip(text: String, isSelected: Boolean, modifier: Modifier = Modifie
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun CategoriesSection() { fun CategoriesSection(
val categories = listOf("Travel", "Business", "Food", "Technology", "Slang", "Academic", "Relationships") selectedCategories: List<VocabularyCategory>,
var selectedCategories by remember { mutableStateOf(setOf("Travel", "Food")) } 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 { Column {
SectionHeader(title = "Categories") 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( FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
categories.forEach { category -> tagCategories.forEach { category ->
val isSelected = selectedCategories.contains(category) val isSelected = selectedCategories.contains(category)
Surface( Surface(
shape = RoundedCornerShape(20.dp), shape = RoundedCornerShape(20.dp),
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant, color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
onClick = { onClick = {
selectedCategories = if (isSelected) selectedCategories - category else selectedCategories + category val updated = if (isSelected) {
selectedCategories - category
} else {
selectedCategories + category
}
onCategoriesChanged(updated)
} }
) { ) {
Text( Text(
text = category, 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, color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
@@ -234,57 +664,47 @@ fun CategoriesSection() {
} }
@Composable @Composable
fun DifficultySection() { fun NumberOfCardsSection(
val difficulties = listOf("Easy", "Medium", "Hard") totalAvailable: Int,
var selectedDifficulty by remember { mutableStateOf("Medium") } amount: Int,
onAmountChanged: (Int) -> Unit,
dueTodayOnly: Boolean,
onDueTodayOnlyChanged: (Boolean) -> Unit
) {
Column { Column {
SectionHeader(title = "Difficulty Level") OptionItemSwitch(
Surface( title = stringResource(R.string.text_due_today_only),
shape = RoundedCornerShape(50), description = stringResource(R.string.text_due_today_only_description),
color = MaterialTheme.colorScheme.surfaceVariant, checked = dueTodayOnly,
modifier = Modifier.fillMaxWidth().height(56.dp) onCheckedChange = onDueTodayOnlyChanged
) {
Row(
modifier = Modifier.fillMaxSize().padding(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
difficulties.forEach { level ->
val isSelected = selectedDifficulty == level
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.clip(RoundedCornerShape(50))
.background(if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent)
.clickable { selectedDifficulty = level },
contentAlignment = Alignment.Center
) {
Text(
text = level,
color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
style = MaterialTheme.typography.bodyMedium
) )
}
}
}
}
}
}
@Composable Spacer(modifier = Modifier.height(16.dp))
fun NumberOfCardsSection() {
var sliderPosition by remember { mutableFloatStateOf(25f) } 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)
}
Column {
Row( Row(
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = "NUMBER OF CARDS", text = stringResource(R.string.text_amount_of_cards).uppercase(),
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
letterSpacing = 1.sp, letterSpacing = 1.sp,
@@ -295,7 +715,7 @@ fun NumberOfCardsSection() {
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
) { ) {
Text( Text(
text = sliderPosition.toInt().toString(), text = "$coercedAmount / $totalAvailable",
color = MaterialTheme.colorScheme.onPrimary, color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
@@ -304,58 +724,73 @@ fun NumberOfCardsSection() {
} }
} }
Slider( AppSlider(
value = sliderPosition, value = coercedAmount.toFloat(),
onValueChange = { sliderPosition = it }, onValueChange = { onAmountChanged(it.toInt().coerceIn(1, maxAvailable)) },
valueRange = 5f..50f, valueRange = 1f..maxAvailable.toFloat(),
steps = 45 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( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Text("5 CARDS", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) availableQuickSelections.forEach { value ->
Text("50 CARDS", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) 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 @Composable
fun QuestionTypesSection() { fun QuestionTypesSection(
var selectedTypes by remember { mutableStateOf(setOf("Multiple Choice", "Spelling")) } selectedTypes: Set<VocabularyExerciseType>,
onTypeSelected: (VocabularyExerciseType) -> Unit
) {
Column { Column {
SectionHeader(title = "Question Types") SectionHeader(title = stringResource(R.string.text_question_types))
QuestionTypeCard( QuestionTypeCard(
title = "Multiple Choice", title = stringResource(R.string.label_guessing_exercise),
subtitle = "Choose the correct meaning", subtitle = stringResource(R.string.flip_card),
icon = Icons.Default.List, icon = AppIcons.Guessing,
isSelected = selectedTypes.contains("Multiple Choice"), isSelected = selectedTypes.contains(VocabularyExerciseType.GUESSING),
onClick = { onClick = { onTypeSelected(VocabularyExerciseType.GUESSING) }
selectedTypes = if (selectedTypes.contains("Multiple Choice")) selectedTypes - "Multiple Choice" else selectedTypes + "Multiple Choice"
}
) )
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
QuestionTypeCard( QuestionTypeCard(
title = "Spelling", title = stringResource(R.string.label_spelling_exercise),
subtitle = "Type the translated word", subtitle = stringResource(R.string.type_the_translation),
icon = Icons.Default.Edit, icon = AppIcons.SpellCheck,
isSelected = selectedTypes.contains("Spelling"), isSelected = selectedTypes.contains(VocabularyExerciseType.SPELLING),
onClick = { onClick = { onTypeSelected(VocabularyExerciseType.SPELLING) }
selectedTypes = if (selectedTypes.contains("Spelling")) selectedTypes - "Spelling" else selectedTypes + "Spelling"
}
) )
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
QuestionTypeCard( QuestionTypeCard(
title = "Listening", title = stringResource(R.string.label_multiple_choice_exercise),
subtitle = "Recognize spoken words", subtitle = stringResource(R.string.label_choose_exercise_types),
icon = Icons.Default.Hearing, icon = AppIcons.CheckList,
isSelected = selectedTypes.contains("Listening"), isSelected = selectedTypes.contains(VocabularyExerciseType.MULTIPLE_CHOICE),
onClick = { onClick = { onTypeSelected(VocabularyExerciseType.MULTIPLE_CHOICE) }
selectedTypes = if (selectedTypes.contains("Listening")) selectedTypes - "Listening" else selectedTypes + "Listening" )
} 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) }
) )
} }
} }
@@ -395,38 +830,103 @@ fun QuestionTypeCard(title: String, subtitle: String, icon: ImageVector, isSelec
} }
} }
} }
@Composable @Composable
fun BottomButtonSection() { fun BottomButtonSection(
enabled: Boolean,
amount: Int,
onStart: () -> Unit
) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(24.dp) .padding(24.dp)
) { ) {
Button( AppButton(
onClick = { /* TODO: Start Session */ }, onClick = onStart,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(56.dp), .height(56.dp),
shape = RoundedCornerShape(28.dp), enabled = enabled,
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary) shape = RoundedCornerShape(28.dp)
) { ) {
Row( Row(
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = "Start Session", text = stringResource(R.string.label_start_exercise_2d, amount),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Icon( Icon(
imageVector = Icons.Default.PlayArrow, imageVector = Icons.Default.PlayArrow,
contentDescription = "Play", contentDescription = stringResource(R.string.cd_play),
modifier = Modifier.size(20.dp) 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

@@ -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

@@ -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()

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

@@ -1,6 +1,6 @@
package eu.gaudian.translator.view.home package eu.gaudian.translator.view.home
import androidx.compose.foundation.Image import android.content.Context
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.Box
@@ -18,12 +18,10 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddCircle import androidx.compose.material.icons.filled.AddCircle
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.LocalFireDepartment import androidx.compose.material.icons.filled.LocalFireDepartment
import androidx.compose.material.icons.filled.Psychology import androidx.compose.material.icons.filled.Psychology
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.TrendingUp
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
@@ -40,15 +38,17 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
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.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.navigation.NavHostController import androidx.navigation.NavHostController
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.NavigationRoutes
import eu.gaudian.translator.view.composable.AppCard import eu.gaudian.translator.view.composable.AppCard
import eu.gaudian.translator.view.composable.Screen 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.view.vocabulary.widgets.WeeklyActivityChartWidget
import eu.gaudian.translator.viewmodel.ProgressViewModel import eu.gaudian.translator.viewmodel.ProgressViewModel
@@ -57,6 +57,17 @@ fun HomeScreen(
navController: NavHostController, navController: NavHostController,
modifier: Modifier = Modifier 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) // A Box with TopCenter alignment keeps the UI centered on wide screens (tablets/foldables)
Box( Box(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
@@ -70,29 +81,40 @@ fun HomeScreen(
verticalArrangement = Arrangement.spacedBy(15.dp) verticalArrangement = Arrangement.spacedBy(15.dp)
) { ) {
item { Spacer(modifier = Modifier.height(16.dp)) } item { Spacer(modifier = Modifier.height(16.dp)) }
item { TopProfileSection(navController = navController) } item { TopProfileSection(
item { StreakAndGoalSection() } navController = navController,
context = LocalContext.current
) }
item { 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( ActionCard(
title = "Daily Review", title = "Daily Review",
subtitle = "42 words need attention", subtitle = "42 words need attention",
icon = Icons.Default.Psychology, icon = Icons.Default.Psychology,
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary contentColor = MaterialTheme.colorScheme.onPrimary
) )
} }
item { item {
ActionCard( ActionCard(
title = "New Words", title = stringResource(R.string.label_new_words),
subtitle = "Expand your vocabulary", subtitle = stringResource(R.string.desc_expand_your_vocabulary),
icon = Icons.Default.AddCircle, icon = Icons.Default.AddCircle,
containerColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant, contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
onClick = { navController.navigate("new_word") } onClick = { navController.navigate(NavigationRoutes.NEW_WORD) }
) )
} }
item { WeeklyProgressSection(navController = navController) } item { WeeklyProgressSection(navController = navController) }
item { BottomStatsSection() } item { BottomStatsSection(navController = navController) }
// Bottom padding for edge-to-edge screens // Bottom padding for edge-to-edge screens
item { Spacer(modifier = Modifier.height(24.dp)) } item { Spacer(modifier = Modifier.height(24.dp)) }
@@ -101,8 +123,8 @@ fun HomeScreen(
} }
@Composable @Composable
fun TopProfileSection(navController: NavHostController) { fun TopProfileSection(navController: NavHostController, context: Context) {
val context = LocalContext.current
val motivationalPhrases = remember { val motivationalPhrases = remember {
context.resources.getStringArray(R.array.motivational_phrases) context.resources.getStringArray(R.array.motivational_phrases)
} }
@@ -110,23 +132,10 @@ fun TopProfileSection(navController: NavHostController) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(8.dp)
) {
// Parrot App Icon
Box(
modifier = Modifier modifier = Modifier
.size(56.dp) .fillMaxWidth()
.clip(CircleShape) .padding(8.dp)
.background(MaterialTheme.colorScheme.primaryContainer),
contentAlignment = Alignment.Center
) { ) {
Image(
painter = painterResource(id = R.mipmap.ic_launcher_foreground),
contentDescription = "Polly Parrot",
)
}
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
@@ -145,7 +154,7 @@ fun TopProfileSection(navController: NavHostController) {
) { ) {
Icon( Icon(
imageVector = Icons.Default.Settings, imageVector = Icons.Default.Settings,
contentDescription = "Settings", contentDescription = stringResource(R.string.label_settings),
tint = MaterialTheme.colorScheme.onSurfaceVariant tint = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
@@ -153,7 +162,13 @@ fun TopProfileSection(navController: NavHostController) {
} }
@Composable @Composable
fun StreakAndGoalSection() { fun StreakAndGoalSection(
streak: Int,
progress: Float,
progressTitle: String,
onGoalClick: () -> Unit,
onStreakClick: () -> Unit
) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
@@ -162,15 +177,17 @@ fun StreakAndGoalSection() {
StatCard( StatCard(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
icon = Icons.Default.LocalFireDepartment, icon = Icons.Default.LocalFireDepartment,
title = "7 Days", title = stringResource(R.string.label_2d_days, streak),
subtitle = "CURRENT STREAK" subtitle = stringResource(R.string.label_current_streak).uppercase(),
onClick = onStreakClick
) )
// Goal Card // Goal Card
GoalCard( GoalCard(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
progress = 0.7f, progress = progress,
title = "14 / 20", title = progressTitle,
subtitle = "DAILY GOAL" subtitle = stringResource(R.string.label_daily_goal).uppercase(),
onClick = onGoalClick
) )
} }
} }
@@ -180,13 +197,35 @@ fun StatCard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
icon: ImageVector, icon: ImageVector,
title: String, title: String,
subtitle: String subtitle: String,
onClick: (() -> Unit)? = null
) { ) {
if (onClick != null) {
AppCard(
modifier = modifier,
onClick = onClick
) {
StatCardContent(icon = icon, title = title, subtitle = subtitle)
}
} else {
AppCard( AppCard(
modifier = modifier, modifier = modifier,
) { ) {
StatCardContent(icon = icon, title = title, subtitle = subtitle)
}
}
}
@Composable
private fun StatCardContent(
icon: ImageVector,
title: String,
subtitle: String
) {
Column( Column(
modifier = Modifier.padding(20.dp), modifier = Modifier
.padding(20.dp)
.height(120.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
@@ -201,7 +240,6 @@ fun StatCard(
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Text(text = subtitle, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)) Text(text = subtitle, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))
} }
}
} }
@Composable @Composable
@@ -209,13 +247,35 @@ fun GoalCard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
progress: Float, progress: Float,
title: String, title: String,
subtitle: String subtitle: String,
onClick: (() -> Unit)? = null
) { ) {
if (onClick != null) {
AppCard(
modifier = modifier,
onClick = onClick
) {
GoalCardContent(progress = progress, title = title, subtitle = subtitle)
}
} else {
AppCard( AppCard(
modifier = modifier, modifier = modifier,
) { ) {
GoalCardContent(progress = progress, title = title, subtitle = subtitle)
}
}
}
@Composable
private fun GoalCardContent(
progress: Float,
title: String,
subtitle: String
) {
Column( Column(
modifier = Modifier.padding(20.dp), modifier = Modifier
.padding(20.dp)
.height(120.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
@@ -234,7 +294,6 @@ fun GoalCard(
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Text(text = subtitle, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)) Text(text = subtitle, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))
} }
}
} }
@Composable @Composable
@@ -242,7 +301,6 @@ fun ActionCard(
title: String, title: String,
subtitle: String, subtitle: String,
icon: ImageVector, icon: ImageVector,
containerColor: Color,
contentColor: Color, contentColor: Color,
onClick: (() -> Unit)? = null onClick: (() -> Unit)? = null
) { ) {
@@ -263,7 +321,7 @@ fun ActionCard(
} }
Icon( Icon(
imageVector = Icons.Default.ChevronRight, imageVector = Icons.Default.ChevronRight,
contentDescription = "Go", contentDescription = stringResource(R.string.cd_go),
modifier = Modifier.size(24.dp) modifier = Modifier.size(24.dp)
) )
} }
@@ -300,9 +358,9 @@ fun WeeklyProgressSection(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text(text = "Weekly Progress", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) Text(text = stringResource(R.string.label_weekly_progress), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
TextButton(onClick = { navController.navigate("vocabulary_heatmap") }) { TextButton(onClick = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) }) {
Text("See History") Text(stringResource(R.string.label_see_history))
} }
} }
@@ -310,6 +368,7 @@ fun WeeklyProgressSection(
AppCard( AppCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
onClick = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) }
) { ) {
if (weeklyActivityStats.isEmpty()) { if (weeklyActivityStats.isEmpty()) {
Column( Column(
@@ -320,7 +379,7 @@ fun WeeklyProgressSection(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text( Text(
text = "No activity data available", text = stringResource(R.string.text_desc_no_activity_data_available),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
@@ -333,7 +392,14 @@ fun WeeklyProgressSection(
} }
@Composable @Composable
fun BottomStatsSection() { 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( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
@@ -341,34 +407,26 @@ fun BottomStatsSection() {
// Total Words // Total Words
AppCard( AppCard(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
onClick = { navController.navigate(Screen.Library.route) }
) { ) {
Column(modifier = Modifier.padding(20.dp)) { Column(modifier = Modifier.padding(20.dp)) {
Text(text = "TOTAL WORDS", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)) 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)) Spacer(modifier = Modifier.height(8.dp))
Text(text = "1,284", style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold) Text(text = totalWords.toString(), style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.TrendingUp, contentDescription = null, tint = Color.Green, modifier = Modifier.size(16.dp))
Spacer(modifier = Modifier.width(4.dp))
Text(text = "+12 today", style = MaterialTheme.typography.labelSmall, color = Color.Green)
}
} }
} }
// Accuracy // Learned
AppCard( AppCard(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
onClick = { navController.navigate(NavigationRoutes.STATS_LANGUAGE_PROGRESS) }
) { ) {
Column(modifier = Modifier.padding(20.dp)) { Column(modifier = Modifier.padding(20.dp)) {
Text(text = "ACCURACY", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)) Text(text = "LEARNED", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text(text = "92%", style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold) Text(text = learnedWords.toString(), style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.CheckCircle, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(16.dp))
Spacer(modifier = Modifier.width(4.dp))
Text(text = "Master level", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.primary)
}
} }
} }
} }

View File

@@ -1,4 +1,4 @@
@file:Suppress("HardCodedStringLiteral") @file:Suppress("AssignedValueIsNeverRead")
package eu.gaudian.translator.view.library package eu.gaudian.translator.view.library
@@ -26,6 +26,9 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape 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.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.BottomSheetDefaults
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
@@ -41,6 +44,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
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.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@@ -58,17 +62,17 @@ 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 eu.gaudian.translator.R 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.model.VocabularyStage
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppDropDownMenu import eu.gaudian.translator.view.NavigationRoutes
import eu.gaudian.translator.view.composable.AppIcons import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppSwitch import eu.gaudian.translator.view.composable.AppSwitch
import eu.gaudian.translator.view.composable.LargeDropdownMenuItem import eu.gaudian.translator.view.composable.BottomSheetMenuItem
import eu.gaudian.translator.view.composable.MultipleLanguageDropdown import eu.gaudian.translator.view.composable.MultipleLanguageDropdown
import eu.gaudian.translator.view.dialogs.AddCategoryDialog import eu.gaudian.translator.view.dialogs.AddCategoryDialog
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.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
import eu.gaudian.translator.viewmodel.VocabularyViewModel import eu.gaudian.translator.viewmodel.VocabularyViewModel
@@ -97,8 +101,6 @@ fun LibraryScreen(
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity) val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity) val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
val context = LocalContext.current
var filterState by rememberSaveable { mutableStateOf(LibraryFilterState()) } var filterState by rememberSaveable { mutableStateOf(LibraryFilterState()) }
var showFilterSheet by remember { mutableStateOf(false) } var showFilterSheet by remember { mutableStateOf(false) }
@@ -133,8 +135,8 @@ fun LibraryScreen(
val vocabularyItems by vocabularyItemsFlow.collectAsStateWithLifecycle(initialValue = emptyList()) val vocabularyItems by vocabularyItemsFlow.collectAsStateWithLifecycle(initialValue = emptyList())
var isHeaderVisible by remember { mutableStateOf(true) } var isHeaderVisible by remember { mutableStateOf(true) }
var previousIndex by remember { mutableStateOf(0) } var previousIndex by remember { mutableIntStateOf(0) }
var previousScrollOffset by remember { mutableStateOf(0) } var previousScrollOffset by remember { mutableIntStateOf(0) }
// Set navigation context when vocabulary items are loaded // Set navigation context when vocabulary items are loaded
LaunchedEffect(vocabularyItems) { LaunchedEffect(vocabularyItems) {
@@ -227,10 +229,11 @@ fun LibraryScreen(
CategoriesView( CategoriesView(
categories = categories, categories = categories,
onCategoryClick = { category -> onCategoryClick = { category ->
navController.navigate("category_detail/${category.id}") @Suppress("HardCodedStringLiteral")
navController.navigate("${NavigationRoutes.CATEGORY_DETAIL}/${category.id}")
}, },
onExploreMoreClick = { onExploreMoreClick = {
navController.navigate("category_list_screen") navController.navigate(NavigationRoutes.CATEGORY_LIST)
} }
) )
}, },
@@ -249,7 +252,8 @@ fun LibraryScreen(
} }
} else { } else {
vocabularyViewModel.setNavigationContext(vocabularyItems, item.id) vocabularyViewModel.setNavigationContext(vocabularyItems, item.id)
navController.navigate("vocabulary_detail/${item.id}") @Suppress("HardCodedStringLiteral")
navController.navigate("${NavigationRoutes.VOCABULARY_DETAIL}/${item.id}")
} }
}, },
onItemLongClick = { item -> onItemLongClick = { item ->
@@ -285,7 +289,7 @@ fun LibraryScreen(
modifier = Modifier.size(50.dp), modifier = Modifier.size(50.dp),
containerColor = MaterialTheme.colorScheme.surfaceVariant containerColor = MaterialTheme.colorScheme.surfaceVariant
) { ) {
Icon(AppIcons.ArrowCircleUp, contentDescription = "Scroll to top") Icon(AppIcons.ArrowCircleUp, contentDescription = stringResource(R.string.cd_scroll_to_top))
} }
} }
} }
@@ -329,23 +333,30 @@ fun LibraryScreen(
} }
if (showAddMenu) { if (showAddMenu) {
AppDropDownMenu( ModalBottomSheet(
expanded = showAddMenu, onDismissRequest = { showAddMenu = false },
onDismissRequest = { showAddMenu = false } sheetState = sheetState,
containerColor = MaterialTheme.colorScheme.surface,
) { ) {
LargeDropdownMenuItem( Column(
text = stringResource(R.string.label_add_vocabulary), modifier = Modifier
selected = false, .fillMaxWidth()
enabled = true, .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 = { onClick = {
showAddMenu = false showAddMenu = false
navController.navigate("new_word") navController.navigate(NavigationRoutes.NEW_WORD)
} }
) )
LargeDropdownMenuItem(
text = stringResource(R.string.label_add_category), BottomSheetMenuItem(
selected = false, icon = Icons.Rounded.Folder,
enabled = true, title = stringResource(R.string.label_add_category),
subtitle = stringResource(R.string.text_desc_organize_vocabulary_groups), // Suggest adding this to strings.xml
onClick = { onClick = {
showAddMenu = false showAddMenu = false
showAddCategoryDialog = true showAddCategoryDialog = true
@@ -353,6 +364,7 @@ fun LibraryScreen(
) )
} }
} }
}
if (showAddCategoryDialog) { if (showAddCategoryDialog) {
AddCategoryDialog(onDismiss = { showAddCategoryDialog = false }) AddCategoryDialog(onDismiss = { showAddCategoryDialog = false })
@@ -408,7 +420,7 @@ fun FilterBottomSheetContent(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = "Filter Cards", text = stringResource(R.string.label_filter_cards),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
@@ -420,7 +432,7 @@ fun FilterBottomSheetContent(
sortOrder = SortOrder.NEWEST_FIRST sortOrder = SortOrder.NEWEST_FIRST
onResetClick() onResetClick()
}) { }) {
Text("Reset") Text(stringResource(R.string.label_reset))
} }
} }
@@ -436,7 +448,7 @@ fun FilterBottomSheetContent(
// Sort Order // Sort Order
Column { Column {
Text( Text(
text = "SORT BY", text = stringResource(R.string.label_sort_by).uppercase(),
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
letterSpacing = 1.sp, letterSpacing = 1.sp,
@@ -579,7 +591,7 @@ fun FilterBottomSheetContent(
shape = RoundedCornerShape(28.dp) shape = RoundedCornerShape(28.dp)
) { ) {
Text( Text(
text = "Apply Filters", text = stringResource(R.string.label_apply_filters),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )

View File

@@ -128,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

@@ -96,7 +96,6 @@ 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(

View File

@@ -105,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

@@ -77,7 +77,7 @@ fun VocabularyProgressOptionsScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = stringResource(R.string.vocabulary_settings), title = stringResource(R.string.label_vocabulary_settings),
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },
hintContent = HintDefinition.VOCABULARY_PROGRESS.hint() hintContent = HintDefinition.VOCABULARY_PROGRESS.hint()
) )
@@ -91,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
)
} }
} }
@@ -163,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

@@ -1,4 +1,4 @@
@file:Suppress("HardCodedStringLiteral", "AssignedValueIsNeverRead", "UnusedMaterial3ScaffoldPaddingParameter") @file:Suppress("AssignedValueIsNeverRead")
package eu.gaudian.translator.view.stats package eu.gaudian.translator.view.stats
@@ -56,8 +56,8 @@ 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.NavigationRoutes
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
import eu.gaudian.translator.view.composable.AppOutlinedCard import eu.gaudian.translator.view.composable.AppOutlinedCard
@@ -66,7 +66,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
@@ -83,14 +82,11 @@ import kotlinx.coroutines.launch
@SuppressLint("FrequentlyChangingValue") @SuppressLint("FrequentlyChangingValue")
@Composable @Composable
fun StatsScreen( fun StatsScreen(
modifier: Modifier = Modifier,
navController: NavHostController, navController: NavHostController,
onShowCustomExerciseDialog: () -> Unit = {},
startDailyExercise: (Boolean) -> Unit = {},
onNavigateToCategoryDetail: ((Int) -> Unit)? = null, onNavigateToCategoryDetail: ((Int) -> Unit)? = null,
onNavigateToCategoryList: (() -> Unit)? = null, onNavigateToCategoryList: (() -> Unit)? = null,
onShowWordPairExerciseDialog: () -> Unit = {},
onScroll: (Boolean) -> Unit = {}, onScroll: (Boolean) -> Unit = {},
modifier: Modifier = Modifier
) { ) {
val activity = LocalContext.current.findActivity() val activity = LocalContext.current.findActivity()
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity) val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
@@ -129,10 +125,11 @@ fun StatsScreen(
} }
val handleNavigateToCategoryDetail = onNavigateToCategoryDetail ?: { categoryId -> val handleNavigateToCategoryDetail = onNavigateToCategoryDetail ?: { categoryId ->
navController.navigate("stats/category_detail/$categoryId") @Suppress("HardCodedStringLiteral")
navController.navigate("${NavigationRoutes.STATS_CATEGORY_DETAIL}/$categoryId")
} }
val handleNavigateToCategoryList = onNavigateToCategoryList ?: { val handleNavigateToCategoryList = onNavigateToCategoryList ?: {
navController.navigate("stats/category_list_screen") navController.navigate(NavigationRoutes.STATS_CATEGORY_LIST)
} }
AppOutlinedCard(modifier = modifier) { AppOutlinedCard(modifier = modifier) {
@@ -271,11 +268,8 @@ fun StatsScreen(
navController = navController, navController = navController,
vocabularyViewModel = vocabularyViewModel, vocabularyViewModel = vocabularyViewModel,
progressViewModel = progressViewModel, progressViewModel = progressViewModel,
onShowCustomExerciseDialog = onShowCustomExerciseDialog,
startDailyExercise = startDailyExercise,
onNavigateToCategoryDetail = handleNavigateToCategoryDetail, onNavigateToCategoryDetail = handleNavigateToCategoryDetail,
onNavigateToCategoryList = handleNavigateToCategoryList, onNavigateToCategoryList = handleNavigateToCategoryList,
onShowWordPairExerciseDialog = onShowWordPairExerciseDialog,
onMissingLanguage = { missingId -> onMissingLanguage = { missingId ->
selectedMissingLanguageId = missingId selectedMissingLanguageId = missingId
showMissingLanguageDialog = true showMissingLanguageDialog = true
@@ -525,38 +519,26 @@ class DragDropState(
// Remainder of your existing components // Remainder of your existing components
// -------------------------------------------------------------------------------- // --------------------------------------------------------------------------------
@Suppress("HardCodedStringLiteral")
@Composable @Composable
private fun LazyWidget( private fun LazyWidget(
widgetType: WidgetType, widgetType: WidgetType,
navController: NavController, navController: NavController,
vocabularyViewModel: VocabularyViewModel, vocabularyViewModel: VocabularyViewModel,
progressViewModel: ProgressViewModel, progressViewModel: ProgressViewModel,
onShowCustomExerciseDialog: () -> Unit,
startDailyExercise: (Boolean) -> Unit,
onNavigateToCategoryDetail: (Int) -> Unit, onNavigateToCategoryDetail: (Int) -> Unit,
onNavigateToCategoryList: () -> Unit, onNavigateToCategoryList: () -> Unit,
onShowWordPairExerciseDialog: () -> Unit,
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,
onNavigateToNew = { navController.navigate("stats/vocabulary_sorting?mode=NEW") }, onNavigateToNew = { navController.navigate("${NavigationRoutes.STATS_VOCABULARY_SORTING}?mode=NEW") },
onNavigateToDuplicates = { navController.navigate("stats/vocabulary_sorting?mode=DUPLICATES") }, onNavigateToDuplicates = { navController.navigate("${NavigationRoutes.STATS_VOCABULARY_SORTING}?mode=DUPLICATES") },
onNavigateToFaulty = { navController.navigate("stats/vocabulary_sorting?mode=FAULTY") }, onNavigateToFaulty = { navController.navigate("${NavigationRoutes.STATS_VOCABULARY_SORTING}?mode=FAULTY") },
onNavigateToNoGrammar = { navController.navigate("stats/no_grammar_items") }, onNavigateToNoGrammar = { navController.navigate(NavigationRoutes.STATS_NO_GRAMMAR_ITEMS) },
onNavigateToMissingLanguage = onMissingLanguage onNavigateToMissingLanguage = onMissingLanguage
) )
@@ -568,7 +550,7 @@ private fun LazyWidget(
lastSevenDays = progressViewModel.lastSevenDays.collectAsState().value, lastSevenDays = progressViewModel.lastSevenDays.collectAsState().value,
dueTodayCount = progressViewModel.dueTodayCount.collectAsState().value, dueTodayCount = progressViewModel.dueTodayCount.collectAsState().value,
wordsCompleted = progressViewModel.totalWordsCompleted.collectAsState().value, wordsCompleted = progressViewModel.totalWordsCompleted.collectAsState().value,
onStatisticsClicked = { navController.navigate("stats/vocabulary_heatmap") } onStatisticsClicked = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) }
) )
WidgetType.WeeklyActivityChart -> WeeklyActivityChartWidget( WidgetType.WeeklyActivityChart -> WeeklyActivityChartWidget(
@@ -577,13 +559,19 @@ private fun LazyWidget(
WidgetType.AllVocabulary -> AllVocabularyWidget( WidgetType.AllVocabulary -> AllVocabularyWidget(
vocabularyViewModel = vocabularyViewModel, vocabularyViewModel = vocabularyViewModel,
onOpenAllVocabulary = { navController.navigate("stats/vocabulary_list/false/null") }, onOpenAllVocabulary = { navController.navigate("${NavigationRoutes.STATS_VOCABULARY_LIST}/false/null") },
onStageClicked = { stage -> navController.navigate("stats/vocabulary_list/false/$stage") } onStageClicked = { stage ->
@Suppress("HardCodedStringLiteral")
navController.navigate("${NavigationRoutes.STATS_VOCABULARY_LIST}/false/$stage")
}
) )
WidgetType.DueToday -> DueTodayWidget( WidgetType.DueToday -> DueTodayWidget(
vocabularyViewModel = vocabularyViewModel, vocabularyViewModel = vocabularyViewModel,
onStageClicked = { stage -> navController.navigate("stats/vocabulary_list/false/$stage") } onStageClicked = { stage ->
@Suppress("HardCodedStringLiteral")
navController.navigate("${NavigationRoutes.STATS_VOCABULARY_LIST}/false/$stage")
}
) )
WidgetType.CategoryProgress -> CategoryProgressWidget( WidgetType.CategoryProgress -> CategoryProgressWidget(
@@ -597,7 +585,7 @@ private fun LazyWidget(
totalWords = vocabularyViewModel.vocabularyItems.collectAsState().value.size, totalWords = vocabularyViewModel.vocabularyItems.collectAsState().value.size,
learnedWords = vocabularyViewModel.stageStats.collectAsState().value learnedWords = vocabularyViewModel.stageStats.collectAsState().value
.firstOrNull { it.stage == VocabularyStage.LEARNED }?.itemCount ?: 0, .firstOrNull { it.stage == VocabularyStage.LEARNED }?.itemCount ?: 0,
onNavigateToProgress = { navController.navigate("stats/language_progress") } onNavigateToProgress = { navController.navigate(NavigationRoutes.STATS_LANGUAGE_PROGRESS) }
) )
} }
@@ -660,11 +648,8 @@ fun StatsScreenPreview() {
val navController = rememberNavController() val navController = rememberNavController()
StatsScreen( StatsScreen(
navController = navController, navController = navController,
onShowCustomExerciseDialog = {},
onNavigateToCategoryDetail = {}, onNavigateToCategoryDetail = {},
startDailyExercise = {},
onNavigateToCategoryList = {}, onNavigateToCategoryList = {},
onShowWordPairExerciseDialog = {},
) )
} }
@@ -686,6 +671,7 @@ fun WidgetContainerPreview() {
.padding(16.dp), .padding(16.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
@Suppress("HardCodedStringLiteral")
Text("Preview Content") 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
@@ -71,7 +72,7 @@ fun TopBarActions(
languageViewModel: LanguageViewModel, languageViewModel: LanguageViewModel,
onSettingsClick: () -> Unit, onSettingsClick: () -> Unit,
onNavigateBack: (() -> Unit)? = null, onNavigateBack: (() -> Unit)? = null,
hintContent: (@Composable () -> 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)) {

View File

@@ -177,7 +177,7 @@ private fun LoadedTranslationContent(
languageViewModel = languageViewModel, languageViewModel = languageViewModel,
onSettingsClick = onSettingsClick, onSettingsClick = onSettingsClick,
onNavigateBack = onNavigateBack, onNavigateBack = onNavigateBack,
hintContent = { HintDefinition.TRANSLATION.Render() } 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

@@ -179,7 +179,7 @@ fun CategoryDetailScreen(
} }
) { 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,
@@ -324,8 +324,8 @@ fun CategoryHeaderCardWithProgressPreview() {
MaterialTheme { MaterialTheme {
CategoryHeaderCard( CategoryHeaderCard(
subtitle = "Travel Vocabulary", subtitle = "Travel Vocabulary",
categoryProgress = eu.gaudian.translator.viewmodel.CategoryProgress( categoryProgress = CategoryProgress(
vocabularyCategory = eu.gaudian.translator.model.TagCategory( vocabularyCategory = TagCategory(
1, 1,
"Travel" "Travel"
), ),

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

@@ -1,483 +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.Screen
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
}
},
onNavigateBack = {
if (!navController.popBackStack()) {
navController.navigate(Screen.Home.route) {
launchSingleTop = true
restoreState = false
}
}
}
)
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

@@ -136,6 +136,7 @@ fun NewWordReviewScreen(
onConfirm = { onConfirm = {
val selectedCategoryIds = selectedCategories.filterNotNull().map { it.id } val selectedCategoryIds = selectedCategories.filterNotNull().map { it.id }
vocabularyViewModel.addVocabularyItems(selectedItems.toList(), selectedCategoryIds) vocabularyViewModel.addVocabularyItems(selectedItems.toList(), selectedCategoryIds)
@Suppress("HardCodedStringLiteral")
navController.popBackStack("new_word", inclusive = false) navController.popBackStack("new_word", inclusive = false)
}, },
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp)

View File

@@ -23,7 +23,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AutoAwesome import androidx.compose.material.icons.filled.AutoAwesome
import androidx.compose.material.icons.filled.DriveFolderUpload import androidx.compose.material.icons.filled.DriveFolderUpload
import androidx.compose.material.icons.filled.EditNote import androidx.compose.material.icons.filled.EditNote
import androidx.compose.material.icons.filled.LibraryBooks
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
@@ -62,14 +61,18 @@ import eu.gaudian.translator.model.VocabularyItem
import eu.gaudian.translator.utils.StatusMessageId import eu.gaudian.translator.utils.StatusMessageId
import eu.gaudian.translator.utils.StatusMessageService import eu.gaudian.translator.utils.StatusMessageService
import eu.gaudian.translator.utils.findActivity 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.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.AppSlider import eu.gaudian.translator.view.composable.AppSlider
import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.composable.InspiringSearchField 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.SourceLanguageDropdown
import eu.gaudian.translator.view.composable.TargetLanguageDropdown 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.view.library.VocabularyCard
import eu.gaudian.translator.viewmodel.LanguageViewModel import eu.gaudian.translator.viewmodel.LanguageViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel import eu.gaudian.translator.viewmodel.VocabularyViewModel
@@ -95,7 +98,7 @@ fun NewWordScreen(
LaunchedEffect(isGenerating, generatedItems, navigateToReview) { LaunchedEffect(isGenerating, generatedItems, navigateToReview) {
if (navigateToReview && !isGenerating) { if (navigateToReview && !isGenerating) {
if (generatedItems.isNotEmpty()) { if (generatedItems.isNotEmpty()) {
navController.navigate("new_word_review") navController.navigate(NavigationRoutes.NEW_WORD_REVIEW)
} }
navigateToReview = false navigateToReview = false
} }
@@ -111,7 +114,6 @@ fun NewWordScreen(
var skipHeader by remember { mutableStateOf(true) } var skipHeader by remember { mutableStateOf(true) }
var selectedLangFirst by remember { mutableStateOf<Language?>(null) } var selectedLangFirst by remember { mutableStateOf<Language?>(null) }
var selectedLangSecond by remember { mutableStateOf<Language?>(null) } var selectedLangSecond by remember { mutableStateOf<Language?>(null) }
var parseError by remember { mutableStateOf<String?>(null) }
val recentlyAdded = remember(recentItems) { val recentlyAdded = remember(recentItems) {
recentItems.sortedByDescending { it.id }.take(4) recentItems.sortedByDescending { it.id }.take(4)
@@ -171,8 +173,6 @@ fun NewWordScreen(
}.filter { r -> r.any { it.isNotBlank() } } }.filter { r -> r.any { it.isNotBlank() } }
} }
val errorParsingTable = stringResource(R.string.error_parsing_table)
val errorParsingTableWithReason = stringResource(R.string.error_parsing_table_with_reason)
val importTableLauncher = rememberLauncherForActivityResult( val importTableLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument(), contract = ActivityResultContracts.OpenDocument(),
onResult = { uri -> onResult = { uri ->
@@ -196,34 +196,32 @@ fun NewWordScreen(
selectedColFirst = 0 selectedColFirst = 0
selectedColSecond = 1.coerceAtMost(rows.first().size - 1) selectedColSecond = 1.coerceAtMost(rows.first().size - 1)
showTableImportDialog.value = true showTableImportDialog.value = true
parseError = null
} else { } else {
parseError = errorParsingTable statusMessageService.showErrorById(StatusMessageId.ERROR_PARSING_TABLE)
statusMessageService.showErrorMessage(parseError!!)
} }
} }
} catch (e: Exception) { } catch (_: Exception) {
parseError = e.message statusMessageService.showErrorById(StatusMessageId.ERROR_PARSING_TABLE_WITH_REASON)
statusMessageService.showErrorMessage(
(errorParsingTableWithReason + " " + e.message)
)
} }
} }
} }
) )
Box( Box(
modifier = modifier.fillMaxSize().padding(16.dp), modifier = modifier
.fillMaxSize()
.padding(16.dp),
contentAlignment = Alignment.TopCenter contentAlignment = Alignment.TopCenter
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.widthIn(max = 700.dp) // Perfect scaling for tablets/foldables .widthIn(max = 700.dp) // Perfect scaling for tablets/foldables
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()).padding(0.dp) .verticalScroll(rememberScrollState())
.padding(0.dp)
) { ) {
AppTopAppBar( AppTopAppBar(
title = "New Words", title = stringResource(R.string.label_new_words),
onNavigateBack = { navController.popBackStack() } onNavigateBack = { navController.popBackStack() }
) )
@@ -281,12 +279,12 @@ fun NewWordScreen(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = "Recently Added", text = stringResource(R.string.label_recently_added),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
TextButton(onClick = { navController.navigate("library") }) { TextButton(onClick = { navController.navigate(Screen.Library.route) }) {
Text("View All") Text(stringResource(R.string.label_view_all))
} }
} }
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
@@ -296,7 +294,10 @@ fun NewWordScreen(
item = item, item = item,
allLanguages = allLanguages, allLanguages = allLanguages,
isSelected = false, isSelected = false,
onItemClick = { navController.navigate("vocabulary_detail/${item.id}") }, onItemClick = {
@Suppress("HardCodedStringLiteral")
navController.navigate("${NavigationRoutes.VOCABULARY_DETAIL}/${item.id}")
},
onItemLongClick = {}, onItemLongClick = {},
onDeleteClick = {} onDeleteClick = {}
) )
@@ -387,19 +388,15 @@ fun NewWordScreen(
} }
}, },
confirmButton = { confirmButton = {
val errorSelectTwoColumns = stringResource(R.string.error_select_two_columns)
val errorSelectLanguages = stringResource(R.string.error_select_languages)
val errorNoRowsToImport = stringResource(R.string.error_no_rows_to_import)
val infoImportedItemsFrom = stringResource(R.string.info_imported_items_from)
TextButton(onClick = { TextButton(onClick = {
if (selectedColFirst == selectedColSecond) { if (selectedColFirst == selectedColSecond) {
statusMessageService.showErrorMessage(errorSelectTwoColumns) statusMessageService.showErrorById(StatusMessageId.ERROR_SELECT_TWO_COLUMNS)
return@TextButton return@TextButton
} }
val langA = selectedLangFirst val langA = selectedLangFirst
val langB = selectedLangSecond val langB = selectedLangSecond
if (langA == null || langB == null) { if (langA == null || langB == null) {
statusMessageService.showErrorMessage(errorSelectLanguages) statusMessageService.showErrorById(StatusMessageId.ERROR_SELECT_LANGUAGES)
return@TextButton return@TextButton
} }
val startIdx = if (skipHeader) 1 else 0 val startIdx = if (skipHeader) 1 else 0
@@ -415,11 +412,11 @@ fun NewWordScreen(
) )
} }
if (items.isEmpty()) { if (items.isEmpty()) {
statusMessageService.showErrorMessage(errorNoRowsToImport) statusMessageService.showErrorById(StatusMessageId.ERROR_NO_ROWS_TO_IMPORT)
return@TextButton return@TextButton
} }
vocabularyViewModel.addVocabularyItems(items) vocabularyViewModel.addVocabularyItems(items)
statusMessageService.showSuccessMessage(infoImportedItemsFrom + " " + items.size) statusMessageService.showSuccessById(StatusMessageId.SUCCESS_ITEMS_IMPORTED)
showTableImportDialog.value = false showTableImportDialog.value = false
}) { Text(stringResource(R.string.label_import)) } }) { Text(stringResource(R.string.label_import)) }
}, },
@@ -445,26 +442,17 @@ fun AIGeneratorCard(
onGenerate: () -> Unit, onGenerate: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val icon = Icons.Default.AutoAwesome
val hints = stringArrayResource(R.array.vocabulary_hints) val hints = stringArrayResource(R.array.vocabulary_hints)
AppCard( AppCard(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
title = stringResource(R.string.label_ai_generator),
icon = icon,
hintContent = HintDefinition.VOCABULARY_GENERATE_AI.hint(),
) { ) {
Column(modifier = Modifier.padding(24.dp)) { Column(modifier = Modifier.padding(8.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Icons.Default.AutoAwesome,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "AI Generator",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.height(24.dp))
Text( Text(
text = stringResource(R.string.text_search_term), text = stringResource(R.string.text_search_term),
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
@@ -569,12 +557,6 @@ fun AddManuallyCard(
val selectedSourceLanguage by languageViewModel.selectedSourceLanguage.collectAsState() val selectedSourceLanguage by languageViewModel.selectedSourceLanguage.collectAsState()
val selectedTargetLanguage by languageViewModel.selectedTargetLanguage.collectAsState() val selectedTargetLanguage by languageViewModel.selectedTargetLanguage.collectAsState()
val languageLabel = when {
selectedSourceLanguage != null && selectedTargetLanguage != null ->
"${selectedSourceLanguage?.name}${selectedTargetLanguage?.name}"
else -> stringResource(R.string.text_select_languages)
}
val canAdd = wordText.isNotBlank() && translationText.isNotBlank() && val canAdd = wordText.isNotBlank() && translationText.isNotBlank() &&
selectedSourceLanguage != null && selectedTargetLanguage != null selectedSourceLanguage != null && selectedTargetLanguage != null
@@ -717,9 +699,11 @@ fun BottomActionCardsRow(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
// Explore Packs Card //TODO Explore Packs Card
AppCard( AppCard(
modifier = Modifier.weight(1f).height(120.dp), modifier = Modifier
.weight(1f)
.height(120.dp),
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
@@ -736,12 +720,13 @@ fun BottomActionCardsRow(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( Icon(
imageVector = Icons.Default.LibraryBooks, imageVector = AppIcons.Vocabulary,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
} }
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
@Suppress("HardCodedStringLiteral")
Text( Text(
text = "Explore Packs", text = "Explore Packs",
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
@@ -749,6 +734,7 @@ fun BottomActionCardsRow(
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
Spacer(modifier = Modifier.height(6.dp)) Spacer(modifier = Modifier.height(6.dp))
@Suppress("HardCodedStringLiteral")
Text( Text(
text = "Coming soon", text = "Coming soon",
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
@@ -759,7 +745,9 @@ fun BottomActionCardsRow(
// Import CSV Card // Import CSV Card
AppCard( AppCard(
modifier = Modifier.weight(1f).height(120.dp), modifier = Modifier
.weight(1f)
.height(120.dp),
onClick = onImportCsvClick onClick = onImportCsvClick
) { ) {
Column( Column(
@@ -782,7 +770,7 @@ fun BottomActionCardsRow(
} }
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
Text( Text(
text = "Import CSV", text = stringResource(R.string.label_import_csv),
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )

View File

@@ -54,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(
@@ -80,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

@@ -49,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 = 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,6 +78,10 @@ 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(
@@ -78,6 +89,7 @@ fun VocabularyCardHost(
title = stringResource(R.string.item_details), title = stringResource(R.string.item_details),
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },
actions = { actions = {
if (!isEditing) {
// Previous button // Previous button
if (navigationPosition > 0) { if (navigationPosition > 0) {
IconButton(onClick = { IconButton(onClick = {
@@ -117,6 +129,7 @@ fun VocabularyCardHost(
} }
} }
} }
}
) )
} }
) { paddingValues -> ) { paddingValues ->
@@ -133,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
@@ -186,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 },
@@ -196,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) {
@@ -246,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

@@ -284,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)

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,
@@ -505,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() }
@@ -518,37 +470,6 @@ private fun SearchTopAppBar(
AppTopAppBar( AppTopAppBar(
modifier = Modifier.height(56.dp), modifier = Modifier.height(56.dp),
title = "TODO", title = "TODO",
additionalContent = {
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(
@@ -566,8 +487,6 @@ private fun SearchTopAppBar(
@Composable @Composable
fun SearchTopAppBarPreview() { fun SearchTopAppBarPreview() {
SearchTopAppBar( SearchTopAppBar(
searchQuery = stringResource(R.string.search_query),
onQueryChanged = {},
onCloseSearch = {} onCloseSearch = {}
) )
} }
@@ -670,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(
@@ -785,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) }
@@ -931,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

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

@@ -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>

View File

@@ -36,7 +36,6 @@
<string name="title_show_success_message">Mostrar Mensagem de Sucesso</string> <string name="title_show_success_message">Mostrar Mensagem de Sucesso</string>
<string name="label_add_category">Adicionar Categoria</string> <string name="label_add_category">Adicionar Categoria</string>
<string name="title_settings">Configurações</string> <string name="title_settings">Configurações</string>
<string name="title_dashboard">Painel</string>
<string name="title_developer_options">Opções do Desenvolvedor</string> <string name="title_developer_options">Opções do Desenvolvedor</string>
<string name="title_multiple">Múltiplos</string> <string name="title_multiple">Múltiplos</string>
<string name="label_translation_settings">Configurações de Tradução</string> <string name="label_translation_settings">Configurações de Tradução</string>
@@ -61,7 +60,6 @@
<string name="error_no_rows_to_import">Nenhuma linha para importar. Verifique as colunas e o cabeçalho.</string> <string name="error_no_rows_to_import">Nenhuma linha para importar. Verifique as colunas e o cabeçalho.</string>
<string name="info_imported_items_from">%1$d itens de vocabulário importados.</string> <string name="info_imported_items_from">%1$d itens de vocabulário importados.</string>
<string name="label_import">Importar</string> <string name="label_import">Importar</string>
<string name="menu_import_vocabulary">Gerar vocabulário com IA</string>
<string name="menu_create_youtube_exercise">Criar Exercício do YouTube</string> <string name="menu_create_youtube_exercise">Criar Exercício do YouTube</string>
<string name="text_youtube_link">Link do YouTube</string> <string name="text_youtube_link">Link do YouTube</string>
<string name="text_customize_the_intervals">Personalize os intervalos e critérios para mover os cartões de vocabulário. Cartões em estágios iniciais são perguntados com mais frequência.</string> <string name="text_customize_the_intervals">Personalize os intervalos e critérios para mover os cartões de vocabulário. Cartões em estágios iniciais são perguntados com mais frequência.</string>
@@ -83,7 +81,6 @@
<string name="text_shuffle_languages">Embaralhar Idiomas</string> <string name="text_shuffle_languages">Embaralhar Idiomas</string>
<string name="text_training_mode">Modo de Treino</string> <string name="text_training_mode">Modo de Treino</string>
<string name="text_amount_of_cards">Quantidade de cartões</string> <string name="text_amount_of_cards">Quantidade de cartões</string>
<string name="text_interval_settings_in_days">Intervalos (em dias)</string>
<string name="text_label_word">Insira uma palavra</string> <string name="text_label_word">Insira uma palavra</string>
<string name="text_translation">Insira a tradução</string> <string name="text_translation">Insira a tradução</string>
<string name="text_due_today_only">Apenas para hoje</string> <string name="text_due_today_only">Apenas para hoje</string>
@@ -94,10 +91,7 @@
<string name="text_loading_3d">Carregando…</string> <string name="text_loading_3d">Carregando…</string>
<string name="text_show_loading">Mostrar Carregamento</string> <string name="text_show_loading">Mostrar Carregamento</string>
<string name="text_cancel_loading">Cancelar Carregamento</string> <string name="text_cancel_loading">Cancelar Carregamento</string>
<string name="text_sentence_this_is_an_info_message">Esta é uma mensagem informativa.</string>
<string name="text_show_info_message">Mostrar Mensagem Informativa</string> <string name="text_show_info_message">Mostrar Mensagem Informativa</string>
<string name="text_success_em">Sucesso!</string>
<string name="text_sentence_oops_something_went_wrong">Oops! Algo deu errado.</string>
<string name="text_show_error_message">Mostrar Mensagem de Erro</string> <string name="text_show_error_message">Mostrar Mensagem de Erro</string>
<string name="text_reset_intro">Resetar Introdução</string> <string name="text_reset_intro">Resetar Introdução</string>
<string name="text_sentenc_version_information_not_available">Informação de versão não disponível.</string> <string name="text_sentenc_version_information_not_available">Informação de versão não disponível.</string>
@@ -126,7 +120,6 @@
<string name="text_enter_api_key">Inserir Chave de API</string> <string name="text_enter_api_key">Inserir Chave de API</string>
<string name="text_save_key">Salvar Chave</string> <string name="text_save_key">Salvar Chave</string>
<string name="text_select_model">Selecionar Modelo</string> <string name="text_select_model">Selecionar Modelo</string>
<string name="title_title_preview_title">Título de Prévia</string>
<string name="text_none">Nenhum</string> <string name="text_none">Nenhum</string>
<string name="text_manual_vocabulary_list">Lista de vocabulário manual</string> <string name="text_manual_vocabulary_list">Lista de vocabulário manual</string>
<string name="text_filter_all_items">Filtro: Todos os itens</string> <string name="text_filter_all_items">Filtro: Todos os itens</string>
@@ -192,7 +185,6 @@
<string name="text_difficulty_2d">Dificuldade: %1$s</string> <string name="text_difficulty_2d">Dificuldade: %1$s</string>
<string name="text_amount_2d_questions">Quantidade: %1$d Perguntas</string> <string name="text_amount_2d_questions">Quantidade: %1$d Perguntas</string>
<string name="text_generate">Gerar</string> <string name="text_generate">Gerar</string>
<string name="text_let_ai_find_vocabulary_for_you">Deixe a IA encontrar vocabulário para você!</string>
<string name="text_search_term">Termo de Busca</string> <string name="text_search_term">Termo de Busca</string>
<string name="text_select_languages">Selecionar Idiomas</string> <string name="text_select_languages">Selecionar Idiomas</string>
<string name="text_select_amount">Selecionar Quantidade</string> <string name="text_select_amount">Selecionar Quantidade</string>
@@ -223,19 +215,15 @@
<string name="cd_target_met">Meta Atingida</string> <string name="cd_target_met">Meta Atingida</string>
<string name="text_no_vocabulary_due_today">Nenhum Vocabulário para Hoje</string> <string name="text_no_vocabulary_due_today">Nenhum Vocabulário para Hoje</string>
<string name="text_view_all">Ver Todos</string> <string name="text_view_all">Ver Todos</string>
<string name="text_custom_exercise">Exercício Personalizado</string>
<string name="text_daily_exercise">Exercício Diário</string>
<string name="label_total_words">Total de Palavras</string> <string name="label_total_words">Total de Palavras</string>
<string name="label_learned">Aprendidas</string> <string name="label_learned">Aprendidas</string>
<string name="remaining">Restantes</string> <string name="remaining">Restantes</string>
<string name="label_ai_model_and_prompt"><![CDATA[Modelo de IA & Prompt]]></string> <string name="label_ai_model_and_prompt"><![CDATA[Modelo de IA & Prompt]]></string>
<string name="examples">Exemplos</string> <string name="examples">Exemplos</string>
<string name="vocabulary_settings">Configurações de Vocabulário</string>
<string name="label_learning_criteria">Critérios de Aprendizagem</string> <string name="label_learning_criteria">Critérios de Aprendizagem</string>
<string name="min_correct_to_advance">Mín. de Acertos para Avançar</string> <string name="min_correct_to_advance">Mín. de Acertos para Avançar</string>
<string name="max_wrong_to_demote">Máx. de Erros para Regredir</string> <string name="max_wrong_to_demote">Máx. de Erros para Regredir</string>
<string name="daily_learning_goal">Meta de Aprendizagem Diária</string> <string name="label_target_correct_answers_per_day">Meta de Respostas Corretas por Dia</string>
<string name="target_correct_answers_per_day">Meta de Respostas Corretas por Dia</string>
<string name="label_backup_and_restore">Backup e Restauração</string> <string name="label_backup_and_restore">Backup e Restauração</string>
<string name="export_vocabulary_data">Exportar Dados do Vocabulário</string> <string name="export_vocabulary_data">Exportar Dados do Vocabulário</string>
<string name="import_vocabulary_data">Importar Dados do Vocabulário</string> <string name="import_vocabulary_data">Importar Dados do Vocabulário</string>
@@ -334,7 +322,6 @@
<string name="last_incorrect">Último erro: %1$s</string> <string name="last_incorrect">Último erro: %1$s</string>
<string name="correct_answers_">Respostas corretas: %1$d</string> <string name="correct_answers_">Respostas corretas: %1$d</string>
<string name="incorrect_answers">Respostas incorretas: %1$d</string> <string name="incorrect_answers">Respostas incorretas: %1$d</string>
<string name="label_card_with_position">Cartão (%1$d/%2$d)</string>
<string name="item_id">ID do Item: %1$d</string> <string name="item_id">ID do Item: %1$d</string>
<string name="statistics_are_loading">Carregando estatísticas…</string> <string name="statistics_are_loading">Carregando estatísticas…</string>
<string name="to_d">para %1$s</string> <string name="to_d">para %1$s</string>
@@ -365,7 +352,6 @@
<string name="more_actions">Mais ações</string> <string name="more_actions">Mais ações</string>
<string name="select_all">Selecionar Tudo</string> <string name="select_all">Selecionar Tudo</string>
<string name="deselect_all">Desmarcar Tudo</string> <string name="deselect_all">Desmarcar Tudo</string>
<string name="search_vocabulary">Pesquisar vocabulário…</string>
<string name="no_vocabulary_items_found_perhaps_try_changing_the_filters">Nenhum item de vocabulário encontrado. Que tal tentar mudar os filtros?</string> <string name="no_vocabulary_items_found_perhaps_try_changing_the_filters">Nenhum item de vocabulário encontrado. Que tal tentar mudar os filtros?</string>
<string name="label_category_2d">Categoria: %1$s</string> <string name="label_category_2d">Categoria: %1$s</string>
<string name="repository_state_imported_from">Estado do repositório importado de %1$s</string> <string name="repository_state_imported_from">Estado do repositório importado de %1$s</string>
@@ -469,8 +455,6 @@
<string name="text_assign_these_items_2d">Associe estes itens:</string> <string name="text_assign_these_items_2d">Associe estes itens:</string>
<string name="translate_the_following_d">Traduza o seguinte (%1$s):</string> <string name="translate_the_following_d">Traduza o seguinte (%1$s):</string>
<string name="label_your_translation">Sua tradução</string> <string name="label_your_translation">Sua tradução</string>
<string name="this_is_a_hint">Esta é uma dica.</string>
<string name="this_is_the_main_content">Este é o conteúdo principal.</string>
<string name="this_is_the_content_inside_the_card">Este é o conteúdo dentro do cartão.</string> <string name="this_is_the_content_inside_the_card">Este é o conteúdo dentro do cartão.</string>
<string name="primary_button">Botão Primário</string> <string name="primary_button">Botão Primário</string>
<string name="primary_with_icon">Primário com Ícone</string> <string name="primary_with_icon">Primário com Ícone</string>
@@ -488,15 +472,12 @@
<string name="text_base_url_and_example">URL Base (ex: \'http://192.168.0.99:1234/\')</string> <string name="text_base_url_and_example">URL Base (ex: \'http://192.168.0.99:1234/\')</string>
<string name="label_close_selection_mode">Fechar modo de seleção</string> <string name="label_close_selection_mode">Fechar modo de seleção</string>
<string name="d_selected">%1$d Selecionado(s)</string> <string name="d_selected">%1$d Selecionado(s)</string>
<string name="search_query">Termo de pesquisa</string>
<string name="label_close_search">Fechar pesquisa</string> <string name="label_close_search">Fechar pesquisa</string>
<string name="generate_related_vocabulary_items">Gerar itens de vocabulário relacionados</string>
<string name="dismiss">Dispensar</string> <string name="dismiss">Dispensar</string>
<string name="edit_features_for">Editar Recursos para \'%1$s\'</string> <string name="edit_features_for">Editar Recursos para \'%1$s\'</string>
<string name="no_grammar_configuration_found_for_this_language">Nenhuma configuração de gramática encontrada para este idioma.</string> <string name="no_grammar_configuration_found_for_this_language">Nenhuma configuração de gramática encontrada para este idioma.</string>
<string name="word_type">Tipo de Palavra</string> <string name="word_type">Tipo de Palavra</string>
<string name="levels">Níveis</string> <string name="levels">Níveis</string>
<string name="quick_word_pairs">Pares de palavras rápidos</string>
<string name="stage_filter">Filtro de Estágio</string> <string name="stage_filter">Filtro de Estágio</string>
<string name="language_pair">Par de Idiomas</string> <string name="language_pair">Par de Idiomas</string>
<string name="language_filter">Filtro de Idioma</string> <string name="language_filter">Filtro de Idioma</string>
@@ -546,10 +527,6 @@
<string name="friendly">Amigável</string> <string name="friendly">Amigável</string>
<string name="label_academic">Acadêmico</string> <string name="label_academic">Acadêmico</string>
<string name="creative">Criativo</string> <string name="creative">Criativo</string>
<string name="editing_text">Editando Texto: %1$s</string>
<string name="no_text_received">Nenhum texto recebido!</string>
<string name="error_no_text_to_edit">Erro: Nenhum texto para editar</string>
<string name="not_launched_with_text_to_edit">Não iniciado com texto para editar</string>
<string name="text_a_simple_list_to">Uma lista simples para organizar o seu vocabulário manualmente</string> <string name="text_a_simple_list_to">Uma lista simples para organizar o seu vocabulário manualmente</string>
<string name="settings_title_voice">Voz</string> <string name="settings_title_voice">Voz</string>
<string name="default_value">Padrão</string> <string name="default_value">Padrão</string>
@@ -590,21 +567,11 @@
<string name="intro_if_you_need_help_you">Se precisar de ajuda, você pode encontrar dicas em todas as seções do aplicativo.</string> <string name="intro_if_you_need_help_you">Se precisar de ajuda, você pode encontrar dicas em todas as seções do aplicativo.</string>
<string name="text_navigation_bar_labels">Rótulos da Barra de Navegação</string> <string name="text_navigation_bar_labels">Rótulos da Barra de Navegação</string>
<string name="text_show_text_labels_on_the_main_navigation_bar">Mostrar rótulos de texto na barra de navegação principal.</string> <string name="text_show_text_labels_on_the_main_navigation_bar">Mostrar rótulos de texto na barra de navegação principal.</string>
<string name="text_word_pair_settings">Configurações de Pares de Palavras</string>
<string name="text_amount_of_questions_2d">Quantidade de perguntas: %1$d</string>
<string name="text_shuffle_questions">Embaralhar perguntas</string>
<string name="tetx_training_mode">Modo de treino</string>
<string name="text_match_the_pairs">Combine os pares</string>
<string name="text_word_pair_exercise">Exercício de Pares de Palavras</string>
<string name="text_training_mode_description">Modo de treino ativado: respostas não afetarão o progresso.</string> <string name="text_training_mode_description">Modo de treino ativado: respostas não afetarão o progresso.</string>
<string name="text_days">" dias"</string> <string name="text_days">" dias"</string>
<string name="label_add_vocabulary">Adicionar Vocabulário</string> <string name="label_add_vocabulary">Adicionar Vocabulário</string>
<string name="label_create_vocabulary_with_ai">Criar Vocabulário com IA</string>
<string name="text_vocab_empty">Nenhum item de vocabulário encontrado. Adicionar agora?</string>
<string name="text_this_will_remove_all">Isso removerá todos os provedores de API, modelos e chaves de API configurados. Esta ação não pode ser desfeita.</string> <string name="text_this_will_remove_all">Isso removerá todos os provedores de API, modelos e chaves de API configurados. Esta ação não pode ser desfeita.</string>
<string name="text_delete_all_providers_and_models_qm">Excluir todos os provedores e modelos?</string> <string name="text_delete_all_providers_and_models_qm">Excluir todos os provedores e modelos?</string>
<string name="text_swap_sides">Trocar lados</string>
<string name="text_no_progress">Sem progresso</string>
<string name="text_theme_preview">Prévia do Tema</string> <string name="text_theme_preview">Prévia do Tema</string>
<string name="text_sample_word">Palavra de Exemplo</string> <string name="text_sample_word">Palavra de Exemplo</string>
<string name="toggle_use_libretranslate">Usar servidor de Tradução</string> <string name="toggle_use_libretranslate">Usar servidor de Tradução</string>
@@ -687,6 +654,7 @@
<string name="text_due_today_only_description">Apenas perguntar cartas que estão a vencer hoje.</string> <string name="text_due_today_only_description">Apenas perguntar cartas que estão a vencer hoje.</string>
<string name="text_shuffle_card_order_description">Embaralhar Ordem das Cartas</string> <string name="text_shuffle_card_order_description">Embaralhar Ordem das Cartas</string>
<string name="text_shuffle_languages_description">Embaralhar qual idioma vem primeiro. Não afeta as preferências de direção do idioma.</string> <string name="text_shuffle_languages_description">Embaralhar qual idioma vem primeiro. Não afeta as preferências de direção do idioma.</string>
<string name="text_shuffle_languages_disabled_by_direction">Desative a preferência de direção do idioma para habilitar o embaralhamento.</string>
<string name="label_conjugation">Conjugação: %1$s</string> <string name="label_conjugation">Conjugação: %1$s</string>
<string name="label_collapse">Recolher</string> <string name="label_collapse">Recolher</string>
<string name="label_expand">Expandir</string> <string name="label_expand">Expandir</string>
@@ -846,6 +814,7 @@
<string name="text_failed_to_fetch_manifest">Falha ao buscar informações de download sobre dicionários disponíveis: %1$s</string> <string name="text_failed_to_fetch_manifest">Falha ao buscar informações de download sobre dicionários disponíveis: %1$s</string>
<string name="text_translation_instructions">Defina o modelo para tradução e dê instruções opcionais sobre como traduzir.</string> <string name="text_translation_instructions">Defina o modelo para tradução e dê instruções opcionais sobre como traduzir.</string>
<string name="text_language_direction_explanation">Você pode definir uma preferência opcional sobre qual idioma deve vir primeiro ou segundo.</string> <string name="text_language_direction_explanation">Você pode definir uma preferência opcional sobre qual idioma deve vir primeiro ou segundo.</string>
<string name="text_language_direction_disabled_with_pairs">Limpe a seleção de pares de idiomas para escolher uma direção.</string>
<string name="label_all_categories">Todas as Categorias</string> <string name="label_all_categories">Todas as Categorias</string>
<string name="text_description_dictionary_prompt">Defina um modelo para gerar conteúdo do dicionário e dê instruções opcionais.</string> <string name="text_description_dictionary_prompt">Defina um modelo para gerar conteúdo do dicionário e dê instruções opcionais.</string>
<string name="hint_vocabulary_progress_hint_title">Acompanhamento de Progresso de Vocabulário</string> <string name="hint_vocabulary_progress_hint_title">Acompanhamento de Progresso de Vocabulário</string>

View File

@@ -59,24 +59,70 @@
</string-array> </string-array>
<string-array name="motivational_phrases"> <string-array name="motivational_phrases">
<item>Your inner parrot is learning new words! Keep talking! 🦜</item> <item>The limits of your language are the limits of your world. Let\'s expand the cage.</item>
<item>Even parrots start with one word. You\'re doing great! 🌟</item> <item>You\'re basically a meat computer running a language simulation. Time to upgrade your firmware.</item>
<item>Time to spread your wings and learn something new! 🪶</item> <item>To speak another language is to possess a second soul. Or at least a very good party trick.</item>
<item>Your feathered friend is proud of your progress! 💚</item> <item>Someday you will articulate your existential dread in a completely different dialect.</item>
<item>Repeat after me: You\'re getting better every day! 🗣️</item> <item>Neuroplasticity sounds like a fancy surgical procedure, but it\'s just your brain finally doing some heavy lifting.</item>
<item>Fly high! Every new word is a new trick! 🎯</item> <item>Every new word you learn is a concept you steal back from the void.</item>
<item>Your vocabulary is taking flight! Keep it up! 🦜</item> <item>Speak less, say more. Wait, you\'re learning vocabulary—speak terribly, but speak often.</item>
<item>Sing a new song! Learn a new word today! 🎵</item> <item>Today\'s word count is statistically insignificant in the cosmos, but monumental for your ego.</item>
<item>Cartesian dualism suggests mind and body are separate. Let\'s make sure the mind pulls its weight today.</item>
<item>Fluency is just a series of well-disguised mistakes. Go make a few.</item>
<item>Knowledge is the only thing that actually satiates the void. Feed it.</item>
<item>Your short-term memory is a sieve. We are going to force these words into the long-term vault through sheer repetition.</item>
<item>True freedom is knowing exactly which highly specific adjective to use when you silently judge someone.</item>
<item>You are an artist, and your medium is slightly mispronounced nouns.</item>
<item>A hungry stomach growls. A hungry mind just doomscrolls. Learn a word instead.</item>
<item>Spaced repetition: because your brain aggressively attempts to delete everything you don\'t actively use.</item>
<item>Curate your lexicon like a museum where the main exhibit is your own superiority.</item>
<item>Solipsism is the belief that only your mind exists. If so, you should probably give it a better vocabulary.</item>
<item>A person without language is a person without a country. Or just a very quiet tourist.</item>
<item>Building a vocabulary empire, one aggressively misunderstood idiom at a time.</item>
<item>The owl of Minerva spreads its wings only with the falling of the dusk. And after a solid study session.</item>
<item>Escaping the gravity well of monolingualism requires serious thrust. Keep burning fuel.</item>
<item>Language is the operating system of consciousness. Yours is currently installing an update.</item>
<item>We are all in the gutter, but some of us are looking at foreign language dictionaries.</item>
<item>To name a thing is to own it. It\'s time to become an absolute landlord of reality.</item>
<item>Grammar is the underlying structure of reality. Ignore it at your own ontological peril.</item>
<item>Even Socrates knew he knew nothing. But at least he knew the word for \'nothing\' in Greek.</item>
<item>The journey of a thousand miles begins with awkwardly ordering a coffee in the wrong tense.</item>
<item>Your brain consumes roughly twenty percent of your body\'s energy. Make it earn its keep.</item>
<item>Motivation is a fleeting emotion. Habit is a relentless machine. Be the machine.</item>
<item>If words are weapons, your arsenal is currently a collection of dull spoons. Let\'s sharpen them.</item>
<item>To learn is to voluntarily subject yourself to temporary incompetence. Embrace the awkwardness.</item>
<item>Language learning is the myth of Sisyphus, but occasionally the boulder rolls downhill and you actually understand a podcast.</item>
<item>Your future self is currently judging your present work ethic. Prove them wrong.</item>
<item>Entropy dictates that the universe tends toward chaos. Your vocabulary practice is a daily rebellion against physics.</item>
<item>10 words a day equals 3,650 a year. Still not enough to explain the human condition, but a decent start.</item>
<item>Compound interest applies to cognitive assets too. Go invest in some verbs.</item>
<item>You are the sum of your memories. Let\'s make sure those memories aren\'t entirely made up of pop songs and embarrassments.</item>
<item>English is just three different languages standing on each other\'s shoulders in a trench coat. If it can fake it, so can you.</item>
<item>The French word for 99 translates literally to \'four-twenties-ten-nine\'. Your current study struggles are nothing compared to their math.</item>
<item>In Mandarin, using the wrong tone turns \'mother\' into \'horse\'. Let\'s avoid any Oedipal equestrian incidents today.</item>
<item>German lets you smash nouns together until you\'ve constructed a word long enough to express your exact flavor of misery. Aim for that power.</item>
<item>Mastering grammatical gender means accepting that the universe arbitrarily decided a table is feminine and a bridge is masculine. Submit to the absurd.</item>
<item>Japanese has specific counter words depending on whether an object is flat, cylindrical, or a small animal. Precision is a virtue. Start practicing.</item>
<item>Spanish speakers rattle off syllables about 25% faster than English speakers. You don\'t need to be smarter, you just need to think faster.</item>
<item>Finnish has fifteen noun cases. Suddenly, memorizing a few flashcards doesn\'t seem like such a tragedy, does it?</item>
<item>To learn a language is to realize that idioms are just culturally sanctioned, collective hallucinations. Let\'s hallucinate.</item>
<item>Written Welsh looks like someone fell asleep on a vowel-deficient keyboard, yet it possesses a dark, ancient poetry. Find the poetry in your target language.</item>
<item>A robust vocabulary is the most socially acceptable way to be a condescending snob. Earn your arrogance.</item>
<item>You are currently battling the Ebbinghaus forgetting curve. It is a ruthless, unforgiving mathematical reality. Fight back.</item>
<item>Learning Latin is useless unless you plan on arguing with medieval monks or accidentally summoning something. Either way, it\'s good preparation.</item>
<item>Without vocabulary, grammar is just a beautifully structured silence. Give it some noise.</item>
<item>Irish has no words for \'yes\' or \'no\'. You have to repeat the verb back to them. Language is entirely about committing to the bit.</item>
<item>The Hawaiian alphabet only has 13 letters. The language you are learning has more. Stop complaining and memorize them.</item>
<item>Translating word-for-word is a fool\'s errand. Every language maps reality differently. Time to learn a new map.</item>
<item>Someday you will be able to watch a foreign film without staring exclusively at the bottom third of the screen. Keep going.</item>
</string-array> </string-array>
<string-array name="changelog_entries"> <string-array name="changelog_entries">
<item>Version 0.3.0 \n• Enabled CSV Import for Vocabulary\n• Option to use a translation server for translations instead of AI models for some supported langugaes\n• UI bug fixes \n• Show word frequency \n• Performance optimizations \n• Improved translations (German and Portuguese)</item> <item>Version 0.3.0 \n• Enabled CSV Import for Vocabulary\n• Option to use a translation server for translations instead of AI models for some supported langugaes\n• UI bug fixes \n• Show word frequency \n• Performance optimizations \n• Improved translations (German and Portuguese)</item>
<item>Version 0.4.0 \n• Added dictionary download (beta) \n• UI enhancements \n• Bugfixes \n• Re-designed vocabulary card with improved UI \n• More pre-configured providers \n• Improved performance</item> <item>Version 0.4.0 \n• Added dictionary download (beta) \n• UI enhancements \n• Bugfixes \n• Re-designed vocabulary card with improved UI \n• More pre-configured providers \n• Improved performance</item>
<item>Version 0.5.0 \n• Reworked UI with new focus on Flashcards and Exercises \n• Adding vocabulary is easier and more intuitive now </item> <item>Version 0.5.0 \n• Reworked UI with new focus on Flashcards and Exercises \n• Adding vocabulary is easier and more intuitive now \n• Exercises are more fun now </item>
<item> </item> <item> </item>
</string-array> </string-array>
</resources> </resources>

View File

@@ -57,8 +57,6 @@
<string name="d_selected">%1$d Selected</string> <string name="d_selected">%1$d Selected</string>
<string name="d_the_quick_brown_fox_jumps_over_the_lazy_dog">%1$s: The quick brown fox jumps over the lazy dog.</string> <string name="d_the_quick_brown_fox_jumps_over_the_lazy_dog">%1$s: The quick brown fox jumps over the lazy dog.</string>
<string name="daily_learning_goal">Daily Learning Goal</string>
<string name="danger_zone">Danger Zone</string> <string name="danger_zone">Danger Zone</string>
<string name="days_2d">%1$d days</string> <string name="days_2d">%1$d days</string>
@@ -96,8 +94,6 @@
<string name="edit">Edit</string> <string name="edit">Edit</string>
<string name="edit_features_for">Edit Features for \'%1$s\'</string> <string name="edit_features_for">Edit Features for \'%1$s\'</string>
<string name="editing_text">Editing Text: %1$s</string>
<string name="email_address">Email Address</string> <string name="email_address">Email Address</string>
<string name="email_log">Email Log</string> <string name="email_log">Email Log</string>
@@ -106,7 +102,6 @@
<string name="endpoint_e_g_api_chat">Endpoint (e.g., /v1/chat/completions/)</string> <string name="endpoint_e_g_api_chat">Endpoint (e.g., /v1/chat/completions/)</string>
<string name="error_no_rows_to_import">No rows to import. Please check the selected columns and header row.</string> <string name="error_no_rows_to_import">No rows to import. Please check the selected columns and header row.</string>
<string name="error_no_text_to_edit">Error: No text to edit</string>
<string name="error_parsing_table">Error parsing table</string> <string name="error_parsing_table">Error parsing table</string>
<string name="error_parsing_table_with_reason">Error parsing table: %1$s</string> <string name="error_parsing_table_with_reason">Error parsing table: %1$s</string>
<string name="error_select_languages">Please select two languages.</string> <string name="error_select_languages">Please select two languages.</string>
@@ -160,8 +155,6 @@
<string name="general_settings">General Settings</string> <string name="general_settings">General Settings</string>
<string name="generate_related_vocabulary_items">Generate related vocabulary items</string>
<string name="get_started">Get Started</string> <string name="get_started">Get Started</string>
<string name="got_it">Got it!</string> <string name="got_it">Got it!</string>
@@ -225,13 +218,11 @@
<string name="label_amount_models">%1$d models</string> <string name="label_amount_models">%1$d models</string>
<string name="label_analyze_grammar">Analyze Grammar</string> <string name="label_analyze_grammar">Analyze Grammar</string>
<string name="label_appearance">Appearance</string> <string name="label_appearance">Appearance</string>
<string name="hint_settings_title_help">Help</string>
<string name="label_apply_filters">Apply Filters</string> <string name="label_apply_filters">Apply Filters</string>
<string name="label_article">Article</string> <string name="label_article">Article</string>
<string name="label_backup_and_restore">Backup and Restore</string> <string name="label_backup_and_restore">Backup and Restore</string>
<string name="label_by_language">By Language</string> <string name="label_by_language">By Language</string>
<string name="label_cancel">Cancel</string> <string name="label_cancel">Cancel</string>
<string name="label_card_with_position">Card (%1$d/%2$d)</string>
<string name="label_casual">Casual</string> <string name="label_casual">Casual</string>
<string name="label_categories">Categories</string> <string name="label_categories">Categories</string>
<string name="label_category">Category</string> <string name="label_category">Category</string>
@@ -249,7 +240,6 @@
<string name="label_continue">Continue</string> <string name="label_continue">Continue</string>
<string name="label_correct">Correct</string> <string name="label_correct">Correct</string>
<string name="label_create_exercise">Create Exercise</string> <string name="label_create_exercise">Create Exercise</string>
<string name="label_create_vocabulary_with_ai">Create Vocabulary with AI</string>
<string name="label_custom">Custom</string> <string name="label_custom">Custom</string>
<string name="label_definitions">Definitions</string> <string name="label_definitions">Definitions</string>
<string name="label_delete">Delete</string> <string name="label_delete">Delete</string>
@@ -418,7 +408,6 @@
<string name="max_wrong_to_demote">Max Wrong to Demote</string> <string name="max_wrong_to_demote">Max Wrong to Demote</string>
<string name="menu_create_youtube_exercise">Create YouTube Exercise</string> <string name="menu_create_youtube_exercise">Create YouTube Exercise</string>
<string name="menu_import_vocabulary">Generate vocabulary with AI</string>
<string name="merge">Merge</string> <string name="merge">Merge</string>
<string name="merge_items">Merge Items</string> <string name="merge_items">Merge Items</string>
@@ -461,11 +450,9 @@
<string name="no_models_configured">No Models Configured</string> <string name="no_models_configured">No Models Configured</string>
<string name="no_models_found">No models found</string> <string name="no_models_found">No models found</string>
<string name="no_new_vocabulary_to_sort">No New Vocabulary to Sort</string> <string name="no_new_vocabulary_to_sort">No New Vocabulary to Sort</string>
<string name="no_text_received">No text received!</string>
<string name="no_vocabulary_items_found_perhaps_try_changing_the_filters">No vocabulary items found. Perhaps try changing the filters?</string> <string name="no_vocabulary_items_found_perhaps_try_changing_the_filters">No vocabulary items found. Perhaps try changing the filters?</string>
<string name="not_available">Not available</string> <string name="not_available">Not available</string>
<string name="not_launched_with_text_to_edit">Not launched with text to edit</string>
<string name="number_of_cards">Number of Cards: %1$d / %2$d</string> <string name="number_of_cards">Number of Cards: %1$d / %2$d</string>
@@ -563,8 +550,6 @@
<string name="questions">%1$d questions</string> <string name="questions">%1$d questions</string>
<string name="quick_word_pairs">Quick word pairs</string>
<string name="quit">Quit</string> <string name="quit">Quit</string>
<string name="refresh_word_of_the_day">Refresh Word of the Day</string> <string name="refresh_word_of_the_day">Refresh Word of the Day</string>
@@ -591,8 +576,6 @@
<string name="search_for_a_word_s_origin">Search for a word\'s origin</string> <string name="search_for_a_word_s_origin">Search for a word\'s origin</string>
<string name="search_models">Search Models</string> <string name="search_models">Search Models</string>
<string name="search_query">Search query</string>
<string name="search_vocabulary">Search vocabulary…</string>
<string name="secondary_button">Secondary Button</string> <string name="secondary_button">Secondary Button</string>
<string name="secondary_inverse">Secondary Inverse</string> <string name="secondary_inverse">Secondary Inverse</string>
@@ -673,12 +656,10 @@
<string name="tap_the_words_below_to_form_the_sentence">Tap the words below to form the sentence</string> <string name="tap_the_words_below_to_form_the_sentence">Tap the words below to form the sentence</string>
<string name="target_correct_answers_per_day">Target Correct Answers Per Day</string> <string name="label_target_correct_answers_per_day">Target Correct Answers Per Day</string>
<string name="test">Test</string> <string name="test">Test</string>
<string name="tetx_training_mode">Training mode</string>
<string name="text_200_ok">200 OK</string> <string name="text_200_ok">200 OK</string>
<string name="text_2d_categories_selected">%1$d categories selected</string> <string name="text_2d_categories_selected">%1$d categories selected</string>
<string name="text_2d_languages_selected">%1$d Languages Selected</string> <string name="text_2d_languages_selected">%1$d Languages Selected</string>
@@ -702,7 +683,6 @@
<string name="text_amount_2d">Amount: %1$d</string> <string name="text_amount_2d">Amount: %1$d</string>
<string name="text_amount_2d_questions">Amount: %1$d Questions</string> <string name="text_amount_2d_questions">Amount: %1$d Questions</string>
<string name="text_amount_of_cards">Amount of cards</string> <string name="text_amount_of_cards">Amount of cards</string>
<string name="text_amount_of_questions_2d">Amount of questions: %1$d</string>
<string name="text_an_unexpected_condition_was_encountered_on_the_server">An unexpected condition was encountered on the server.</string> <string name="text_an_unexpected_condition_was_encountered_on_the_server">An unexpected condition was encountered on the server.</string>
<string name="text_an_unknown_error_occurred">An unknown error occurred.</string> <string name="text_an_unknown_error_occurred">An unknown error occurred.</string>
<string name="text_and_many_more">And many more! …</string> <string name="text_and_many_more">And many more! …</string>
@@ -741,9 +721,7 @@
<string name="text_copy_corrected_text">Copy corrected text</string> <string name="text_copy_corrected_text">Copy corrected text</string>
<string name="text_correct_em">Correct!</string> <string name="text_correct_em">Correct!</string>
<string name="text_could_not_fetch_a_new_word">Could not fetch a new word.</string> <string name="text_could_not_fetch_a_new_word">Could not fetch a new word.</string>
<string name="text_custom_exercise">Custom Exercise</string>
<string name="text_customize_the_intervals">Customize the intervals and criteria for moving vocabulary cards between stages. Cards in lower stages should be asked more often than those in higher stages.</string> <string name="text_customize_the_intervals">Customize the intervals and criteria for moving vocabulary cards between stages. Cards in lower stages should be asked more often than those in higher stages.</string>
<string name="text_daily_exercise">Daily Exercise</string>
<string name="text_daily_goal_description">How many words do you want to answer correctly each day?</string> <string name="text_daily_goal_description">How many words do you want to answer correctly each day?</string>
<string name="text_dark">Dark</string> <string name="text_dark">Dark</string>
<string name="text_day_streak">Day Streak</string> <string name="text_day_streak">Day Streak</string>
@@ -818,20 +796,19 @@
<string name="text_in_progress">In Progress</string> <string name="text_in_progress">In Progress</string>
<string name="text_incorrect_em">Incorrect!</string> <string name="text_incorrect_em">Incorrect!</string>
<string name="text_infrequent">Rare</string> <string name="text_infrequent">Rare</string>
<string name="text_interval_settings_in_days">Interval Settings (in days)</string> <string name="label_interval_settings_in_days">Interval Settings</string>
<string name="text_key_active">Key Active</string> <string name="text_key_active">Key Active</string>
<string name="text_key_optional">Key Optional</string> <string name="text_key_optional">Key Optional</string>
<string name="text_label_word">Enter a word\n</string> <string name="text_label_word">Enter a word\n</string>
<string name="text_language_code">Language Code</string> <string name="text_language_code">Language Code</string>
<string name="text_language_direction_explanation">You can set an optional preference which language should come first or second.</string> <string name="text_language_direction_explanation">You can set an optional preference which language should come first or second.</string>
<string name="text_language_direction_disabled_with_pairs">Clear language pair selection to choose a direction.</string>
<string name="text_language_options">Language Options</string> <string name="text_language_options">Language Options</string>
<string name="text_last_7_days">Last 7 Days</string> <string name="text_last_7_days">Last 7 Days</string>
<string name="text_let_ai_find_vocabulary_for_you">Let AI find vocabulary for you!</string>
<string name="text_light">Light</string> <string name="text_light">Light</string>
<string name="text_list">List</string> <string name="text_list">List</string>
<string name="text_loading_3d">Loading…</string> <string name="text_loading_3d">Loading…</string>
<string name="text_manual_vocabulary_list">Manual vocabulary list</string> <string name="text_manual_vocabulary_list">Manual vocabulary list</string>
<string name="text_match_the_pairs">Match the pairs</string>
<string name="text_mismatch_between_question_ids_in_exercise_and_questions_found_in_repository">Mismatch between question IDs in exercise and questions found in repository.</string> <string name="text_mismatch_between_question_ids_in_exercise_and_questions_found_in_repository">Mismatch between question IDs in exercise and questions found in repository.</string>
<string name="text_mistral">Mistral</string> <string name="text_mistral">Mistral</string>
<string name="text_more_options">More options</string> <string name="text_more_options">More options</string>
@@ -845,7 +822,6 @@
<string name="text_no_items_available">No items available</string> <string name="text_no_items_available">No items available</string>
<string name="text_no_key">No Key</string> <string name="text_no_key">No Key</string>
<string name="text_no_models_found">No models found</string> <string name="text_no_models_found">No models found</string>
<string name="text_no_progress">No progress</string>
<string name="text_no_valid_api_configuration_could_be_found">No valid API configuration could be found in the settings. Before using this app, you have to configure at least one API provider.</string> <string name="text_no_valid_api_configuration_could_be_found">No valid API configuration could be found in the settings. Before using this app, you have to configure at least one API provider.</string>
<string name="text_no_vocabulary_available">No vocabulary available.</string> <string name="text_no_vocabulary_available">No vocabulary available.</string>
<string name="text_no_vocabulary_due_today">No Vocabulary Due Today</string> <string name="text_no_vocabulary_due_today">No Vocabulary Due Today</string>
@@ -890,8 +866,6 @@
<string name="text_select_translations_to_add">Select Translations to Add</string> <string name="text_select_translations_to_add">Select Translations to Add</string>
<string name="text_selected">Selected</string> <string name="text_selected">Selected</string>
<string name="text_sentenc_version_information_not_available">Version information not available.</string> <string name="text_sentenc_version_information_not_available">Version information not available.</string>
<string name="text_sentence_oops_something_went_wrong">Oops! Something went wrong.</string>
<string name="text_sentence_this_is_an_info_message">This is an info message.</string>
<string name="text_show_error_message">Show Error Message</string> <string name="text_show_error_message">Show Error Message</string>
<string name="text_show_info_message">Show Info Message</string> <string name="text_show_info_message">Show Info Message</string>
<string name="text_show_loading">Show Loading</string> <string name="text_show_loading">Show Loading</string>
@@ -900,12 +874,10 @@
<string name="text_shuffle_card_order_description">Shuffle Card Order</string> <string name="text_shuffle_card_order_description">Shuffle Card Order</string>
<string name="text_shuffle_languages">Shuffle Languages</string> <string name="text_shuffle_languages">Shuffle Languages</string>
<string name="text_shuffle_languages_description">Shuffle what language comes first. Does not affect language direction preferences.</string> <string name="text_shuffle_languages_description">Shuffle what language comes first. Does not affect language direction preferences.</string>
<string name="text_shuffle_questions">Shuffle questions</string> <string name="text_shuffle_languages_disabled_by_direction">Disable language direction preference to enable shuffling.</string>
<string name="text_some_items_are_in_the_wrong_category">Some items are in the wrong category.</string> <string name="text_some_items_are_in_the_wrong_category">Some items are in the wrong category.</string>
<string name="text_stage_2d">Stage %1$s</string> <string name="text_stage_2d">Stage %1$s</string>
<string name="text_start_over">Start Over</string> <string name="text_start_over">Start Over</string>
<string name="text_success_em">Success!</string>
<string name="text_swap_sides">Swap sides</string>
<string name="text_text">Text</string> <string name="text_text">Text</string>
<string name="text_that_s_not_quite_right">That\'s not quite right.</string> <string name="text_that_s_not_quite_right">That\'s not quite right.</string>
<string name="text_the_correct_answer_is_2d">The correct answer is:</string> <string name="text_the_correct_answer_is_2d">The correct answer is:</string>
@@ -932,13 +904,10 @@
<string name="text_very_frequent">Very Frequent</string> <string name="text_very_frequent">Very Frequent</string>
<string name="text_view_all">View All</string> <string name="text_view_all">View All</string>
<string name="text_visit_my_website">Visit my website</string> <string name="text_visit_my_website">Visit my website</string>
<string name="text_vocab_empty">No Vocabulary Items could be found. Add now?</string>
<string name="text_vocabulary_prompt">Vocabulary Prompt</string> <string name="text_vocabulary_prompt">Vocabulary Prompt</string>
<string name="text_watch_video_again">Watch Video Again</string> <string name="text_watch_video_again">Watch Video Again</string>
<string name="text_widget_title_weekly_activity">Weekly Activity</string> <string name="text_widget_title_weekly_activity">Weekly Activity</string>
<string name="text_word_of_the_day">Word of the Day</string> <string name="text_word_of_the_day">Word of the Day</string>
<string name="text_word_pair_exercise">Word Pair Exercise</string>
<string name="text_word_pair_settings">Word Pair Settings</string>
<string name="text_your_own_ai">Your Own AI</string> <string name="text_your_own_ai">Your Own AI</string>
<string name="text_youtube_link">YouTube Link</string> <string name="text_youtube_link">YouTube Link</string>
@@ -947,16 +916,13 @@
<string name="the_server_could_not_understand_the_request">The server could not understand the request.</string> <string name="the_server_could_not_understand_the_request">The server could not understand the request.</string>
<string name="the_server_understood_the_request_but_is_refusing_to_authorize_it">The server understood the request, but is refusing to authorize it.</string> <string name="the_server_understood_the_request_but_is_refusing_to_authorize_it">The server understood the request, but is refusing to authorize it.</string>
<string name="this_is_a_hint">This is a hint.</string>
<string name="this_is_a_sample_output_text">This is a sample output text.</string> <string name="this_is_a_sample_output_text">This is a sample output text.</string>
<string name="this_is_the_content_inside_the_card">This is the content inside the card.</string> <string name="this_is_the_content_inside_the_card">This is the content inside the card.</string>
<string name="this_is_the_main_content">This is the main content.</string>
<string name="this_mode_will_not_affect_your_progress_in_stages">This mode will not affect your progress in stages.</string> <string name="this_mode_will_not_affect_your_progress_in_stages">This mode will not affect your progress in stages.</string>
<string name="timeout">Timeout</string> <string name="timeout">Timeout</string>
<string name="title_corrector">Corrector</string> <string name="title_corrector">Corrector</string>
<string name="title_dashboard">Dashboard</string>
<string name="title_developer_options">Developer Options</string> <string name="title_developer_options">Developer Options</string>
<string name="title_http_status_codes">HTTP Status Codes</string> <string name="title_http_status_codes">HTTP Status Codes</string>
<string name="title_items_without_grammar">Items Without Grammar</string> <string name="title_items_without_grammar">Items Without Grammar</string>
@@ -964,7 +930,6 @@
<string name="title_settings">Settings</string> <string name="title_settings">Settings</string>
<string name="title_show_success_message">Show Success Message</string> <string name="title_show_success_message">Show Success Message</string>
<string name="title_single">Single</string> <string name="title_single">Single</string>
<string name="title_title_preview_title">Preview Title</string>
<string name="title_widget_due_today">Due Today</string> <string name="title_widget_due_today">Due Today</string>
<string name="title_widget_streak">Streak</string> <string name="title_widget_streak">Streak</string>
@@ -988,7 +953,7 @@
<string name="vocabulary_added_successfully">Vocabulary Added</string> <string name="vocabulary_added_successfully">Vocabulary Added</string>
<string name="vocabulary_repository">Vocabulary Repository</string> <string name="vocabulary_repository">Vocabulary Repository</string>
<string name="vocabulary_settings">Vocabulary Settings</string> <string name="label_vocabulary_settings">Progress Settings</string>
<string name="website_url">Website URL</string> <string name="website_url">Website URL</string>
@@ -1041,7 +1006,6 @@
<string name="hint_scan_hint_title">Finding the right AI model</string> <string name="hint_scan_hint_title">Finding the right AI model</string>
<string name="hint_translate_how_it_works">How translation works</string> <string name="hint_translate_how_it_works">How translation works</string>
<string name="label_no_category">None</string> <string name="label_no_category">None</string>
<string name="text_select">Select</string>
<string name="text_search">Search</string> <string name="text_search">Search</string>
<string name="text_language_settings_description">Set what languages you want to use in the app. Languages that are not activated will not appear in this app. You can also add your own language to the list, or change an existing language (region/locale)</string> <string name="text_language_settings_description">Set what languages you want to use in the app. Languages that are not activated will not appear in this app. You can also add your own language to the list, or change an existing language (region/locale)</string>
@@ -1117,6 +1081,27 @@
<string name="message_test_error">Oops, something went wrong :(</string> <string name="message_test_error">Oops, something went wrong :(</string>
<string name="label_stats">Stats</string> <string name="label_stats">Stats</string>
<string name="label_library">Library</string> <string name="label_library">Library</string>
<string name="label_legacy_vocabulary">Legacy Vocabulary</string>
<string name="label_edit">Edit</string> <string name="label_edit">Edit</string>
<string name="label_new_words">New Words</string>
<string name="desc_expand_your_vocabulary">Expand your vocabulary</string>
<string name="label_settings">Settings</string>
<string name="label_2d_days">%1$d Days</string>
<string name="label_current_streak">Current Streak</string>
<string name="label_daily_goal">Daily Goal</string>
<string name="text_desc_no_activity_data_available">No activity data available</string>
<string name="label_see_history">See History</string>
<string name="label_weekly_progress">Weekly Progress</string>
<string name="cd_go">Go</string>
<string name="label_sort_by">Sort By</string>
<string name="label_reset">Reset</string>
<string name="label_filter_cards">Filter Cards</string>
<string name="text_desc_organize_vocabulary_groups">Organize Your Vocabulary in Groups</string>
<string name="text_add_new_word_to_list">Extract a New Word to Your List</string>
<string name="cd_scroll_to_top">Scroll to top</string>
<string name="cd_settings">Settings</string>
<string name="label_import_csv">Import CSV</string>
<string name="label_ai_generator">AI Generator</string>
<string name="label_new_wordss">New Words</string>
<string name="label_recently_added">Recently Added</string>
<string name="label_view_all">View All</string>
</resources> </resources>

View File

@@ -1,511 +0,0 @@
@file:Suppress("HardCodedStringLiteral")
package eu.gaudian.translator.utils
import android.content.Context
import eu.gaudian.translator.model.Language
import eu.gaudian.translator.model.communication.ApiManager
import eu.gaudian.translator.model.communication.ModelType
import eu.gaudian.translator.utils.dictionary.DictionaryService
import io.mockk.every
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
class JsonHelperTest {
private lateinit var jsonHelper: JsonHelper
@Before
fun setup() {
jsonHelper = JsonHelper()
}
@Test
fun `parseJson should successfully parse valid JSON`() = runTest {
// Given
val validJson = """{"word": "hello", "parts": [{"title": "Definition", "content": "A greeting"}]}"""
// When & Then
// This test would need proper serializer setup
// For now, just test the validation
assertTrue(jsonHelper.validateRequiredFields(validJson, listOf("word", "parts")))
}
@Test
fun `validateRequiredFields should return false for missing fields`() {
// Given
val incompleteJson = """{"word": "hello"}"""
val requiredFields = listOf("word", "parts")
// When
val result = jsonHelper.validateRequiredFields(incompleteJson, requiredFields)
// Then
assertFalse(result)
}
@Test
fun `validateRequiredFields should return true for complete fields`() {
// Given
val completeJson = """{"word": "hello", "parts": []}"""
val requiredFields = listOf("word", "parts")
// When
val result = jsonHelper.validateRequiredFields(completeJson, requiredFields)
// Then
assertTrue(result)
}
@Test
fun `extractField should return correct value`() {
// Given
val json = """{"word": "hello", "parts": []}"""
// When
val result = jsonHelper.extractField(json, "word")
// Then
assertEquals("hello", result)
}
@Test
fun `extractField should return null for missing field`() {
// Given
val json = """{"word": "hello"}"""
// When
val result = jsonHelper.extractField(json, "missing")
// Then
assertEquals(null, result)
}
}
class ApiRequestHandlerTest {
private lateinit var apiManager: ApiManager
private lateinit var context: Context
private lateinit var apiRequestHandler: ApiRequestHandler
@Before
fun setup() {
apiManager = mockk(relaxed = true)
context = mockk(relaxed = true)
apiRequestHandler = ApiRequestHandler(apiManager, context)
}
@Test
fun `executeRequest with template should handle successful response`() = runTest {
// Given
val template = DictionaryDefinitionRequest(
word = "test",
language = "English",
requestedParts = "Definition"
)
// Mock the API manager response
every {
apiManager.getCompletion(
prompt = any(),
callback = any(),
modelType = ModelType.DICTIONARY
)
} answers {
val callback = thirdArg<(String?) -> Unit>()
callback("""{"word": "test", "parts": [{"title": "Definition", "content": "A test"}]}""")
}
// When
val result = apiRequestHandler.executeRequest(template)
// Then
assertTrue(result.isSuccess)
result.getOrNull()?.let { response ->
assertEquals("test", response.word)
assertEquals(1, response.parts.size)
}
}
@Test
fun `executeRequest with template should handle API failure`() = runTest {
// Given
val template = DictionaryDefinitionRequest(
word = "test",
language = "English",
requestedParts = "Definition"
)
every {
apiManager.getCompletion(
prompt = any(),
callback = any(),
modelType = ModelType.DICTIONARY
)
} answers {
val callback = secondArg<(String) -> Unit>()
callback("API Error")
}
// When
val result = apiRequestHandler.executeRequest(template)
// Then
assertTrue(result.isFailure)
}
}
class ApiRequestTemplatesTest {
@Test
fun `DictionaryDefinitionRequest should build correct prompt`() {
// Given
val template = DictionaryDefinitionRequest(
word = "hello",
language = "English",
requestedParts = "Definition, Origin"
)
// When
val prompt = template.buildPrompt()
// Then
assertTrue(prompt.contains("hello"))
assertTrue(prompt.contains("English"))
assertTrue(prompt.contains("Definition, Origin"))
assertTrue(prompt.contains("JSON object"))
}
@Test
fun `VocabularyTranslationRequest should build correct prompt`() {
// Given
val words = listOf("hello", "world")
val template = VocabularyTranslationRequest(
words = words,
languageFirst = "English",
languageSecond = "Spanish"
)
// When
val prompt = template.buildPrompt()
// Then
assertTrue(prompt.contains("English"))
assertTrue(prompt.contains("Spanish"))
assertTrue(prompt.contains("hello"))
assertTrue(prompt.contains("world"))
assertTrue(prompt.contains("flashcards"))
}
@Test
fun `TextCorrectionRequest should build correct prompt`() {
// Given
val template = TextCorrectionRequest(
textToCorrect = "Helo world",
language = "English",
grammarOnly = true,
tone = null
)
// When
val prompt = template.buildPrompt()
// Then
assertTrue(prompt.contains("Helo world"))
assertTrue(prompt.contains("English"))
assertTrue(prompt.contains("grammar, spelling, and punctuation"))
assertTrue(prompt.contains("correctedText"))
assertTrue(prompt.contains("explanation"))
}
@Test
fun `SynonymGenerationRequest should build correct prompt`() {
// Given
val template = SynonymGenerationRequest(
amount = 5,
language = "English",
term = "happy",
translation = "feliz",
translationLanguage = "Spanish",
languageCode = "en"
)
// When
val prompt = template.buildPrompt()
// Then
assertTrue(prompt.contains("happy"))
assertTrue(prompt.contains("feliz"))
assertTrue(prompt.contains("English"))
assertTrue(prompt.contains("Spanish"))
assertTrue(prompt.contains("synonyms"))
assertTrue(prompt.contains("proximity"))
}
}
class DictionaryServiceTest {
private lateinit var context: Context
private lateinit var dictionaryService: DictionaryService
@Before
fun setup() {
context = mockk(relaxed = true)
dictionaryService = DictionaryService(context)
}
@Test
fun `searchDefinition should handle successful response`() = runTest {
// This test would require mocking the ApiRequestHandler
// For now, just verify the method exists and basic structure
val language = Language(
name = "English",
nameResId = 1,
code = "en",
englishName = "English",
region = ""
)
// When & Then
// This would need proper mocking setup
assertNotNull(language)
assertEquals("English", language.name)
}
@Test
fun `getExampleSentence should handle successful response`() = runTest {
val languageFirst = Language(
name = "English",
nameResId = 1,
code = "en",
englishName = "English",
region = ""
)
val languageSecond = Language(
name = "Spanish",
nameResId = 2,
code = "es",
englishName = "Spanish",
region = ""
)
// When & Then
assertNotNull(languageFirst)
assertNotNull(languageSecond)
}
}
class VocabularyServiceTest {
private lateinit var context: Context
private lateinit var vocabularyService: VocabularyService
@Before
fun setup() {
context = mockk(relaxed = true)
vocabularyService = VocabularyService(context)
}
@Test
fun `translateWordsBatch should handle empty list`() = runTest {
// Given
val emptyWords = emptyList<String>()
val languageFirst = Language(
name = "English",
nameResId = 1,
code = "en",
englishName = "English",
region = ""
)
val languageSecond = Language(
name = "Spanish",
nameResId = 2,
code = "es",
englishName = "Spanish",
region = ""
)
// When
val result = vocabularyService.translateWordsBatch(emptyWords, languageFirst, languageSecond)
// Then
assertTrue(result.isSuccess)
assertEquals(0, result.getOrNull()?.size)
}
@Test
fun `translateWordsBatch should filter blank words`() = runTest {
// Given
val wordsWithBlanks = listOf("hello", "", "world", " ")
// When & Then
// This would need proper mocking setup for actual API calls
assertEquals(2, wordsWithBlanks.filter { it.isNotBlank() }.size)
}
@Test
fun `generateSynonyms should use correct parameters`() = runTest {
// Given
val language = Language(
name = "English",
nameResId = 1,
code = "en",
englishName = "English",
region = ""
)
val translationLanguage = Language(
name = "Spanish",
nameResId = 2,
code = "es",
englishName = "Spanish",
region = ""
)
// When & Then
// This would need proper mocking setup for actual API calls
assertNotNull(language)
assertNotNull(translationLanguage)
assertEquals("English", language.englishName)
assertEquals("Spanish", translationLanguage.englishName)
}
}
class TranslationServiceTest {
private lateinit var context: Context
private lateinit var translationService: TranslationService
@Before
fun setup() {
context = mockk(relaxed = true)
translationService = TranslationService(context)
}
@Test
fun `simpleTranslateTo should handle basic translation`() = runTest {
// Given
val targetLanguage = Language(
name = "Spanish",
nameResId = 1,
code = "es",
englishName = "Spanish",
region = ""
)
// When & Then
// This would need proper mocking setup for actual API calls
assertNotNull(targetLanguage)
assertEquals("Spanish", targetLanguage.name)
}
@Test
fun `translateSentence should handle sentence translation`() = runTest {
// Given
val sentence = "Hello world"
// When & Then
// This would need proper mocking setup for actual API calls
assertNotNull(sentence)
assertEquals("Hello world", sentence)
}
}
class CorrectionServiceTest {
private lateinit var context: Context
private lateinit var correctionService: CorrectionService
@Before
fun setup() {
context = mockk(relaxed = true)
correctionService = CorrectionService(context)
}
@Test
fun `correctText should handle basic correction`() = runTest {
// Given
val language = Language(
name = "English",
nameResId = 1,
code = "en",
englishName = "English",
region = ""
)
// When & Then
// This would need proper mocking setup for actual API calls
assertNotNull(language)
assertEquals("English", language.name)
}
@Test
fun `correctText should handle grammar only mode`() = runTest {
// Given
val textToCorrect = "Helo world"
val language = Language(
name = "English",
nameResId = 1,
code = "en",
englishName = "English",
region = ""
)
// When & Then
// This would need proper mocking setup for actual API calls
assertNotNull(textToCorrect)
assertNotNull(language)
assertEquals("Helo world", textToCorrect)
}
}
// Integration test example
class ApiArchitectureIntegrationTest {
@Test
fun `end to end dictionary lookup should work`() = runTest {
// This would be an integration test that tests the full flow
// from service -> template -> API handler -> JSON parsing
// Given
val mockContext = mockk<Context>(relaxed = true)
val mockApiManager = mockk<ApiManager>(relaxed = true)
// Setup mock API response
every {
mockApiManager.getCompletion(
prompt = any(),
callback = any(),
modelType = ModelType.DICTIONARY
)
} answers {
val callback = thirdArg<(String?) -> Unit>()
callback("""{"word": "test", "parts": [{"title": "Definition", "content": "A test word"}]}""")
}
// When
val apiHandler = ApiRequestHandler(mockApiManager, mockContext)
val template = DictionaryDefinitionRequest(
word = "test",
language = "English",
requestedParts = "Definition"
)
val result = apiHandler.executeRequest(template)
// Then
assertTrue(result.isSuccess)
result.getOrNull()?.let { response ->
assertEquals("test", response.word)
assertEquals(1, response.parts.size)
assertEquals("Definition", response.parts[0].title)
}
}
}

View File

@@ -1,36 +0,0 @@
package eu.gaudian.translator.utils
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.junit.rules.TestWatcher
import org.junit.runner.Description
/**
* Test rule for setting up coroutine testing environment
*/
class CoroutineTestRule @OptIn(ExperimentalCoroutinesApi::class) constructor(
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() {
lateinit var testScope: TestScope
private set
override fun starting(description: Description) {
super.starting(description)
testScope = TestScope(testDispatcher)
}
}
/**
* Base test class for API architecture tests
*/
abstract class BaseApiTest {
@get:org.junit.Rule
val coroutineRule = CoroutineTestRule()
protected val testDispatcher = coroutineRule.testDispatcher
protected val testScope = coroutineRule.testScope
}

View File

@@ -1,323 +0,0 @@
@file:Suppress("HardCodedStringLiteral")
package eu.gaudian.translator.utils
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertTrue
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.junit.Before
import org.junit.Test
class JsonHelperComprehensiveTest {
private lateinit var jsonHelper: JsonHelper
private val jsonParser = Json { ignoreUnknownKeys = true; isLenient = true }
@Before
fun setup() {
jsonHelper = JsonHelper()
}
@Test
fun `parseJson should handle valid DictionaryApiResponse`() {
// Given
val validJson = """{"word": "hello", "parts": [{"title": "Definition", "content": "A greeting"}]}"""
// When
val result = jsonHelper.validateRequiredFields(validJson, listOf("word", "parts"))
// Then
assertTrue(result)
}
@Test
fun `parseJson should handle valid VocabularyApiResponse`() {
// Given
val validJson = """{"flashcards": [{"front": {"language": "English", "word": "hello"}, "back": {"language": "Spanish", "word": "hola"}}]}"""
// When
val result = jsonHelper.validateRequiredFields(validJson, listOf("flashcards"))
// Then
assertTrue(result)
}
@Test
fun `parseJson should handle valid TranslationApiResponse`() {
// Given
val validJson = """{"translatedText": "hola"}"""
// When
val result = jsonHelper.validateRequiredFields(validJson, listOf("translatedText"))
// Then
assertTrue(result)
}
@Test
fun `parseJson should handle valid CorrectionResponse`() {
// Given
val validJson = """{"correctedText": "Hello world", "explanation": "Capitalized first letter"}"""
// When
val result = jsonHelper.validateRequiredFields(validJson, listOf("correctedText", "explanation"))
// Then
assertTrue(result)
}
@Test
fun `parseJson should handle valid EtymologyApiResponse`() {
// Given
val validJson = """{"word": "hello", "timeline": [{"year": "1890", "language": "Old English", "description": "From greeting"}], "relatedWords": []}"""
// When
val result = jsonHelper.validateRequiredFields(validJson, listOf("word", "timeline", "relatedWords"))
// Then
assertTrue(result)
}
@Test
fun `parseJson should handle valid SynonymApiResponse`() {
// Given
val validJson = """{"synonyms": [{"word": "hi", "proximity": 95}, {"word": "greetings", "proximity": 85}]}"""
// When
val result = jsonHelper.validateRequiredFields(validJson, listOf("synonyms"))
// Then
assertTrue(result)
}
@Test
fun `validateRequiredFields should return false for empty JSON`() {
// Given
val emptyJson = """{}"""
val requiredFields = listOf("word")
// When
val result = jsonHelper.validateRequiredFields(emptyJson, requiredFields)
// Then
assertFalse(result)
}
@Test
fun `validateRequiredFields should return false for malformed JSON`() {
// Given
val malformedJson = """{"word": "hello", "parts": ["""
val requiredFields = listOf("word", "parts")
// When
val result = jsonHelper.validateRequiredFields(malformedJson, requiredFields)
// Then
assertFalse(result)
}
@Test
fun `validateRequiredFields should handle nested objects`() {
// Given
val nestedJson = """{"flashcards": [{"front": {"language": "English", "word": "hello"}]}"""
val requiredFields = listOf("flashcards")
// When
val result = jsonHelper.validateRequiredFields(nestedJson, requiredFields)
// Then
assertTrue(result)
}
@Test
fun `extractField should extract simple field`() {
// Given
val json = """{"word": "hello", "parts": []}"""
// When
val result = jsonHelper.extractField(json, "word")
// Then
assertEquals("hello", result)
}
@Test
fun `extractField should extract nested field`() {
// Given
val json = """{"flashcards": [{"front": {"language": "English", "word": "hello"}}]}"""
// When
val result = jsonHelper.extractField(json, "flashcards")
// Then
assertNotNull(result)
assertTrue(result!!.contains("English"))
assertTrue(result.contains("hello"))
}
@Test
fun `extractField should return null for non-existent field`() {
// Given
val json = """{"word": "hello"}"""
// When
val result = jsonHelper.extractField(json, "nonexistent")
// Then
assertEquals(null, result)
}
@Test
fun `extractField should handle array fields`() {
// Given
val json = """{"synonyms": [{"word": "hi", "proximity": 95}]}"""
// When
val result = jsonHelper.extractField(json, "synonyms")
// Then
assertNotNull(result)
assertTrue(result!!.contains("hi"))
assertTrue(result.contains("95"))
}
@Test
fun `formatForDisplay should format simple JSON`() {
// Given
val json = """{"word": "hello", "parts": []}"""
// When
val result = jsonHelper.formatForDisplay(json)
// Then
assertTrue(result.contains("{\n"))
assertTrue(result.contains("}\n"))
assertTrue(result.contains(",\n"))
}
@Test
fun `formatForDisplay should handle malformed JSON gracefully`() {
// Given
val malformedJson = """{"word": "hello", "parts": ["""
// When
val result = jsonHelper.formatForDisplay(malformedJson)
// Then
assertEquals(malformedJson, result) // Should return original if formatting fails
}
@Test
fun `cleanAndValidateJson should handle markdown wrapped JSON`() {
// Given
val markdownJson = """
```json
{"word": "hello", "parts": []}
```
""".trim()
// When
val result = jsonHelper.validateRequiredFields(markdownJson, listOf("word", "parts"))
// Then
assertTrue(result)
}
@Test
fun `cleanAndValidateJson should handle JSON with comments`() {
// Given
val jsonWithComments = """
{
"word": "hello", // This is the word
"parts": [] /* This is the parts array */
}
""".trim()
// When
val result = jsonHelper.validateRequiredFields(jsonWithComments, listOf("word", "parts"))
// Then
assertTrue(result)
}
@Test
fun `cleanAndValidateJson should handle JSON with trailing commas`() {
// Given
val jsonWithTrailingComma = """
{
"word": "hello",
"parts": [],
}
""".trim()
// When
val result = jsonHelper.validateRequiredFields(jsonWithTrailingComma, listOf("word", "parts"))
// Then
assertTrue(result)
}
// Test data classes
@Serializable
data class TestDictionaryResponse(
val word: String,
val parts: List<TestEntryPart>
)
@Serializable
data class TestEntryPart(
val title: String,
val content: String
)
@Serializable
data class TestVocabularyResponse(
val flashcards: List<TestFlashcard>
)
@Serializable
data class TestFlashcard(
val front: TestCardSide,
val back: TestCardSide
)
@Serializable
data class TestCardSide(
val language: String,
val word: String
)
@Test
fun `real parsing test with DictionaryApiResponse`() {
// Given
val json = """{"word": "hello", "parts": [{"title": "Definition", "content": "A greeting"}]}"""
// When
val result = jsonParser.decodeFromString(TestDictionaryResponse.serializer(), json)
// Then
assertEquals("hello", result.word)
assertEquals(1, result.parts.size)
assertEquals("Definition", result.parts[0].title)
assertEquals("A greeting", result.parts[0].content)
}
@Test
fun `real parsing test with VocabularyApiResponse`() {
// Given
val json = """{"flashcards": [{"front": {"language": "English", "word": "hello"}, "back": {"language": "Spanish", "word": "hola"}}]}"""
// When
val result = jsonParser.decodeFromString(TestVocabularyResponse.serializer(), json)
// Then
assertEquals(1, result.flashcards.size)
assertEquals("English", result.flashcards[0].front.language)
assertEquals("hello", result.flashcards[0].front.word)
assertEquals("Spanish", result.flashcards[0].back.language)
assertEquals("hola", result.flashcards[0].back.word)
}
}

View File

@@ -1,203 +0,0 @@
@file:Suppress("HardCodedStringLiteral")
package eu.gaudian.translator.utils
import android.content.Context
import eu.gaudian.translator.model.Language
import eu.gaudian.translator.model.VocabularyItem
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import org.junit.Before
import org.junit.Test
class ServiceFixesTest {
private lateinit var context: Context
private lateinit var vocabularyService: VocabularyService
private lateinit var exerciseService: ExerciseService
@Before
fun setup() {
context = mockk(relaxed = true)
vocabularyService = VocabularyService(context)
exerciseService = ExerciseService(context)
}
@Test
fun `VocabularyService generateSynonyms should have correct return type`() = runTest {
// Given
val language = Language(
name = "English",
nameResId = 1,
code = "en",
englishName = "English",
region = ""
)
val translationLanguage = Language(
name = "Spanish",
nameResId = 2,
code = "es",
englishName = "Spanish",
region = ""
)
// When & Then
// This test verifies the method signature is correct
// The actual API call would need mocking for full testing
assertNotNull(language)
assertNotNull(translationLanguage)
assertEquals("English", language.englishName)
assertEquals("Spanish", translationLanguage.englishName)
}
@Test
fun `ExerciseService should use JsonHelper instead of VocabularyParser`() = runTest {
// Given
val exerciseTitle = "Test Exercise"
// When & Then
// This test verifies the ExerciseService can be instantiated without errors
assertNotNull(exerciseService)
assertNotNull(exerciseTitle)
assertEquals("Test Exercise", exerciseTitle)
}
@Test
fun `parseVocabularyFromJson should handle simple JSON`() {
// Given
val jsonResponse = """{"hello": "hola", "world": "mundo"}"""
// When
val result = parseVocabularyFromJson(jsonResponse)
// Then
assertEquals(2, result.size)
assertEquals("hello", result[0].wordFirst)
assertEquals("hola", result[0].wordSecond)
assertEquals("world", result[1].wordFirst)
assertEquals("mundo", result[1].wordSecond)
}
@Test
fun `parseVocabularyFromJson should handle empty JSON`() {
// Given
val jsonResponse = """{}"""
// When
val result = parseVocabularyFromJson(jsonResponse)
// Then
assertEquals(0, result.size)
}
@Test
fun `parseVocabularyFromJson should handle malformed JSON`() {
// Given
val malformedJson = """{"hello": "hola", "world": """
// When
val result = parseVocabularyFromJson(malformedJson)
// Then
assertEquals(0, result.size)
}
/**
* Helper method to test the private parseVocabularyFromJson method
*/
private fun parseVocabularyFromJson(jsonResponse: String): List<VocabularyItem> {
return try {
// Clean and validate the JSON first
val jsonHelper = JsonHelper()
val cleanedJson = jsonHelper.cleanAndValidateJson(jsonResponse)
// Parse the JSON object for vocabulary items
val jsonObject = kotlinx.serialization.json.Json.parseToJsonElement(cleanedJson).jsonObject
val vocabularyItems = mutableListOf<VocabularyItem>()
var id = 1
for ((wordFirst, wordSecondElement) in jsonObject) {
val wordSecond = wordSecondElement.jsonPrimitive.content.trim()
val vocabularyItem = VocabularyItem(
id = id++,
languageFirstId = -1, // Will be set by caller
languageSecondId = -1, // Will be set by caller
wordFirst = wordFirst.trim(),
wordSecond = wordSecond
)
vocabularyItems.add(vocabularyItem)
}
vocabularyItems
} catch (e: Exception) {
emptyList()
}
}
@Test
fun `VocabularyService translateWordsBatch should handle empty list`() = runTest {
// Given
val emptyWords = emptyList<String>()
val languageFirst = Language(
name = "English",
nameResId = 1,
code = "en",
englishName = "English",
region = ""
)
val languageSecond = Language(
name = "Spanish",
nameResId = 2,
code = "es",
englishName = "Spanish",
region = ""
)
// When
val result = vocabularyService.translateWordsBatch(emptyWords, languageFirst, languageSecond)
// Then
assertTrue(result.isSuccess)
assertEquals(0, result.getOrNull()?.size)
}
@Test
fun `VocabularyService generateVocabularyItems should handle basic parameters`() = runTest {
// Given
val category = "Basic"
val languageFirst = Language(
name = "English",
nameResId = 1,
code = "en",
englishName = "English",
region = ""
)
val languageSecond = Language(
name = "Spanish",
nameResId = 2,
code = "es",
englishName = "Spanish",
region = ""
)
val amount = 5
// When & Then
// This test verifies the method exists and accepts parameters correctly
assertNotNull(category)
assertNotNull(languageFirst)
assertNotNull(languageSecond)
assertEquals("Basic", category)
assertEquals("English", languageFirst.name)
assertEquals("Spanish", languageSecond.name)
assertEquals(5, amount)
}
}

View File

@@ -1,12 +1,12 @@
[versions] [versions]
agp = "9.0.0" agp = "9.0.1"
annotation = "1.9.1" annotation = "1.9.1"
converterGson = "3.0.0" converterGson = "3.0.0"
core = "13.0.0" core = "13.0.0"
coreSplashscreen = "1.2.0" coreSplashscreen = "1.2.0"
coreTesting = "2.2.0" coreTesting = "2.2.0"
datastorePreferences = "1.2.0" datastorePreferences = "1.2.0"
foundation = "1.10.2" foundation = "1.10.3"
hiltAndroidTesting = "2.59.1" hiltAndroidTesting = "2.59.1"
jsoup = "1.22.1" jsoup = "1.22.1"
kotlin = "2.3.10" kotlin = "2.3.10"
@@ -21,14 +21,14 @@ kotlinxCoroutinesTest = "1.10.2"
kotlinxDatetime = "0.7.1" kotlinxDatetime = "0.7.1"
kotlinxSerializationJson = "1.10.0" kotlinxSerializationJson = "1.10.0"
lifecycleRuntimeKtx = "2.10.0" lifecycleRuntimeKtx = "2.10.0"
activityCompose = "1.12.3" activityCompose = "1.12.4"
composeBom = "2026.01.01" composeBom = "2026.02.00"
loggingInterceptor = "5.3.2" loggingInterceptor = "5.3.2"
materialIconsExtended = "1.7.8" materialIconsExtended = "1.7.8"
mockitoCore = "5.21.0" mockitoCore = "5.21.0"
mockitoKotlin = "6.2.3" mockitoKotlin = "6.2.3"
navigationCompose = "2.9.7" navigationCompose = "2.9.7"
pagingRuntimeKtx = "3.4.0" pagingRuntimeKtx = "3.4.1"
reorderable = "0.9.6" reorderable = "0.9.6"
retrofit = "3.0.0" retrofit = "3.0.0"
material = "1.13.0" material = "1.13.0"
@@ -36,13 +36,12 @@ material3 = "1.4.0"
runner = "1.7.0" runner = "1.7.0"
timber = "5.0.1" timber = "5.0.1"
navigationTesting = "2.9.7" navigationTesting = "2.9.7"
foundationLayout = "1.10.2" foundationLayout = "1.10.3"
room = "2.8.4" room = "2.8.4"
coreKtxVersion = "1.7.0" coreKtxVersion = "1.7.0"
truth = "1.4.5" truth = "1.4.5"
zstdJni = "1.5.7-7" zstdJni = "1.5.7-7"
composeMarkdown = "0.5.8" composeMarkdown = "0.5.8"
jitpack = "1.0.10"
[libraries] [libraries]
@@ -106,6 +105,5 @@ compose-markdown = { module = "com.github.jeziellago:compose-markdown", version.
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
hilt-android = { id = "com.google.dagger.hilt.android", version = "2.59.1" } hilt-android = { id = "com.google.dagger.hilt.android", version = "2.59.1" }

View File

@@ -14,7 +14,9 @@ pluginManagement {
} }
} }
dependencyResolutionManagement { dependencyResolutionManagement {
@Suppress("UnstableApiUsage")
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
@Suppress("UnstableApiUsage")
repositories { repositories {
google() google()
mavenCentral() mavenCentral()