Compare commits
14 Commits
f50c0c08a5
...
f39375e9df
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f39375e9df | ||
|
|
db959dab20 | ||
|
|
02530dafbf | ||
|
|
85c407481d | ||
|
|
d14940ed11 | ||
|
|
a0b6509367 | ||
|
|
d249da5f52 | ||
|
|
c061e41cc6 | ||
|
|
2db2b47c38 | ||
|
|
f779da470f | ||
|
|
4855a347b9 | ||
|
|
4dd9fe86aa | ||
|
|
35080c208b | ||
|
|
142eb5a31d |
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 ,
|
||||||
|
|||||||
@@ -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?,
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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.") }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 ---
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>() }
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ fun LanguageOptionsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showAddLanguageDialog) {
|
if (showAddLanguageDialog) {
|
||||||
|
@Suppress("KotlinConstantConditions")
|
||||||
AddCustomLanguageDialog(
|
AddCustomLanguageDialog(
|
||||||
showDialog = showAddLanguageDialog,
|
showDialog = showAddLanguageDialog,
|
||||||
onDismiss = { showAddLanguageDialog = false },
|
onDismiss = { showAddLanguageDialog = false },
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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 ->
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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"
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -49,7 +49,7 @@ fun StageDetailScreen(
|
|||||||
onStageTapped = {},
|
onStageTapped = {},
|
||||||
)
|
)
|
||||||
|
|
||||||
VocabularyListScreen(
|
AllCardsListScreen(
|
||||||
categoryId = null,
|
categoryId = null,
|
||||||
showDueTodayOnly = true,
|
showDueTodayOnly = true,
|
||||||
stage = stage,
|
stage = stage,
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 = {},
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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 = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>) {
|
||||||
|
|||||||
@@ -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 |
@@ -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 & Wiederherstellung</string>
|
<string name="label_backup_and_restore">Sicherung & 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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" }
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user