Compare commits
36 Commits
glassmorph
...
64dcc5d0d5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64dcc5d0d5 | ||
|
|
f39375e9df | ||
|
|
db959dab20 | ||
|
|
02530dafbf | ||
|
|
85c407481d | ||
|
|
d14940ed11 | ||
|
|
a0b6509367 | ||
|
|
d249da5f52 | ||
|
|
c061e41cc6 | ||
|
|
2db2b47c38 | ||
|
|
f779da470f | ||
|
|
4855a347b9 | ||
|
|
4dd9fe86aa | ||
|
|
35080c208b | ||
|
|
142eb5a31d | ||
|
|
f50c0c08a5 | ||
|
|
dc629a54ef | ||
|
|
0c54d6f9c5 | ||
|
|
059e5d9d3f | ||
|
|
3e3d6d9cd1 | ||
|
|
a7c83bb846 | ||
|
|
70e416d5e1 | ||
|
|
84cad31810 | ||
|
|
89ac7cd9eb | ||
|
|
47d7e01f7f | ||
|
|
eae37715cd | ||
|
|
6c669ac310 | ||
|
|
af78bd316d | ||
|
|
24cebc4b15 | ||
|
|
cd5a53ff5f | ||
|
|
972b2226d0 | ||
|
|
5ae96d1f5c | ||
|
|
ef90df2150 | ||
|
|
d2d2f53b59 | ||
|
|
7fccda7f77 | ||
|
|
801b6f6404 |
4
.idea/deploymentTargetSelector.xml
generated
4
.idea/deploymentTargetSelector.xml
generated
@@ -4,10 +4,10 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2026-02-15T19:51:37.987601800Z">
|
||||
<DropdownSelection timestamp="2026-02-16T10:13:39.492968600Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\jonas\.android\avd\Medium_Phone_28.avd" />
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=RFCW11ML1WV" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
|
||||
@@ -6,7 +6,6 @@ import java.util.Locale
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
alias(libs.plugins.hilt.android)
|
||||
id("kotlin-parcelize")
|
||||
@@ -62,11 +61,8 @@ android {
|
||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi"
|
||||
)
|
||||
}
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
viewBinding = false
|
||||
@@ -130,7 +126,7 @@ dependencies {
|
||||
implementation(libs.androidx.room.runtime) // ADDED: Explicitly add runtime
|
||||
implementation(libs.androidx.room.ktx)
|
||||
implementation(libs.core.ktx)
|
||||
ksp(libs.room.compiler) // CHANGED: Use ksp instead of implementation
|
||||
ksp(libs.room.compiler)
|
||||
|
||||
// Networking
|
||||
implementation(libs.retrofit)
|
||||
|
||||
@@ -6,9 +6,6 @@ object TestConfig {
|
||||
// REPLACE with your actual API Key for the test
|
||||
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")
|
||||
const val PROVIDER_NAME = "Mistral"
|
||||
|
||||
|
||||
@@ -32,17 +32,6 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".CorrectActivity"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
@file:Suppress("HardCodedStringLiteral")
|
||||
|
||||
package eu.gaudian.translator
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.gaudian.translator.utils.Log
|
||||
|
||||
class CorrectActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val intent = intent
|
||||
val action = intent.action
|
||||
val type = intent.type
|
||||
|
||||
if (Intent.ACTION_SEND == action && type == "text/plain") {
|
||||
val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
if (sharedText != null) {
|
||||
Log.d("EditActivity", "Received text: $sharedText")
|
||||
setContent {
|
||||
Text(stringResource(R.string.editing_text, sharedText))
|
||||
|
||||
}
|
||||
} else {
|
||||
Log.e("EditActivity", getString(R.string.no_text_received))
|
||||
setContent {
|
||||
Text(stringResource(R.string.error_no_text_to_edit))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d("EditActivity", "Not launched with ACTION_SEND")
|
||||
setContent {
|
||||
Text(stringResource(R.string.not_launched_with_text_to_edit))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
@file:Suppress("unused", "HardCodedStringLiteral")
|
||||
|
||||
package eu.gaudian.translator.di
|
||||
|
||||
import android.app.Application
|
||||
|
||||
@@ -9,7 +9,6 @@ import eu.gaudian.translator.R
|
||||
sealed class WidgetType(val id: String, @param:StringRes val titleRes: Int) {
|
||||
data object Status : WidgetType("status", R.string.label_status)
|
||||
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 DueToday : WidgetType("due_today", R.string.title_widget_due_today)
|
||||
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(
|
||||
Status,
|
||||
Streak,
|
||||
StartButtons,
|
||||
AllVocabulary,
|
||||
DueToday,
|
||||
CategoryProgress ,
|
||||
|
||||
@@ -56,6 +56,7 @@ object LocalDictionaryMorphologyMapper {
|
||||
/**
|
||||
* Overload that uses parsed [DictionaryEntryData] instead of raw JSON.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
fun parseMorphology(
|
||||
langCode: String,
|
||||
pos: String?,
|
||||
|
||||
@@ -144,19 +144,6 @@ class ApiRepository(private val context: Context) {
|
||||
|
||||
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
|
||||
if (currentTrans == null || !availableModels.any { it.modelId == currentTrans.modelId && it.providerKey == currentTrans.providerKey }) {
|
||||
findFallbackModel(availableModels)?.let { setTranslationModel(it) } ?: run { configurationValid = false }
|
||||
|
||||
@@ -76,6 +76,7 @@ class LanguageRepository(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
suspend fun wipeHistoryAndFavorites() {
|
||||
clearLanguages(LanguageListType.HISTORY)
|
||||
clearLanguages(LanguageListType.FAVORITE)
|
||||
|
||||
@@ -129,25 +129,6 @@ class JsonHelper {
|
||||
*/
|
||||
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 {
|
||||
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
|
||||
* development purposes only.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
object Log {
|
||||
|
||||
@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),
|
||||
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_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
|
||||
|
||||
@@ -75,7 +75,6 @@ object StatusMessageService {
|
||||
* @deprecated Use showMessageById() instead for internationalization support.
|
||||
*/
|
||||
@Deprecated("Use showMessageById() for internationalization support", ReplaceWith("showMessageById(messageId)"))
|
||||
@Suppress("unused")
|
||||
fun showSimpleMessage(text: String, type: MessageDisplayType = MessageDisplayType.INFO) {
|
||||
scope.launch {
|
||||
_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) {
|
||||
val statusMessageService = StatusMessageService
|
||||
val additionalInstructions = settingsRepository.customPromptTranslation.flow.first()
|
||||
val selectedSource = languageRepository.loadSelectedSourceLanguage().first()
|
||||
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.
|
||||
*/
|
||||
sealed class DisplayInflectionData {
|
||||
data class VerbConjugation(
|
||||
val gerund: String? = null,
|
||||
val participle: String? = null,
|
||||
val moods: List<DisplayMood>
|
||||
) : DisplayInflectionData()
|
||||
}
|
||||
|
||||
data class DisplayMood(
|
||||
|
||||
@@ -253,7 +253,19 @@ fun TranslatorApp(
|
||||
val currentDestination = navBackStackEntry?.destination
|
||||
val selectedScreen = Screen.fromDestination(currentDestination)
|
||||
|
||||
@Suppress("HardCodedStringLiteral") val isBottomBarHidden = currentDestination?.route?.startsWith("vocabulary_exercise") == true
|
||||
@Suppress("HardCodedStringLiteral") val isBottomBarHidden = currentDestination?.hierarchy?.any { destination ->
|
||||
destination.route in setOf(
|
||||
Screen.Translation.route,
|
||||
Screen.Dictionary.route,
|
||||
Screen.Exercises.route,
|
||||
Screen.Settings.route
|
||||
)
|
||||
} == true || currentDestination?.route in setOf(
|
||||
"start_exercise",
|
||||
"new_word",
|
||||
"new_word_review",
|
||||
"vocabulary_detail/{itemId}"
|
||||
)
|
||||
val showBottomNavLabels by settingsViewModel.showBottomNavLabels.collectAsStateWithLifecycle(initialValue = false)
|
||||
|
||||
BottomNavigationBar(
|
||||
@@ -262,6 +274,12 @@ fun TranslatorApp(
|
||||
showLabels = showBottomNavLabels,
|
||||
onItemSelected = { screen ->
|
||||
val inSameSection = currentDestination?.hierarchy?.any { it.route == screen.route } == true
|
||||
val isMoreSection = screen in setOf(
|
||||
Screen.Translation,
|
||||
Screen.Dictionary,
|
||||
Screen.Settings,
|
||||
Screen.Exercises
|
||||
)
|
||||
|
||||
// Always reset the selected section to its root and clear back stack between sections
|
||||
if (inSameSection) {
|
||||
@@ -274,6 +292,11 @@ fun TranslatorApp(
|
||||
launchSingleTop = true
|
||||
restoreState = false
|
||||
}
|
||||
} else if (isMoreSection) {
|
||||
navController.navigate(screen.route) {
|
||||
launchSingleTop = true
|
||||
restoreState = false
|
||||
}
|
||||
} else {
|
||||
// Switching sections: clear entire back stack to start to avoid back navigation results
|
||||
navController.navigate(screen.route) {
|
||||
@@ -285,6 +308,10 @@ fun TranslatorApp(
|
||||
restoreState = false
|
||||
}
|
||||
}
|
||||
},
|
||||
onPlayClicked = {
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
navController.navigate("start_exercise")
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
@@ -26,27 +26,48 @@ import eu.gaudian.translator.view.dictionary.EtymologyResultScreen
|
||||
import eu.gaudian.translator.view.dictionary.MainDictionaryScreen
|
||||
import eu.gaudian.translator.view.exercises.ExerciseSessionScreen
|
||||
import eu.gaudian.translator.view.exercises.MainExerciseScreen
|
||||
import eu.gaudian.translator.view.exercises.StartExerciseScreen
|
||||
import eu.gaudian.translator.view.exercises.YouTubeBrowserScreen
|
||||
import eu.gaudian.translator.view.exercises.YouTubeExerciseScreen
|
||||
import eu.gaudian.translator.view.home.HomeScreen
|
||||
import eu.gaudian.translator.view.library.LibraryScreen
|
||||
import eu.gaudian.translator.view.settings.DictionaryOptionsScreen
|
||||
import eu.gaudian.translator.view.settings.SettingsRoutes
|
||||
import eu.gaudian.translator.view.settings.TranslationSettingsScreen
|
||||
import eu.gaudian.translator.view.settings.settingsGraph
|
||||
import eu.gaudian.translator.view.stats.StatsScreen
|
||||
import eu.gaudian.translator.view.translation.TranslationScreen
|
||||
import eu.gaudian.translator.view.vocabulary.AllCardsListScreen
|
||||
import eu.gaudian.translator.view.vocabulary.CategoryDetailScreen
|
||||
import eu.gaudian.translator.view.vocabulary.CategoryListScreen
|
||||
import eu.gaudian.translator.view.vocabulary.LanguageProgressScreen
|
||||
import eu.gaudian.translator.view.vocabulary.MainVocabularyScreen
|
||||
import eu.gaudian.translator.view.vocabulary.NewWordReviewScreen
|
||||
import eu.gaudian.translator.view.vocabulary.NewWordScreen
|
||||
import eu.gaudian.translator.view.vocabulary.NoGrammarItemsScreen
|
||||
import eu.gaudian.translator.view.vocabulary.StageDetailScreen
|
||||
import eu.gaudian.translator.view.vocabulary.VocabularyCardHost
|
||||
import eu.gaudian.translator.view.vocabulary.VocabularyExerciseHostScreen
|
||||
import eu.gaudian.translator.view.vocabulary.VocabularyHeatmapScreen
|
||||
import eu.gaudian.translator.view.vocabulary.VocabularyListScreen
|
||||
import eu.gaudian.translator.view.vocabulary.VocabularySortingScreen
|
||||
|
||||
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
|
||||
fun AppNavHost(
|
||||
navController: NavHostController,
|
||||
@@ -57,11 +78,12 @@ fun AppNavHost(
|
||||
|
||||
// 1. Define your Main Tab "Leaf" Routes (the actual start destinations of your graphs)
|
||||
val mainTabRoutes = setOf(
|
||||
Screen.Home.route, // "home" or "main_translation" depending on which one you actually navigate to
|
||||
"main_translation",
|
||||
"main_dictionary",
|
||||
"main_vocabulary",
|
||||
"main_exercise",
|
||||
Screen.Home.route,
|
||||
Screen.Library.route,
|
||||
Screen.Stats.route,
|
||||
Screen.Translation.route,
|
||||
Screen.Dictionary.route,
|
||||
Screen.Exercises.route,
|
||||
SettingsRoutes.LIST
|
||||
)
|
||||
|
||||
@@ -121,77 +143,50 @@ fun AppNavHost(
|
||||
}
|
||||
) {
|
||||
composable(Screen.Home.route) {
|
||||
TranslationScreen(navController = navController)
|
||||
HomeScreen(navController = navController)
|
||||
}
|
||||
|
||||
composable(NavigationRoutes.NEW_WORD) {
|
||||
NewWordScreen(navController = navController)
|
||||
}
|
||||
|
||||
composable(NavigationRoutes.NEW_WORD_REVIEW) {
|
||||
NewWordReviewScreen(navController = navController)
|
||||
}
|
||||
|
||||
composable(NavigationRoutes.START_EXERCISE) {
|
||||
StartExerciseScreen(navController = navController)
|
||||
}
|
||||
|
||||
// Define all other navigation graphs at the same top level.
|
||||
homeGraph(navController)
|
||||
libraryGraph(navController)
|
||||
statsGraph(navController)
|
||||
translationGraph(navController)
|
||||
dictionaryGraph(navController)
|
||||
vocabularyGraph(navController)
|
||||
exerciseGraph(navController)
|
||||
settingsGraph(navController)
|
||||
}
|
||||
}
|
||||
@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
|
||||
fun NavGraphBuilder.translationGraph(navController: NavHostController) {
|
||||
|
||||
fun NavGraphBuilder.homeGraph(navController: NavHostController) {
|
||||
navigation(
|
||||
startDestination = "main_translation",
|
||||
startDestination = "main_home",
|
||||
route = Screen.Home.route
|
||||
) {
|
||||
composable("main_translation") {
|
||||
TranslationScreen(navController = navController)
|
||||
}
|
||||
composable("custom_translation_prompt") {
|
||||
TranslationSettingsScreen(navController = navController)
|
||||
composable("main_home") {
|
||||
HomeScreen(navController = navController)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
|
||||
fun NavGraphBuilder.dictionaryGraph(navController: NavHostController) {
|
||||
fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
|
||||
navigation(
|
||||
startDestination = "main_dictionary",
|
||||
route = Screen.Dictionary.route
|
||||
startDestination = "main_library",
|
||||
route = Screen.Library.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("main_library") {
|
||||
LibraryScreen(navController = navController)
|
||||
}
|
||||
composable("vocabulary_sorting") {
|
||||
VocabularySortingScreen(
|
||||
@@ -224,7 +219,7 @@ fun NavGraphBuilder.vocabularyGraph(
|
||||
composable("vocabulary_list/{showDueTodayOnly}/{categoryId}") { backStackEntry ->
|
||||
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
|
||||
val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull()
|
||||
VocabularyListScreen(
|
||||
AllCardsListScreen(
|
||||
navController = navController,
|
||||
showDueTodayOnly = showDueTodayOnly,
|
||||
categoryId = categoryId,
|
||||
@@ -241,7 +236,7 @@ fun NavGraphBuilder.vocabularyGraph(
|
||||
)
|
||||
|
||||
}
|
||||
composable("vocabulary_heatmap") {
|
||||
composable(NavigationRoutes.STATS_VOCABULARY_HEATMAP) {
|
||||
VocabularyHeatmapScreen(
|
||||
navController = navController,
|
||||
)
|
||||
@@ -253,7 +248,7 @@ fun NavGraphBuilder.vocabularyGraph(
|
||||
if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it)
|
||||
}
|
||||
|
||||
VocabularyListScreen(
|
||||
AllCardsListScreen(
|
||||
navController = navController,
|
||||
showDueTodayOnly = showDueTodayOnly,
|
||||
stage = stage,
|
||||
@@ -376,6 +371,159 @@ fun NavGraphBuilder.vocabularyGraph(
|
||||
}
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.statsGraph(
|
||||
navController: NavHostController,
|
||||
) {
|
||||
navigation(
|
||||
startDestination = "main_stats",
|
||||
route = Screen.Stats.route
|
||||
) {
|
||||
composable("main_stats") {
|
||||
StatsScreen(navController = navController)
|
||||
}
|
||||
composable("stats/vocabulary_sorting") {
|
||||
VocabularySortingScreen(
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
composable("stats/vocabulary_list/{showDueTodayOnly}/{categoryId}") { backStackEntry ->
|
||||
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
|
||||
val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull()
|
||||
AllCardsListScreen(
|
||||
navController = navController,
|
||||
showDueTodayOnly = showDueTodayOnly,
|
||||
categoryId = categoryId,
|
||||
onNavigateToItem = { item ->
|
||||
navController.navigate("vocabulary_detail/${item.id}")
|
||||
},
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
enableNavigationButtons = true
|
||||
)
|
||||
}
|
||||
composable(NavigationRoutes.STATS_LANGUAGE_PROGRESS) {
|
||||
LanguageProgressScreen(
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
composable(NavigationRoutes.STATS_VOCABULARY_HEATMAP) {
|
||||
VocabularyHeatmapScreen(
|
||||
navController = navController,
|
||||
)
|
||||
}
|
||||
composable("stats/vocabulary_list/{showDueTodayOnly}/{stage}") { backStackEntry ->
|
||||
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
|
||||
val stageString = backStackEntry.arguments?.getString("stage")
|
||||
val stage = stageString?.let {
|
||||
if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it)
|
||||
}
|
||||
|
||||
AllCardsListScreen(
|
||||
navController = navController,
|
||||
showDueTodayOnly = showDueTodayOnly,
|
||||
stage = stage,
|
||||
onNavigateToItem = { item ->
|
||||
navController.navigate("vocabulary_detail/${item.id}")
|
||||
},
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
categoryId = 0,
|
||||
enableNavigationButtons = true
|
||||
)
|
||||
}
|
||||
composable("stats/category_detail/{categoryId}") { backStackEntry ->
|
||||
val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull()
|
||||
|
||||
if (categoryId != null) {
|
||||
CategoryDetailScreen(
|
||||
categoryId = categoryId,
|
||||
onBackClick = { navController.popBackStack() },
|
||||
onNavigateToItem = { item ->
|
||||
navController.navigate("vocabulary_detail/${item.id}")
|
||||
},
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
}
|
||||
composable("stats/category_list_screen") {
|
||||
CategoryListScreen(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onCategoryClicked = { categoryId ->
|
||||
navController.navigate("stats/category_detail/$categoryId")
|
||||
}
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = "stats/vocabulary_sorting?mode={mode}",
|
||||
arguments = listOf(
|
||||
navArgument("mode") {
|
||||
type = NavType.StringType
|
||||
nullable = true
|
||||
}
|
||||
)
|
||||
) { backStackEntry ->
|
||||
VocabularySortingScreen(
|
||||
navController = navController,
|
||||
initialFilterMode = backStackEntry.arguments?.getString("mode")
|
||||
)
|
||||
}
|
||||
composable("stats/no_grammar_items") {
|
||||
NoGrammarItemsScreen(
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
|
||||
fun NavGraphBuilder.translationGraph(navController: NavHostController) {
|
||||
navigation(
|
||||
startDestination = "main_translation",
|
||||
route = Screen.Translation.route
|
||||
) {
|
||||
composable("main_translation") {
|
||||
TranslationScreen(navController = navController)
|
||||
}
|
||||
composable("custom_translation_prompt") {
|
||||
TranslationSettingsScreen(navController = navController)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
|
||||
fun NavGraphBuilder.dictionaryGraph(navController: NavHostController) {
|
||||
navigation(
|
||||
startDestination = "main_dictionary",
|
||||
route = Screen.Dictionary.route
|
||||
) {
|
||||
composable("main_dictionary") {
|
||||
MainDictionaryScreen(navController = navController)
|
||||
}
|
||||
composable("dictionary_result/{entryId}") { backStackEntry ->
|
||||
val entryId = backStackEntry.arguments?.getString("entryId")?.toIntOrNull()
|
||||
if (entryId != null) {
|
||||
DictionaryResultScreen(
|
||||
entryId = entryId,
|
||||
navController = navController,
|
||||
)
|
||||
} else {
|
||||
Text("Error: Invalid Entry ID")
|
||||
}
|
||||
}
|
||||
composable("dictionary_options") {
|
||||
DictionaryOptionsScreen(navController = navController)
|
||||
}
|
||||
composable("etymology_result/{word}/{languageCode}") { backStackEntry ->
|
||||
val word = backStackEntry.arguments?.getString("word") ?: ""
|
||||
val languageCode = backStackEntry.arguments?.getString("languageCode")?.toIntOrNull() ?: 1
|
||||
EtymologyResultScreen(
|
||||
navController = navController,
|
||||
word = word,
|
||||
languageCode = languageCode
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
|
||||
fun NavGraphBuilder.exerciseGraph(
|
||||
navController: NavHostController,
|
||||
|
||||
@@ -115,7 +115,7 @@ fun AppAlertDialog(
|
||||
title: @Composable (() -> Unit)? = null,
|
||||
text: @Composable (() -> Unit)? = null,
|
||||
properties: DialogProperties = DialogProperties(),
|
||||
hintContent: @Composable (() -> Unit)? = null,
|
||||
hintContent:Hint? = null,
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
var showBottomSheet by remember { mutableStateOf(false) }
|
||||
@@ -142,11 +142,13 @@ fun AppAlertDialog(
|
||||
)
|
||||
|
||||
if (showBottomSheet) {
|
||||
HintBottomSheet(
|
||||
onDismissRequest = { showBottomSheet = false },
|
||||
sheetState = sheetState,
|
||||
content = hintContent
|
||||
)
|
||||
hintContent?.let {
|
||||
HintBottomSheet(
|
||||
onDismissRequest = { showBottomSheet = false },
|
||||
sheetState = sheetState,
|
||||
content = it
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +214,7 @@ private fun DialogHeader(
|
||||
@Composable
|
||||
private fun DialogTitleWithHint(
|
||||
title: @Composable () -> Unit,
|
||||
hintContent: @Composable (() -> Unit)?,
|
||||
hintContent: Hint? = null,
|
||||
onHintClick: () -> Unit
|
||||
) {
|
||||
val showHints = LocalShowHints.current
|
||||
@@ -424,7 +426,6 @@ fun AppAlertDialogPreview() {
|
||||
},
|
||||
title = { Text("Alert Dialog Title") },
|
||||
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.")
|
||||
}
|
||||
},
|
||||
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.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
@@ -550,7 +551,55 @@ fun AppDropdownMenu(
|
||||
// =========================================
|
||||
// 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
|
||||
fun LargeDropdownMenuItem(
|
||||
text: String,
|
||||
|
||||
@@ -10,7 +10,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.Icons.Default
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBackIos
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||
import androidx.compose.material.icons.automirrored.filled.DriveFileMove
|
||||
import androidx.compose.material.icons.automirrored.filled.ExitToApp
|
||||
@@ -81,6 +81,7 @@ import androidx.compose.material.icons.filled.MenuBook
|
||||
import androidx.compose.material.icons.filled.Merge
|
||||
import androidx.compose.material.icons.filled.ModelTraining
|
||||
import androidx.compose.material.icons.filled.MonitorHeart
|
||||
import androidx.compose.material.icons.filled.MoreHoriz
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.NoteAdd
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
@@ -135,7 +136,7 @@ object AppIcons {
|
||||
val AI = Default.AutoAwesome
|
||||
val Appearance = Icons.Filled.ColorLens
|
||||
val ApiKey = Default.Key
|
||||
val ArrowBack = Icons.AutoMirrored.Filled.ArrowBack
|
||||
val ArrowBack = Icons.AutoMirrored.Filled.ArrowBackIos
|
||||
val ArrowCircleUp = Icons.Filled.ArrowCircleUp
|
||||
val ArrowDropDown = Icons.Filled.KeyboardArrowDown
|
||||
val ArrowDropUp = Icons.Filled.KeyboardArrowUp
|
||||
@@ -202,6 +203,7 @@ object AppIcons {
|
||||
val Merge = Icons.Filled.Merge
|
||||
val ModelTraining = Icons.Filled.ModelTraining
|
||||
val More = Default.MoreVert
|
||||
val MoreHorizontal = Icons.Filled.MoreHoriz
|
||||
val MoreVert = Default.MoreVert
|
||||
val MoveTo = Icons.AutoMirrored.Filled.DriveFileMove
|
||||
val Paste = Default.ContentPaste
|
||||
|
||||
@@ -2,26 +2,15 @@ package eu.gaudian.translator.view.composable
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
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.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.ScaffoldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.gaudian.translator.R
|
||||
|
||||
@Composable
|
||||
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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
@@ -20,9 +18,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -36,6 +38,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||
|
||||
/**
|
||||
@@ -46,35 +49,57 @@ interface TabItem {
|
||||
val title: String
|
||||
val icon: ImageVector
|
||||
}
|
||||
|
||||
/**
|
||||
* 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")
|
||||
@SuppressLint("UnusedBoxWithConstraintsScope", "LocalContextResourcesRead", "DiscouragedApi",
|
||||
"SuspiciousIndentation"
|
||||
)
|
||||
@Composable
|
||||
fun <T : TabItem> AppTabLayout(
|
||||
tabs: List<T>,
|
||||
selectedTab: T,
|
||||
onTabSelected: (T) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
onNavigateBack: (() -> Unit)? = null
|
||||
) {
|
||||
val selectedIndex = tabs.indexOf(selectedTab)
|
||||
|
||||
BoxWithConstraints(
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp, horizontal = 8.dp)
|
||||
.height(56.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||
shape = ComponentDefaults.CardShape
|
||||
)
|
||||
.padding(vertical = 8.dp, horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val tabWidth = maxWidth / tabs.size
|
||||
if (onNavigateBack != null) {
|
||||
IconButton(
|
||||
onClick = onNavigateBack,
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp)
|
||||
.size(40.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||
shape = CircleShape
|
||||
),
|
||||
colors = IconButtonDefaults.iconButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||
contentColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = AppIcons.ArrowBack,
|
||||
contentDescription = stringResource(R.string.cd_navigate_back)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(56.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||
shape = ComponentDefaults.CardShape
|
||||
)
|
||||
) {
|
||||
val tabWidth = maxWidth / tabs.size
|
||||
|
||||
val indicatorOffset by animateDpAsState(
|
||||
targetValue = tabWidth * selectedIndex,
|
||||
@@ -82,58 +107,59 @@ fun <T : TabItem> AppTabLayout(
|
||||
label = "IndicatorOffset"
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.offset(x = indicatorOffset)
|
||||
.width(tabWidth)
|
||||
.fillMaxHeight()
|
||||
.padding(4.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.offset(x = indicatorOffset)
|
||||
.width(tabWidth)
|
||||
.fillMaxHeight()
|
||||
.padding(4.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
)
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
tabs.forEach { tab ->
|
||||
val isSelected = tab == selectedTab
|
||||
val contentColor by animateColorAsState(
|
||||
targetValue = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
animationSpec = spring(stiffness = Spring.StiffnessLow)
|
||||
)
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
tabs.forEach { tab ->
|
||||
val isSelected = tab == selectedTab
|
||||
val contentColor by animateColorAsState(
|
||||
targetValue = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
animationSpec = spring(stiffness = Spring.StiffnessLow)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.clickable(
|
||||
onClick = { onTabSelected(tab) },
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.clickable(
|
||||
onClick = { onTabSelected(tab) },
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val resolvedTitle = run {
|
||||
val resId = context.resources.getIdentifier(tab.title, "string", context.packageName)
|
||||
if (resId != 0) stringResource(resId) else tab.title
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val resolvedTitle = run {
|
||||
val resId = context.resources.getIdentifier(tab.title, "string", context.packageName)
|
||||
if (resId != 0) stringResource(resId) else tab.title
|
||||
}
|
||||
Icon(
|
||||
modifier = Modifier.padding(4.dp),
|
||||
imageVector = tab.icon,
|
||||
contentDescription = resolvedTitle,
|
||||
tint = contentColor
|
||||
)
|
||||
Spacer(modifier = Modifier.width(2.dp))
|
||||
Text(
|
||||
text = resolvedTitle,
|
||||
color = contentColor,
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
modifier = Modifier.padding(4.dp),
|
||||
imageVector = tab.icon,
|
||||
contentDescription = resolvedTitle,
|
||||
tint = contentColor
|
||||
)
|
||||
Spacer(modifier = Modifier.width(2.dp))
|
||||
Text(
|
||||
text = resolvedTitle,
|
||||
color = contentColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,6 +167,7 @@ fun <T : TabItem> AppTabLayout(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun ModernTabLayoutPreview() {
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
@file:Suppress("HardCodedStringLiteral")
|
||||
|
||||
package eu.gaudian.translator.view.composable
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
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.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBackIosNew
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarColors
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
@@ -25,8 +30,10 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||
@@ -36,8 +43,8 @@ import eu.gaudian.translator.view.hints.LocalShowHints
|
||||
|
||||
@Composable
|
||||
fun AppTopAppBar(
|
||||
title: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
onNavigateBack: (() -> Unit)? = null,
|
||||
navigationIcon: @Composable (() -> Unit)? = null,
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
@@ -47,89 +54,83 @@ fun AppTopAppBar(
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
var showBottomSheet by remember { mutableStateOf(false) }
|
||||
|
||||
TopAppBar(
|
||||
// Changed to CenterAlignedTopAppBar to perfectly match the design requirements
|
||||
CenterAlignedTopAppBar(
|
||||
modifier = modifier.height(56.dp),
|
||||
windowInsets = WindowInsets(0.dp),
|
||||
colors = colors,
|
||||
title = {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val showHints = LocalShowHints.current
|
||||
if (showHints && hintContent != null) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
title()
|
||||
}
|
||||
Box {
|
||||
IconButton(onClick = { showBottomSheet = true }) {
|
||||
Icon(
|
||||
imageVector = AppIcons.Help,
|
||||
contentDescription = stringResource(R.string.show_hint),
|
||||
tint = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
val showHints = LocalShowHints.current
|
||||
if (showHints && hintContent != null) {
|
||||
// Simplified row: keeps the title and hint icon neatly centered together
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.weight(1f),
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||
)
|
||||
IconButton(onClick = { showBottomSheet = true }) {
|
||||
Icon(
|
||||
imageVector = AppIcons.Help,
|
||||
contentDescription = stringResource(R.string.show_hint),
|
||||
tint = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
}
|
||||
} else {
|
||||
title()
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||
)
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
if (onNavigateBack != null) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
contentAlignment = Alignment.Center
|
||||
IconButton(
|
||||
onClick = onNavigateBack,
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||
) {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(
|
||||
AppIcons.ArrowBack,
|
||||
contentDescription = stringResource(R.string.cd_navigate_back),
|
||||
tint = LocalContentColor.current
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBackIosNew,
|
||||
contentDescription = "Back",
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
} else if (navigationIcon != null) {
|
||||
Box(modifier = Modifier.fillMaxHeight(), contentAlignment = Alignment.Center) {
|
||||
navigationIcon()
|
||||
}
|
||||
} else {
|
||||
// No navigation icon
|
||||
navigationIcon()
|
||||
}
|
||||
},
|
||||
actions = actions
|
||||
)
|
||||
|
||||
if (showBottomSheet) {
|
||||
HintBottomSheet(
|
||||
onDismissRequest = {
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
showBottomSheet = false
|
||||
},
|
||||
sheetState = sheetState,
|
||||
content = {
|
||||
hintContent?.Render()
|
||||
}
|
||||
)
|
||||
hintContent?.let {
|
||||
HintBottomSheet(
|
||||
onDismissRequest = {
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
showBottomSheet = false
|
||||
},
|
||||
sheetState = sheetState,
|
||||
content = it
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A composable that acts as a TopAppBar, containing a back navigation icon
|
||||
* and an [AppTabLayout].
|
||||
*
|
||||
* @param T The type of the tab item, must implement [TabItem].
|
||||
* @param tabs The list of tab items to display.
|
||||
* @param selectedTab The currently selected tab item.
|
||||
* @param onTabSelected Callback function when a tab is selected.
|
||||
* @param onNavigateBack Callback function when the back arrow is clicked.
|
||||
* @param modifier The modifier to be applied to the layout.
|
||||
*/
|
||||
@Composable
|
||||
fun <T : TabItem> TabbedTopAppBar(
|
||||
@@ -139,7 +140,6 @@ fun <T : TabItem> TabbedTopAppBar(
|
||||
onNavigateBack: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
// Use a Surface to provide background color and context for the app bar
|
||||
Surface(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.surface
|
||||
@@ -148,20 +148,21 @@ fun <T : TabItem> TabbedTopAppBar(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Back navigation icon, similar to its usage in AppTopAppBar
|
||||
// Updated back icon here as well to keep your entire app consistent!
|
||||
IconButton(
|
||||
onClick = onNavigateBack,
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp, end = 4.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||
) {
|
||||
Icon(
|
||||
imageVector = AppIcons.ArrowBack,
|
||||
contentDescription = stringResource(R.string.cd_navigate_back),
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
|
||||
// The AppTabLayout, taking up the remaining space.
|
||||
// Its appearance matches the provided image.
|
||||
AppTabLayout(
|
||||
tabs = tabs,
|
||||
selectedTab = selectedTab,
|
||||
@@ -172,11 +173,12 @@ fun <T : TabItem> TabbedTopAppBar(
|
||||
}
|
||||
}
|
||||
|
||||
// ... [Previews remain exactly the same below]
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun TabbedTopAppBarPreview() {
|
||||
// Sample data for preview, similar to ModernTabLayoutPreview
|
||||
data class SampleTabItem(override val title: String, override val icon: ImageVector) : TabItem
|
||||
|
||||
val tabs = listOf(
|
||||
@@ -202,7 +204,7 @@ fun TabbedTopAppBarPreview() {
|
||||
@Composable
|
||||
fun AppTopAppBarPreview() {
|
||||
AppTopAppBar(
|
||||
title = { Text("Preview Title") }
|
||||
title = "Previwe Title"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -210,7 +212,7 @@ fun AppTopAppBarPreview() {
|
||||
@Composable
|
||||
fun AppTopAppBarWithNavigationIconPreview() {
|
||||
AppTopAppBar(
|
||||
title = { Text(stringResource(R.string.title_title_preview_title)) },
|
||||
title = "Preview Title",
|
||||
onNavigateBack = {}
|
||||
)
|
||||
}
|
||||
@@ -219,13 +221,13 @@ fun AppTopAppBarWithNavigationIconPreview() {
|
||||
@Composable
|
||||
fun AppTopAppBarWithActionsPreview() {
|
||||
AppTopAppBar(
|
||||
title = { Text(stringResource(R.string.title_title_preview_title)) },
|
||||
title = "Preview Title",
|
||||
actions = {
|
||||
IconButton(onClick = {}) {
|
||||
Icon(AppIcons.Settings, contentDescription = stringResource(R.string.title_settings))
|
||||
}
|
||||
IconButton(onClick = {}) {
|
||||
AppIcons.ArrowBack
|
||||
Icon(AppIcons.ArrowBack, contentDescription = null)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@file:Suppress("HardCodedStringLiteral")
|
||||
@file:Suppress("HardCodedStringLiteral", "AssignedValueIsNeverRead")
|
||||
|
||||
package eu.gaudian.translator.view.composable
|
||||
|
||||
@@ -11,23 +11,44 @@ import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.NavigationBarItemDefaults
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
@@ -41,6 +62,7 @@ import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||
import eu.gaudian.translator.view.LocalShowExperimentalFeatures
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
sealed class Screen(
|
||||
val route: String,
|
||||
@@ -48,34 +70,42 @@ sealed class Screen(
|
||||
val selectedIcon: ImageVector,
|
||||
val unselectedIcon: ImageVector
|
||||
) {
|
||||
object Home : Screen("home", R.string.label_translation, AppIcons.TranslateFilled, AppIcons.TranslateOutlined)
|
||||
object Home : Screen("home", R.string.label_home, AppIcons.Home, AppIcons.Home)
|
||||
object Stats : Screen("stats", R.string.label_stats, AppIcons.Statistics, AppIcons.Statistics)
|
||||
object Translation : Screen("translation", R.string.label_translation, AppIcons.TranslateFilled, AppIcons.TranslateOutlined)
|
||||
object Library : Screen("library", R.string.label_library, AppIcons.VocabularyFilled, AppIcons.VocabularyOutlined)
|
||||
object Settings : Screen("settings", R.string.title_settings, AppIcons.SettingsFilled, AppIcons.SettingsOutlined)
|
||||
object More : Screen("more", R.string.label_more, AppIcons.MoreVert, AppIcons.MoreHorizontal)
|
||||
object Dictionary : Screen("dictionary", R.string.label_dictionary, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined)
|
||||
object Exercises : Screen("exercises", R.string.label_exercises, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined)
|
||||
object Vocabulary : Screen("vocabulary", R.string.label_vocabulary, AppIcons.VocabularyFilled, AppIcons.VocabularyOutlined)
|
||||
object Settings : Screen("settings", R.string.title_settings, AppIcons.SettingsFilled, AppIcons.SettingsOutlined)
|
||||
|
||||
companion object {
|
||||
fun getAllScreens(showExperimental: Boolean = false): List<Screen> {
|
||||
val screens = mutableListOf(Home, Dictionary, Vocabulary, Settings)
|
||||
return listOf(Home, Library, Stats)
|
||||
}
|
||||
|
||||
fun getMoreMenuItems(showExperimental: Boolean = false): List<Screen> {
|
||||
val items = mutableListOf<Screen>()
|
||||
items.add(Translation)
|
||||
items.add(Dictionary)
|
||||
items.add(Settings)
|
||||
if (showExperimental) {
|
||||
screens.add(2, Exercises)
|
||||
items.add(Exercises)
|
||||
}
|
||||
return screens
|
||||
return items
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun fromDestination(destination: NavDestination?): Screen {
|
||||
val showExperimental = LocalShowExperimentalFeatures.current
|
||||
return getAllScreens(showExperimental).find { screen ->
|
||||
val allScreens = getAllScreens(showExperimental) + getMoreMenuItems(showExperimental) + More
|
||||
return allScreens.find { screen ->
|
||||
destination?.hierarchy?.any { it.route == screen.route } == true
|
||||
} ?: Home
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A modernized Material 3 bottom navigation bar with spring animations and haptic feedback.
|
||||
*/
|
||||
@SuppressLint("UnusedBoxWithConstraintsScope")
|
||||
@Composable
|
||||
fun BottomNavigationBar(
|
||||
@@ -84,89 +114,274 @@ fun BottomNavigationBar(
|
||||
showLabels: Boolean,
|
||||
onItemSelected: (Screen) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onPlayClicked: () -> Unit = {}
|
||||
) {
|
||||
val showExperimental = LocalShowExperimentalFeatures.current
|
||||
val screens = remember(showExperimental) { Screen.getAllScreens(showExperimental) }
|
||||
val moreScreen = remember { Screen.More }
|
||||
val haptic = LocalHapticFeedback.current
|
||||
var showMoreMenu by remember { mutableStateOf(false) }
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// Configuration for the play button
|
||||
val playButtonSize = 56.dp
|
||||
val glowPadding = 12.dp // Total extra space for the glow (16dp on each side)
|
||||
|
||||
// This dictates how far up the button shifts.
|
||||
// Setting it to around half the button size centers it on the top border.
|
||||
val upwardOffset = 16.dp
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = isVisible,
|
||||
enter = slideInVertically(
|
||||
animationSpec = androidx.compose.animation.core.tween(durationMillis = 220),
|
||||
animationSpec = spring(stiffness = Spring.StiffnessHigh),
|
||||
initialOffsetY = { it }
|
||||
),
|
||||
exit = slideOutVertically(
|
||||
animationSpec = androidx.compose.animation.core.tween(durationMillis = 220),
|
||||
animationSpec = spring(stiffness = Spring.StiffnessHigh),
|
||||
targetOffsetY = { it }
|
||||
)
|
||||
) {
|
||||
|
||||
val baseHeight = if (showLabels) 80.dp else 56.dp
|
||||
val density = LocalDensity.current
|
||||
val navBarDp = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() }
|
||||
val height = baseHeight + navBarDp
|
||||
|
||||
NavigationBar(
|
||||
modifier = modifier.height(height),
|
||||
containerColor = MaterialTheme.colorScheme.surface, // Cleaner look than surfaceVariant
|
||||
tonalElevation = 8.dp, // Slight elevation for depth
|
||||
// Outer Box height is purely determined by the NavigationBar now
|
||||
Box(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
screens.forEach { screen ->
|
||||
val isSelected = screen == selectedItem
|
||||
val title = stringResource(id = screen.title)
|
||||
|
||||
// 1. Spring Animation for the Icon Scale
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = if (isSelected) 1.2f else 1.0f, // Subtle pop effect
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
),
|
||||
label = "iconScale"
|
||||
)
|
||||
// The actual Navigation Bar
|
||||
NavigationBar(
|
||||
modifier = Modifier.height(height),
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
tonalElevation = 8.dp,
|
||||
) {
|
||||
// Create a list of 5 items (2 left, 1 empty spacer, 2 right)
|
||||
val allNavItems = buildList {
|
||||
addAll(screens.take(2))
|
||||
add(null) // Empty spacer for Play Button gap
|
||||
if (screens.size > 2) {
|
||||
addAll(screens.drop(2))
|
||||
}
|
||||
add(moreScreen)
|
||||
}
|
||||
|
||||
NavigationBarItem(
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
if (!isSelected) {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress) // 2. Tactile feedback
|
||||
onItemSelected(screen)
|
||||
}
|
||||
},
|
||||
label = if (showLabels) {
|
||||
{
|
||||
Text(
|
||||
text = title,
|
||||
maxLines = 1,
|
||||
fontSize = 10.sp,
|
||||
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
|
||||
color = if(isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
allNavItems.forEach { screen ->
|
||||
if (screen == null) {
|
||||
// Dummy item to create the gap
|
||||
NavigationBarItem(
|
||||
selected = false,
|
||||
onClick = {},
|
||||
enabled = false, // Disables ripples and clicks
|
||||
icon = { Spacer(modifier = Modifier.size(24.dp)) },
|
||||
label = if (showLabels) { { Spacer(modifier = Modifier.size(10.dp)) } } else null,
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
disabledIconColor = Color.Transparent,
|
||||
disabledTextColor = Color.Transparent
|
||||
)
|
||||
)
|
||||
} else {
|
||||
// Regular or More items
|
||||
val isSelected = if (screen == Screen.More) {
|
||||
selectedItem is Screen.More || Screen.getMoreMenuItems(showExperimental).contains(selectedItem)
|
||||
} else {
|
||||
screen == selectedItem
|
||||
}
|
||||
} else null,
|
||||
icon = {
|
||||
// 3. Crossfade between Outlined and Filled icons
|
||||
Crossfade(targetState = isSelected, label = "iconFade") { selected ->
|
||||
Icon(
|
||||
imageVector = if (selected) screen.selectedIcon else screen.unselectedIcon,
|
||||
contentDescription = title,
|
||||
modifier = Modifier.scale(scale) // Apply the spring scale
|
||||
val title = stringResource(id = screen.title)
|
||||
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = if (isSelected) 1.2f else 1.0f,
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
),
|
||||
label = "iconScale"
|
||||
)
|
||||
|
||||
NavigationBarItem(
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
if (!isSelected) {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
if (screen == Screen.More) showMoreMenu = true else onItemSelected(screen)
|
||||
}
|
||||
},
|
||||
label = if (showLabels) {
|
||||
{
|
||||
Text(
|
||||
text = title,
|
||||
maxLines = 1,
|
||||
fontSize = 10.sp,
|
||||
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
|
||||
color = if(isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
} else null,
|
||||
icon = {
|
||||
Crossfade(targetState = isSelected, label = "iconFade") { selected ->
|
||||
Icon(
|
||||
imageVector = if (selected) screen.selectedIcon else screen.unselectedIcon,
|
||||
contentDescription = title,
|
||||
modifier = Modifier.scale(scale)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
indicatorColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
selectedTextColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
indicatorColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
selectedTextColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The Glowing Play Button
|
||||
Box(
|
||||
modifier = Modifier
|
||||
// This negative offset pulls the button UP out of the bounding box
|
||||
// without increasing the layout height of the parent Box.
|
||||
.offset(y = -upwardOffset)
|
||||
.size(playButtonSize + glowPadding),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// Background radial glow
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.background(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.5f),
|
||||
Color.Transparent
|
||||
)
|
||||
),
|
||||
shape = CircleShape
|
||||
)
|
||||
)
|
||||
|
||||
// Actual clickable button
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(playButtonSize)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||
.clickable {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onPlayClicked()
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.PlayArrow,
|
||||
contentDescription = "Play",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Modal Bottom Sheet for More menu (Remains exactly the same)
|
||||
if (showMoreMenu) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showMoreMenu = false },
|
||||
sheetState = sheetState
|
||||
) {
|
||||
MoreBottomSheetContent(
|
||||
showExperimental = showExperimental,
|
||||
onItemSelected = { screen ->
|
||||
scope.launch {
|
||||
sheetState.hide()
|
||||
showMoreMenu = false
|
||||
onItemSelected(screen)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MoreBottomSheetContent(
|
||||
showExperimental: Boolean,
|
||||
onItemSelected: (Screen) -> Unit
|
||||
) {
|
||||
val moreItems = remember(showExperimental) { Screen.getMoreMenuItems(showExperimental) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 32.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.label_more),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold, // Added bold to match the new style
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
|
||||
)
|
||||
|
||||
// Removed HorizontalDivider() for a cleaner look
|
||||
|
||||
moreItems.forEach { screen ->
|
||||
MoreMenuItem(
|
||||
screen = screen,
|
||||
onClick = { onItemSelected(screen) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Composable
|
||||
fun MoreMenuItem(
|
||||
screen: Screen,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() }
|
||||
.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Circular Icon Background
|
||||
Surface(
|
||||
shape = CircleShape,
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
modifier = Modifier.size(52.dp)
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
// Adjust this depending on whether your Screen uses ImageVector or Drawable Res
|
||||
Icon(
|
||||
imageVector = screen.selectedIcon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
// Title
|
||||
Text(
|
||||
text = stringResource(id = screen.title), // Adjust to your actual string property
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun BottomNavigationBarPreview() {
|
||||
|
||||
@@ -28,6 +28,7 @@ import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CheckboxDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
@@ -36,6 +37,7 @@ import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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.semanticColors
|
||||
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 {
|
||||
@@ -97,13 +102,16 @@ object ComponentDefaults {
|
||||
fun AppCard(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String? = null,
|
||||
icon: ImageVector? = null, // New optional icon parameter
|
||||
icon: ImageVector? = null,
|
||||
text: String? = null,
|
||||
expandable: Boolean = false,
|
||||
initiallyExpanded: Boolean = false,
|
||||
onClick: (() -> Unit)? = null,
|
||||
hintContent : Hint? = null,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
var isExpanded by remember { mutableStateOf(initiallyExpanded) }
|
||||
val showHints = LocalShowHints.current
|
||||
|
||||
val rotationState by animateFloatAsState(
|
||||
targetValue = if (isExpanded) 180f else 0f,
|
||||
@@ -113,6 +121,21 @@ fun AppCard(
|
||||
// Check if we need to render the header row
|
||||
// Updated to include icon in the check
|
||||
val hasHeader = title != null || text != null || expandable || icon != null
|
||||
val 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(
|
||||
modifier = modifier
|
||||
@@ -125,7 +148,7 @@ fun AppCard(
|
||||
// Animate height changes when expanding/collapsing
|
||||
.animateContentSize(),
|
||||
shape = ComponentDefaults.CardShape,
|
||||
color = MaterialTheme.colorScheme.surfaceContainer
|
||||
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
) {
|
||||
Column {
|
||||
// --- Header Row ---
|
||||
@@ -133,12 +156,18 @@ fun AppCard(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = expandable) { isExpanded = !isExpanded }
|
||||
.clickable(enabled = canClickHeader) {
|
||||
if (expandable) {
|
||||
isExpanded = !isExpanded
|
||||
}
|
||||
onClick?.invoke()
|
||||
}
|
||||
.padding(ComponentDefaults.CardPadding),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 1. Optional Icon on the left
|
||||
if (icon != null) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
@@ -172,6 +201,16 @@ fun AppCard(
|
||||
}
|
||||
}
|
||||
|
||||
if (showHints && hintContent != null) {
|
||||
IconButton(onClick = { showBottomSheet = true }) {
|
||||
Icon(
|
||||
imageVector = AppIcons.Help,
|
||||
contentDescription = stringResource(R.string.show_hint),
|
||||
tint = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Expand Chevron (Far right)
|
||||
if (expandable) {
|
||||
Icon(
|
||||
@@ -182,21 +221,32 @@ fun AppCard(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// --- Content Area ---
|
||||
if (!expandable || isExpanded) {
|
||||
Column(
|
||||
modifier = Modifier.padding(
|
||||
val contentModifier = Modifier
|
||||
.padding(
|
||||
start = ComponentDefaults.CardPadding,
|
||||
end = ComponentDefaults.CardPadding,
|
||||
bottom = ComponentDefaults.CardPadding,
|
||||
// If we have a header, remove the top padding so content sits closer to the title.
|
||||
// If no header (legacy behavior), keep the top padding.
|
||||
top = if (hasHeader) 0.dp else ComponentDefaults.CardPadding
|
||||
),
|
||||
content = content
|
||||
)
|
||||
)
|
||||
|
||||
if (!hasHeader && onClick != null) {
|
||||
Column(
|
||||
modifier = contentModifier.clickable { onClick() },
|
||||
content = content
|
||||
)
|
||||
} else {
|
||||
Column(
|
||||
modifier = contentModifier,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,8 @@ fun BaseLanguageDropDown(
|
||||
enableMultipleSelection: Boolean = false,
|
||||
onLanguagesSelected: (List<Language>) -> Unit = {},
|
||||
alternateLanguages: List<Language> = emptyList(),
|
||||
restrictToAlternateLanguages: Boolean = false,
|
||||
enabled: Boolean = true,
|
||||
iconEnabled: Boolean = true,
|
||||
noBorder: Boolean = false,
|
||||
) {
|
||||
@@ -68,8 +70,12 @@ fun BaseLanguageDropDown(
|
||||
var tempSelection by remember { mutableStateOf<List<Language>>(emptyList()) }
|
||||
var selectedLanguagesCount by remember { mutableIntStateOf(if (enableMultipleSelection) tempSelection.size else 0) }
|
||||
|
||||
val languages = remember(alternateLanguages, defaultLanguages) {
|
||||
alternateLanguages.ifEmpty { defaultLanguages }
|
||||
val languages = remember(alternateLanguages, defaultLanguages, restrictToAlternateLanguages) {
|
||||
if (restrictToAlternateLanguages) {
|
||||
alternateLanguages
|
||||
} else {
|
||||
alternateLanguages.ifEmpty { defaultLanguages }
|
||||
}
|
||||
}
|
||||
|
||||
val buttonText = when {
|
||||
@@ -90,6 +96,7 @@ fun BaseLanguageDropDown(
|
||||
AppOutlinedButton(
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
onClick = { expanded = true },
|
||||
enabled = enabled,
|
||||
contentPadding = if (!iconEnabled) PaddingValues(0.dp) else PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
||||
borderColor = if (noBorder) Color.Unspecified else null
|
||||
) {
|
||||
@@ -221,8 +228,13 @@ fun BaseLanguageDropDown(
|
||||
) {
|
||||
val isSearching = searchText.isNotBlank()
|
||||
|
||||
if (isSearching) {
|
||||
val searchResults = (favoriteLanguages + languageHistory + languages)
|
||||
if (isSearching) {
|
||||
val searchBase = if (restrictToAlternateLanguages) {
|
||||
alternateLanguages
|
||||
} else {
|
||||
favoriteLanguages + languageHistory + languages
|
||||
}
|
||||
val searchResults = searchBase
|
||||
.distinctBy { it.nameResId }
|
||||
.filter { language ->
|
||||
val matchesName = language.name.contains(searchText, ignoreCase = true)
|
||||
@@ -237,8 +249,18 @@ fun BaseLanguageDropDown(
|
||||
searchResults.forEach { language -> SingleSelectItem(language) }
|
||||
}
|
||||
|
||||
} else if (alternateLanguages.isNotEmpty()) {
|
||||
val sortedAlternate = alternateLanguages.sortedBy { it.name }
|
||||
} 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()) {
|
||||
val sortedAlternate = alternateLanguages.sortedBy { it.name }
|
||||
if (enableMultipleSelection) {
|
||||
DropdownHeader(text = stringResource(R.string.text_all_languages))
|
||||
sortedAlternate.forEach { language -> MultiSelectItem(language) }
|
||||
@@ -458,7 +480,9 @@ fun SingleLanguageDropDown(
|
||||
onAutoSelected: () -> Unit = {},
|
||||
showNoneOption: Boolean = false,
|
||||
onNoneSelected: () -> Unit = {},
|
||||
alternateLanguages: List<Language> = emptyList()
|
||||
alternateLanguages: List<Language> = emptyList(),
|
||||
restrictToAlternateLanguages: Boolean = false,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
val languageHistory by languageViewModel.languageHistory.collectAsState()
|
||||
|
||||
@@ -477,6 +501,10 @@ fun SingleLanguageDropDown(
|
||||
showNoneOption = showNoneOption,
|
||||
onNoneSelected = onNoneSelected,
|
||||
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.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -29,8 +27,6 @@ fun CategorySelectionDialog(
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
|
||||
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
||||
|
||||
AppDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
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.AppTopAppBar
|
||||
import eu.gaudian.translator.view.hints.HintDefinition
|
||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
|
||||
@Composable
|
||||
@@ -45,10 +44,8 @@ fun VocabularyReviewScreen(
|
||||
) {
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(activity)
|
||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
|
||||
val generatedItems: List<VocabularyItem> by vocabularyViewModel.generatedVocabularyItems.collectAsState()
|
||||
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
||||
|
||||
val selectedItems = remember { mutableStateListOf<VocabularyItem>() }
|
||||
val duplicates = remember { mutableStateListOf<Boolean>() }
|
||||
@@ -65,7 +62,7 @@ fun VocabularyReviewScreen(
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = { Text(stringResource(R.string.found_items)) },
|
||||
title = stringResource(R.string.found_items),
|
||||
hintContent = HintDefinition.REVIEW.hint()
|
||||
)
|
||||
},
|
||||
|
||||
@@ -381,7 +381,7 @@ fun DefinitionPart(part: EntryPart) {
|
||||
// Fallback for JsonObject or other top-level types
|
||||
else -> contentElement.toString()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
// Ultimate fallback if something else goes wrong during parsing
|
||||
part.content.toString()
|
||||
}
|
||||
@@ -466,12 +466,6 @@ fun DefinitionPartPreview() {
|
||||
DefinitionPart(part = mockPart)
|
||||
}
|
||||
|
||||
// Data classes for the refactored components
|
||||
data class EntryData(
|
||||
val entry: DictionaryEntry,
|
||||
val language: Language?
|
||||
)
|
||||
|
||||
data class BreadcrumbItem(
|
||||
val word: String,
|
||||
val entryId: Int
|
||||
|
||||
@@ -10,8 +10,6 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -28,7 +26,6 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.navigation.NavController
|
||||
@@ -346,28 +343,8 @@ fun DictionarySimpleTopBar(
|
||||
onNavigateBack: () -> Unit
|
||||
) {
|
||||
AppTopAppBar(
|
||||
title = {
|
||||
Column {
|
||||
Text(
|
||||
text = word ?: stringResource(R.string.text_loading_3d),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
languageName?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontStyle = FontStyle.Italic
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||
}
|
||||
},
|
||||
actions = {}
|
||||
title = "TODO",
|
||||
onNavigateBack = onNavigateBack
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("SameParameterValue")
|
||||
|
||||
package eu.gaudian.translator.view.dictionary
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
|
||||
@@ -30,7 +30,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
@@ -94,27 +93,8 @@ fun EtymologyResultScreen(
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = {
|
||||
Column {
|
||||
Text(
|
||||
text = word,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
language?.name?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||
}
|
||||
},
|
||||
title = "TODO",
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
actions = {
|
||||
etymologyData?.let { data ->
|
||||
if (isTtsAvailable) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import eu.gaudian.translator.view.LocalConnectionConfigured
|
||||
import eu.gaudian.translator.view.NoConnectionScreen
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppTabLayout
|
||||
import eu.gaudian.translator.view.composable.Screen
|
||||
import eu.gaudian.translator.view.composable.TabItem
|
||||
import eu.gaudian.translator.view.settings.SettingsRoutes
|
||||
import eu.gaudian.translator.viewmodel.CorrectionViewModel
|
||||
@@ -63,7 +64,15 @@ fun MainDictionaryScreen(
|
||||
AppTabLayout(
|
||||
tabs = dictionaryTabs,
|
||||
selectedTab = selectedTab,
|
||||
onTabSelected = { selectedTab = it }
|
||||
onTabSelected = { selectedTab = it },
|
||||
onNavigateBack = {
|
||||
if (!navController.popBackStack()) {
|
||||
navController.navigate(Screen.Home.route) {
|
||||
launchSingleTop = true
|
||||
restoreState = false
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
when (selectedTab) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import androidx.navigation.NavHostController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.view.composable.AppButton
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.view.vocabulary.VocabularyListScreen
|
||||
import eu.gaudian.translator.view.vocabulary.AllCardsListScreen
|
||||
|
||||
@Composable
|
||||
fun ExerciseVocabularyScreen(
|
||||
@@ -24,7 +24,7 @@ fun ExerciseVocabularyScreen(
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(title = { Text(stringResource(R.string.text_new_vocabulary_for_this_exercise)) })
|
||||
AppTopAppBar(title =stringResource(R.string.text_new_vocabulary_for_this_exercise))
|
||||
},
|
||||
bottomBar = {
|
||||
Surface(shadowElevation = 8.dp) {
|
||||
@@ -41,7 +41,7 @@ fun ExerciseVocabularyScreen(
|
||||
) { paddingValues ->
|
||||
Box(modifier = Modifier.padding(paddingValues)) {
|
||||
|
||||
VocabularyListScreen(
|
||||
AllCardsListScreen(
|
||||
navController = navController as NavHostController?,
|
||||
onNavigateToItem = { item ->
|
||||
// Navigate to the detail screen for a specific vocabulary item
|
||||
|
||||
@@ -38,6 +38,7 @@ import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedCard
|
||||
import eu.gaudian.translator.view.composable.AppTabLayout
|
||||
import eu.gaudian.translator.view.composable.DialogButton
|
||||
import eu.gaudian.translator.view.composable.Screen
|
||||
import eu.gaudian.translator.view.composable.TabItem
|
||||
import eu.gaudian.translator.viewmodel.AiGenerationState
|
||||
import eu.gaudian.translator.viewmodel.ExerciseViewModel
|
||||
@@ -76,7 +77,15 @@ fun MainExerciseScreen(
|
||||
AppTabLayout(
|
||||
tabs = ExerciseTab.entries,
|
||||
selectedTab = selectedTab,
|
||||
onTabSelected = { selectedTab = it }
|
||||
onTabSelected = { selectedTab = it },
|
||||
onNavigateBack = {
|
||||
if (!navController.popBackStack()) {
|
||||
navController.navigate(Screen.Home.route) {
|
||||
launchSingleTop = true
|
||||
restoreState = false
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
|
||||
@@ -0,0 +1,932 @@
|
||||
package eu.gaudian.translator.view.exercises
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBackIosNew
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.outlined.Circle
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.Language
|
||||
import eu.gaudian.translator.model.TagCategory
|
||||
import eu.gaudian.translator.model.VocabularyCategory
|
||||
import eu.gaudian.translator.model.VocabularyStage
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppButton
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
||||
import eu.gaudian.translator.view.composable.AppSlider
|
||||
import eu.gaudian.translator.view.composable.OptionItemSwitch
|
||||
import eu.gaudian.translator.view.dialogs.CategoryDropdown
|
||||
import eu.gaudian.translator.view.vocabulary.VocabularyExerciseType
|
||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyExerciseViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun StartExerciseScreen(
|
||||
navController: NavHostController,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val activity = androidx.compose.ui.platform.LocalContext.current.findActivity()
|
||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val exerciseViewModel: VocabularyExerciseViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
|
||||
val exerciseConfig by exerciseViewModel.pendingExerciseConfig.collectAsState()
|
||||
var selectedLanguagePairs by remember { mutableStateOf<List<Pair<Language, Language>>>(emptyList()) }
|
||||
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory>>(emptyList()) }
|
||||
var selectedStages by remember { mutableStateOf<List<VocabularyStage>>(emptyList()) }
|
||||
|
||||
var selectedOriginLanguage by remember { mutableStateOf<Language?>(null) }
|
||||
var selectedTargetLanguage by remember { mutableStateOf<Language?>(null) }
|
||||
val isDirectionPreferenceSet = selectedOriginLanguage != null || selectedTargetLanguage != null
|
||||
|
||||
val selectedPairsIds = remember(selectedLanguagePairs) {
|
||||
selectedLanguagePairs.map { it.first.nameResId to it.second.nameResId }
|
||||
}
|
||||
val selectedCategoryIds = remember(selectedCategories) {
|
||||
selectedCategories.map { it.id }
|
||||
}
|
||||
|
||||
val filteredItemsFlow = remember(
|
||||
selectedPairsIds,
|
||||
selectedCategoryIds,
|
||||
selectedStages,
|
||||
exerciseConfig.dueTodayOnly
|
||||
) {
|
||||
vocabularyViewModel.filterVocabularyItemsByPairs(
|
||||
languagePairs = selectedPairsIds.ifEmpty { null },
|
||||
query = null,
|
||||
categoryIds = selectedCategoryIds.ifEmpty { null },
|
||||
stages = selectedStages.ifEmpty { null },
|
||||
sortOrder = VocabularyViewModel.SortOrder.NEWEST_FIRST,
|
||||
dueTodayOnly = exerciseConfig.dueTodayOnly
|
||||
)
|
||||
}
|
||||
val itemsToShow by filteredItemsFlow.collectAsState(initial = emptyList())
|
||||
val totalItemCount = itemsToShow.size
|
||||
|
||||
val availableLanguagesFromItems = remember(itemsToShow, selectedPairsIds) {
|
||||
val ids = if (selectedPairsIds.isNotEmpty()) {
|
||||
selectedPairsIds.flatMap { pair -> listOf(pair.first, pair.second) }.toSet()
|
||||
} else {
|
||||
itemsToShow.flatMap { listOfNotNull(it.languageFirstId, it.languageSecondId) }.toSet()
|
||||
}
|
||||
ids
|
||||
}
|
||||
|
||||
var amount by remember { mutableIntStateOf(0) }
|
||||
androidx.compose.runtime.LaunchedEffect(totalItemCount) {
|
||||
amount = totalItemCount
|
||||
}
|
||||
|
||||
val updateConfig: (eu.gaudian.translator.viewmodel.ExerciseConfig) -> Unit = { config ->
|
||||
exerciseViewModel.updatePendingExerciseConfig(config)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.widthIn(max = 700.dp) // Keeps it from over-stretching on tablets
|
||||
.fillMaxSize()
|
||||
) {
|
||||
TopBarSection(
|
||||
onBackClick = { navController.popBackStack() },
|
||||
shuffleCards = exerciseConfig.shuffleCards,
|
||||
onShuffleCardsChanged = { updateConfig(exerciseConfig.copy(shuffleCards = it)) },
|
||||
shuffleLanguages = exerciseConfig.shuffleLanguages,
|
||||
onShuffleLanguagesChanged = { updateConfig(exerciseConfig.copy(shuffleLanguages = it)) },
|
||||
shuffleLanguagesEnabled = !isDirectionPreferenceSet,
|
||||
trainingMode = exerciseConfig.trainingMode,
|
||||
onTrainingModeChanged = { updateConfig(exerciseConfig.copy(trainingMode = it)) },
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(32.dp)
|
||||
) {
|
||||
item { Spacer(modifier = Modifier.height(8.dp)) }
|
||||
item {
|
||||
LanguagePairSection(
|
||||
selectedPairs = selectedLanguagePairs,
|
||||
availableLanguageIds = availableLanguagesFromItems,
|
||||
onPairsChanged = { updatedPairs ->
|
||||
val hadPairs = selectedLanguagePairs.isNotEmpty()
|
||||
selectedLanguagePairs = updatedPairs
|
||||
if (updatedPairs.isNotEmpty()) {
|
||||
selectedOriginLanguage = null
|
||||
selectedTargetLanguage = null
|
||||
updateConfig(exerciseConfig.copy(originLanguageId = null, targetLanguageId = null))
|
||||
} else if (hadPairs) {
|
||||
updateConfig(exerciseConfig.copy(originLanguageId = null, targetLanguageId = null))
|
||||
}
|
||||
},
|
||||
onOriginLanguageSelected = { language ->
|
||||
if (language?.nameResId == selectedOriginLanguage?.nameResId) {
|
||||
selectedOriginLanguage = null
|
||||
updateConfig(exerciseConfig.copy(originLanguageId = null))
|
||||
} else {
|
||||
selectedOriginLanguage = language
|
||||
if (selectedTargetLanguage?.nameResId == language?.nameResId) {
|
||||
selectedTargetLanguage = null
|
||||
updateConfig(exerciseConfig.copy(targetLanguageId = null))
|
||||
}
|
||||
updateConfig(exerciseConfig.copy(originLanguageId = language?.nameResId))
|
||||
}
|
||||
},
|
||||
onTargetLanguageSelected = { language ->
|
||||
if (language?.nameResId == selectedTargetLanguage?.nameResId) {
|
||||
selectedTargetLanguage = null
|
||||
updateConfig(exerciseConfig.copy(targetLanguageId = null))
|
||||
} else {
|
||||
selectedTargetLanguage = language
|
||||
if (selectedOriginLanguage?.nameResId == language?.nameResId) {
|
||||
selectedOriginLanguage = null
|
||||
updateConfig(exerciseConfig.copy(originLanguageId = null))
|
||||
}
|
||||
updateConfig(exerciseConfig.copy(targetLanguageId = language?.nameResId))
|
||||
}
|
||||
},
|
||||
languageSelectionEnabled = true,
|
||||
selectedPairsCount = selectedLanguagePairs.size,
|
||||
selectedOriginLanguage = selectedOriginLanguage,
|
||||
selectedTargetLanguage = selectedTargetLanguage
|
||||
)
|
||||
}
|
||||
item {
|
||||
CategoriesSection(
|
||||
selectedCategories = selectedCategories,
|
||||
onCategoriesChanged = { selectedCategories = it }
|
||||
)
|
||||
}
|
||||
item {
|
||||
DifficultySection(
|
||||
selectedStages = selectedStages,
|
||||
onStagesChanged = { selectedStages = it }
|
||||
)
|
||||
}
|
||||
item {
|
||||
NumberOfCardsSection(
|
||||
totalAvailable = totalItemCount,
|
||||
amount = amount,
|
||||
onAmountChanged = { amount = it },
|
||||
dueTodayOnly = exerciseConfig.dueTodayOnly,
|
||||
onDueTodayOnlyChanged = { updateConfig(exerciseConfig.copy(dueTodayOnly = it)) }
|
||||
)
|
||||
}
|
||||
item {
|
||||
QuestionTypesSection(
|
||||
selectedTypes = exerciseConfig.selectedExerciseTypes,
|
||||
onTypeSelected = { type ->
|
||||
val current = exerciseConfig.selectedExerciseTypes.toMutableSet()
|
||||
if (type in current) {
|
||||
if (current.size > 1) current.remove(type)
|
||||
} else {
|
||||
current.add(type)
|
||||
}
|
||||
updateConfig(exerciseConfig.copy(selectedExerciseTypes = current))
|
||||
}
|
||||
)
|
||||
}
|
||||
item { Spacer(modifier = Modifier.height(24.dp)) }
|
||||
}
|
||||
|
||||
BottomButtonSection(
|
||||
enabled = totalItemCount > 0 && amount > 0,
|
||||
amount = amount,
|
||||
onStart = {
|
||||
val finalItems = if (exerciseConfig.shuffleCards) {
|
||||
itemsToShow.shuffled().take(amount)
|
||||
} else {
|
||||
itemsToShow.take(amount)
|
||||
}
|
||||
|
||||
|
||||
exerciseViewModel.startExerciseWithConfig(
|
||||
finalItems,
|
||||
exerciseConfig.copy(
|
||||
exerciseItemCount = finalItems.size,
|
||||
originalExerciseItems = finalItems,
|
||||
originLanguageId = selectedOriginLanguage?.nameResId,
|
||||
targetLanguageId = selectedTargetLanguage?.nameResId
|
||||
)
|
||||
)
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
navController.navigate("vocabulary_exercise/false")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TopBarSection(
|
||||
onBackClick: () -> Unit,
|
||||
shuffleCards: Boolean,
|
||||
onShuffleCardsChanged: (Boolean) -> Unit,
|
||||
shuffleLanguages: Boolean,
|
||||
onShuffleLanguagesChanged: (Boolean) -> Unit,
|
||||
shuffleLanguagesEnabled: Boolean,
|
||||
trainingMode: Boolean,
|
||||
onTrainingModeChanged: (Boolean) -> Unit
|
||||
) {
|
||||
var showSettings by remember { mutableStateOf(false) }
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onBackClick,
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBackIosNew,
|
||||
contentDescription = stringResource(R.string.cd_back),
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.label_start_exercise),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.weight(1f),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
IconButton(
|
||||
onClick = { showSettings = true },
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Settings,
|
||||
contentDescription = stringResource(R.string.cd_settings),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showSettings) {
|
||||
StartExerciseSettingsBottomSheet(
|
||||
sheetState = sheetState,
|
||||
shuffleCards = shuffleCards,
|
||||
onShuffleCardsChanged = onShuffleCardsChanged,
|
||||
shuffleLanguages = shuffleLanguages,
|
||||
onShuffleLanguagesChanged = onShuffleLanguagesChanged,
|
||||
shuffleLanguagesEnabled = shuffleLanguagesEnabled,
|
||||
trainingMode = trainingMode,
|
||||
onTrainingModeChanged = onTrainingModeChanged,
|
||||
onDismiss = {
|
||||
scope.launch { sheetState.hide() }.invokeOnCompletion {
|
||||
if (!sheetState.isVisible) {
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
showSettings = false
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SectionHeader(title: String, actionText: String? = null, onActionClick: () -> Unit = {}) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = title.uppercase(),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = 1.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
if (actionText != null) {
|
||||
Text(
|
||||
text = actionText,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.clickable { onActionClick() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LanguagePairSection(
|
||||
selectedPairs: List<Pair<Language, Language>>,
|
||||
availableLanguageIds: Set<Int>,
|
||||
onPairsChanged: (List<Pair<Language, Language>>) -> Unit,
|
||||
onOriginLanguageSelected: (Language?) -> Unit,
|
||||
onTargetLanguageSelected: (Language?) -> Unit,
|
||||
languageSelectionEnabled: Boolean,
|
||||
selectedPairsCount: Int,
|
||||
selectedOriginLanguage: Language?,
|
||||
selectedTargetLanguage: Language?
|
||||
) {
|
||||
val activity = androidx.compose.ui.platform.LocalContext.current.findActivity()
|
||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val allLanguages by languageViewModel.allLanguages.collectAsState(initial = emptyList())
|
||||
|
||||
val availableLanguages = remember(availableLanguageIds, allLanguages) {
|
||||
allLanguages.filter { it.nameResId in availableLanguageIds }
|
||||
}
|
||||
|
||||
val allItems by vocabularyViewModel.vocabularyItems.collectAsState(initial = emptyList())
|
||||
val pairCounts = remember(allItems) {
|
||||
allItems.mapNotNull { item ->
|
||||
val first = item.languageFirstId
|
||||
val second = item.languageSecondId
|
||||
if (first != null && second != null) {
|
||||
val pair = if (first < second) first to second else second to first
|
||||
pair
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.groupingBy { it }.eachCount()
|
||||
}
|
||||
|
||||
val availablePairs = remember(pairCounts, allLanguages) {
|
||||
pairCounts.entries
|
||||
.sortedByDescending { it.value }
|
||||
.mapNotNull { (pairIds, _) ->
|
||||
val first = allLanguages.find { it.nameResId == pairIds.first }
|
||||
val second = allLanguages.find { it.nameResId == pairIds.second }
|
||||
if (first != null && second != null) first to second else null
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
SectionHeader(title = stringResource(R.string.language_pair))
|
||||
|
||||
if (availablePairs.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.text_no_dictionary_language_pairs_found),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
} else {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
availablePairs.forEach { pair ->
|
||||
val isSelected = selectedPairs.contains(pair)
|
||||
LanguageChip(
|
||||
text = "${pair.first.name} ⇄ ${pair.second.name}",
|
||||
isSelected = isSelected,
|
||||
modifier = Modifier.widthIn(min = 160.dp),
|
||||
onClick = {
|
||||
val updated = if (isSelected) {
|
||||
selectedPairs - pair
|
||||
} else {
|
||||
selectedPairs + pair
|
||||
}
|
||||
onPairsChanged(updated)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.label_language_direction),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.text_language_direction_explanation),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
if (!languageSelectionEnabled && selectedPairsCount > 0) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.text_language_direction_disabled_with_pairs),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringResource(R.string.label_origin_language),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
eu.gaudian.translator.view.composable.SingleLanguageDropDown(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
languageViewModel = languageViewModel,
|
||||
selectedLanguage = selectedOriginLanguage,
|
||||
onLanguageSelected = { language ->
|
||||
if (selectedTargetLanguage?.nameResId == language.nameResId) return@SingleLanguageDropDown
|
||||
onOriginLanguageSelected(language)
|
||||
},
|
||||
showNoneOption = true,
|
||||
onNoneSelected = { onOriginLanguageSelected(null) },
|
||||
alternateLanguages = availableLanguages,
|
||||
restrictToAlternateLanguages = true,
|
||||
enabled = languageSelectionEnabled
|
||||
)
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringResource(R.string.label_target_language),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
eu.gaudian.translator.view.composable.SingleLanguageDropDown(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
languageViewModel = languageViewModel,
|
||||
selectedLanguage = selectedTargetLanguage,
|
||||
onLanguageSelected = { language ->
|
||||
if (selectedOriginLanguage?.nameResId == language.nameResId) return@SingleLanguageDropDown
|
||||
onTargetLanguageSelected(language)
|
||||
},
|
||||
showNoneOption = true,
|
||||
onNoneSelected = { onTargetLanguageSelected(null) },
|
||||
alternateLanguages = availableLanguages,
|
||||
restrictToAlternateLanguages = true,
|
||||
enabled = languageSelectionEnabled
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun LanguageChip(text: String, isSelected: Boolean, modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||
Surface(
|
||||
modifier = modifier.height(56.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) else MaterialTheme.colorScheme.surfaceVariant,
|
||||
border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null,
|
||||
onClick = onClick
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
// Dummy overlapping flags
|
||||
Box(modifier = Modifier.width(32.dp)) {
|
||||
Box(modifier = Modifier
|
||||
.size(20.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.Red)
|
||||
.align(Alignment.CenterStart))
|
||||
Box(modifier = Modifier
|
||||
.size(20.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.Blue)
|
||||
.align(Alignment.CenterEnd))
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text = text, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun CategoriesSection(
|
||||
selectedCategories: List<VocabularyCategory>,
|
||||
onCategoriesChanged: (List<VocabularyCategory>) -> Unit
|
||||
) {
|
||||
val activity = androidx.compose.ui.platform.LocalContext.current.findActivity()
|
||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
||||
|
||||
Column {
|
||||
SectionHeader(title = stringResource(R.string.label_categories))
|
||||
|
||||
val tagCategories = categories.filterIsInstance<TagCategory>()
|
||||
if (tagCategories.size > 15) {
|
||||
CategoryDropdown(
|
||||
onCategorySelected = { selections ->
|
||||
onCategoriesChanged(selections.filterNotNull())
|
||||
},
|
||||
multipleSelectable = true,
|
||||
onlyLists = false,
|
||||
addCategory = false,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enableSearch = true
|
||||
)
|
||||
} else {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
tagCategories.forEach { category ->
|
||||
val isSelected = selectedCategories.contains(category)
|
||||
Surface(
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
|
||||
onClick = {
|
||||
val updated = if (isSelected) {
|
||||
selectedCategories - category
|
||||
} else {
|
||||
selectedCategories + category
|
||||
}
|
||||
onCategoriesChanged(updated)
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = category.name,
|
||||
color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//Make it a flow row, as all stages in one row does not work
|
||||
@Composable
|
||||
fun DifficultySection(
|
||||
selectedStages: List<VocabularyStage>,
|
||||
onStagesChanged: (List<VocabularyStage>) -> Unit
|
||||
) {
|
||||
val context = androidx.compose.ui.platform.LocalContext.current
|
||||
Column {
|
||||
SectionHeader(title = stringResource(R.string.label_filter_by_stage))
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
VocabularyStage.entries.forEach { stage ->
|
||||
val isSelected = selectedStages.contains(stage)
|
||||
Surface(
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
|
||||
onClick = {
|
||||
val updated = if (isSelected) {
|
||||
selectedStages - stage
|
||||
} else {
|
||||
selectedStages + stage
|
||||
}
|
||||
onStagesChanged(updated)
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = stage.toString(context),
|
||||
color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NumberOfCardsSection(
|
||||
totalAvailable: Int,
|
||||
amount: Int,
|
||||
onAmountChanged: (Int) -> Unit,
|
||||
dueTodayOnly: Boolean,
|
||||
onDueTodayOnlyChanged: (Boolean) -> Unit
|
||||
) {
|
||||
Column {
|
||||
OptionItemSwitch(
|
||||
title = stringResource(R.string.text_due_today_only),
|
||||
description = stringResource(R.string.text_due_today_only_description),
|
||||
checked = dueTodayOnly,
|
||||
onCheckedChange = onDueTodayOnlyChanged
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
if (totalAvailable == 0) {
|
||||
Text(
|
||||
text = stringResource(R.string.no_cards_found_for_the_selected_filters),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
return@Column
|
||||
}
|
||||
|
||||
val maxAvailable = totalAvailable.coerceAtLeast(1)
|
||||
val coercedAmount = amount.coerceIn(1, maxAvailable)
|
||||
if (coercedAmount != amount) {
|
||||
onAmountChanged(coercedAmount)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.text_amount_of_cards).uppercase(),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = 1.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
) {
|
||||
Text(
|
||||
text = "$coercedAmount / $totalAvailable",
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AppSlider(
|
||||
value = coercedAmount.toFloat(),
|
||||
onValueChange = { onAmountChanged(it.toInt().coerceIn(1, maxAvailable)) },
|
||||
valueRange = 1f..maxAvailable.toFloat(),
|
||||
steps = if (maxAvailable > 1) maxAvailable - 2 else 0,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
val quickSelectValues = listOf(10, 25, 50, 100)
|
||||
val availableQuickSelections = quickSelectValues.filter { it <= maxAvailable }
|
||||
if (availableQuickSelections.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
availableQuickSelections.forEach { value ->
|
||||
AppOutlinedButton(
|
||||
onClick = { onAmountChanged(value) },
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(value.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//TODO use our four question types here, from StartScreen.kt, user must select at least one
|
||||
@Composable
|
||||
fun QuestionTypesSection(
|
||||
selectedTypes: Set<VocabularyExerciseType>,
|
||||
onTypeSelected: (VocabularyExerciseType) -> Unit
|
||||
) {
|
||||
Column {
|
||||
SectionHeader(title = stringResource(R.string.text_question_types))
|
||||
|
||||
QuestionTypeCard(
|
||||
title = stringResource(R.string.label_guessing_exercise),
|
||||
subtitle = stringResource(R.string.flip_card),
|
||||
icon = AppIcons.Guessing,
|
||||
isSelected = selectedTypes.contains(VocabularyExerciseType.GUESSING),
|
||||
onClick = { onTypeSelected(VocabularyExerciseType.GUESSING) }
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
QuestionTypeCard(
|
||||
title = stringResource(R.string.label_spelling_exercise),
|
||||
subtitle = stringResource(R.string.type_the_translation),
|
||||
icon = AppIcons.SpellCheck,
|
||||
isSelected = selectedTypes.contains(VocabularyExerciseType.SPELLING),
|
||||
onClick = { onTypeSelected(VocabularyExerciseType.SPELLING) }
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
QuestionTypeCard(
|
||||
title = stringResource(R.string.label_multiple_choice_exercise),
|
||||
subtitle = stringResource(R.string.label_choose_exercise_types),
|
||||
icon = AppIcons.CheckList,
|
||||
isSelected = selectedTypes.contains(VocabularyExerciseType.MULTIPLE_CHOICE),
|
||||
onClick = { onTypeSelected(VocabularyExerciseType.MULTIPLE_CHOICE) }
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
QuestionTypeCard(
|
||||
title = stringResource(R.string.label_word_jumble_exercise),
|
||||
subtitle = stringResource(R.string.text_assemble_the_word_here),
|
||||
icon = AppIcons.Extension,
|
||||
isSelected = selectedTypes.contains(VocabularyExerciseType.WORD_JUMBLE),
|
||||
onClick = { onTypeSelected(VocabularyExerciseType.WORD_JUMBLE) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun QuestionTypeCard(title: String, subtitle: String, icon: ImageVector, isSelected: Boolean, onClick: () -> Unit) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.05f) else MaterialTheme.colorScheme.surfaceVariant,
|
||||
border = if (isSelected) BorderStroke(1.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)) else null,
|
||||
onClick = onClick
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.surface),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(imageVector = icon, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(text = title, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.bodyLarge)
|
||||
Text(text = subtitle, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
Icon(
|
||||
imageVector = if (isSelected) Icons.Default.CheckCircle else Icons.Outlined.Circle,
|
||||
contentDescription = null,
|
||||
tint = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
fun BottomButtonSection(
|
||||
enabled: Boolean,
|
||||
amount: Int,
|
||||
onStart: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp)
|
||||
) {
|
||||
AppButton(
|
||||
onClick = onStart,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
enabled = enabled,
|
||||
shape = RoundedCornerShape(28.dp)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.label_start_exercise_2d, amount),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = stringResource(R.string.cd_play),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StartExerciseSettingsBottomSheet(
|
||||
sheetState: SheetState,
|
||||
shuffleCards: Boolean,
|
||||
onShuffleCardsChanged: (Boolean) -> Unit,
|
||||
shuffleLanguages: Boolean,
|
||||
onShuffleLanguagesChanged: (Boolean) -> Unit,
|
||||
shuffleLanguagesEnabled: Boolean,
|
||||
trainingMode: Boolean,
|
||||
onTrainingModeChanged: (Boolean) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.options),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
OptionItemSwitch(
|
||||
title = stringResource(R.string.shuffle_cards),
|
||||
description = stringResource(R.string.text_shuffle_card_order_description),
|
||||
checked = shuffleCards,
|
||||
onCheckedChange = onShuffleCardsChanged
|
||||
)
|
||||
OptionItemSwitch(
|
||||
title = stringResource(R.string.text_shuffle_languages),
|
||||
description = stringResource(R.string.text_shuffle_languages_description),
|
||||
checked = shuffleLanguages && shuffleLanguagesEnabled,
|
||||
onCheckedChange = { enabled ->
|
||||
if (shuffleLanguagesEnabled) {
|
||||
onShuffleLanguagesChanged(enabled)
|
||||
} else {
|
||||
onShuffleLanguagesChanged(false)
|
||||
}
|
||||
}
|
||||
)
|
||||
if (!shuffleLanguagesEnabled) {
|
||||
Text(
|
||||
text = stringResource(R.string.text_shuffle_languages_disabled_by_direction),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
OptionItemSwitch(
|
||||
title = stringResource(R.string.label_training_mode),
|
||||
description = stringResource(R.string.text_training_mode_description),
|
||||
checked = trainingMode,
|
||||
onCheckedChange = onTrainingModeChanged
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -26,12 +24,10 @@ 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.compose.ui.viewinterop.AndroidView
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.navigation.NavController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppScaffold
|
||||
@@ -61,12 +57,8 @@ fun YouTubeBrowserScreen(
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = { Text("YouTube") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(AppIcons.ArrowBack, stringResource(R.string.cd_back))
|
||||
}
|
||||
}
|
||||
title = "YouTube" ,
|
||||
onNavigateBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
|
||||
@@ -183,14 +183,8 @@ fun YouTubeExerciseScreen(
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = { Text(title, maxLines = 1) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(
|
||||
R.string.cd_back
|
||||
))
|
||||
}
|
||||
},
|
||||
title = title,
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = { onFinishVideo() },
|
||||
|
||||
@@ -21,7 +21,7 @@ enum class HintDefinition(
|
||||
CATEGORY("category_hint", R.string.category_hint_intro),
|
||||
DICTIONARY_OPTIONS("dictionary_hint", R.string.label_dictionary_options),
|
||||
EXERCISE("exercise_hint", R.string.label_exercise),
|
||||
IMPORT("import_hint", R.string.hint_how_to_generate_vocabulary_with_ai),
|
||||
VOCABULARY_GENERATE_AI("import_hint", R.string.hint_how_to_generate_vocabulary_with_ai),
|
||||
LEARNING_STAGES("learning_stages_hint", R.string.learning_stages_title),
|
||||
REVIEW("review_hint", R.string.review_intro),
|
||||
SORTING("sorting_hint", R.string.sorting_hint_title),
|
||||
@@ -40,7 +40,6 @@ enum class HintDefinition(
|
||||
@Composable
|
||||
fun hint(definition: HintDefinition): Hint = definition.hint()
|
||||
|
||||
@Composable fun HintContent(definition: HintDefinition) = definition.Render()
|
||||
@Composable fun HintScreen(navController: NavController, definition: HintDefinition) = HintScreen(
|
||||
navController = navController,
|
||||
title = stringResource(definition.titleRes),
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
@file:Suppress("HardCodedStringLiteral")
|
||||
|
||||
package eu.gaudian.translator.view.hints
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
||||
@@ -32,7 +32,7 @@ import kotlinx.coroutines.launch
|
||||
fun HintBottomSheet(
|
||||
onDismissRequest: () -> Unit,
|
||||
sheetState: SheetState,
|
||||
content: @Composable (() -> Unit)?
|
||||
content: Hint,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
ModalBottomSheet(
|
||||
@@ -50,7 +50,7 @@ fun HintBottomSheet(
|
||||
.weight(1f, fill = false)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
content?.invoke()
|
||||
content.Render()
|
||||
}
|
||||
|
||||
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.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
@@ -16,7 +15,6 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
@@ -39,8 +37,8 @@ val LocalShowHints = compositionLocalOf { false }
|
||||
*/
|
||||
@Composable
|
||||
fun WithHint(
|
||||
hintContent: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
hintContent: Hint? = null,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val showHints = LocalShowHints.current
|
||||
@@ -69,27 +67,16 @@ fun WithHint(
|
||||
}
|
||||
|
||||
if (showBottomSheet) {
|
||||
HintBottomSheet(
|
||||
onDismissRequest = {
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
showBottomSheet = false
|
||||
},
|
||||
sheetState = sheetState,
|
||||
hintContent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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))
|
||||
hintContent?.let {
|
||||
HintBottomSheet(
|
||||
onDismissRequest = {
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
showBottomSheet = false
|
||||
},
|
||||
sheetState = sheetState,
|
||||
content = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,15 +5,9 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.navigation.NavController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppScaffold
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
|
||||
@@ -30,12 +24,8 @@ fun HintScreen(
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = { Text(title) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||
}
|
||||
}
|
||||
title = title,
|
||||
onNavigateBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
|
||||
@@ -40,7 +40,7 @@ fun HintsOverviewScreen(
|
||||
val showExperimental = LocalShowExperimentalFeatures.current
|
||||
|
||||
// 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 dictionaryOptionsHint = HintDefinition.DICTIONARY_OPTIONS.hint()
|
||||
val translationScreenHint = HintDefinition.TRANSLATION.hint()
|
||||
@@ -77,7 +77,7 @@ fun HintsOverviewScreen(
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = { Text(stringResource(R.string.hint_title_hints_overview), style = MaterialTheme.typography.titleLarge) }
|
||||
title = stringResource(R.string.hint_title_hints_overview)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
|
||||
@@ -47,6 +47,7 @@ object MarkdownHintLoader {
|
||||
append(language.lowercase())
|
||||
}
|
||||
if (country.isNotEmpty()) {
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
append("-r")
|
||||
append(country.uppercase())
|
||||
}
|
||||
|
||||
433
app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt
Normal file
433
app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt
Normal file
@@ -0,0 +1,433 @@
|
||||
package eu.gaudian.translator.view.home
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AddCircle
|
||||
import androidx.compose.material.icons.filled.ChevronRight
|
||||
import androidx.compose.material.icons.filled.LocalFireDepartment
|
||||
import androidx.compose.material.icons.filled.Psychology
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.NavigationRoutes
|
||||
import eu.gaudian.translator.view.composable.AppCard
|
||||
import eu.gaudian.translator.view.composable.Screen
|
||||
import eu.gaudian.translator.view.settings.SettingsRoutes
|
||||
import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget
|
||||
import eu.gaudian.translator.viewmodel.ProgressViewModel
|
||||
|
||||
@Composable
|
||||
fun HomeScreen(
|
||||
navController: NavHostController,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val viewModel: ProgressViewModel = hiltViewModel(activity)
|
||||
val streak by viewModel.streak.collectAsState()
|
||||
val dailyGoal by viewModel.dailyGoal.collectAsState()
|
||||
val todayCompletedCount by viewModel.todayCompletedCount.collectAsState()
|
||||
|
||||
// Calculate daily goal progress
|
||||
val progress = if (dailyGoal > 0) {
|
||||
(todayCompletedCount.toFloat() / dailyGoal).coerceIn(0f, 1f)
|
||||
} else 0f
|
||||
|
||||
// A Box with TopCenter alignment keeps the UI centered on wide screens (tablets/foldables)
|
||||
Box(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.widthIn(max = 700.dp) // Prevents extreme stretching on tablets
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp, vertical = 0.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(15.dp)
|
||||
) {
|
||||
item { Spacer(modifier = Modifier.height(16.dp)) }
|
||||
item { TopProfileSection(
|
||||
navController = navController,
|
||||
context = LocalContext.current
|
||||
) }
|
||||
item {
|
||||
StreakAndGoalSection(
|
||||
streak = streak,
|
||||
progress = progress,
|
||||
progressTitle = "$todayCompletedCount / $dailyGoal",
|
||||
onGoalClick = { navController.navigate(SettingsRoutes.VOCABULARY_OPTIONS) },
|
||||
onStreakClick = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) }
|
||||
)
|
||||
}
|
||||
item {
|
||||
//TODO replace with actual implementation
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
ActionCard(
|
||||
title = "Daily Review",
|
||||
subtitle = "42 words need attention",
|
||||
icon = Icons.Default.Psychology,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
}
|
||||
item {
|
||||
ActionCard(
|
||||
title = stringResource(R.string.label_new_words),
|
||||
subtitle = stringResource(R.string.desc_expand_your_vocabulary),
|
||||
icon = Icons.Default.AddCircle,
|
||||
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
onClick = { navController.navigate(NavigationRoutes.NEW_WORD) }
|
||||
)
|
||||
}
|
||||
item { WeeklyProgressSection(navController = navController) }
|
||||
item { BottomStatsSection(navController = navController) }
|
||||
|
||||
// Bottom padding for edge-to-edge screens
|
||||
item { Spacer(modifier = Modifier.height(24.dp)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TopProfileSection(navController: NavHostController, context: Context) {
|
||||
|
||||
val motivationalPhrases = remember {
|
||||
context.resources.getStringArray(R.array.motivational_phrases)
|
||||
}
|
||||
val randomPhrase = remember { motivationalPhrases.random() }
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = randomPhrase,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = { navController.navigate(Screen.Settings.route) },
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Settings,
|
||||
contentDescription = stringResource(R.string.label_settings),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StreakAndGoalSection(
|
||||
streak: Int,
|
||||
progress: Float,
|
||||
progressTitle: String,
|
||||
onGoalClick: () -> Unit,
|
||||
onStreakClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Streak Card
|
||||
StatCard(
|
||||
modifier = Modifier.weight(1f),
|
||||
icon = Icons.Default.LocalFireDepartment,
|
||||
title = stringResource(R.string.label_2d_days, streak),
|
||||
subtitle = stringResource(R.string.label_current_streak).uppercase(),
|
||||
onClick = onStreakClick
|
||||
)
|
||||
// Goal Card
|
||||
GoalCard(
|
||||
modifier = Modifier.weight(1f),
|
||||
progress = progress,
|
||||
title = progressTitle,
|
||||
subtitle = stringResource(R.string.label_daily_goal).uppercase(),
|
||||
onClick = onGoalClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StatCard(
|
||||
modifier: Modifier = Modifier,
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
onClick: (() -> Unit)? = null
|
||||
) {
|
||||
if (onClick != null) {
|
||||
AppCard(
|
||||
modifier = modifier,
|
||||
onClick = onClick
|
||||
) {
|
||||
StatCardContent(icon = icon, title = title, subtitle = subtitle)
|
||||
}
|
||||
} else {
|
||||
AppCard(
|
||||
modifier = modifier,
|
||||
) {
|
||||
StatCardContent(icon = icon, title = title, subtitle = subtitle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatCardContent(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
subtitle: String
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(20.dp)
|
||||
.height(120.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(text = title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(text = subtitle, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GoalCard(
|
||||
modifier: Modifier = Modifier,
|
||||
progress: Float,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
onClick: (() -> Unit)? = null
|
||||
) {
|
||||
if (onClick != null) {
|
||||
AppCard(
|
||||
modifier = modifier,
|
||||
onClick = onClick
|
||||
) {
|
||||
GoalCardContent(progress = progress, title = title, subtitle = subtitle)
|
||||
}
|
||||
} else {
|
||||
AppCard(
|
||||
modifier = modifier,
|
||||
) {
|
||||
GoalCardContent(progress = progress, title = title, subtitle = subtitle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GoalCardContent(
|
||||
progress: Float,
|
||||
title: String,
|
||||
subtitle: String
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(20.dp)
|
||||
.height(120.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator(
|
||||
progress = { progress },
|
||||
modifier = Modifier.size(48.dp),
|
||||
strokeWidth = 4.dp,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
trackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
|
||||
)
|
||||
Text(text = "${(progress * 100).toInt()}%", style = MaterialTheme.typography.labelSmall, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(text = title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(text = subtitle, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ActionCard(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
icon: ImageVector,
|
||||
contentColor: Color,
|
||||
onClick: (() -> Unit)? = null
|
||||
) {
|
||||
val cardContent: @Composable () -> Unit = {
|
||||
Row(
|
||||
modifier = Modifier.padding(20.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(text = title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
Text(text = subtitle, style = MaterialTheme.typography.bodyMedium, color = contentColor.copy(alpha = 0.8f))
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Default.ChevronRight,
|
||||
contentDescription = stringResource(R.string.cd_go),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (onClick != null) {
|
||||
AppCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = onClick
|
||||
) {
|
||||
cardContent()
|
||||
}
|
||||
} else {
|
||||
AppCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
cardContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WeeklyProgressSection(
|
||||
navController: NavHostController,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val viewModel: ProgressViewModel = hiltViewModel(activity)
|
||||
val weeklyActivityStats by viewModel.weeklyActivityStats.collectAsState()
|
||||
|
||||
Column(modifier = modifier) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = stringResource(R.string.label_weekly_progress), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
TextButton(onClick = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) }) {
|
||||
Text(stringResource(R.string.label_see_history))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
AppCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) }
|
||||
) {
|
||||
if (weeklyActivityStats.isEmpty()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.text_desc_no_activity_data_available),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
} else {
|
||||
WeeklyActivityChartWidget(weeklyStats = weeklyActivityStats)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BottomStatsSection(
|
||||
navController: NavHostController
|
||||
) {
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val viewModel: ProgressViewModel = hiltViewModel(activity)
|
||||
val totalWords by viewModel.totalWords.collectAsState()
|
||||
val learnedWords by viewModel.totalWordsCompleted.collectAsState()
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Total Words
|
||||
AppCard(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = { navController.navigate(Screen.Library.route) }
|
||||
) {
|
||||
Column(modifier = Modifier.padding(20.dp)) {
|
||||
Text(text = stringResource(R.string.label_total_words).uppercase(), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(text = totalWords.toString(), style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
// Learned
|
||||
AppCard(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = { navController.navigate(NavigationRoutes.STATS_LANGUAGE_PROGRESS) }
|
||||
) {
|
||||
Column(modifier = Modifier.padding(20.dp)) {
|
||||
Text(text = stringResource(R.string.label_learned).uppercase(), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(text = learnedWords.toString(), style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,730 @@
|
||||
package eu.gaudian.translator.view.library
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.AddCircleOutline
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.LocalMall
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Tune
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.Language
|
||||
import eu.gaudian.translator.model.VocabularyCategory
|
||||
import eu.gaudian.translator.model.VocabularyItem
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.insertBreakOpportunities
|
||||
|
||||
/**
|
||||
* Top bar for the library screen with title and add button
|
||||
*/
|
||||
@Composable
|
||||
fun LibraryTopBar(
|
||||
onAddClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.label_library),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
IconButton(
|
||||
onClick = onAddClick,
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = stringResource(R.string.cd_add),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Top bar shown when items are selected for batch operations
|
||||
*/
|
||||
@Composable
|
||||
fun SelectionTopBar(
|
||||
selectionCount: Int,
|
||||
onCloseClick: () -> Unit,
|
||||
onSelectAllClick: () -> Unit,
|
||||
onDeleteClick: () -> Unit,
|
||||
onMoveToCategoryClick: () -> Unit,
|
||||
onMoveToStageClick: () -> Unit,
|
||||
isRemoveEnabled: Boolean,
|
||||
onRemoveFromCategoryClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var showOverflowMenu by remember { mutableStateOf(false) }
|
||||
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton(onClick = onCloseClick) {
|
||||
Icon(imageVector = AppIcons.Close, contentDescription = stringResource(R.string.label_close_selection_mode))
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.d_selected, selectionCount),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
|
||||
Row {
|
||||
IconButton(onClick = onSelectAllClick) {
|
||||
Icon(
|
||||
imageVector = AppIcons.SelectAll,
|
||||
contentDescription = stringResource(R.string.select_all)
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onDeleteClick) {
|
||||
Icon(AppIcons.Delete, contentDescription = stringResource(R.string.label_delete))
|
||||
}
|
||||
Box {
|
||||
IconButton(onClick = { showOverflowMenu = true }) {
|
||||
Icon(imageVector = AppIcons.More, contentDescription = stringResource(R.string.more_actions))
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showOverflowMenu,
|
||||
onDismissRequest = { showOverflowMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.move_to_category)) },
|
||||
onClick = {
|
||||
onMoveToCategoryClick()
|
||||
showOverflowMenu = false
|
||||
},
|
||||
leadingIcon = { Icon(AppIcons.Category, contentDescription = null) }
|
||||
)
|
||||
if (isRemoveEnabled) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.remove_from_category)) },
|
||||
onClick = {
|
||||
onRemoveFromCategoryClick()
|
||||
showOverflowMenu = false
|
||||
},
|
||||
leadingIcon = { Icon(AppIcons.Remove, contentDescription = null) }
|
||||
)
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.move_to_stage)) },
|
||||
onClick = {
|
||||
onMoveToStageClick()
|
||||
showOverflowMenu = false
|
||||
},
|
||||
leadingIcon = { Icon(AppIcons.Stages, contentDescription = null) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search bar with filter button
|
||||
*/
|
||||
@Composable
|
||||
fun SearchBar(
|
||||
searchQuery: String,
|
||||
onQueryChanged: (String) -> Unit,
|
||||
onFilterClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||
.padding(start = 16.dp, end = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = stringResource(R.string.cd_search),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
androidx.compose.foundation.text.BasicTextField(
|
||||
value = searchQuery,
|
||||
onValueChange = onQueryChanged,
|
||||
modifier = Modifier.weight(1f),
|
||||
textStyle = MaterialTheme.typography.bodyLarge.copy(
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
),
|
||||
singleLine = true,
|
||||
cursorBrush = androidx.compose.ui.graphics.SolidColor(MaterialTheme.colorScheme.primary),
|
||||
decorationBox = { innerTextField ->
|
||||
Box(contentAlignment = Alignment.CenterStart) {
|
||||
if (searchQuery.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.label_search_cards),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
IconButton(onClick = onFilterClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Tune,
|
||||
contentDescription = stringResource(R.string.cd_filter_options),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Segmented control for switching between All Cards and Categories view
|
||||
*/
|
||||
@Composable
|
||||
fun SegmentedControl(
|
||||
isCategoriesView: Boolean,
|
||||
onTabSelected: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||
.padding(4.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(if (!isCategoriesView) MaterialTheme.colorScheme.primary else Color.Transparent)
|
||||
.clickable { onTabSelected(false) },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.label_all_cards),
|
||||
color = if (!isCategoriesView) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontWeight = FontWeight.Bold,
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(if (isCategoriesView) MaterialTheme.colorScheme.primary else Color.Transparent)
|
||||
.clickable { onTabSelected(true) },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.label_categories),
|
||||
color = if (isCategoriesView) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontWeight = FontWeight.Bold,
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List view of all vocabulary cards
|
||||
*/
|
||||
@Composable
|
||||
fun AllCardsView(
|
||||
vocabularyItems: List<VocabularyItem>,
|
||||
allLanguages: List<Language>,
|
||||
selection: Set<Long>,
|
||||
onItemClick: (VocabularyItem) -> Unit,
|
||||
onItemLongClick: (VocabularyItem) -> Unit,
|
||||
onDeleteClick: (VocabularyItem) -> Unit,
|
||||
listState: LazyListState,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (vocabularyItems.isEmpty()) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.size(200.dp),
|
||||
painter = painterResource(id = R.drawable.ic_nothing_found),
|
||||
contentDescription = stringResource(id = R.string.no_vocabulary_items_found_perhaps_try_changing_the_filters)
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.no_vocabulary_items_found_perhaps_try_changing_the_filters),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(bottom = 100.dp)
|
||||
) {
|
||||
items(
|
||||
items = vocabularyItems,
|
||||
key = { it.id }
|
||||
) { item ->
|
||||
val isSelected = selection.contains(item.id.toLong())
|
||||
VocabularyCard(
|
||||
item = item,
|
||||
allLanguages = allLanguages,
|
||||
isSelected = isSelected,
|
||||
onItemClick = { onItemClick(item) },
|
||||
onItemLongClick = { onItemLongClick(item) },
|
||||
onDeleteClick = { onDeleteClick(item) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Individual vocabulary card component
|
||||
*/
|
||||
@Composable
|
||||
fun VocabularyCard(
|
||||
item: VocabularyItem,
|
||||
allLanguages: List<Language>,
|
||||
isSelected: Boolean,
|
||||
onItemClick: () -> Unit,
|
||||
onItemLongClick: () -> Unit,
|
||||
onDeleteClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val languageMap = remember(allLanguages) { allLanguages.associateBy { it.nameResId } }
|
||||
val langFirst = item.languageFirstId?.let { languageMap[it]?.name } ?: ""
|
||||
val langSecond = item.languageSecondId?.let { languageMap[it]?.name } ?: ""
|
||||
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.combinedClickable(
|
||||
onClick = onItemClick,
|
||||
onLongClick = onItemLongClick
|
||||
),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceContainer,
|
||||
contentColor = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.surfaceContainer
|
||||
),
|
||||
border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
// Top row: First word + Language Pill
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = insertBreakOpportunities(item.wordFirst),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = langFirst,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
// Bottom row: Second word + Language Pill
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = insertBreakOpportunities(item.wordSecond),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = langSecond,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CheckCircle,
|
||||
contentDescription = stringResource(R.string.cd_selected),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
} else {
|
||||
IconButton(onClick = { /* Options menu could go here */ }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = stringResource(R.string.cd_options),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grid view of categories
|
||||
*/
|
||||
@Composable
|
||||
fun CategoriesView(
|
||||
categories: List<VocabularyCategory>,
|
||||
onCategoryClick: (VocabularyCategory) -> Unit,
|
||||
onExploreMoreClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(bottom = 100.dp)
|
||||
) {
|
||||
items(categories) { category ->
|
||||
CategoryCard(
|
||||
category = category,
|
||||
onClick = { onCategoryClick(category) }
|
||||
)
|
||||
}
|
||||
|
||||
item(span = { GridItemSpan(2) }) {
|
||||
ExploreMoreCard(onClick = onExploreMoreClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Individual category card in grid view
|
||||
*/
|
||||
@Composable
|
||||
fun CategoryCard(
|
||||
category: VocabularyCategory,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(140.dp)
|
||||
.clickable(onClick = onClick),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.LocalMall,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = category.name,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
LinearProgressIndicator(
|
||||
progress = { 0.5f },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(4.dp)
|
||||
.clip(RoundedCornerShape(2.dp)),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
trackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Card to explore more categories
|
||||
*/
|
||||
@Composable
|
||||
fun ExploreMoreCard(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val borderColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp)
|
||||
.height(80.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.clickable(onClick = onClick)
|
||||
.drawBehind {
|
||||
val stroke = Stroke(
|
||||
width = 2.dp.toPx(),
|
||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 20f), 0f)
|
||||
)
|
||||
drawRoundRect(
|
||||
color = borderColor,
|
||||
style = stroke,
|
||||
cornerRadius = CornerRadius(16.dp.toPx(), 16.dp.toPx())
|
||||
)
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.AddCircleOutline,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.text_explore_more_categories),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crossfade container for switching between views
|
||||
*/
|
||||
@Composable
|
||||
fun LibraryViewContainer(
|
||||
isCategoriesView: Boolean,
|
||||
categoriesContent: @Composable () -> Unit,
|
||||
allCardsContent: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Crossfade(
|
||||
targetState = isCategoriesView,
|
||||
label = "LibraryViewTransition",
|
||||
modifier = modifier
|
||||
) { showCategories ->
|
||||
if (showCategories) {
|
||||
categoriesContent()
|
||||
} else {
|
||||
allCardsContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== PREVIEWS ====================
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun LibraryTopBarPreview() {
|
||||
MaterialTheme {
|
||||
LibraryTopBar(onAddClick = {})
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun SelectionTopBarPreview() {
|
||||
MaterialTheme {
|
||||
SelectionTopBar(
|
||||
selectionCount = 5,
|
||||
onCloseClick = {},
|
||||
onSelectAllClick = {},
|
||||
onDeleteClick = {},
|
||||
onMoveToCategoryClick = {},
|
||||
onMoveToStageClick = {},
|
||||
isRemoveEnabled = true,
|
||||
onRemoveFromCategoryClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun SearchBarPreview() {
|
||||
MaterialTheme {
|
||||
SearchBar(
|
||||
searchQuery = "",
|
||||
onQueryChanged = {},
|
||||
onFilterClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun SegmentedControlPreview() {
|
||||
MaterialTheme {
|
||||
SegmentedControl(
|
||||
isCategoriesView = false,
|
||||
onTabSelected = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun VocabularyCardPreview() {
|
||||
MaterialTheme {
|
||||
VocabularyCard(
|
||||
item = VocabularyItem(
|
||||
id = 1,
|
||||
wordFirst = "Hello",
|
||||
wordSecond = "Hola",
|
||||
languageFirstId = 1,
|
||||
languageSecondId = 2,
|
||||
createdAt = null,
|
||||
features = null,
|
||||
zipfFrequencyFirst = null,
|
||||
zipfFrequencySecond = null
|
||||
),
|
||||
allLanguages = emptyList(),
|
||||
isSelected = false,
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
onDeleteClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryCardPreview() {
|
||||
MaterialTheme {
|
||||
CategoryCard(
|
||||
category = eu.gaudian.translator.model.TagCategory(
|
||||
1,
|
||||
"Travel Phrases"
|
||||
),
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun ExploreMoreCardPreview() {
|
||||
MaterialTheme {
|
||||
ExploreMoreCard(onClick = {})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,600 @@
|
||||
@file:Suppress("AssignedValueIsNeverRead")
|
||||
|
||||
package eu.gaudian.translator.view.library
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Add
|
||||
import androidx.compose.material.icons.rounded.Folder
|
||||
import androidx.compose.material3.BottomSheetDefaults
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavHostController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.R.string.text_add_new_word_to_list
|
||||
import eu.gaudian.translator.model.VocabularyStage
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.NavigationRoutes
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppSwitch
|
||||
import eu.gaudian.translator.view.composable.BottomSheetMenuItem
|
||||
import eu.gaudian.translator.view.composable.MultipleLanguageDropdown
|
||||
import eu.gaudian.translator.view.dialogs.AddCategoryDialog
|
||||
import eu.gaudian.translator.view.dialogs.CategorySelectionDialog
|
||||
import eu.gaudian.translator.view.dialogs.StageSelectionDialog
|
||||
import eu.gaudian.translator.viewmodel.LanguageConfigViewModel
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel.SortOrder
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class LibraryFilterState(
|
||||
val searchQuery: String = "",
|
||||
val selectedStage: VocabularyStage? = null,
|
||||
val sortOrder: SortOrder = SortOrder.NEWEST_FIRST,
|
||||
val categoryIds: List<Int> = emptyList(),
|
||||
val dueTodayOnly: Boolean = false,
|
||||
val selectedLanguageIds: List<Int> = emptyList(),
|
||||
val selectedWordClass: String? = null
|
||||
) : Parcelable
|
||||
|
||||
@Composable
|
||||
fun LibraryScreen(
|
||||
navController: NavHostController,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val lazyListState = rememberLazyListState()
|
||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
|
||||
var filterState by rememberSaveable { mutableStateOf(LibraryFilterState()) }
|
||||
var showFilterSheet by remember { mutableStateOf(false) }
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
var selection by remember { mutableStateOf<Set<Long>>(emptySet()) }
|
||||
val isInSelectionMode = selection.isNotEmpty()
|
||||
|
||||
var showCategoryDialog by remember { mutableStateOf(false) }
|
||||
var showStageDialog by remember { mutableStateOf(false) }
|
||||
var showAddMenu by remember { mutableStateOf(false) }
|
||||
var showAddCategoryDialog by remember { mutableStateOf(false) }
|
||||
|
||||
var isCategoriesView by remember { mutableStateOf(false) }
|
||||
|
||||
val allLanguages by languageViewModel.allLanguages.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
val languagesPresent by vocabularyViewModel.languagesPresent.collectAsStateWithLifecycle(initialValue = emptySet())
|
||||
val categories by vocabularyViewModel.categories.collectAsStateWithLifecycle()
|
||||
|
||||
val vocabularyItemsFlow = remember(filterState) {
|
||||
vocabularyViewModel.filterVocabularyItems(
|
||||
languages = filterState.selectedLanguageIds,
|
||||
query = filterState.searchQuery.takeIf { it.isNotBlank() },
|
||||
categoryIds = filterState.categoryIds,
|
||||
stage = filterState.selectedStage,
|
||||
wordClass = filterState.selectedWordClass,
|
||||
dueTodayOnly = filterState.dueTodayOnly,
|
||||
sortOrder = filterState.sortOrder
|
||||
)
|
||||
}
|
||||
|
||||
val vocabularyItems by vocabularyItemsFlow.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
|
||||
var isHeaderVisible by remember { mutableStateOf(true) }
|
||||
var previousIndex by remember { mutableIntStateOf(0) }
|
||||
var previousScrollOffset by remember { mutableIntStateOf(0) }
|
||||
|
||||
// Set navigation context when vocabulary items are loaded
|
||||
LaunchedEffect(vocabularyItems) {
|
||||
if (vocabularyItems.isNotEmpty()) {
|
||||
vocabularyViewModel.setNavigationContext(vocabularyItems, vocabularyItems.first().id)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(isCategoriesView, isInSelectionMode) {
|
||||
if (isCategoriesView || isInSelectionMode) {
|
||||
isHeaderVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(lazyListState, isCategoriesView, isInSelectionMode) {
|
||||
if (isCategoriesView || isInSelectionMode) return@LaunchedEffect
|
||||
snapshotFlow { lazyListState.firstVisibleItemIndex to lazyListState.firstVisibleItemScrollOffset }
|
||||
.collect { (index, offset) ->
|
||||
val isScrollingDown = index > previousIndex || (index == previousIndex && offset > previousScrollOffset)
|
||||
val isAtTop = index == 0 && offset <= 4
|
||||
isHeaderVisible = if (isAtTop) true else !isScrollingDown
|
||||
previousIndex = index
|
||||
previousScrollOffset = offset
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.widthIn(max = 700.dp)
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp),
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = isHeaderVisible,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
Column {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
if (isInSelectionMode) {
|
||||
SelectionTopBar(
|
||||
selectionCount = selection.size,
|
||||
onCloseClick = { selection = emptySet() },
|
||||
onSelectAllClick = {
|
||||
selection = if (selection.size == vocabularyItems.size) emptySet()
|
||||
else vocabularyItems.map { it.id.toLong() }.toSet()
|
||||
},
|
||||
onDeleteClick = {
|
||||
vocabularyViewModel.deleteVocabularyItemsById(selection.map { it.toInt() })
|
||||
selection = emptySet()
|
||||
},
|
||||
onMoveToCategoryClick = { showCategoryDialog = true },
|
||||
onMoveToStageClick = { showStageDialog = true },
|
||||
isRemoveEnabled = false,
|
||||
onRemoveFromCategoryClick = {}
|
||||
)
|
||||
} else {
|
||||
LibraryTopBar(
|
||||
onAddClick = { showAddMenu = true }
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
SearchBar(
|
||||
searchQuery = filterState.searchQuery,
|
||||
onQueryChanged = { filterState = filterState.copy(searchQuery = it) },
|
||||
onFilterClick = { showFilterSheet = true }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
SegmentedControl(
|
||||
isCategoriesView = isCategoriesView,
|
||||
onTabSelected = { isCategoriesView = it }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
}
|
||||
|
||||
LibraryViewContainer(
|
||||
isCategoriesView = isCategoriesView,
|
||||
categoriesContent = {
|
||||
CategoriesView(
|
||||
categories = categories,
|
||||
onCategoryClick = { category ->
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
navController.navigate("${NavigationRoutes.CATEGORY_DETAIL}/${category.id}")
|
||||
},
|
||||
onExploreMoreClick = {
|
||||
navController.navigate(NavigationRoutes.CATEGORY_LIST)
|
||||
}
|
||||
)
|
||||
},
|
||||
allCardsContent = {
|
||||
AllCardsView(
|
||||
vocabularyItems = vocabularyItems,
|
||||
allLanguages = allLanguages,
|
||||
selection = selection,
|
||||
listState = lazyListState,
|
||||
onItemClick = { item ->
|
||||
if (isInSelectionMode) {
|
||||
selection = if (selection.contains(item.id.toLong())) {
|
||||
selection - item.id.toLong()
|
||||
} else {
|
||||
selection + item.id.toLong()
|
||||
}
|
||||
} else {
|
||||
vocabularyViewModel.setNavigationContext(vocabularyItems, item.id)
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
navController.navigate("${NavigationRoutes.VOCABULARY_DETAIL}/${item.id}")
|
||||
}
|
||||
},
|
||||
onItemLongClick = { item ->
|
||||
if (!isInSelectionMode) {
|
||||
selection = setOf(item.id.toLong())
|
||||
}
|
||||
},
|
||||
onDeleteClick = { item ->
|
||||
vocabularyViewModel.deleteData(
|
||||
VocabularyViewModel.DeleteType.VOCABULARY_ITEM,
|
||||
item = item
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
// Floating Action Button for scrolling to top
|
||||
val showFab by remember {
|
||||
derivedStateOf { lazyListState.firstVisibleItemIndex > 5 && !isInSelectionMode }
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = showFab,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 16.dp)
|
||||
) {
|
||||
FloatingActionButton(
|
||||
onClick = { scope.launch { lazyListState.animateScrollToItem(0) } },
|
||||
shape = CircleShape,
|
||||
modifier = Modifier.size(50.dp),
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
) {
|
||||
Icon(AppIcons.ArrowCircleUp, contentDescription = stringResource(R.string.cd_scroll_to_top))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showFilterSheet) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showFilterSheet = false },
|
||||
sheetState = sheetState,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
dragHandle = { BottomSheetDefaults.DragHandle() }
|
||||
) {
|
||||
FilterBottomSheetContent(
|
||||
currentFilterState = filterState,
|
||||
languageViewModel = languageViewModel,
|
||||
languagesPresent = allLanguages.filter { it.nameResId in languagesPresent },
|
||||
onApplyFilters = { newState ->
|
||||
filterState = newState
|
||||
showFilterSheet = false
|
||||
scope.launch { lazyListState.scrollToItem(0) }
|
||||
},
|
||||
onResetClick = {
|
||||
filterState = LibraryFilterState()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showCategoryDialog) {
|
||||
val selectedItems = vocabularyItems.filter { selection.contains(it.id.toLong()) }
|
||||
CategorySelectionDialog(
|
||||
onCategorySelected = {
|
||||
vocabularyViewModel.addVocabularyItemToCategories(
|
||||
selectedItems,
|
||||
it.mapNotNull { category -> category?.id }
|
||||
)
|
||||
showCategoryDialog = false
|
||||
selection = emptySet()
|
||||
},
|
||||
onDismissRequest = { showCategoryDialog = false }
|
||||
)
|
||||
}
|
||||
|
||||
if (showAddMenu) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showAddMenu = false },
|
||||
sheetState = sheetState,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 32.dp) // Extra padding for system navigation bar
|
||||
) {
|
||||
BottomSheetMenuItem(
|
||||
icon = Icons.Rounded.Add, // Or any custom vector icon you prefer
|
||||
title = stringResource(R.string.label_add_vocabulary),
|
||||
subtitle = stringResource(text_add_new_word_to_list), // Suggest adding this to strings.xml
|
||||
onClick = {
|
||||
showAddMenu = false
|
||||
navController.navigate(NavigationRoutes.NEW_WORD)
|
||||
}
|
||||
)
|
||||
|
||||
BottomSheetMenuItem(
|
||||
icon = Icons.Rounded.Folder,
|
||||
title = stringResource(R.string.label_add_category),
|
||||
subtitle = stringResource(R.string.text_desc_organize_vocabulary_groups), // Suggest adding this to strings.xml
|
||||
onClick = {
|
||||
showAddMenu = false
|
||||
showAddCategoryDialog = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showAddCategoryDialog) {
|
||||
AddCategoryDialog(onDismiss = { showAddCategoryDialog = false })
|
||||
}
|
||||
|
||||
if (showStageDialog) {
|
||||
val selectedItems = vocabularyItems.filter { selection.contains(it.id.toLong()) }
|
||||
StageSelectionDialog(
|
||||
onStageSelected = { selectedStage ->
|
||||
selectedStage?.let {
|
||||
vocabularyViewModel.addVocabularyItemToStage(selectedItems, it)
|
||||
}
|
||||
showStageDialog = false
|
||||
selection = emptySet()
|
||||
},
|
||||
onDismissRequest = { showStageDialog = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FilterBottomSheetContent(
|
||||
currentFilterState: LibraryFilterState,
|
||||
languageViewModel: LanguageViewModel,
|
||||
languagesPresent: List<eu.gaudian.translator.model.Language>,
|
||||
onApplyFilters: (LibraryFilterState) -> Unit,
|
||||
onResetClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var selectedStage by rememberSaveable { mutableStateOf(currentFilterState.selectedStage) }
|
||||
var dueTodayOnly by rememberSaveable { mutableStateOf(currentFilterState.dueTodayOnly) }
|
||||
var selectedLanguageIds by rememberSaveable { mutableStateOf(currentFilterState.selectedLanguageIds) }
|
||||
var selectedWordClass by rememberSaveable { mutableStateOf(currentFilterState.selectedWordClass) }
|
||||
var sortOrder by rememberSaveable { mutableStateOf(currentFilterState.sortOrder) }
|
||||
|
||||
val context = LocalContext.current
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val languageConfigViewModel: LanguageConfigViewModel = hiltViewModel(activity)
|
||||
val allWordClasses by languageConfigViewModel.allWordClasses.collectAsStateWithLifecycle()
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp)
|
||||
.padding(bottom = 32.dp)
|
||||
.navigationBarsPadding()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.label_filter_cards),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
TextButton(onClick = {
|
||||
selectedStage = null
|
||||
dueTodayOnly = false
|
||||
selectedLanguageIds = emptyList()
|
||||
selectedWordClass = null
|
||||
sortOrder = SortOrder.NEWEST_FIRST
|
||||
onResetClick()
|
||||
}) {
|
||||
Text(stringResource(R.string.label_reset))
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp)
|
||||
) {
|
||||
// Sort Order
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.label_sort_by).uppercase(),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = 1.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
)
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
SortOrder.entries.forEach { order ->
|
||||
FilterChip(
|
||||
selected = sortOrder == order,
|
||||
onClick = { sortOrder = order },
|
||||
label = {
|
||||
Text(order.name.replace('_', ' ').lowercase()
|
||||
.replaceFirstChar { it.titlecase() })
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Due Today
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.text_due_today_only).uppercase(),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = 1.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
AppSwitch(checked = dueTodayOnly, onCheckedChange = { dueTodayOnly = it })
|
||||
}
|
||||
|
||||
// Stages
|
||||
Column {
|
||||
Text(
|
||||
text = "STAGES",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = 1.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
)
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
FilterChip(
|
||||
selected = selectedStage == null,
|
||||
onClick = { selectedStage = null },
|
||||
label = { Text(stringResource(R.string.label_all_stages)) }
|
||||
)
|
||||
VocabularyStage.entries.forEach { stage ->
|
||||
FilterChip(
|
||||
selected = selectedStage == stage,
|
||||
onClick = { selectedStage = stage },
|
||||
label = { Text(stage.toString(context)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Languages
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.language).uppercase(),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = 1.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
)
|
||||
MultipleLanguageDropdown(
|
||||
languageViewModel = languageViewModel,
|
||||
onLanguagesSelected = { languages ->
|
||||
selectedLanguageIds = languages.map { it.nameResId }
|
||||
},
|
||||
alternateLanguages = languagesPresent
|
||||
)
|
||||
}
|
||||
|
||||
// Word Class
|
||||
if (allWordClasses.isNotEmpty()) {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.filter_by_word_type).uppercase(),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = 1.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
)
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
FilterChip(
|
||||
selected = selectedWordClass == null,
|
||||
onClick = { selectedWordClass = null },
|
||||
label = { Text(stringResource(R.string.label_all_types)) }
|
||||
)
|
||||
allWordClasses.forEach { wordClass ->
|
||||
FilterChip(
|
||||
selected = selectedWordClass == wordClass,
|
||||
onClick = { selectedWordClass = wordClass },
|
||||
label = { Text(wordClass.replaceFirstChar { it.titlecase() }) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
onApplyFilters(
|
||||
currentFilterState.copy(
|
||||
selectedStage = selectedStage,
|
||||
dueTodayOnly = dueTodayOnly,
|
||||
selectedLanguageIds = selectedLanguageIds,
|
||||
selectedWordClass = selectedWordClass,
|
||||
sortOrder = sortOrder
|
||||
)
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(28.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.label_apply_filters),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
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
|
||||
@@ -73,12 +72,8 @@ fun AboutScreen(
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = { Text(stringResource(R.string.label_about)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||
}
|
||||
}
|
||||
title = stringResource(R.string.label_about),
|
||||
onNavigateBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
|
||||
@@ -134,12 +134,8 @@ fun AddModelScreen(navController: NavController, providerKey: String) {
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = { Text(providerName) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||
}
|
||||
},
|
||||
title = providerName,
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
hintContent = HintDefinition.ADD_MODEL_SCAN.hint()
|
||||
)
|
||||
},
|
||||
|
||||
@@ -115,12 +115,8 @@ fun ApiKeyScreen(navController: NavController) {
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = { Text(stringResource(R.string.label_ai_configuration)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||
}
|
||||
},
|
||||
title = stringResource(R.string.label_ai_configuration),
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
hintContent = HintDefinition.API_KEY.hint()
|
||||
)
|
||||
}
|
||||
@@ -137,7 +133,7 @@ fun ApiKeyScreen(navController: NavController) {
|
||||
AppTabLayout(
|
||||
tabs = apiTabs,
|
||||
selectedTab = selectedTab,
|
||||
onTabSelected = { selectedTab = it }
|
||||
onTabSelected = { selectedTab = it },
|
||||
)
|
||||
|
||||
// Tab Content
|
||||
|
||||
@@ -5,9 +5,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -22,7 +19,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppScaffold
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.viewmodel.ApiViewModel
|
||||
@@ -55,12 +51,8 @@ fun CustomVocabularyPromptScreen(
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = { Text(stringResource(R.string.text_vocabulary_prompt)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||
}
|
||||
},
|
||||
title = stringResource(R.string.text_vocabulary_prompt),
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
hintContent = null //TODO: Add hint
|
||||
|
||||
)
|
||||
|
||||
@@ -8,9 +8,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -31,7 +28,6 @@ import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.LocalShowExperimentalFeatures
|
||||
import eu.gaudian.translator.view.composable.AppCard
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppScaffold
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.view.composable.OptionItemSwitch
|
||||
@@ -66,12 +62,8 @@ fun DictionaryOptionsScreen(
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = { Text(stringResource(R.string.label_dictionary_options)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||
}
|
||||
},
|
||||
title = stringResource(R.string.label_dictionary_options),
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
hintContent = HintDefinition.DICTIONARY_OPTIONS.hint()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -31,7 +29,6 @@ import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.ApiModelDropDown
|
||||
import eu.gaudian.translator.view.composable.AppCard
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedTextField
|
||||
import eu.gaudian.translator.view.composable.AppScaffold
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
@@ -71,12 +68,8 @@ fun ExerciseSettingsScreen(
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = { Text(stringResource(R.string.exercise_settings)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||
}
|
||||
}
|
||||
title = stringResource(R.string.exercise_settings),
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
|
||||
@@ -7,8 +7,6 @@ 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.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -24,7 +22,6 @@ import androidx.navigation.NavController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppCard
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppScaffold
|
||||
import eu.gaudian.translator.view.composable.AppSwitch
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
@@ -41,12 +38,8 @@ fun GeneralSettingsScreen(
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = { Text(stringResource(R.string.label_general)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||
}
|
||||
}
|
||||
title = stringResource(R.string.label_general),
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
|
||||
@@ -61,12 +61,8 @@ fun LanguageOptionsScreen(
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = { Text(stringResource(R.string.text_language_options)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||
}
|
||||
}
|
||||
title = stringResource(R.string.text_language_options),
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
@@ -132,6 +128,7 @@ fun LanguageOptionsScreen(
|
||||
}
|
||||
|
||||
if (showAddLanguageDialog) {
|
||||
@Suppress("KotlinConstantConditions")
|
||||
AddCustomLanguageDialog(
|
||||
showDialog = showAddLanguageDialog,
|
||||
onDismiss = { showAddLanguageDialog = false },
|
||||
|
||||
@@ -35,7 +35,6 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
@@ -97,16 +96,11 @@ fun LayoutOptionsScreen(navController: NavController) {
|
||||
val selectedFontName by settingsViewModel.fontPreference.collectAsStateWithLifecycle()
|
||||
val showBottomNavLabels by settingsViewModel.showBottomNavLabels.collectAsStateWithLifecycle()
|
||||
|
||||
val cdBack = stringResource(R.string.cd_back)
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = { Text(stringResource(R.string.label_appearance)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(AppIcons.ArrowBack, contentDescription = cdBack)
|
||||
}
|
||||
}
|
||||
title = stringResource(R.string.label_appearance),
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
|
||||
@@ -101,15 +101,8 @@ fun LogsScreen(navController: NavController) {
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = { Text(stringResource(R.string.label_logs)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(
|
||||
imageVector = AppIcons.ArrowBack,
|
||||
contentDescription = stringResource(R.string.cd_back)
|
||||
)
|
||||
}
|
||||
},
|
||||
title = stringResource(R.string.label_logs),
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
actions = {
|
||||
TextButton(onClick = {
|
||||
settingsViewModel.clearApiLogs()
|
||||
|
||||
@@ -27,6 +27,7 @@ import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedCard
|
||||
import eu.gaudian.translator.view.composable.AppScaffold
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.view.composable.Screen
|
||||
|
||||
private data class Setting(val titleRes: Int, val icon: ImageVector, val route: String)
|
||||
|
||||
@@ -84,7 +85,15 @@ fun MainSettingsScreen(
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = { Text(stringResource(R.string.title_settings), style = MaterialTheme.typography.titleLarge) }
|
||||
title =stringResource(R.string.title_settings),
|
||||
onNavigateBack = {
|
||||
if (!navController.popBackStack()) {
|
||||
navController.navigate(Screen.Home.route) {
|
||||
launchSingleTop = true
|
||||
restoreState = false
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
@@ -96,7 +105,7 @@ fun MainSettingsScreen(
|
||||
}
|
||||
item {
|
||||
AppCard(
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
) {
|
||||
Column {
|
||||
settings.forEachIndexed { index, setting ->
|
||||
|
||||
@@ -113,7 +113,7 @@ fun NavGraphBuilder.settingsGraph(navController: NavController) {
|
||||
HintScreen(navController, HintDefinition.DICTIONARY_OPTIONS)
|
||||
}
|
||||
composable(SettingsRoutes.HINTS_IMPORT) {
|
||||
HintScreen(navController, HintDefinition.IMPORT)
|
||||
HintScreen(navController, HintDefinition.VOCABULARY_GENERATE_AI)
|
||||
}
|
||||
composable(SettingsRoutes.HINTS_SORTING) {
|
||||
HintScreen(navController, HintDefinition.SORTING)
|
||||
|
||||
@@ -15,8 +15,6 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
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
|
||||
@@ -86,12 +84,8 @@ fun TextToSpeechSettingsScreen(
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = { Text(stringResource(R.string.settings_title_voice)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||
}
|
||||
}
|
||||
title = stringResource(R.string.settings_title_voice),
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
|
||||
@@ -9,9 +9,6 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -27,7 +24,6 @@ import androidx.navigation.NavController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppCard
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppScaffold
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.view.composable.OptionItemSwitch
|
||||
@@ -64,12 +60,8 @@ fun TranslationSettingsScreen(
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = { Text(stringResource(R.string.label_translation_settings)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||
}
|
||||
},
|
||||
title = stringResource(R.string.label_translation_settings),
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
hintContent = null //TODO add hint
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -78,13 +77,8 @@ fun VocabularyProgressOptionsScreen(
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = { Text(stringResource(R.string.vocabulary_settings)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||
}
|
||||
},
|
||||
// Here is the new hint content
|
||||
title = stringResource(R.string.label_vocabulary_settings),
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
hintContent = HintDefinition.VOCABULARY_PROGRESS.hint()
|
||||
)
|
||||
}
|
||||
@@ -97,44 +91,27 @@ fun VocabularyProgressOptionsScreen(
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Interval Settings
|
||||
AppCard(
|
||||
expandable = true,
|
||||
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()
|
||||
// Daily Goal Settings
|
||||
AppCard {
|
||||
val dailyGoal by settingsViewModel.dailyGoal.collectAsStateWithLifecycle()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.animateContentSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.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))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
TextButton(onClick = { settingsViewModel.resetIntervalsToDefaults() }) {
|
||||
Text(stringResource(R.string.reset_to_defaults))
|
||||
}
|
||||
}
|
||||
@Suppress("USELESS_ELVIS", "HardCodedStringLiteral")
|
||||
SettingsSlider(
|
||||
label = stringResource(R.string.label_target_correct_answers_per_day),
|
||||
value = dailyGoal ?: 10,
|
||||
onValueChange = { settingsViewModel.setDailyGoal(it) },
|
||||
valueRange = 10f..100f,
|
||||
steps = 17 // Allows snapping in steps of 5
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.text_daily_goal_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,30 +146,41 @@ fun VocabularyProgressOptionsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Daily Goal Settings
|
||||
AppCard {
|
||||
val dailyGoal by settingsViewModel.dailyGoal.collectAsStateWithLifecycle()
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
// Interval Settings
|
||||
AppCard(
|
||||
expandable = true,
|
||||
initiallyExpanded = true,
|
||||
title = stringResource(R.string.label_interval_settings_in_days),
|
||||
text = stringResource(R.string.text_customize_the_intervals),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.daily_learning_goal),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
@Suppress("USELESS_ELVIS", "HardCodedStringLiteral")
|
||||
SettingsSlider(
|
||||
label = stringResource(R.string.target_correct_answers_per_day),
|
||||
value = dailyGoal ?: 10,
|
||||
onValueChange = { settingsViewModel.setDailyGoal(it) },
|
||||
valueRange = 10f..100f,
|
||||
steps = 17 // Allows snapping in steps of 5
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.text_daily_goal_description),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
val intervals by settingsViewModel.intervals.collectAsStateWithLifecycle()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.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))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
TextButton(onClick = { settingsViewModel.resetIntervalsToDefaults() }) {
|
||||
Text(stringResource(R.string.reset_to_defaults))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,6 @@ import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -44,7 +42,6 @@ import eu.gaudian.translator.utils.StatusMessageService
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppButton
|
||||
import eu.gaudian.translator.view.composable.AppCard
|
||||
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.AppTopAppBar
|
||||
@@ -200,12 +197,8 @@ fun VocabularyRepositoryOptionsScreen(
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = { Text(stringResource(R.string.vocabulary_repository)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||
}
|
||||
}
|
||||
title = stringResource(R.string.vocabulary_repository),
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
|
||||
@@ -0,0 +1,678 @@
|
||||
@file:Suppress("AssignedValueIsNeverRead")
|
||||
|
||||
package eu.gaudian.translator.view.stats
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.VisibilityThreshold
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
import androidx.compose.foundation.gestures.scrollBy
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.VocabularyStage
|
||||
import eu.gaudian.translator.model.WidgetType
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.NavigationRoutes
|
||||
import eu.gaudian.translator.view.composable.AppCard
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedCard
|
||||
import eu.gaudian.translator.view.dialogs.MissingLanguageDialog
|
||||
import eu.gaudian.translator.view.vocabulary.widgets.AllVocabularyWidget
|
||||
import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressWidget
|
||||
import eu.gaudian.translator.view.vocabulary.widgets.DueTodayWidget
|
||||
import eu.gaudian.translator.view.vocabulary.widgets.LevelWidget
|
||||
import eu.gaudian.translator.view.vocabulary.widgets.StatusWidget
|
||||
import eu.gaudian.translator.view.vocabulary.widgets.StreakWidget
|
||||
import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
import eu.gaudian.translator.viewmodel.ProgressViewModel
|
||||
import eu.gaudian.translator.viewmodel.SettingsViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@SuppressLint("FrequentlyChangingValue")
|
||||
@Composable
|
||||
fun StatsScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
navController: NavHostController,
|
||||
onNavigateToCategoryDetail: ((Int) -> Unit)? = null,
|
||||
onNavigateToCategoryList: (() -> Unit)? = null,
|
||||
onScroll: (Boolean) -> Unit = {},
|
||||
) {
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val settingsViewModel: SettingsViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val progressViewModel: ProgressViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
|
||||
var showMissingLanguageDialog by remember { mutableStateOf(false) }
|
||||
var selectedMissingLanguageId by remember { mutableStateOf<Int?>(null) }
|
||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
|
||||
val affectedItems by remember(selectedMissingLanguageId) {
|
||||
selectedMissingLanguageId?.let {
|
||||
vocabularyViewModel.getItemsForLanguage(it)
|
||||
} ?: flowOf(emptyList())
|
||||
}.collectAsState(initial = emptyList())
|
||||
|
||||
if (showMissingLanguageDialog && selectedMissingLanguageId != null) {
|
||||
MissingLanguageDialog(
|
||||
showDialog = true,
|
||||
missingLanguageId = selectedMissingLanguageId!!,
|
||||
affectedItems = affectedItems,
|
||||
onDismiss = { showMissingLanguageDialog = false },
|
||||
onDelete = { items ->
|
||||
vocabularyViewModel.deleteVocabularyItemsById(items.map { it.id })
|
||||
showMissingLanguageDialog = false
|
||||
},
|
||||
onReplace = { oldId, newId ->
|
||||
vocabularyViewModel.replaceLanguageId(oldId, newId)
|
||||
showMissingLanguageDialog = false
|
||||
},
|
||||
onCreate = { newLanguage ->
|
||||
languageViewModel.addCustomLanguage(newLanguage)
|
||||
},
|
||||
languageViewModel = languageViewModel
|
||||
)
|
||||
}
|
||||
|
||||
val handleNavigateToCategoryDetail = onNavigateToCategoryDetail ?: { categoryId ->
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
navController.navigate("${NavigationRoutes.STATS_CATEGORY_DETAIL}/$categoryId")
|
||||
}
|
||||
val handleNavigateToCategoryList = onNavigateToCategoryList ?: {
|
||||
navController.navigate(NavigationRoutes.STATS_CATEGORY_LIST)
|
||||
}
|
||||
|
||||
AppOutlinedCard(modifier = modifier) {
|
||||
// We collect the order from DB initially
|
||||
val initialWidgetOrder by settingsViewModel.widgetOrder.collectAsState(initial = null)
|
||||
val collapsedWidgetIds by settingsViewModel.collapsedWidgetIds.collectAsState(initial = emptySet())
|
||||
val dashboardScrollState by settingsViewModel.dashboardScrollState.collectAsState()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
if (initialWidgetOrder == null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(vertical = 64.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else {
|
||||
// BEST PRACTICE: Use a SnapshotStateList for immediate UI updates.
|
||||
// We only initialize this once, so DB updates don't reset the list while dragging.
|
||||
val orderedWidgets = remember { mutableStateListOf<WidgetType>() }
|
||||
|
||||
// Sync with DB only on first load
|
||||
LaunchedEffect(initialWidgetOrder) {
|
||||
if (orderedWidgets.isEmpty() && !initialWidgetOrder.isNullOrEmpty()) {
|
||||
orderedWidgets.addAll(initialWidgetOrder!!)
|
||||
} else if (orderedWidgets.isEmpty()) {
|
||||
orderedWidgets.addAll(WidgetType.DEFAULT_ORDER)
|
||||
}
|
||||
}
|
||||
|
||||
val lazyListState = rememberLazyListState(
|
||||
initialFirstVisibleItemIndex = dashboardScrollState.first,
|
||||
initialFirstVisibleItemScrollOffset = dashboardScrollState.second
|
||||
)
|
||||
|
||||
// Save scroll state
|
||||
LaunchedEffect(lazyListState.firstVisibleItemIndex, lazyListState.firstVisibleItemScrollOffset) {
|
||||
settingsViewModel.saveDashboardScrollState(
|
||||
lazyListState.firstVisibleItemIndex,
|
||||
lazyListState.firstVisibleItemScrollOffset
|
||||
)
|
||||
}
|
||||
|
||||
// Detect scroll and notify parent
|
||||
LaunchedEffect(lazyListState.isScrollInProgress) {
|
||||
onScroll(lazyListState.isScrollInProgress)
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
settingsViewModel.saveDashboardScrollState(
|
||||
lazyListState.firstVisibleItemIndex,
|
||||
lazyListState.firstVisibleItemScrollOffset
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Robust Drag and Drop State ---
|
||||
val dragDropState = rememberDragDropState(
|
||||
lazyListState = lazyListState,
|
||||
onSwap = { fromIndex, toIndex ->
|
||||
// Swap data immediately for responsiveness
|
||||
orderedWidgets.apply {
|
||||
add(toIndex, removeAt(fromIndex))
|
||||
}
|
||||
},
|
||||
onDragEnd = {
|
||||
// Persist to DB only when user drops
|
||||
settingsViewModel.saveWidgetOrder(orderedWidgets.toList())
|
||||
}
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.dragContainer(dragDropState),
|
||||
contentPadding = PaddingValues(bottom = 160.dp)
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = orderedWidgets,
|
||||
key = { _, widget -> widget.id }
|
||||
) { index, widgetType ->
|
||||
|
||||
val isDragging = index == dragDropState.draggingItemIndex
|
||||
|
||||
// Calculate translation: distinct logic for dragged vs. stationary items
|
||||
val translationY = if (isDragging) {
|
||||
dragDropState.draggingItemOffset
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.zIndex(if (isDragging) 1f else 0f)
|
||||
.graphicsLayer {
|
||||
this.translationY = translationY
|
||||
this.shadowElevation = if (isDragging) 8.dp.toPx() else 0f
|
||||
this.scaleX = if (isDragging) 1.02f else 1f
|
||||
this.scaleY = if (isDragging) 1.02f else 1f
|
||||
}
|
||||
// CRITICAL FIX: Only apply animation to items NOT being dragged.
|
||||
// This prevents the "flicker" by stopping the layout animation
|
||||
// from fighting your manual drag offset.
|
||||
.then(
|
||||
if (!isDragging) {
|
||||
Modifier.animateItem(
|
||||
placementSpec = spring(
|
||||
stiffness = Spring.StiffnessLow,
|
||||
visibilityThreshold = IntOffset.VisibilityThreshold
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
) {
|
||||
WidgetContainer(
|
||||
widgetType = widgetType,
|
||||
isExpanded = widgetType.id !in collapsedWidgetIds,
|
||||
onExpandedChange = { newExpandedState ->
|
||||
scope.launch {
|
||||
settingsViewModel.setWidgetExpandedState(widgetType.id, newExpandedState)
|
||||
}
|
||||
},
|
||||
onDragStart = { dragDropState.onDragStart(index) },
|
||||
onDrag = { dragAmount -> dragDropState.onDrag(dragAmount) },
|
||||
onDragEnd = { dragDropState.onDragEnd() },
|
||||
onDragCancel = { dragDropState.onDragInterrupted() },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
LazyWidget(
|
||||
widgetType = widgetType,
|
||||
navController = navController,
|
||||
vocabularyViewModel = vocabularyViewModel,
|
||||
progressViewModel = progressViewModel,
|
||||
onNavigateToCategoryDetail = handleNavigateToCategoryDetail,
|
||||
onNavigateToCategoryList = handleNavigateToCategoryList,
|
||||
onMissingLanguage = { missingId ->
|
||||
selectedMissingLanguageId = missingId
|
||||
showMissingLanguageDialog = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WidgetContainer(
|
||||
widgetType: WidgetType,
|
||||
isExpanded: Boolean,
|
||||
onExpandedChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onDragStart: () -> Unit,
|
||||
onDrag: (Float) -> Unit,
|
||||
onDragEnd: () -> Unit,
|
||||
onDragCancel: () -> Unit,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
AppCard(
|
||||
modifier = modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateContentSize(animationSpec = tween(300, easing = FastOutSlowInEasing))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(widgetType.titleRes),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
IconButton(onClick = { onExpandedChange(!isExpanded) }) {
|
||||
Icon(
|
||||
imageVector = if (isExpanded) AppIcons.ArrowDropUp
|
||||
else AppIcons.ArrowDropDown,
|
||||
contentDescription = if (isExpanded) stringResource(R.string.text_collapse_widget)
|
||||
else stringResource(R.string.text_expand_widget)
|
||||
)
|
||||
}
|
||||
|
||||
// Drag Handle with specific pointer input
|
||||
Icon(
|
||||
imageVector = AppIcons.DragHandle,
|
||||
contentDescription = stringResource(R.string.text_drag_to_reorder),
|
||||
tint = if (isExpanded) MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||
else MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp, start = 8.dp)
|
||||
.pointerInput(Unit) {
|
||||
detectDragGestures(
|
||||
onDragStart = { _ -> onDragStart() },
|
||||
onDrag = { change, dragAmount ->
|
||||
change.consume()
|
||||
onDrag(dragAmount.y)
|
||||
},
|
||||
onDragEnd = { onDragEnd() },
|
||||
onDragCancel = { onDragCancel() }
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
if (isExpanded) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
// Fixed Drag and Drop Logic
|
||||
// --------------------------------------------------------------------------------
|
||||
|
||||
@Composable
|
||||
fun rememberDragDropState(
|
||||
lazyListState: LazyListState,
|
||||
onSwap: (Int, Int) -> Unit,
|
||||
onDragEnd: () -> Unit
|
||||
): DragDropState {
|
||||
val scope = rememberCoroutineScope()
|
||||
return remember(lazyListState, scope) {
|
||||
DragDropState(
|
||||
state = lazyListState,
|
||||
onSwap = onSwap,
|
||||
onDragFinished = onDragEnd,
|
||||
scope = scope
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun Modifier.dragContainer(dragDropState: DragDropState): Modifier {
|
||||
return this.pointerInput(dragDropState) {
|
||||
// Just allows the modifier to exist in the chain, logic is in the handle
|
||||
}
|
||||
}
|
||||
|
||||
class DragDropState(
|
||||
private val state: LazyListState,
|
||||
private val onSwap: (Int, Int) -> Unit,
|
||||
private val onDragFinished: () -> Unit,
|
||||
private val scope: CoroutineScope
|
||||
) {
|
||||
var draggingItemIndex by mutableIntStateOf(-1)
|
||||
private set
|
||||
|
||||
private val _draggingItemOffset = Animatable(0f)
|
||||
val draggingItemOffset: Float
|
||||
get() = _draggingItemOffset.value
|
||||
|
||||
private val scrollChannel = Channel<Float>(Channel.CONFLATED)
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
for (scrollAmount in scrollChannel) {
|
||||
if (scrollAmount != 0f) {
|
||||
state.scrollBy(scrollAmount)
|
||||
checkSwap()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onDragStart(index: Int) {
|
||||
draggingItemIndex = index
|
||||
scope.launch { _draggingItemOffset.snapTo(0f) }
|
||||
}
|
||||
|
||||
fun onDrag(dragAmount: Float) {
|
||||
if (draggingItemIndex == -1) return
|
||||
|
||||
scope.launch {
|
||||
_draggingItemOffset.snapTo(_draggingItemOffset.value + dragAmount)
|
||||
checkSwap()
|
||||
checkOverscroll()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkSwap() {
|
||||
val draggedIndex = draggingItemIndex
|
||||
if (draggedIndex == -1) return
|
||||
|
||||
val visibleItems = state.layoutInfo.visibleItemsInfo
|
||||
val draggedItemInfo = visibleItems.find { it.index == draggedIndex } ?: return
|
||||
|
||||
// Calculate the visual center of the dragged item
|
||||
val draggedCenter = draggedItemInfo.offset + (draggedItemInfo.size / 2) + _draggingItemOffset.value
|
||||
|
||||
// Find a target to swap with
|
||||
// FIX: We strictly check if we have crossed the CENTER of the target item.
|
||||
// This acts as a hysteresis buffer to prevent flickering at the edges.
|
||||
val targetItem = visibleItems.find { item ->
|
||||
item.index != draggedIndex &&
|
||||
draggedCenter > item.offset &&
|
||||
draggedCenter < (item.offset + item.size)
|
||||
}
|
||||
|
||||
if (targetItem != null) {
|
||||
// Extra Check: Ensure we have actually crossed the midpoint of the target
|
||||
val targetCenter = itemCenter(targetItem.offset, targetItem.size)
|
||||
val isAboveAndMovingDown = draggedIndex < targetItem.index && draggedCenter > targetCenter
|
||||
val isBelowAndMovingUp = draggedIndex > targetItem.index && draggedCenter < targetCenter
|
||||
|
||||
if (isAboveAndMovingDown || isBelowAndMovingUp) {
|
||||
val targetIndex = targetItem.index
|
||||
|
||||
// 1. Swap Data
|
||||
onSwap(draggedIndex, targetIndex)
|
||||
|
||||
// 2. Adjust Offset
|
||||
// We calculate the physical distance the item moved in the layout (e.g. 150px).
|
||||
// We subtract this from the current drag offset to keep the item visually stationary under the finger.
|
||||
val layoutJumpDistance = (targetItem.offset - draggedItemInfo.offset).toFloat()
|
||||
|
||||
scope.launch {
|
||||
_draggingItemOffset.snapTo(_draggingItemOffset.value - layoutJumpDistance)
|
||||
}
|
||||
|
||||
// 3. Update Index
|
||||
draggingItemIndex = targetIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun itemCenter(offset: Int, size: Int): Float {
|
||||
return offset + (size / 2f)
|
||||
}
|
||||
|
||||
private fun checkOverscroll() {
|
||||
val draggedIndex = draggingItemIndex
|
||||
if (draggedIndex == -1) {
|
||||
scrollChannel.trySend(0f)
|
||||
return
|
||||
}
|
||||
|
||||
val layoutInfo = state.layoutInfo
|
||||
val visibleItems = layoutInfo.visibleItemsInfo
|
||||
val draggedItemInfo = visibleItems.find { it.index == draggedIndex } ?: return
|
||||
|
||||
val viewportStart = layoutInfo.viewportStartOffset
|
||||
val viewportEnd = layoutInfo.viewportEndOffset
|
||||
// Increased threshold slightly for smoother top-edge scrolling
|
||||
val boundsStart = viewportStart + (viewportEnd * 0.15f)
|
||||
val boundsEnd = viewportEnd - (viewportEnd * 0.15f)
|
||||
|
||||
val itemTop = draggedItemInfo.offset + _draggingItemOffset.value
|
||||
val itemBottom = itemTop + draggedItemInfo.size
|
||||
|
||||
val scrollAmount = when {
|
||||
itemTop < boundsStart -> -10f // Slower, more controlled scroll speed
|
||||
itemBottom > boundsEnd -> 10f
|
||||
else -> 0f
|
||||
}
|
||||
|
||||
scrollChannel.trySend(scrollAmount)
|
||||
}
|
||||
|
||||
fun onDragEnd() {
|
||||
resetDrag()
|
||||
onDragFinished()
|
||||
}
|
||||
|
||||
fun onDragInterrupted() {
|
||||
resetDrag()
|
||||
}
|
||||
|
||||
private fun resetDrag() {
|
||||
draggingItemIndex = -1
|
||||
scrollChannel.trySend(0f)
|
||||
scope.launch { _draggingItemOffset.snapTo(0f) }
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
// Remainder of your existing components
|
||||
// --------------------------------------------------------------------------------
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Composable
|
||||
private fun LazyWidget(
|
||||
widgetType: WidgetType,
|
||||
navController: NavController,
|
||||
vocabularyViewModel: VocabularyViewModel,
|
||||
progressViewModel: ProgressViewModel,
|
||||
onNavigateToCategoryDetail: (Int) -> Unit,
|
||||
onNavigateToCategoryList: () -> Unit,
|
||||
onMissingLanguage: (Int) -> Unit
|
||||
) {
|
||||
when (widgetType) {
|
||||
|
||||
|
||||
WidgetType.Status -> LazyStatusWidget(
|
||||
vocabularyViewModel = vocabularyViewModel,
|
||||
onNavigateToNew = { navController.navigate("${NavigationRoutes.STATS_VOCABULARY_SORTING}?mode=NEW") },
|
||||
onNavigateToDuplicates = { navController.navigate("${NavigationRoutes.STATS_VOCABULARY_SORTING}?mode=DUPLICATES") },
|
||||
onNavigateToFaulty = { navController.navigate("${NavigationRoutes.STATS_VOCABULARY_SORTING}?mode=FAULTY") },
|
||||
onNavigateToNoGrammar = { navController.navigate(NavigationRoutes.STATS_NO_GRAMMAR_ITEMS) },
|
||||
onNavigateToMissingLanguage = onMissingLanguage
|
||||
)
|
||||
|
||||
else -> {
|
||||
// Regular widgets that load immediately
|
||||
when (widgetType) {
|
||||
WidgetType.Streak -> StreakWidget(
|
||||
streak = progressViewModel.streak.collectAsState(initial = 0).value,
|
||||
lastSevenDays = progressViewModel.lastSevenDays.collectAsState().value,
|
||||
dueTodayCount = progressViewModel.dueTodayCount.collectAsState().value,
|
||||
wordsCompleted = progressViewModel.totalWordsCompleted.collectAsState().value,
|
||||
onStatisticsClicked = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) }
|
||||
)
|
||||
|
||||
WidgetType.WeeklyActivityChart -> WeeklyActivityChartWidget(
|
||||
weeklyStats = progressViewModel.weeklyActivityStats.collectAsState().value
|
||||
)
|
||||
|
||||
WidgetType.AllVocabulary -> AllVocabularyWidget(
|
||||
vocabularyViewModel = vocabularyViewModel,
|
||||
onOpenAllVocabulary = { navController.navigate("${NavigationRoutes.STATS_VOCABULARY_LIST}/false/null") },
|
||||
onStageClicked = { stage ->
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
navController.navigate("${NavigationRoutes.STATS_VOCABULARY_LIST}/false/$stage")
|
||||
}
|
||||
)
|
||||
|
||||
WidgetType.DueToday -> DueTodayWidget(
|
||||
vocabularyViewModel = vocabularyViewModel,
|
||||
onStageClicked = { stage ->
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
navController.navigate("${NavigationRoutes.STATS_VOCABULARY_LIST}/false/$stage")
|
||||
}
|
||||
)
|
||||
|
||||
WidgetType.CategoryProgress -> CategoryProgressWidget(
|
||||
onCategoryClicked = { category ->
|
||||
category?.let { onNavigateToCategoryDetail(it.id) }
|
||||
},
|
||||
onViewAllClicked = onNavigateToCategoryList
|
||||
)
|
||||
|
||||
WidgetType.Levels -> LevelWidget(
|
||||
totalWords = vocabularyViewModel.vocabularyItems.collectAsState().value.size,
|
||||
learnedWords = vocabularyViewModel.stageStats.collectAsState().value
|
||||
.firstOrNull { it.stage == VocabularyStage.LEARNED }?.itemCount ?: 0,
|
||||
onNavigateToProgress = { navController.navigate(NavigationRoutes.STATS_LANGUAGE_PROGRESS) }
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LazyStatusWidget(
|
||||
vocabularyViewModel: VocabularyViewModel,
|
||||
onNavigateToNew: () -> Unit,
|
||||
onNavigateToDuplicates: () -> Unit,
|
||||
onNavigateToFaulty: () -> Unit,
|
||||
onNavigateToNoGrammar: () -> Unit,
|
||||
onNavigateToMissingLanguage: (Int) -> Unit
|
||||
) {
|
||||
var isLoading by remember { mutableStateOf(true) }
|
||||
|
||||
// Collect all flows asynchronously
|
||||
val newItemsCount by vocabularyViewModel.newItemsCount.collectAsState()
|
||||
val duplicateCount by vocabularyViewModel.duplicateItemsCount.collectAsState()
|
||||
val faultyItemsCount by vocabularyViewModel.faultyItemsCount.collectAsState()
|
||||
val itemsWithoutGrammarCount by vocabularyViewModel.itemsWithoutGrammarCount.collectAsState()
|
||||
val missingLanguageInfo by vocabularyViewModel.missingLanguageInfo.collectAsState()
|
||||
|
||||
LaunchedEffect(
|
||||
newItemsCount,
|
||||
duplicateCount,
|
||||
faultyItemsCount,
|
||||
itemsWithoutGrammarCount,
|
||||
missingLanguageInfo
|
||||
) {
|
||||
delay(100)
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 32.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(modifier = Modifier.padding(16.dp))
|
||||
}
|
||||
} else {
|
||||
StatusWidget(
|
||||
onNavigateToNew = onNavigateToNew,
|
||||
onNavigateToDuplicates = onNavigateToDuplicates,
|
||||
onNavigateToFaulty = onNavigateToFaulty,
|
||||
onNavigateToNoGrammar = onNavigateToNoGrammar,
|
||||
onNavigateToMissingLanguage = onNavigateToMissingLanguage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun StatsScreenPreview() {
|
||||
val navController = rememberNavController()
|
||||
StatsScreen(
|
||||
navController = navController,
|
||||
onNavigateToCategoryDetail = {},
|
||||
onNavigateToCategoryList = {},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun WidgetContainerPreview() {
|
||||
WidgetContainer(
|
||||
widgetType = WidgetType.Streak,
|
||||
isExpanded = true,
|
||||
onExpandedChange = {},
|
||||
onDragStart = { },
|
||||
onDrag = { },
|
||||
onDragEnd = { },
|
||||
onDragCancel = { }
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
Text("Preview Content")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.SourceLanguageDropdown
|
||||
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.viewmodel.LanguageViewModel
|
||||
|
||||
@@ -67,9 +68,23 @@ fun ActionBar(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TopBarActions(languageViewModel: LanguageViewModel, onSettingsClick: () -> Unit, hintContent: (@Composable () -> Unit)? = null) {
|
||||
fun TopBarActions(
|
||||
languageViewModel: LanguageViewModel,
|
||||
onSettingsClick: () -> Unit,
|
||||
onNavigateBack: (() -> Unit)? = null,
|
||||
hintContent: Hint? = null
|
||||
) {
|
||||
|
||||
ActionBar(modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp)) {
|
||||
if (onNavigateBack != null) {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(
|
||||
imageVector = AppIcons.ArrowBack,
|
||||
contentDescription = stringResource(R.string.cd_navigate_back)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (hintContent != null) {
|
||||
WithHint(hintContent = hintContent) {
|
||||
}
|
||||
|
||||
@@ -106,6 +106,14 @@ fun TranslationScreen(
|
||||
settingsViewModel = settingsViewModel,
|
||||
onHistoryClick = onHistoryClick,
|
||||
onSettingsClick = onSettingsClick,
|
||||
onNavigateBack = {
|
||||
if (!navController.popBackStack()) {
|
||||
navController.navigate(eu.gaudian.translator.view.composable.Screen.Home.route) {
|
||||
launchSingleTop = true
|
||||
restoreState = false
|
||||
}
|
||||
}
|
||||
},
|
||||
context = context
|
||||
)
|
||||
}
|
||||
@@ -119,6 +127,7 @@ private fun LoadedTranslationContent(
|
||||
settingsViewModel: SettingsViewModel,
|
||||
onHistoryClick: () -> Unit,
|
||||
onSettingsClick: () -> Unit,
|
||||
onNavigateBack: () -> Unit,
|
||||
context: Context
|
||||
) {
|
||||
val inputText by translationViewModel.inputText.collectAsState()
|
||||
@@ -167,7 +176,8 @@ private fun LoadedTranslationContent(
|
||||
TopBarActions(
|
||||
languageViewModel = languageViewModel,
|
||||
onSettingsClick = onSettingsClick,
|
||||
hintContent = { HintDefinition.TRANSLATION.Render() }
|
||||
onNavigateBack = onNavigateBack,
|
||||
hintContent = HintDefinition.TRANSLATION.hint()
|
||||
)
|
||||
|
||||
AppCard(modifier = Modifier.padding(8.dp, end = 8.dp, bottom = 8.dp, top = 0.dp)) {
|
||||
|
||||
@@ -5,14 +5,16 @@ package eu.gaudian.translator.view.vocabulary
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -30,7 +32,8 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
@@ -43,10 +46,12 @@ import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppScaffold
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.view.composable.PrimaryButton
|
||||
import eu.gaudian.translator.view.composable.SecondaryButton
|
||||
import eu.gaudian.translator.view.dialogs.DeleteCategoryDialog
|
||||
import eu.gaudian.translator.view.dialogs.DeleteItemsDialog
|
||||
import eu.gaudian.translator.view.dialogs.EditCategoryDialog
|
||||
import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressCircle
|
||||
import eu.gaudian.translator.viewmodel.CategoryProgress
|
||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
import eu.gaudian.translator.viewmodel.ProgressViewModel
|
||||
@@ -89,11 +94,10 @@ fun CategoryDetailScreen(
|
||||
if (!hasLangList && !hasPair && !hasStages) {
|
||||
append(stringResource(R.string.text_filter_all_items))
|
||||
} else {
|
||||
//append(stringResource(R.string.filter))
|
||||
append(" ")
|
||||
if (hasPair) {
|
||||
val (a,b) = cat.languagePairs
|
||||
append("[${languages.value.find{ it.nameResId == a }?.name} - ${languages.value.find{ it.nameResId == b }?.name}]")
|
||||
val (a, b) = cat.languagePairs
|
||||
append("[${languages.value.find { it.nameResId == a }?.name} - ${languages.value.find { it.nameResId == b }?.name}]")
|
||||
} else if (hasLangList) {
|
||||
append(cat.languages.joinToString(", ") { langId -> languages.value.find { it.nameResId == langId }?.name.toString() })
|
||||
} else {
|
||||
@@ -118,30 +122,8 @@ fun CategoryDetailScreen(
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.surface)
|
||||
) {
|
||||
AppTopAppBar(
|
||||
title = {
|
||||
Column {
|
||||
Text(
|
||||
text = title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
AppIcons.ArrowBack,
|
||||
contentDescription = stringResource(R.string.cd_back)
|
||||
)
|
||||
}
|
||||
},
|
||||
title = title,
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
actions = {
|
||||
IconButton(onClick = { showMenu = !showMenu }) {
|
||||
Icon(
|
||||
@@ -150,94 +132,58 @@ fun CategoryDetailScreen(
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false },
|
||||
modifier = Modifier.width(220.dp)
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.text_edit_category)) },
|
||||
onClick = {
|
||||
categoryViewModel.setShowEditCategoryDialog(true, categoryId)
|
||||
showMenu = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.text_export_category)) },
|
||||
onClick = {
|
||||
vocabularyViewModel.saveCategory(categoryId)
|
||||
showMenu = false
|
||||
}
|
||||
},
|
||||
leadingIcon = { Icon(AppIcons.Share, contentDescription = null) }
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.delete_items_category)) },
|
||||
onClick = {
|
||||
categoryViewModel.setShowDeleteItemsDialog(true, categoryId)
|
||||
showMenu = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.text_delete_category)) },
|
||||
onClick = {
|
||||
categoryViewModel.setShowDeleteCategoryDialog(true, categoryId)
|
||||
showMenu = false
|
||||
}
|
||||
},
|
||||
leadingIcon = { Icon(AppIcons.Delete, contentDescription = null) }
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
// TODO: Review this
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (categoryProgress != null) {
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
CategoryProgressCircle(
|
||||
totalItems = categoryProgress.totalItems,
|
||||
itemsCompleted = categoryProgress.itemsCompleted,
|
||||
itemsInStages = categoryProgress.itemsInStages,
|
||||
newItems = categoryProgress.newItems,
|
||||
circleSize = 80.dp,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
// Category Header Card with Progress and Action Buttons
|
||||
CategoryHeaderCard(
|
||||
subtitle = subtitle,
|
||||
categoryProgress = categoryProgress,
|
||||
onStartExerciseClick = {
|
||||
val categories = listOf(category)
|
||||
val categoryIds = categories.joinToString(",") { it?.id.toString() }
|
||||
navController.navigate("vocabulary_exercise/false?categories=$categoryIds")
|
||||
},
|
||||
onEditClick = {
|
||||
categoryViewModel.setShowEditCategoryDialog(true, categoryId)
|
||||
},
|
||||
onDeleteClick = {
|
||||
categoryViewModel.setShowDeleteCategoryDialog(true, categoryId)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 8.dp)
|
||||
) {
|
||||
PrimaryButton(
|
||||
text = stringResource(R.string.label_start),
|
||||
icon = AppIcons.Play,
|
||||
onClick = {
|
||||
val categories = listOf(category)
|
||||
val categoryIds = categories.joinToString(",") { it?.id.toString() }
|
||||
navController.navigate("vocabulary_exercise/false?categories=$categoryIds")
|
||||
},
|
||||
modifier = Modifier.heightIn(max = 80.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(modifier = Modifier.padding(paddingValues)) {
|
||||
VocabularyListScreen(
|
||||
AllCardsListScreen(
|
||||
categoryId = categoryId,
|
||||
showDueTodayOnly = false,
|
||||
onNavigateToItem = onNavigateToItem,
|
||||
navController = navController, // Pass the received navController here
|
||||
navController = navController,
|
||||
isRemoveFromCategoryEnabled = category is TagCategory,
|
||||
showTopBar = false,
|
||||
enableNavigationButtons = true
|
||||
@@ -266,3 +212,131 @@ fun CategoryDetailScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CategoryHeaderCard(
|
||||
subtitle: String,
|
||||
categoryProgress: CategoryProgress?,
|
||||
onStartExerciseClick: () -> Unit,
|
||||
onEditClick: () -> Unit,
|
||||
onDeleteClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// Subtitle
|
||||
if (subtitle.isNotBlank()) {
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// Progress Circle
|
||||
if (categoryProgress != null) {
|
||||
CategoryProgressCircle(
|
||||
totalItems = categoryProgress.totalItems,
|
||||
itemsCompleted = categoryProgress.itemsCompleted,
|
||||
itemsInStages = categoryProgress.itemsInStages,
|
||||
newItems = categoryProgress.newItems,
|
||||
circleSize = 120.dp,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
|
||||
// Action Buttons
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// Start Exercise Button (Primary)
|
||||
PrimaryButton(
|
||||
text = stringResource(R.string.label_start_exercise),
|
||||
icon = AppIcons.Play,
|
||||
onClick = onStartExerciseClick,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// Secondary Action Buttons
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// Edit Button
|
||||
SecondaryButton(
|
||||
text = stringResource(R.string.label_edit),
|
||||
icon = AppIcons.Edit,
|
||||
onClick = onEditClick,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
// Delete Button
|
||||
SecondaryButton(
|
||||
text = stringResource(R.string.label_delete),
|
||||
icon = AppIcons.Delete,
|
||||
onClick = onDeleteClick,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== PREVIEWS ====================
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryHeaderCardPreview() {
|
||||
MaterialTheme {
|
||||
CategoryHeaderCard(
|
||||
subtitle = "German - English | All Stages",
|
||||
categoryProgress = null,
|
||||
onStartExerciseClick = {},
|
||||
onEditClick = {},
|
||||
onDeleteClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryHeaderCardWithProgressPreview() {
|
||||
MaterialTheme {
|
||||
CategoryHeaderCard(
|
||||
subtitle = "Travel Vocabulary",
|
||||
categoryProgress = CategoryProgress(
|
||||
vocabularyCategory = TagCategory(
|
||||
1,
|
||||
"Travel"
|
||||
),
|
||||
totalItems = 50,
|
||||
newItems = 15,
|
||||
itemsInStages = 25,
|
||||
itemsCompleted = 10
|
||||
),
|
||||
onStartExerciseClick = {},
|
||||
onEditClick = {},
|
||||
onDeleteClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,13 +100,7 @@ fun CategoryListScreen(
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = {
|
||||
if (isSelectionMode && selectedCategories.isNotEmpty()) {
|
||||
Text(stringResource(R.string.text_2d_categories_selected, selectedCategories.size))
|
||||
} else {
|
||||
Text(stringResource(R.string.label_categories))
|
||||
}
|
||||
},
|
||||
title = "TODO",
|
||||
navigationIcon = {
|
||||
if (isSelectionMode) {
|
||||
IconButton(onClick = {
|
||||
|
||||
@@ -55,7 +55,6 @@ import androidx.navigation.compose.rememberNavController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.VocabularyStage
|
||||
import eu.gaudian.translator.model.WidgetType
|
||||
import eu.gaudian.translator.utils.Log
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppCard
|
||||
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.DueTodayWidget
|
||||
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.StreakWidget
|
||||
import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget
|
||||
@@ -530,17 +528,7 @@ private fun LazyWidget(
|
||||
onMissingLanguage: (Int) -> Unit
|
||||
) {
|
||||
when (widgetType) {
|
||||
WidgetType.StartButtons -> ModernStartButtons(
|
||||
onCustomClick = onShowCustomExerciseDialog,
|
||||
onDailyClick = { isSpelling ->
|
||||
if (isSpelling) {
|
||||
onShowWordPairExerciseDialog()
|
||||
} else {
|
||||
startDailyExercise(true)
|
||||
Log.d("DailyExercise", "Starting daily exercise")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
WidgetType.Status -> LazyStatusWidget(
|
||||
vocabularyViewModel = vocabularyViewModel,
|
||||
|
||||
@@ -78,7 +78,7 @@ fun LanguageProgressScreen(navController: NavController) {
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = { Text(stringResource(R.string.your_language_journey)) },
|
||||
title = stringResource(R.string.your_language_journey),
|
||||
onNavigateBack = { navController.popBackStack() }
|
||||
)
|
||||
},
|
||||
|
||||
@@ -1,474 +0,0 @@
|
||||
@file:Suppress("HardCodedStringLiteral", "AssignedValueIsNeverRead")
|
||||
|
||||
package eu.gaudian.translator.view.vocabulary
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.Exercise
|
||||
import eu.gaudian.translator.model.MatchingPairsQuestion
|
||||
import eu.gaudian.translator.model.Question
|
||||
import eu.gaudian.translator.model.VocabularyCategory
|
||||
import eu.gaudian.translator.model.VocabularyStage
|
||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||
import eu.gaudian.translator.utils.Log
|
||||
import eu.gaudian.translator.view.composable.AppDialog
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedCard
|
||||
import eu.gaudian.translator.view.composable.AppSlider
|
||||
import eu.gaudian.translator.view.composable.AppTabLayout
|
||||
import eu.gaudian.translator.view.composable.OptionItemSwitch
|
||||
import eu.gaudian.translator.view.composable.TabItem
|
||||
import eu.gaudian.translator.view.dialogs.StartExerciseDialog
|
||||
import eu.gaudian.translator.view.dialogs.VocabularyMenu
|
||||
import eu.gaudian.translator.viewmodel.ExerciseViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
enum class VocabularyTab(
|
||||
override val title: String,
|
||||
override val icon: ImageVector,
|
||||
val route: String
|
||||
) : TabItem {
|
||||
Dashboard(title = "title_dashboard", icon = AppIcons.Dashboard, route = "dashboard"),
|
||||
Statistics(title = "label_all_vocabulary", icon = AppIcons.BarChart, route = "statistics")
|
||||
}
|
||||
|
||||
@Suppress("unused", "HardCodedStringLiteral", "UnusedVariable")
|
||||
@Composable
|
||||
fun Dummy() {
|
||||
|
||||
val dummy = listOf(
|
||||
stringResource(id = R.string.title_dashboard),
|
||||
stringResource(id = R.string.label_all_vocabulary),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainVocabularyScreen(
|
||||
navController: NavController
|
||||
) {
|
||||
|
||||
val activity = LocalActivity.current as ComponentActivity
|
||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val exerciseViewModel: ExerciseViewModel = hiltViewModel(activity)
|
||||
val vocabularyNavController = rememberNavController()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var showCustomExerciseDialog by remember { mutableStateOf(false) }
|
||||
var startDailyExercise by remember { mutableStateOf(false) }
|
||||
var showWordPairExerciseDialog by remember { mutableStateOf(false) }
|
||||
|
||||
// Word Pair settings and temporary selections
|
||||
var showWordPairSettingsDialog by remember { mutableStateOf(false) }
|
||||
var tempWpCategories by remember { mutableStateOf<List<VocabularyCategory>>(emptyList()) }
|
||||
var tempWpStages by remember { mutableStateOf<List<VocabularyStage>>(emptyList()) }
|
||||
var tempWpLanguageIds by remember { mutableStateOf<List<Int>>(emptyList()) }
|
||||
|
||||
var wpQuestionCount by remember { mutableIntStateOf(5) }
|
||||
var wpShuffleQuestions by remember { mutableStateOf(true) }
|
||||
var wpShuffleWordOrder by remember { mutableStateOf(true) }
|
||||
var wpTrainingMode by remember { mutableStateOf(false) }
|
||||
var wpDueTodayOnly by remember { mutableStateOf(false) }
|
||||
|
||||
var isScrolling by remember { mutableStateOf(false) }
|
||||
|
||||
|
||||
if (showCustomExerciseDialog) {
|
||||
StartExerciseDialog(
|
||||
onDismiss = { showCustomExerciseDialog = false },
|
||||
onConfirm = { categories, stages, languageIds ->
|
||||
showCustomExerciseDialog = false
|
||||
val categoryIds = categories.joinToString(",") { it.id.toString() }
|
||||
val stageNames = stages.joinToString(",") { it.name }
|
||||
val languageIdsStr = languageIds.joinToString(",") { it.toString() }
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
navController.navigate("vocabulary_exercise/false?categories=$categoryIds&stages=$stageNames&languages=$languageIdsStr")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showWordPairExerciseDialog) {
|
||||
StartExerciseDialog(
|
||||
onDismiss = { showWordPairExerciseDialog = false },
|
||||
onConfirm = { categories, stages, languageIds ->
|
||||
// Store selections and open settings dialog instead of starting immediately
|
||||
tempWpCategories = categories
|
||||
tempWpStages = stages
|
||||
tempWpLanguageIds = languageIds
|
||||
showWordPairExerciseDialog = false
|
||||
showWordPairSettingsDialog = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val textWordPairSettings = stringResource(R.string.text_word_pair_settings)
|
||||
|
||||
// Settings dialog for Word Pair Exercise
|
||||
if (showWordPairSettingsDialog) {
|
||||
AppDialog(
|
||||
onDismissRequest = { showWordPairSettingsDialog = false },
|
||||
title = { Text(textWordPairSettings) }
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
// Amount of questions
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.text_amount_of_questions_2d,
|
||||
wpQuestionCount
|
||||
))
|
||||
AppSlider(
|
||||
value = wpQuestionCount.toFloat(),
|
||||
onValueChange = { wpQuestionCount = it.toInt().coerceIn(1, 20) },
|
||||
valueRange = 1f..20f,
|
||||
steps = 18
|
||||
)
|
||||
|
||||
// Toggles
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
OptionItemSwitch(
|
||||
title = stringResource(R.string.text_shuffle_questions),
|
||||
checked = wpShuffleQuestions,
|
||||
onCheckedChange = { wpShuffleQuestions = it },
|
||||
)
|
||||
}
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
OptionItemSwitch(
|
||||
title = stringResource(R.string.text_shuffle_card_order),
|
||||
description = stringResource(R.string.text_swap_sides),
|
||||
checked = wpShuffleWordOrder,
|
||||
onCheckedChange = { wpShuffleWordOrder = it },
|
||||
)
|
||||
}
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
OptionItemSwitch(
|
||||
title = stringResource(R.string.tetx_training_mode),
|
||||
description = stringResource(R.string.text_no_progress),
|
||||
checked = wpTrainingMode,
|
||||
onCheckedChange = { wpTrainingMode = it },
|
||||
)
|
||||
}
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
OptionItemSwitch(
|
||||
title = stringResource(R.string.text_due_today_only),
|
||||
checked = wpDueTodayOnly,
|
||||
onCheckedChange = { wpDueTodayOnly = it },
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
TextButton(onClick = { showWordPairSettingsDialog = false }) {
|
||||
Text(stringResource(id = R.string.label_cancel))
|
||||
}
|
||||
val textMatchThePairs = stringResource(R.string.text_match_the_pairs)
|
||||
val textWordPairExercise = stringResource(R.string.text_word_pair_exercise)
|
||||
val textTrainingModeDescription = stringResource(R.string.text_training_mode_description)
|
||||
val labelTrainingMode = stringResource(R.string.label_training_mode)
|
||||
TextButton(onClick = {
|
||||
showWordPairSettingsDialog = false
|
||||
// Build a Word Pair Exercise using matching pairs from selected vocabulary with options
|
||||
coroutineScope.launch {
|
||||
val items = vocabularyViewModel.filterVocabularyItems(
|
||||
languages = tempWpLanguageIds,
|
||||
query = null,
|
||||
categoryIds = tempWpCategories.map { it.id },
|
||||
stage = tempWpStages.firstOrNull(),
|
||||
sortOrder = VocabularyViewModel.SortOrder.NEWEST_FIRST,
|
||||
dueTodayOnly = wpDueTodayOnly
|
||||
).first()
|
||||
|
||||
val maxPairsPerQuestion = 5
|
||||
var pairsList = items.mapNotNull { item ->
|
||||
val k = item.wordFirst.trim()
|
||||
val v = item.wordSecond.trim()
|
||||
if (k.isNotBlank() && v.isNotBlank()) k to v else null
|
||||
}
|
||||
if (wpShuffleWordOrder) {
|
||||
pairsList = pairsList.map { (a, b) -> if ((0..1).random() == 0) a to b else b to a }
|
||||
}
|
||||
if (pairsList.isEmpty()) return@launch
|
||||
|
||||
|
||||
val shuffledPairs = if (wpShuffleQuestions) pairsList.shuffled() else pairsList
|
||||
|
||||
val chunked = shuffledPairs.chunked(maxPairsPerQuestion)
|
||||
val limitedChunks = chunked.take(wpQuestionCount)
|
||||
val questions = mutableListOf<Question>()
|
||||
var qId = 1
|
||||
limitedChunks.forEach { chunk ->
|
||||
if (chunk.size >= 2) {
|
||||
questions.add(
|
||||
MatchingPairsQuestion(
|
||||
id = qId++,
|
||||
name = textMatchThePairs,
|
||||
pairs = chunk.toMap()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (questions.isEmpty()) return@launch
|
||||
|
||||
@Suppress("HardCodedStringLiteral") val exercise = Exercise(
|
||||
id = "wordpair-" + System.currentTimeMillis().toString(),
|
||||
title = textWordPairExercise,
|
||||
questions = questions.map { it.id },
|
||||
contextTitle = if (wpTrainingMode) labelTrainingMode else null,
|
||||
contextText = if (wpTrainingMode) textTrainingModeDescription else null
|
||||
)
|
||||
exerciseViewModel.startAdHocExercise(exercise, questions)
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
navController.navigate("exercise_session")
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(id = R.string.label_start_exercise))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use LaunchedEffect to handle the navigation side effect
|
||||
LaunchedEffect(startDailyExercise) {
|
||||
if (startDailyExercise) {
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
Log.d("DailyExercise", "Starting daily exercise")
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
navController.navigate("vocabulary_exercise/false?categories=&stages=&languages=&dailyOnly=true")
|
||||
startDailyExercise = false
|
||||
}
|
||||
}
|
||||
|
||||
val navBackStackEntry by vocabularyNavController.currentBackStackEntryAsState()
|
||||
val currentRoute = navBackStackEntry?.destination?.route
|
||||
val selectedTab = remember(currentRoute) {
|
||||
VocabularyTab.entries.find { it.route == currentRoute } ?: VocabularyTab.Dashboard
|
||||
}
|
||||
|
||||
val rawShowFabText = selectedTab == VocabularyTab.Dashboard && !isScrolling
|
||||
var showFabText by remember { mutableStateOf(rawShowFabText) }
|
||||
|
||||
LaunchedEffect(rawShowFabText) {
|
||||
if (rawShowFabText) {
|
||||
// Only delay when showing (true), hide immediately
|
||||
kotlinx.coroutines.delay(2000)
|
||||
showFabText = true
|
||||
} else {
|
||||
showFabText = false
|
||||
}
|
||||
}
|
||||
|
||||
val repoEmpty =
|
||||
vocabularyViewModel.vocabularyItems.collectAsState(initial = emptyList()).value.isEmpty()
|
||||
|
||||
if (repoEmpty) {
|
||||
NoVocabularyScreen()
|
||||
return
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
AppTabLayout(
|
||||
tabs = VocabularyTab.entries,
|
||||
selectedTab = selectedTab,
|
||||
onTabSelected = { tab ->
|
||||
vocabularyNavController.navigate(tab.route) {
|
||||
popUpTo(vocabularyNavController.graph.findStartDestination().id) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
NavHost(
|
||||
navController = vocabularyNavController,
|
||||
startDestination = VocabularyTab.Dashboard.route,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
composable(VocabularyTab.Dashboard.route) {
|
||||
DashboardContent(
|
||||
navController = navController,
|
||||
onShowCustomExerciseDialog = { showCustomExerciseDialog = true },
|
||||
onNavigateToCategoryDetail = { categoryId ->
|
||||
navController.navigate("category_detail/$categoryId")
|
||||
},
|
||||
startDailyExercise = { startDailyExercise = true },
|
||||
onNavigateToCategoryList = {
|
||||
navController.navigate("category_list_screen")
|
||||
},
|
||||
onShowWordPairExerciseDialog = { showWordPairExerciseDialog = true },
|
||||
onScroll = { isScrolling = it }
|
||||
)
|
||||
}
|
||||
composable(VocabularyTab.Statistics.route) {
|
||||
StatisticsContent(navController = navController)
|
||||
}
|
||||
composable("category_detail/{categoryId}") { backStackEntry ->
|
||||
|
||||
val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull()
|
||||
|
||||
if (categoryId != null) {
|
||||
CategoryDetailScreen(
|
||||
categoryId = categoryId,
|
||||
onBackClick = { vocabularyNavController.popBackStack() },
|
||||
onNavigateToItem = { item ->
|
||||
navController.navigate("vocabulary_detail/${item.id}")
|
||||
},
|
||||
navController = navController as NavHostController
|
||||
)
|
||||
}
|
||||
}
|
||||
composable("vocabulary_exercise/{isSpelling}") { backStackEntry ->
|
||||
backStackEntry.arguments?.getString("isSpelling")?.toBooleanStrict() ?: false
|
||||
VocabularyExerciseHostScreen(
|
||||
categoryIdsAsJson = null,
|
||||
stageNamesAsJson = null,
|
||||
languageIdsAsJson = null,
|
||||
onClose = { navController.popBackStack() },
|
||||
navController = navController,
|
||||
dailyOnlyAsJson = null
|
||||
)
|
||||
}
|
||||
composable("vocabulary_exercise/{dailyOnly}") { backStackEntry ->
|
||||
backStackEntry.arguments?.getString("dailyOnly")?.toBooleanStrict() ?: false
|
||||
VocabularyExerciseHostScreen(
|
||||
categoryIdsAsJson = null,
|
||||
stageNamesAsJson = null,
|
||||
languageIdsAsJson = null,
|
||||
onClose = { navController.popBackStack() },
|
||||
navController = navController,
|
||||
dailyOnlyAsJson = "{\"dailyOnly\": true}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var menuHeightPx by remember { mutableIntStateOf(0) }
|
||||
val density = LocalDensity.current
|
||||
val menuHeightDp = (menuHeightPx / density.density).dp
|
||||
val animatedBottomPadding by animateDpAsState(targetValue = 16.dp + 8.dp + menuHeightDp, label = "FBottomPadding")
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
VocabularyMenu(modifier = Modifier.onSizeChanged { menuHeightPx = it.height }, showFabText = showFabText)
|
||||
}
|
||||
|
||||
// Place the FAB separately and animate its bottom padding based on the menu height
|
||||
FloatingActionButton(
|
||||
onClick = { showCustomExerciseDialog = true },
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(end = 16.dp, bottom = animatedBottomPadding)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.animateContentSize()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = AppIcons.Quiz,
|
||||
contentDescription = null
|
||||
)
|
||||
if(showFabText) {
|
||||
Text(
|
||||
text = stringResource(R.string.label_start_exercise),
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StatisticsContent(
|
||||
navController: NavController
|
||||
) {
|
||||
|
||||
AppOutlinedCard {
|
||||
VocabularyListScreen(
|
||||
categoryId = null,
|
||||
showDueTodayOnly = false,
|
||||
onNavigateToItem = { item ->
|
||||
navController.navigate("vocabulary_detail/${item.id}")
|
||||
},
|
||||
onNavigateBack = null,
|
||||
navController = navController as NavHostController,
|
||||
enableNavigationButtons = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun VocabularyDashboardScreenPreview() {
|
||||
val navController = rememberNavController()
|
||||
MainVocabularyScreen(navController = navController)
|
||||
}
|
||||
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun StatisticsContentPreview() {
|
||||
val navController = rememberNavController()
|
||||
StatisticsContent(navController = navController)
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
package eu.gaudian.translator.view.vocabulary
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.WarningAmber
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.VocabularyCategory
|
||||
import eu.gaudian.translator.model.VocabularyItem
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppButton
|
||||
import eu.gaudian.translator.view.composable.AppCheckbox
|
||||
import eu.gaudian.translator.view.composable.AppScaffold
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.view.dialogs.CategoryDropdown
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
|
||||
@Composable
|
||||
fun NewWordReviewScreen(
|
||||
navController: NavHostController,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(activity)
|
||||
|
||||
val generatedItems by vocabularyViewModel.generatedVocabularyItems.collectAsState()
|
||||
val selectedItems = remember { mutableStateListOf<VocabularyItem>() }
|
||||
val duplicates = remember { mutableStateListOf<Boolean>() }
|
||||
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory?>>(emptyList()) }
|
||||
|
||||
LaunchedEffect(generatedItems) {
|
||||
val duplicateResults = vocabularyViewModel.findDuplicates(generatedItems)
|
||||
duplicates.clear()
|
||||
duplicates.addAll(duplicateResults)
|
||||
selectedItems.clear()
|
||||
selectedItems.addAll(generatedItems.filterIndexed { index, _ -> !duplicateResults[index] })
|
||||
}
|
||||
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = stringResource(R.string.found_items),
|
||||
onNavigateBack = { navController.popBackStack() }
|
||||
)
|
||||
},
|
||||
modifier = modifier.fillMaxSize()
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
SummaryHeader(
|
||||
totalCount = generatedItems.size,
|
||||
selectedCount = selectedItems.size,
|
||||
modifier = Modifier.padding(horizontal = 20.dp, vertical = 12.dp)
|
||||
)
|
||||
|
||||
if (generatedItems.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.text_no_data_available),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
} else {
|
||||
ReviewList(
|
||||
generatedItems = generatedItems,
|
||||
selectedItems = selectedItems,
|
||||
duplicates = duplicates,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.select_list_optional),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier = Modifier.padding(horizontal = 20.dp, vertical = 8.dp)
|
||||
)
|
||||
CategoryDropdown(
|
||||
onCategorySelected = { selectedCategories = it },
|
||||
noneSelectable = false,
|
||||
multipleSelectable = true,
|
||||
onlyLists = true,
|
||||
addCategory = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp)
|
||||
)
|
||||
|
||||
ActionRow(
|
||||
selectedCount = selectedItems.size,
|
||||
onCancel = { navController.popBackStack() },
|
||||
onConfirm = {
|
||||
val selectedCategoryIds = selectedCategories.filterNotNull().map { it.id }
|
||||
vocabularyViewModel.addVocabularyItems(selectedItems.toList(), selectedCategoryIds)
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
navController.popBackStack("new_word", inclusive = false)
|
||||
},
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SummaryHeader(
|
||||
totalCount: Int,
|
||||
selectedCount: Int,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.found_items),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.text_amount_2d, totalCount),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(
|
||||
text = stringResource(R.string.label_add_, selectedCount),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.select_items_to_add),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReviewList(
|
||||
generatedItems: List<VocabularyItem>,
|
||||
selectedItems: MutableList<VocabularyItem>,
|
||||
duplicates: List<Boolean>,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val duplicateLabel = stringResource(R.string.duplicate)
|
||||
LazyColumn(modifier = modifier) {
|
||||
itemsIndexed(generatedItems) { index, item ->
|
||||
val isDuplicate = duplicates.getOrNull(index) == true
|
||||
val isSelected = selectedItems.contains(item)
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 6.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (isDuplicate) {
|
||||
MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.25f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.25f)
|
||||
}
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AppCheckbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = { checked ->
|
||||
if (checked) {
|
||||
selectedItems.add(item)
|
||||
} else {
|
||||
selectedItems.remove(item)
|
||||
}
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.size(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(text = item.wordFirst, style = MaterialTheme.typography.titleMedium)
|
||||
Text(text = item.wordSecond, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
if (isDuplicate) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(MaterialTheme.colorScheme.error.copy(alpha = 0.15f))
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.WarningAmber,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Spacer(modifier = Modifier.size(4.dp))
|
||||
Text(
|
||||
text = duplicateLabel,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActionRow(
|
||||
selectedCount: Int,
|
||||
onCancel: () -> Unit,
|
||||
onConfirm: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TextButton(onClick = onCancel) {
|
||||
Text(stringResource(R.string.label_cancel))
|
||||
}
|
||||
AppButton(
|
||||
onClick = onConfirm,
|
||||
enabled = selectedCount > 0
|
||||
) {
|
||||
Text(stringResource(R.string.label_add_, selectedCount))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,780 @@
|
||||
package eu.gaudian.translator.view.vocabulary
|
||||
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AutoAwesome
|
||||
import androidx.compose.material.icons.filled.DriveFolderUpload
|
||||
import androidx.compose.material.icons.filled.EditNote
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.Language
|
||||
import eu.gaudian.translator.model.VocabularyItem
|
||||
import eu.gaudian.translator.utils.StatusMessageId
|
||||
import eu.gaudian.translator.utils.StatusMessageService
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.NavigationRoutes
|
||||
import eu.gaudian.translator.view.composable.AppButton
|
||||
import eu.gaudian.translator.view.composable.AppCard
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
||||
import eu.gaudian.translator.view.composable.AppSlider
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.view.composable.InspiringSearchField
|
||||
import eu.gaudian.translator.view.composable.Screen
|
||||
import eu.gaudian.translator.view.composable.SourceLanguageDropdown
|
||||
import eu.gaudian.translator.view.composable.TargetLanguageDropdown
|
||||
import eu.gaudian.translator.view.hints.HintDefinition
|
||||
import eu.gaudian.translator.view.library.VocabularyCard
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun NewWordScreen(
|
||||
navController: NavHostController,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val isGenerating by vocabularyViewModel.isGenerating.collectAsState()
|
||||
val generatedItems by vocabularyViewModel.generatedVocabularyItems.collectAsState()
|
||||
val allLanguages by languageViewModel.allLanguages.collectAsState()
|
||||
val recentItems by vocabularyViewModel.vocabularyItems.collectAsState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var category by remember { mutableStateOf("") }
|
||||
var amount by remember { mutableFloatStateOf(8f) }
|
||||
var navigateToReview by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(isGenerating, generatedItems, navigateToReview) {
|
||||
if (navigateToReview && !isGenerating) {
|
||||
if (generatedItems.isNotEmpty()) {
|
||||
navController.navigate(NavigationRoutes.NEW_WORD_REVIEW)
|
||||
}
|
||||
navigateToReview = false
|
||||
}
|
||||
}
|
||||
|
||||
val statusMessageService = StatusMessageService
|
||||
|
||||
val context = LocalContext.current
|
||||
val showTableImportDialog = remember { mutableStateOf(false) }
|
||||
var parsedTable by remember { mutableStateOf<List<List<String>>>(emptyList()) }
|
||||
var selectedColFirst by remember { mutableIntStateOf(0) }
|
||||
var selectedColSecond by remember { mutableIntStateOf(1) }
|
||||
var skipHeader by remember { mutableStateOf(true) }
|
||||
var selectedLangFirst by remember { mutableStateOf<Language?>(null) }
|
||||
var selectedLangSecond by remember { mutableStateOf<Language?>(null) }
|
||||
|
||||
val recentlyAdded = remember(recentItems) {
|
||||
recentItems.sortedByDescending { it.id }.take(4)
|
||||
}
|
||||
|
||||
fun parseCsv(text: String): List<List<String>> {
|
||||
if (text.isBlank()) return emptyList()
|
||||
val candidates = listOf(',', ';', '\t')
|
||||
val sampleLine = text.lineSequence().firstOrNull { it.isNotBlank() } ?: return emptyList()
|
||||
val delimiter = candidates.maxBy { c -> sampleLine.count { it == c } }
|
||||
|
||||
val rows = mutableListOf<List<String>>()
|
||||
var current = StringBuilder()
|
||||
var inQuotes = false
|
||||
val currentRow = mutableListOf<String>()
|
||||
|
||||
var i = 0
|
||||
while (i < text.length) {
|
||||
when (val ch = text[i]) {
|
||||
'"' -> {
|
||||
if (inQuotes && i + 1 < text.length && text[i + 1] == '"') {
|
||||
current.append('"')
|
||||
i++
|
||||
} else {
|
||||
inQuotes = !inQuotes
|
||||
}
|
||||
}
|
||||
'\r' -> {
|
||||
// ignore
|
||||
}
|
||||
'\n' -> {
|
||||
val field = current.toString()
|
||||
current = StringBuilder()
|
||||
currentRow.add(field)
|
||||
rows.add(currentRow.toList())
|
||||
currentRow.clear()
|
||||
inQuotes = false
|
||||
}
|
||||
else -> {
|
||||
if (ch == delimiter && !inQuotes) {
|
||||
val field = current.toString()
|
||||
currentRow.add(field)
|
||||
current = StringBuilder()
|
||||
} else {
|
||||
current.append(ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
if (current.isNotEmpty() || currentRow.isNotEmpty()) {
|
||||
currentRow.add(current.toString())
|
||||
rows.add(currentRow.toList())
|
||||
}
|
||||
return rows.map { row ->
|
||||
row.map { it.trim().trim('"') }
|
||||
}.filter { r -> r.any { it.isNotBlank() } }
|
||||
}
|
||||
|
||||
val importTableLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocument(),
|
||||
onResult = { uri ->
|
||||
uri?.let { u ->
|
||||
try {
|
||||
context.contentResolver.takePersistableUriPermission(u, android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
} catch (_: Exception) {}
|
||||
try {
|
||||
val mime = context.contentResolver.getType(u)
|
||||
val isExcel = mime == "application/vnd.ms-excel" ||
|
||||
mime == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
if (isExcel) {
|
||||
statusMessageService.showErrorById(StatusMessageId.ERROR_EXCEL_NOT_SUPPORTED)
|
||||
return@let
|
||||
}
|
||||
context.contentResolver.openInputStream(u)?.use { inputStream ->
|
||||
val text = inputStream.bufferedReader().use { it.readText() }
|
||||
val rows = parseCsv(text)
|
||||
if (rows.isNotEmpty() && rows.maxOf { it.size } >= 2) {
|
||||
parsedTable = rows
|
||||
selectedColFirst = 0
|
||||
selectedColSecond = 1.coerceAtMost(rows.first().size - 1)
|
||||
showTableImportDialog.value = true
|
||||
} else {
|
||||
statusMessageService.showErrorById(StatusMessageId.ERROR_PARSING_TABLE)
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
statusMessageService.showErrorById(StatusMessageId.ERROR_PARSING_TABLE_WITH_REASON)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.widthIn(max = 700.dp) // Perfect scaling for tablets/foldables
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(0.dp)
|
||||
) {
|
||||
AppTopAppBar(
|
||||
title = stringResource(R.string.label_new_words),
|
||||
onNavigateBack = { navController.popBackStack() }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
AIGeneratorCard(
|
||||
category = category,
|
||||
onCategoryChange = { category = it },
|
||||
amount = amount,
|
||||
onAmountChange = { amount = it },
|
||||
languageViewModel = languageViewModel,
|
||||
isGenerating = isGenerating,
|
||||
onGenerate = {
|
||||
if (category.isNotBlank() && !isGenerating) {
|
||||
coroutineScope.launch {
|
||||
vocabularyViewModel.generateVocabularyItems(category.trim(), amount.toInt())
|
||||
navigateToReview = true
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
AddManuallyCard(
|
||||
languageViewModel = languageViewModel,
|
||||
vocabularyViewModel = vocabularyViewModel,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
BottomActionCardsRow(
|
||||
onImportCsvClick = {
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
importTableLauncher.launch(
|
||||
arrayOf(
|
||||
"text/csv",
|
||||
"text/comma-separated-values",
|
||||
"text/tab-separated-values",
|
||||
"text/plain",
|
||||
"application/vnd.ms-excel",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
if (recentlyAdded.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.label_recently_added),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
TextButton(onClick = { navController.navigate(Screen.Library.route) }) {
|
||||
Text(stringResource(R.string.label_view_all))
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
recentlyAdded.forEach { item ->
|
||||
VocabularyCard(
|
||||
item = item,
|
||||
allLanguages = allLanguages,
|
||||
isSelected = false,
|
||||
onItemClick = {
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
navController.navigate("${NavigationRoutes.VOCABULARY_DETAIL}/${item.id}")
|
||||
},
|
||||
onItemLongClick = {},
|
||||
onDeleteClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extra padding at the bottom for scroll clearance
|
||||
Spacer(modifier = Modifier.height(100.dp))
|
||||
}
|
||||
}
|
||||
|
||||
if (showTableImportDialog.value) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showTableImportDialog.value = false },
|
||||
title = { Text(stringResource(R.string.label_import_table_csv_excel)) },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
val columnCount = parsedTable.maxOfOrNull { it.size } ?: 0
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(stringResource(R.string.label_first_column), modifier = Modifier.weight(1f))
|
||||
var menu1Expanded by remember { mutableStateOf(false) }
|
||||
AppOutlinedButton(onClick = { menu1Expanded = true }) {
|
||||
Text(stringResource(R.string.label_column_n, selectedColFirst + 1))
|
||||
}
|
||||
DropdownMenu(expanded = menu1Expanded, onDismissRequest = { menu1Expanded = false }) {
|
||||
(0 until columnCount).forEach { idx ->
|
||||
val header = parsedTable.firstOrNull()?.getOrNull(idx).orEmpty()
|
||||
DropdownMenuItem(
|
||||
text = { Text("#${idx + 1} • $header") },
|
||||
onClick = { selectedColFirst = idx; menu1Expanded = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(stringResource(R.string.label_second_column), modifier = Modifier.weight(1f))
|
||||
var menu2Expanded by remember { mutableStateOf(false) }
|
||||
AppOutlinedButton(onClick = { menu2Expanded = true }) {
|
||||
Text(stringResource(R.string.label_column_n, selectedColSecond + 1))
|
||||
}
|
||||
DropdownMenu(expanded = menu2Expanded, onDismissRequest = { menu2Expanded = false }) {
|
||||
(0 until columnCount).forEach { idx ->
|
||||
val header = parsedTable.firstOrNull()?.getOrNull(idx).orEmpty()
|
||||
DropdownMenuItem(
|
||||
text = { Text("#${idx + 1} • $header") },
|
||||
onClick = { selectedColSecond = idx; menu2Expanded = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(stringResource(R.string.label_languages))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(stringResource(R.string.label_first_language))
|
||||
eu.gaudian.translator.view.composable.SingleLanguageDropDown(
|
||||
languageViewModel = languageViewModel,
|
||||
selectedLanguage = selectedLangFirst,
|
||||
onLanguageSelected = { selectedLangFirst = it }
|
||||
)
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(stringResource(R.string.label_second_language))
|
||||
eu.gaudian.translator.view.composable.SingleLanguageDropDown(
|
||||
languageViewModel = languageViewModel,
|
||||
selectedLanguage = selectedLangSecond,
|
||||
onLanguageSelected = { selectedLangSecond = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
androidx.compose.material3.Checkbox(checked = skipHeader, onCheckedChange = { skipHeader = it })
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(stringResource(R.string.label_header_row))
|
||||
}
|
||||
val startIdx = if (skipHeader) 1 else 0
|
||||
val previewA = parsedTable.drop(startIdx).take(5).mapNotNull { it.getOrNull(selectedColFirst) }.joinToString(", ")
|
||||
val previewB = parsedTable.drop(startIdx).take(5).mapNotNull { it.getOrNull(selectedColSecond) }.joinToString(", ")
|
||||
Text(stringResource(R.string.label_preview_first, previewA))
|
||||
Text(stringResource(R.string.label_preview_second, previewB))
|
||||
val totalRows = parsedTable.drop(startIdx).count { row ->
|
||||
val a = row.getOrNull(selectedColFirst).orEmpty().isNotBlank()
|
||||
val b = row.getOrNull(selectedColSecond).orEmpty().isNotBlank()
|
||||
a || b
|
||||
}
|
||||
Text(stringResource(R.string.text_rows_to_import_1d, totalRows))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
if (selectedColFirst == selectedColSecond) {
|
||||
statusMessageService.showErrorById(StatusMessageId.ERROR_SELECT_TWO_COLUMNS)
|
||||
return@TextButton
|
||||
}
|
||||
val langA = selectedLangFirst
|
||||
val langB = selectedLangSecond
|
||||
if (langA == null || langB == null) {
|
||||
statusMessageService.showErrorById(StatusMessageId.ERROR_SELECT_LANGUAGES)
|
||||
return@TextButton
|
||||
}
|
||||
val startIdx = if (skipHeader) 1 else 0
|
||||
val items = parsedTable.drop(startIdx).mapNotNull { row ->
|
||||
val a = row.getOrNull(selectedColFirst)?.trim().orEmpty()
|
||||
val b = row.getOrNull(selectedColSecond)?.trim().orEmpty()
|
||||
if (a.isBlank() && b.isBlank()) null else VocabularyItem(
|
||||
id = 0,
|
||||
languageFirstId = langA.nameResId,
|
||||
languageSecondId = langB.nameResId,
|
||||
wordFirst = a,
|
||||
wordSecond = b
|
||||
)
|
||||
}
|
||||
if (items.isEmpty()) {
|
||||
statusMessageService.showErrorById(StatusMessageId.ERROR_NO_ROWS_TO_IMPORT)
|
||||
return@TextButton
|
||||
}
|
||||
vocabularyViewModel.addVocabularyItems(items)
|
||||
statusMessageService.showSuccessById(StatusMessageId.SUCCESS_ITEMS_IMPORTED)
|
||||
showTableImportDialog.value = false
|
||||
}) { Text(stringResource(R.string.label_import)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showTableImportDialog.value = false }) {
|
||||
Text(stringResource(R.string.label_cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- AI GENERATOR CARD (From previous implementation) ---
|
||||
|
||||
@Composable
|
||||
fun AIGeneratorCard(
|
||||
category: String,
|
||||
onCategoryChange: (String) -> Unit,
|
||||
amount: Float,
|
||||
onAmountChange: (Float) -> Unit,
|
||||
languageViewModel: LanguageViewModel,
|
||||
isGenerating: Boolean,
|
||||
onGenerate: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
|
||||
val icon = Icons.Default.AutoAwesome
|
||||
val hints = stringArrayResource(R.array.vocabulary_hints)
|
||||
AppCard(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
title = stringResource(R.string.label_ai_generator),
|
||||
icon = icon,
|
||||
hintContent = HintDefinition.VOCABULARY_GENERATE_AI.hint(),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(8.dp)) {
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.text_search_term),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
InspiringSearchField(
|
||||
value = category,
|
||||
hints = hints,
|
||||
onValueChange = onCategoryChange
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.text_select_languages),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
SourceLanguageDropdown(
|
||||
modifier = Modifier.weight(1f),
|
||||
languageViewModel = languageViewModel,
|
||||
iconEnabled = false,
|
||||
noBorder = true
|
||||
)
|
||||
TargetLanguageDropdown(
|
||||
modifier = Modifier.weight(1f),
|
||||
languageViewModel = languageViewModel,
|
||||
iconEnabled = false,
|
||||
noBorder = true
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.text_select_amount),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
AppSlider(
|
||||
value = amount,
|
||||
onValueChange = onAmountChange,
|
||||
valueRange = 1f..25f,
|
||||
steps = 24,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.text_amount_2d, amount.toInt()),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
if (isGenerating) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
AppButton(
|
||||
onClick = onGenerate,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
enabled = category.isNotBlank() && !isGenerating
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(imageVector = Icons.Default.AutoAwesome, contentDescription = null, modifier = Modifier.size(20.dp))
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.text_generate),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- NEW COMPONENTS START HERE ---
|
||||
|
||||
@Composable
|
||||
fun AddManuallyCard(
|
||||
languageViewModel: LanguageViewModel,
|
||||
vocabularyViewModel: VocabularyViewModel,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var wordText by remember { mutableStateOf("") }
|
||||
var translationText by remember { mutableStateOf("") }
|
||||
val selectedSourceLanguage by languageViewModel.selectedSourceLanguage.collectAsState()
|
||||
val selectedTargetLanguage by languageViewModel.selectedTargetLanguage.collectAsState()
|
||||
|
||||
val canAdd = wordText.isNotBlank() && translationText.isNotBlank() &&
|
||||
selectedSourceLanguage != null && selectedTargetLanguage != null
|
||||
|
||||
AppCard(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(24.dp)) {
|
||||
// Header Row
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.EditNote,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.label_add_vocabulary),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Input Fields
|
||||
TextField(
|
||||
value = wordText,
|
||||
onValueChange = { wordText = it },
|
||||
placeholder = { Text(stringResource(R.string.text_label_word), color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = MaterialTheme.colorScheme.surface, // Very dark background
|
||||
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
TextField(
|
||||
value = translationText,
|
||||
onValueChange = { translationText = it },
|
||||
placeholder = { Text(stringResource(R.string.text_translation), color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.text_select_languages),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f))
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
SourceLanguageDropdown(
|
||||
modifier = Modifier.weight(1f),
|
||||
languageViewModel = languageViewModel,
|
||||
iconEnabled = false,
|
||||
noBorder = true
|
||||
)
|
||||
TargetLanguageDropdown(
|
||||
modifier = Modifier.weight(1f),
|
||||
languageViewModel = languageViewModel,
|
||||
iconEnabled = false,
|
||||
noBorder = true
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Add to List Button (Darker variant)
|
||||
AppButton(
|
||||
onClick = {
|
||||
val newItem = VocabularyItem(
|
||||
languageFirstId = selectedSourceLanguage?.nameResId,
|
||||
languageSecondId = selectedTargetLanguage?.nameResId,
|
||||
wordFirst = wordText.trim(),
|
||||
wordSecond = translationText.trim(),
|
||||
id = 0
|
||||
)
|
||||
vocabularyViewModel.addVocabularyItems(listOf(newItem))
|
||||
wordText = ""
|
||||
translationText = ""
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
enabled = canAdd
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.label_add),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BottomActionCardsRow(
|
||||
modifier: Modifier = Modifier,
|
||||
onImportCsvClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
//TODO Explore Packs Card
|
||||
AppCard(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(120.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.alpha(0.6f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = AppIcons.Vocabulary,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
Text(
|
||||
text = "Explore Packs",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
Text(
|
||||
text = "Coming soon",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Import CSV Card
|
||||
AppCard(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(120.dp),
|
||||
onClick = onImportCsvClick
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.DriveFolderUpload,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.label_import_csv),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,6 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
@@ -39,7 +37,6 @@ import androidx.navigation.NavController
|
||||
import eu.gaudian.translator.R
|
||||
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.AppSlider
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
@@ -57,7 +54,7 @@ fun NoGrammarItemsScreen(
|
||||
|
||||
var showFetchGrammarDialog by remember { mutableStateOf(false) }
|
||||
|
||||
@Suppress("UnusedVariable", "unused", "HardCodedStringLiteral") val onClose = { navController.popBackStack() }
|
||||
@Suppress("UnusedVariable") val onClose = { navController.popBackStack() }
|
||||
|
||||
if (itemsWithoutGrammar.isEmpty() && !isGenerating) {
|
||||
Column(
|
||||
@@ -66,12 +63,8 @@ fun NoGrammarItemsScreen(
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
AppTopAppBar(
|
||||
title = { Text(stringResource(R.string.title_items_without_grammar)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||
}
|
||||
},
|
||||
title = stringResource(R.string.title_items_without_grammar),
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -87,8 +80,8 @@ fun NoGrammarItemsScreen(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use the generic VocabularyListScreen to display the items
|
||||
VocabularyListScreen(
|
||||
// Use the generic AllCardsListScreen to display the items
|
||||
AllCardsListScreen(
|
||||
itemsToShow = itemsWithoutGrammar,
|
||||
onNavigateToItem = { item ->
|
||||
@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))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,6 @@ package eu.gaudian.translator.view.vocabulary
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -17,7 +14,6 @@ import androidx.navigation.NavHostController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.VocabularyStage
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppScaffold
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.view.vocabulary.widgets.DetailedStageProgressBar
|
||||
@@ -40,15 +36,8 @@ fun StageDetailScreen(
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = { Text(text = stringResource(R.string.due_today_, stage.toString())) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(
|
||||
AppIcons.ArrowBack,
|
||||
contentDescription =stringResource(R.string.cd_back)
|
||||
)
|
||||
}
|
||||
}
|
||||
title = stringResource(R.string.due_today_, stage.toString()),
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
@@ -60,7 +49,7 @@ fun StageDetailScreen(
|
||||
onStageTapped = {},
|
||||
)
|
||||
|
||||
VocabularyListScreen(
|
||||
AllCardsListScreen(
|
||||
categoryId = null,
|
||||
showDueTodayOnly = true,
|
||||
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 = { Text(stringResource(R.string.prepare_exercise)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onClose) {
|
||||
Icon(
|
||||
AppIcons.Close,
|
||||
contentDescription = stringResource(R.string.label_close)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
if (vocabularyItemsCount > 0) {
|
||||
Text(stringResource(R.string.number_of_cards, amount, vocabularyItemsCount))
|
||||
AppSlider(
|
||||
value = amount.toFloat(),
|
||||
onValueChange = { onAmountChanged(it.toInt()) },
|
||||
valueRange = 1f..vocabularyItemsCount.toFloat(),
|
||||
steps = if (vocabularyItemsCount > 1) vocabularyItemsCount - 2 else 0
|
||||
)
|
||||
|
||||
// Quick selection buttons
|
||||
val quickSelectValues = listOf(10, 25, 50, 100)
|
||||
val availableValues =
|
||||
quickSelectValues.filter { it <= vocabularyItemsCount }
|
||||
|
||||
if (availableValues.isNotEmpty()) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
8.dp,
|
||||
Alignment.CenterHorizontally
|
||||
)
|
||||
) {
|
||||
availableValues.forEach { value ->
|
||||
AppOutlinedButton(
|
||||
onClick = { onAmountChanged(value) },
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = value <= vocabularyItemsCount
|
||||
) {
|
||||
Text(value.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
stringResource(R.string.no_cards_found_for_the_selected_filters),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = 24.dp),
|
||||
thickness = DividerDefaults.Thickness,
|
||||
color = DividerDefaults.color
|
||||
)
|
||||
|
||||
|
||||
// Language Selection Section
|
||||
Text(
|
||||
stringResource(R.string.label_language_direction),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
|
||||
Text(
|
||||
stringResource(R.string.text_language_direction_explanation),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
|
||||
// Get available languages from the card set
|
||||
val availableLanguages = remember(allItems) {
|
||||
allItems.flatMap { listOfNotNull(it.languageFirstId, it.languageSecondId) }
|
||||
.distinct()
|
||||
.mapNotNull { languageId ->
|
||||
languageViewModel.allLanguages.value.find { it.nameResId == languageId }
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Origin Language Dropdown
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
stringResource(R.string.label_origin_language),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
SingleLanguageDropDown(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
languageViewModel = languageViewModel,
|
||||
selectedLanguage = selectedOriginLanguage,
|
||||
onLanguageSelected = { language ->
|
||||
onOriginLanguageChanged(language)
|
||||
// Clear target language if it's the same as origin
|
||||
if (selectedTargetLanguage?.nameResId == language.nameResId) {
|
||||
onTargetLanguageChanged(null)
|
||||
}
|
||||
},
|
||||
showNoneOption = true,
|
||||
alternateLanguages = availableLanguages
|
||||
)
|
||||
}
|
||||
|
||||
// Target Language Dropdown
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
stringResource(R.string.label_target_language),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
SingleLanguageDropDown(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
languageViewModel = languageViewModel,
|
||||
selectedLanguage = selectedTargetLanguage,
|
||||
onLanguageSelected = { language ->
|
||||
onTargetLanguageChanged(language)
|
||||
// Clear origin language if it's the same as target
|
||||
if (selectedOriginLanguage?.nameResId == language.nameResId) {
|
||||
onOriginLanguageChanged(null)
|
||||
}
|
||||
},
|
||||
alternateLanguages = availableLanguages,
|
||||
showNoneOption = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = 24.dp),
|
||||
thickness = DividerDefaults.Thickness,
|
||||
color = DividerDefaults.color
|
||||
)
|
||||
|
||||
Text(
|
||||
stringResource(R.string.label_choose_exercise_types),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
ExerciseTypeSelector(
|
||||
selectedTypes = selectedExerciseTypes,
|
||||
onTypeSelected = onExerciseTypeSelected
|
||||
)
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = 24.dp),
|
||||
thickness = DividerDefaults.Thickness,
|
||||
color = DividerDefaults.color
|
||||
)
|
||||
|
||||
Text(
|
||||
stringResource(R.string.options),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OptionItemSwitch(
|
||||
title = stringResource(R.string.shuffle_cards),
|
||||
description = stringResource(R.string.text_shuffle_card_order_description),
|
||||
checked = shuffleCards,
|
||||
onCheckedChange = onShuffleCardsChanged
|
||||
)
|
||||
OptionItemSwitch(
|
||||
title = stringResource(R.string.text_shuffle_languages),
|
||||
description = stringResource(R.string.text_shuffle_languages_description),
|
||||
checked = shuffleLanguages,
|
||||
onCheckedChange = onShuffleLanguagesChanged
|
||||
)
|
||||
OptionItemSwitch(
|
||||
title = stringResource(R.string.label_training_mode),
|
||||
description = stringResource(R.string.text_training_mode_description),
|
||||
checked = trainingMode,
|
||||
onCheckedChange = onTrainingModeChanged
|
||||
)
|
||||
if (!hideTodayOnlySwitch) {
|
||||
OptionItemSwitch(
|
||||
title = stringResource(R.string.text_due_today_only),
|
||||
description = stringResource(R.string.text_due_today_only_description),
|
||||
checked = dueTodayOnly,
|
||||
onCheckedChange = onDueTodayOnlyChanged
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
AppButton(
|
||||
onClick = onStartClicked,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.height(50.dp),
|
||||
enabled = vocabularyItemsCount > 0 && amount > 0
|
||||
) {
|
||||
Text(stringResource(R.string.label_start_exercise_2d, amount))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExerciseTypeSelector(
|
||||
selectedTypes: Set<VocabularyExerciseType>,
|
||||
onTypeSelected: (VocabularyExerciseType) -> Unit,
|
||||
) {
|
||||
// Using FlowRow for a more flexible layout that wraps to the next line if needed
|
||||
FlowRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
ExerciseTypeCard(
|
||||
icon = AppIcons.Guessing,
|
||||
isSelected = VocabularyExerciseType.GUESSING in selectedTypes,
|
||||
onClick = { onTypeSelected(VocabularyExerciseType.GUESSING) },
|
||||
text = stringResource(R.string.label_guessing_exercise),
|
||||
)
|
||||
ExerciseTypeCard(
|
||||
icon = AppIcons.SpellCheck,
|
||||
isSelected = VocabularyExerciseType.SPELLING in selectedTypes,
|
||||
onClick = { onTypeSelected(VocabularyExerciseType.SPELLING) },
|
||||
text = stringResource(R.string.label_spelling_exercise),
|
||||
)
|
||||
ExerciseTypeCard(
|
||||
icon = AppIcons.CheckList,
|
||||
isSelected = VocabularyExerciseType.MULTIPLE_CHOICE in selectedTypes,
|
||||
onClick = { onTypeSelected(VocabularyExerciseType.MULTIPLE_CHOICE) },
|
||||
text = stringResource(R.string.label_multiple_choice_exercise),
|
||||
)
|
||||
ExerciseTypeCard(
|
||||
icon = AppIcons.Extension,
|
||||
isSelected = VocabularyExerciseType.WORD_JUMBLE in selectedTypes,
|
||||
onClick = { onTypeSelected(VocabularyExerciseType.WORD_JUMBLE) },
|
||||
text = stringResource(R.string.label_word_jumble_exercise),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExerciseTypeCard(
|
||||
text: String,
|
||||
icon: ImageVector,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val borderColor by animateColorAsState(
|
||||
targetValue = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline.copy(
|
||||
alpha = 0.5f
|
||||
),
|
||||
label = "borderColorAnimation",
|
||||
animationSpec = tween(300)
|
||||
)
|
||||
val containerColor by animateColorAsState(
|
||||
targetValue = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant,
|
||||
animationSpec = tween(300)
|
||||
)
|
||||
|
||||
Card(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.size(width = 120.dp, height = 100.dp), // Made the cards smaller
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
border = BorderStroke(2.dp, borderColor),
|
||||
colors = CardDefaults.cardColors(containerColor = containerColor),
|
||||
elevation = CardDefaults.cardElevation(if (isSelected) 4.dp else 1.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(icon, contentDescription = null, modifier = Modifier.size(32.dp)) // Smaller icon
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyLarge, // Smaller text
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,23 @@
|
||||
package eu.gaudian.translator.view.vocabulary
|
||||
|
||||
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.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
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.AppTopAppBar
|
||||
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.vocabulary.card.VocabularyCard
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
import eu.gaudian.translator.view.vocabulary.card.VocabularyDisplayCard
|
||||
import eu.gaudian.translator.view.vocabulary.card.VocabularyExerciseCard
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.ExperimentalTime
|
||||
@@ -59,7 +67,6 @@ fun VocabularyCardHost(
|
||||
) {
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val navigationItems by vocabularyViewModel.currentNavigationItems.collectAsState()
|
||||
@@ -71,62 +78,55 @@ fun VocabularyCardHost(
|
||||
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(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
modifier = Modifier.height(56.dp),
|
||||
title = {
|
||||
if (navigationItems.isNotEmpty()) {
|
||||
Text(stringResource(R.string.label_card_with_position, navigationPosition + 1, navigationItems.size))
|
||||
} else {
|
||||
Text(stringResource(R.string.item_details))
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onBackPressed?.invoke() }) {
|
||||
Icon(
|
||||
AppIcons.ArrowBack,
|
||||
contentDescription = stringResource(R.string.cd_back)
|
||||
)
|
||||
}
|
||||
},
|
||||
title = stringResource(R.string.item_details),
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
actions = {
|
||||
// Previous button
|
||||
if (navigationPosition > 0) {
|
||||
IconButton(onClick = {
|
||||
if (vocabularyViewModel.navigateToPreviousItem()) {
|
||||
val prevItem = navigationItems[navigationPosition - 1]
|
||||
scope.launch {
|
||||
if (!isEditing) {
|
||||
// Previous button
|
||||
if (navigationPosition > 0) {
|
||||
IconButton(onClick = {
|
||||
if (vocabularyViewModel.navigateToPreviousItem()) {
|
||||
val prevItem = navigationItems[navigationPosition - 1]
|
||||
scope.launch {
|
||||
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
vocabularyItem = vocabularyViewModel.getVocabularyItemById(prevItem.id)
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
vocabularyItem = vocabularyViewModel.getVocabularyItemById(prevItem.id)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
AppIcons.ArrowLeft,
|
||||
contentDescription = stringResource(R.string.previous_item),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
AppIcons.ArrowLeft,
|
||||
contentDescription = stringResource(R.string.previous_item),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Next button
|
||||
if (navigationPosition < navigationItems.size - 1) {
|
||||
IconButton(onClick = {
|
||||
if (vocabularyViewModel.navigateToNextItem()) {
|
||||
val nextItem = navigationItems[navigationPosition + 1]
|
||||
scope.launch {
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
vocabularyItem = vocabularyViewModel.getVocabularyItemById(nextItem.id)
|
||||
// Next button
|
||||
if (navigationPosition < navigationItems.size - 1) {
|
||||
IconButton(onClick = {
|
||||
if (vocabularyViewModel.navigateToNextItem()) {
|
||||
val nextItem = navigationItems[navigationPosition + 1]
|
||||
scope.launch {
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
vocabularyItem = vocabularyViewModel.getVocabularyItemById(nextItem.id)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
AppIcons.ArrowRight,
|
||||
contentDescription = stringResource(R.string.next_item),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
AppIcons.ArrowRight,
|
||||
contentDescription = stringResource(R.string.next_item),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,8 +146,12 @@ fun VocabularyCardHost(
|
||||
var showStatisticsDialog by remember { mutableStateOf(false) }
|
||||
var showCategoryDialog by remember { mutableStateOf(false) }
|
||||
var showStageDialog by remember { mutableStateOf(false) }
|
||||
var showImportDialog by remember { mutableStateOf(false) }
|
||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(currentVocabularyItem.id) {
|
||||
isEditing = false
|
||||
onSaveEdit = null
|
||||
onCancelEdit = null
|
||||
}
|
||||
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val stats by vocabularyViewModel
|
||||
@@ -199,18 +203,66 @@ fun VocabularyCardHost(
|
||||
}
|
||||
|
||||
// Main content
|
||||
VocabularyCard(
|
||||
vocabularyItem = currentVocabularyItem,
|
||||
exerciseMode = exerciseMode,
|
||||
switchOrder = switchOrder == true,
|
||||
isFlipped = isFlipped,
|
||||
onStatisticsClick = { showStatisticsDialog = true },
|
||||
onMoveToCategoryClick = { showCategoryDialog = true },
|
||||
onMoveToStageClick = { showStageDialog = true },
|
||||
onDeleteClick = { showDeleteDialog = true },
|
||||
navController = navController,
|
||||
isUserSpellingCorrect = false,
|
||||
)
|
||||
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,
|
||||
switchOrder = switchOrder == true,
|
||||
isFlipped = isFlipped,
|
||||
onStatisticsClick = { showStatisticsDialog = true },
|
||||
onMoveToCategoryClick = { showCategoryDialog = true },
|
||||
onMoveToStageClick = { showStageDialog = true },
|
||||
onDeleteClick = { showDeleteDialog = true },
|
||||
navController = navController,
|
||||
onEditStateChange = { editing ->
|
||||
isEditing = editing
|
||||
if (!editing) {
|
||||
onSaveEdit = null
|
||||
onCancelEdit = null
|
||||
}
|
||||
},
|
||||
onEditActionHandlersReady = { onSave, onCancel ->
|
||||
onSaveEdit = onSave
|
||||
onCancelEdit = onCancel
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Dialogs are unaffected by the layout change
|
||||
if (showQuitDialog) {
|
||||
@@ -259,16 +311,6 @@ fun VocabularyCardHost(
|
||||
)
|
||||
}
|
||||
|
||||
if (showImportDialog) {
|
||||
ImportVocabularyDialog(
|
||||
onDismiss = { showImportDialog = false },
|
||||
languageViewModel = languageViewModel,
|
||||
optionalDescription = stringResource(R.string.generate_related_vocabulary_items),
|
||||
optionalSearchTerm = currentVocabularyItem.wordFirst,
|
||||
vocabularyViewModel = vocabularyViewModel
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(spellingMode) {
|
||||
@Suppress("ControlFlowWithEmptyBody")
|
||||
if (spellingMode) {
|
||||
|
||||
@@ -48,7 +48,7 @@ import eu.gaudian.translator.ui.theme.semanticColors
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppButton
|
||||
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
|
||||
|
||||
/**
|
||||
@@ -141,11 +141,10 @@ fun GuessingExercise(
|
||||
navController: NavController,
|
||||
) {
|
||||
|
||||
VocabularyCard(
|
||||
VocabularyExerciseCard(
|
||||
vocabularyItem = state.item,
|
||||
isFlipped = state.isRevealed,
|
||||
navController = navController,
|
||||
exerciseMode = true,
|
||||
switchOrder = state.isSwitched,
|
||||
)
|
||||
}
|
||||
@@ -158,13 +157,12 @@ fun SpellingExercise(
|
||||
navController: NavController,
|
||||
) {
|
||||
|
||||
VocabularyCard(
|
||||
VocabularyExerciseCard(
|
||||
vocabularyItem = state.item,
|
||||
isFlipped = state.isRevealed,
|
||||
userSpellingAnswer = state.userAnswer,
|
||||
isUserSpellingCorrect = state.isCorrect,
|
||||
navController = navController,
|
||||
exerciseMode = true,
|
||||
switchOrder = state.isSwitched,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("HardCodedStringLiteral")
|
||||
|
||||
package eu.gaudian.translator.view.vocabulary
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
@@ -19,7 +21,6 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -28,10 +29,10 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.navigation.NavController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.Language
|
||||
import eu.gaudian.translator.utils.Log
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
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.VocabularyExerciseViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
@@ -57,14 +58,7 @@ fun VocabularyExerciseHostScreen(
|
||||
|
||||
val cardSet by vocabularyViewModel.cardSet.collectAsState()
|
||||
val screenState by exerciseViewModel.screenState.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) }
|
||||
val pendingConfig by exerciseViewModel.pendingExerciseConfig.collectAsState()
|
||||
|
||||
var finalScore by remember { mutableIntStateOf(0) }
|
||||
var finalWrongAnswers by remember { mutableIntStateOf(0) }
|
||||
@@ -76,119 +70,94 @@ fun VocabularyExerciseHostScreen(
|
||||
false
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
// Reset exercise state when starting fresh
|
||||
exerciseViewModel.resetExercise()
|
||||
|
||||
vocabularyViewModel.prepareExercise(
|
||||
categoryIdsAsJson,
|
||||
stageNamesAsJson,
|
||||
languageIdsAsJson,
|
||||
dailyOnly = dailyOnly,
|
||||
)
|
||||
LaunchedEffect(categoryIdsAsJson, stageNamesAsJson, languageIdsAsJson, dailyOnly) {
|
||||
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()
|
||||
vocabularyViewModel.prepareExercise(
|
||||
categoryIdsAsJson,
|
||||
stageNamesAsJson,
|
||||
languageIdsAsJson,
|
||||
dailyOnly = dailyOnly,
|
||||
)
|
||||
} else {
|
||||
Log.d("ExerciseHost", "No filters provided; skipping prepareExercise")
|
||||
}
|
||||
}
|
||||
|
||||
if (cardSet == null && screenState != ScreenState.START) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
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")
|
||||
}
|
||||
}
|
||||
} 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
|
||||
}
|
||||
|
||||
when (screenState) {
|
||||
ScreenState.START -> {
|
||||
Log.d("ExerciseHost", "Rendering START screen")
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
ScreenState.EXERCISE -> {
|
||||
Log.d("ExerciseHost", "Rendering EXERCISE screen")
|
||||
ExerciseScreen(
|
||||
viewModel = exerciseViewModel,
|
||||
onClose = onClose,
|
||||
onFinish = { score, wrong ->
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
finalScore = score
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
finalWrongAnswers = wrong
|
||||
exerciseViewModel.finishExercise(score, wrong)
|
||||
},
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
ScreenState.RESULT -> {
|
||||
Log.d("ExerciseHost", "Rendering RESULT screen")
|
||||
val totalItems by exerciseViewModel.totalItems.collectAsState()
|
||||
val originalItems by exerciseViewModel.originalItems.collectAsState()
|
||||
ResultScreen(
|
||||
score = finalScore,
|
||||
wrongAnswers = finalWrongAnswers,
|
||||
totalItems = totalItems,
|
||||
onRestart = {
|
||||
vocabularyViewModel.clearCardSet()
|
||||
exerciseViewModel.resetExercise()
|
||||
},
|
||||
onRetryWrong = { _ ->
|
||||
exerciseViewModel.retryWrongAnswers(originalItems)
|
||||
},
|
||||
onClose = {
|
||||
navController.navigate(Screen.Home.route) {
|
||||
popUpTo(Screen.Home.route) { inclusive = true }
|
||||
launchSingleTop = true
|
||||
}
|
||||
)
|
||||
}
|
||||
ScreenState.EXERCISE -> {
|
||||
ExerciseScreen(
|
||||
viewModel = exerciseViewModel,
|
||||
onClose = onClose,
|
||||
onFinish = { score, wrong ->
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
finalScore = score
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
finalWrongAnswers = wrong
|
||||
exerciseViewModel.finishExercise(score, wrong)
|
||||
},
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
ScreenState.RESULT -> {
|
||||
val totalItems by exerciseViewModel.totalItems.collectAsState()
|
||||
val originalItems by exerciseViewModel.originalItems.collectAsState()
|
||||
ResultScreen(
|
||||
score = finalScore,
|
||||
wrongAnswers = finalWrongAnswers,
|
||||
totalItems = totalItems,
|
||||
onRestart = {
|
||||
vocabularyViewModel.clearCardSet()
|
||||
exerciseViewModel.resetExercise()
|
||||
},
|
||||
onRetryWrong = { _ ->
|
||||
exerciseViewModel.retryWrongAnswers(originalItems)
|
||||
},
|
||||
onClose = onClose
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,6 @@ import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
@@ -53,6 +51,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.navigation.NavController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppCard
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppScaffold
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
@@ -68,6 +67,7 @@ import kotlinx.datetime.plus
|
||||
import kotlinx.datetime.todayIn
|
||||
import java.time.YearMonth
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale.getDefault
|
||||
import kotlin.math.log2
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.ExperimentalTime
|
||||
@@ -95,7 +95,7 @@ fun VocabularyHeatmapScreen(
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = { Text(stringResource(R.string.label_vocabulary_activity)) },
|
||||
title = stringResource(R.string.label_vocabulary_activity),
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
)
|
||||
},
|
||||
@@ -263,7 +263,8 @@ private fun MonthHeader(
|
||||
Icon(AppIcons.ArrowLeft, contentDescription = stringResource(R.string.previous_month))
|
||||
}
|
||||
Text(
|
||||
text = month.format(formatter),
|
||||
text = month.format(formatter)
|
||||
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(getDefault()) else it.toString() },
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
@@ -283,7 +284,7 @@ private fun MonthGrid(
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
val locale = java.util.Locale.getDefault()
|
||||
val locale = getDefault()
|
||||
// Generate localized short weekday labels for Monday to Sunday.
|
||||
val dayFormatter = remember(locale) {
|
||||
DateTimeFormatter.ofPattern("EEEEE", locale)
|
||||
@@ -385,7 +386,7 @@ private fun Legend(modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.End
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.less),
|
||||
@@ -467,12 +468,11 @@ fun StatsOverview(
|
||||
|
||||
@Composable
|
||||
private fun StatCard(title: String, value: String, modifier: Modifier = Modifier) {
|
||||
Card(
|
||||
modifier = modifier,
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
AppCard(
|
||||
modifier = modifier.padding(0.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
modifier = Modifier.padding(8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
|
||||
@@ -5,19 +5,13 @@ package eu.gaudian.translator.view.vocabulary
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
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.combinedClickable
|
||||
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.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -25,14 +19,10 @@ import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.FabPosition
|
||||
@@ -43,7 +33,6 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
@@ -59,20 +48,15 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
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.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.Language
|
||||
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.AppTopAppBar
|
||||
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.CategorySelectionDialog
|
||||
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.LanguageConfigViewModel
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
@@ -110,7 +94,7 @@ private data class VocabularyFilterState(
|
||||
) : Parcelable
|
||||
|
||||
@Composable
|
||||
fun VocabularyListScreen(
|
||||
fun AllCardsListScreen(
|
||||
categoryId: Int? = null,
|
||||
showDueTodayOnly: Boolean? = null,
|
||||
stage: VocabularyStage? = null,
|
||||
@@ -245,10 +229,6 @@ fun VocabularyListScreen(
|
||||
)
|
||||
|
||||
"search" -> SearchTopAppBar(
|
||||
searchQuery = filterState.searchQuery,
|
||||
onQueryChanged = { newQuery ->
|
||||
filterState = filterState.copy(searchQuery = newQuery)
|
||||
},
|
||||
onCloseSearch = {
|
||||
isSearchActive = false
|
||||
filterState = filterState.copy(searchQuery = "")
|
||||
@@ -295,78 +275,40 @@ fun VocabularyListScreen(
|
||||
floatingActionButtonPosition = FabPosition.Center
|
||||
) { paddingValues ->
|
||||
Column(modifier = Modifier.padding(paddingValues).padding(bottom = 0.dp)) {
|
||||
if (vocabularyItems.isEmpty()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
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
|
||||
AllCardsView(
|
||||
vocabularyItems = vocabularyItems,
|
||||
allLanguages = allLanguages,
|
||||
selection = selection,
|
||||
listState = lazyListState,
|
||||
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
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(vertical = 0.dp)
|
||||
) {
|
||||
items(
|
||||
items = vocabularyItems,
|
||||
key = { it.id }
|
||||
) { item ->
|
||||
val isSelected = selection.contains(item.id.toLong())
|
||||
VocabularyListItem(
|
||||
item = item,
|
||||
allLanguages = allLanguages,
|
||||
isSelected = isSelected,
|
||||
onItemClick = {
|
||||
if (isInSelectionMode) {
|
||||
selection = if (isSelected) {
|
||||
selection - item.id.toLong()
|
||||
} else {
|
||||
selection + item.id.toLong()
|
||||
}
|
||||
} else {
|
||||
if (navController != null && enableNavigationButtons) {
|
||||
vocabularyViewModel.setNavigationContext(vocabularyItems, item.id)
|
||||
navController.navigate("vocabulary_detail/${item.id}")
|
||||
} else {
|
||||
onNavigateToItem(item)
|
||||
}
|
||||
}
|
||||
},
|
||||
onItemLongClick = {
|
||||
if (!isInSelectionMode) {
|
||||
selection = setOf(item.id.toLong())
|
||||
}
|
||||
},
|
||||
onDeleteClick = {
|
||||
vocabularyViewModel.deleteData(VocabularyViewModel.DeleteType.VOCABULARY_ITEM, item = item)
|
||||
},
|
||||
modifier = Modifier.animateItem()
|
||||
)
|
||||
.padding(horizontal = 8.dp),
|
||||
onItemClick = { item ->
|
||||
val isSelected = selection.contains(item.id.toLong())
|
||||
if (isInSelectionMode) {
|
||||
selection = if (isSelected) {
|
||||
selection - item.id.toLong()
|
||||
} else {
|
||||
selection + item.id.toLong()
|
||||
}
|
||||
} else {
|
||||
if (navController != null && enableNavigationButtons) {
|
||||
vocabularyViewModel.setNavigationContext(vocabularyItems, item.id)
|
||||
navController.navigate("vocabulary_detail/${item.id}")
|
||||
} else {
|
||||
onNavigateToItem(item)
|
||||
}
|
||||
}
|
||||
},
|
||||
onItemLongClick = { item ->
|
||||
if (!isInSelectionMode) {
|
||||
selection = setOf(item.id.toLong())
|
||||
}
|
||||
},
|
||||
onDeleteClick = { item ->
|
||||
vocabularyViewModel.deleteData(VocabularyViewModel.DeleteType.VOCABULARY_ITEM, item = item)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -382,8 +324,7 @@ fun VocabularyListScreen(
|
||||
languageViewModel = languageViewModel,
|
||||
languagesPresent = allLanguages.filter { it.nameResId in languagesPresent },
|
||||
hideCategory = categoryId != null && categoryId != 0,
|
||||
hideStage = stage != null,
|
||||
categoryViewModel = categoryViewModel
|
||||
hideStage = stage != null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -417,21 +358,34 @@ fun VocabularyListScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Deprecated("Use AllCardsListScreen which renders AllCardsView")
|
||||
@Composable
|
||||
fun VocabularyListScreenPreview() {
|
||||
val navController = rememberNavController()
|
||||
VocabularyListScreen(
|
||||
categoryId = 1,
|
||||
showDueTodayOnly = false,
|
||||
stage = VocabularyStage.NEW,
|
||||
onNavigateToItem = {},
|
||||
onNavigateBack = {},
|
||||
navController = navController
|
||||
fun VocabularyListScreen(
|
||||
categoryId: Int? = null,
|
||||
showDueTodayOnly: Boolean? = null,
|
||||
stage: VocabularyStage? = null,
|
||||
onNavigateToItem: (VocabularyItem) -> Unit?,
|
||||
onNavigateBack: (() -> Unit)? = null,
|
||||
navController: NavHostController? = null,
|
||||
itemsToShow: List<VocabularyItem> = emptyList(),
|
||||
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
|
||||
private fun DefaultTopAppBar(
|
||||
title: String,
|
||||
@@ -446,25 +400,8 @@ private fun DefaultTopAppBar(
|
||||
var showSortMenu by remember { mutableStateOf(false) }
|
||||
AppTopAppBar(
|
||||
modifier = Modifier.height(56.dp),
|
||||
title = {
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(title)
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
onNavigateBack?.let {
|
||||
IconButton(onClick = it) {
|
||||
Icon(
|
||||
AppIcons.ArrowBack,
|
||||
contentDescription = "stringResource(R.string.navigate_back)"
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
title = title,
|
||||
onNavigateBack = onNavigateBack,
|
||||
actions = {
|
||||
IconButton(onClick = onSearchClick) {
|
||||
Icon(
|
||||
@@ -522,8 +459,6 @@ private fun DefaultTopAppBar(
|
||||
|
||||
@Composable
|
||||
private fun SearchTopAppBar(
|
||||
searchQuery: String,
|
||||
onQueryChanged: (String) -> Unit,
|
||||
onCloseSearch: () -> Unit
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
@@ -534,37 +469,7 @@ private fun SearchTopAppBar(
|
||||
|
||||
AppTopAppBar(
|
||||
modifier = Modifier.height(56.dp),
|
||||
title = {
|
||||
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()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
title = "TODO",
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onCloseSearch) {
|
||||
Icon(
|
||||
@@ -582,8 +487,6 @@ private fun SearchTopAppBar(
|
||||
@Composable
|
||||
fun SearchTopAppBarPreview() {
|
||||
SearchTopAppBar(
|
||||
searchQuery = stringResource(R.string.search_query),
|
||||
onQueryChanged = {},
|
||||
onCloseSearch = {}
|
||||
)
|
||||
}
|
||||
@@ -605,14 +508,7 @@ private fun ContextualTopAppBar(
|
||||
|
||||
AppTopAppBar(
|
||||
modifier = modifier.height(56.dp),
|
||||
title = {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(stringResource(R.string.d_selected, selectionCount))
|
||||
}
|
||||
},
|
||||
title = stringResource(R.string.d_selected, selectionCount),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onCloseClick) {
|
||||
Icon(imageVector = AppIcons.Close, contentDescription = stringResource(R.string.label_close_selection_mode))
|
||||
@@ -693,112 +589,6 @@ fun ContextualTopAppBarPreview() {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VocabularyListItem(
|
||||
item: VocabularyItem,
|
||||
allLanguages: List<Language>,
|
||||
isSelected: Boolean,
|
||||
onItemClick: () -> Unit,
|
||||
onItemLongClick: () -> Unit,
|
||||
onDeleteClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val languageMap = remember(allLanguages) { allLanguages.associateBy { it.nameResId } }
|
||||
val langFirst = item.languageFirstId?.let { languageMap[it]?.name } ?: ""
|
||||
val langSecond = item.languageSecondId?.let { languageMap[it]?.name } ?: ""
|
||||
|
||||
OutlinedCard(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(CardDefaults.shape)
|
||||
.combinedClickable(
|
||||
onClick = onItemClick,
|
||||
onLongClick = onItemLongClick
|
||||
)
|
||||
.animateContentSize(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = if (isSelected) 4.dp else 0.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer
|
||||
else MaterialTheme.colorScheme.surface
|
||||
),
|
||||
border = BorderStroke(
|
||||
width = if (isSelected) 1.5.dp else 1.dp,
|
||||
color = if (isSelected) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 4.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
LanguageRow(word = item.wordFirst, language = langFirst)
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f))
|
||||
LanguageRow(word = item.wordSecond, language = langSecond)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier.padding(4.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Crossfade(targetState = isSelected, label = "action-icon-fade") { selected ->
|
||||
if (selected) {
|
||||
Icon(
|
||||
imageVector = AppIcons.Check,
|
||||
contentDescription = "Selected",
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
} else {
|
||||
IconButton(onClick = onDeleteClick) {
|
||||
Icon(
|
||||
imageVector = AppIcons.Delete,
|
||||
contentDescription = stringResource(id = R.string.label_delete),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Composable
|
||||
private fun LanguageRow(word: String, language: String) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 2.dp)) {
|
||||
Text(
|
||||
text = insertBreakOpportunities(word),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.weight(0.7f)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = language,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
modifier = Modifier.weight(0.3f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun LanguageRowPreview() {
|
||||
LanguageRow(
|
||||
word = "Hello",
|
||||
language = "English"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun FilterSortBottomSheet(
|
||||
@@ -808,8 +598,7 @@ private fun FilterSortBottomSheet(
|
||||
onDismiss: () -> Unit,
|
||||
onApplyFilters: (VocabularyFilterState) -> Unit,
|
||||
hideCategory: Boolean = false,
|
||||
hideStage: Boolean = false,
|
||||
categoryViewModel: CategoryViewModel
|
||||
hideStage: Boolean = false
|
||||
) {
|
||||
var selectedStage by rememberSaveable { mutableStateOf(currentFilterState.selectedStage) }
|
||||
var dueTodayOnly by rememberSaveable { mutableStateOf(currentFilterState.dueTodayOnly) }
|
||||
@@ -954,20 +743,3 @@ private fun FilterSortBottomSheet(
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun FilterSortBottomSheetPreview() {
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
FilterSortBottomSheet(
|
||||
currentFilterState = VocabularyFilterState(),
|
||||
languageViewModel = languageViewModel,
|
||||
languagesPresent = emptyList(),
|
||||
onDismiss = {},
|
||||
onApplyFilters = {},
|
||||
hideCategory = false,
|
||||
hideStage = false,
|
||||
categoryViewModel = categoryViewModel
|
||||
)
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ fun VocabularySortingScreen(
|
||||
var showFilterMenu by remember { mutableStateOf(false) }
|
||||
|
||||
AppTopAppBar(
|
||||
title = { Text(stringResource(R.string.sort_new_vocabulary)) },
|
||||
title = stringResource(R.string.sort_new_vocabulary),
|
||||
actions = {
|
||||
Box {
|
||||
IconButton(onClick = { showFilterMenu = true }) {
|
||||
@@ -231,11 +231,7 @@ fun VocabularySortingScreen(
|
||||
}
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||
}
|
||||
},
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
hintContent = HintDefinition.SORTING.hint()
|
||||
)
|
||||
},
|
||||
@@ -299,7 +295,6 @@ fun VocabularySortingItem(
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val languageConfigViewModel: LanguageConfigViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
var wordFirst by remember { mutableStateOf(item.wordFirst) }
|
||||
var wordSecond by remember { mutableStateOf(item.wordSecond) }
|
||||
var selectedCategories by remember { mutableStateOf<List<Int>>(emptyList()) }
|
||||
@@ -314,7 +309,6 @@ fun VocabularySortingItem(
|
||||
var articlesLangSecond by remember { mutableStateOf(emptySet<String>()) }
|
||||
|
||||
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
|
||||
val isItemNowValid by remember(wordFirst, wordSecond, langFirst, langSecond) {
|
||||
|
||||
@@ -66,8 +66,6 @@ internal fun DraggableActionPanel(
|
||||
onDismiss: () -> Unit,
|
||||
isEditing: Boolean,
|
||||
onEditClick: () -> Unit,
|
||||
onSaveClick: () -> Unit,
|
||||
onCancelClick: () -> Unit,
|
||||
onStatisticsClick: () -> Unit,
|
||||
onMoveToCategoryClick: () -> 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) {
|
||||
ActionItem(icon = AppIcons.Edit, label = stringResource(R.string.edit), isExtended = isExtended, onClick = actionClickHandler(onEditClick))
|
||||
|
||||
if (showAnalyzeGrammarButton) {
|
||||
ActionItem(
|
||||
@@ -252,8 +245,6 @@ fun DraggableActionPanelPreview() {
|
||||
onDismiss = {},
|
||||
isEditing = false,
|
||||
onEditClick = {},
|
||||
onSaveClick = {},
|
||||
onCancelClick = {},
|
||||
|
||||
onStatisticsClick = {},
|
||||
onMoveToCategoryClick = {},
|
||||
|
||||
@@ -87,10 +87,58 @@ import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
@Composable
|
||||
fun VocabularyCard(
|
||||
fun VocabularyDisplayCard(
|
||||
vocabularyItem: VocabularyItem,
|
||||
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,
|
||||
switchOrder: Boolean,
|
||||
onStatisticsClick: () -> Unit = {},
|
||||
@@ -99,6 +147,8 @@ fun VocabularyCard(
|
||||
onDeleteClick: () -> Unit = {},
|
||||
userSpellingAnswer: String? = null,
|
||||
isUserSpellingCorrect: Boolean? = null,
|
||||
onEditStateChange: ((Boolean) -> Unit)? = null,
|
||||
onEditActionHandlersReady: ((onSave: () -> Unit, onCancel: () -> Unit) -> Unit)? = null,
|
||||
) {
|
||||
|
||||
val activity = LocalContext.current.findActivity()
|
||||
@@ -201,6 +251,7 @@ fun VocabularyCard(
|
||||
)
|
||||
vocabularyViewModel.editVocabularyItem(updatedItem)
|
||||
isEditing = false
|
||||
onEditStateChange?.invoke(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,6 +264,7 @@ fun VocabularyCard(
|
||||
editedLangSecondId = item.languageSecondId
|
||||
editedFeatures = item.features?.let { jsonParser.decodeFromString<VocabularyFeatures>(it) } ?: VocabularyFeatures()
|
||||
isEditing = false
|
||||
onEditStateChange?.invoke(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,13 +338,13 @@ fun VocabularyCard(
|
||||
onWordChange = { if (!switchOrder) editedWordFirst = it else editedWordSecond = it },
|
||||
language = if (!switchOrder) languageFirst else languageSecond,
|
||||
onLanguageIdChange = { if (!switchOrder) editedLangFirstId = it else editedLangSecondId = it },
|
||||
isRevealed = isFrontFace || exerciseMode,
|
||||
isRevealed = isFrontFace || isExerciseMode,
|
||||
userSpellingAnswer = userSpellingAnswer,
|
||||
isUserSpellingCorrect = isUserSpellingCorrect,
|
||||
correctWord = if (switchOrder) item.wordFirst else item.wordSecond,
|
||||
wordDetails = if (!switchOrder) editedFeatures.first else editedFeatures.second,
|
||||
onEditGrammarClick = { showGrammarDialogFor = "first" },
|
||||
isExerciseMode = exerciseMode,
|
||||
isExerciseMode = isExerciseMode,
|
||||
vocabularyItem = item,
|
||||
onMoreClick = {
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@@ -317,7 +369,7 @@ fun VocabularyCard(
|
||||
modifier = Modifier.weight(1f),
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
||||
)
|
||||
if (!exerciseMode && !isFlipped) {
|
||||
if (!isExerciseMode && !isEditing && !isFlipped) {
|
||||
IconButton(onClick = { showActionPanel = true }) {
|
||||
Icon(
|
||||
imageVector = AppIcons.MoreVert,
|
||||
@@ -339,7 +391,7 @@ fun VocabularyCard(
|
||||
onWordChange = { if (switchOrder) editedWordFirst = it else editedWordSecond = it },
|
||||
language = if (switchOrder) languageFirst else languageSecond,
|
||||
onLanguageIdChange = { if (switchOrder) editedLangFirstId = it else editedLangSecondId = it },
|
||||
isRevealed = !(!isFlipped && exerciseMode),
|
||||
isRevealed = !(!isFlipped && isExerciseMode),
|
||||
userSpellingAnswer = userSpellingAnswer,
|
||||
isUserSpellingCorrect = isUserSpellingCorrect,
|
||||
correctWord = if (switchOrder) item.wordFirst else item.wordSecond,
|
||||
@@ -348,7 +400,7 @@ fun VocabularyCard(
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
showGrammarDialogFor = "second"
|
||||
},
|
||||
isExerciseMode = exerciseMode,
|
||||
isExerciseMode = isExerciseMode,
|
||||
vocabularyItem = item,
|
||||
onMoreClick = {
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@@ -361,7 +413,7 @@ fun VocabularyCard(
|
||||
|
||||
|
||||
!switchOrder
|
||||
if(isFlipped || !exerciseMode)
|
||||
if(isFlipped || !isExerciseMode)
|
||||
DraggableActionPanel(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
@@ -369,9 +421,14 @@ fun VocabularyCard(
|
||||
isOpen = showActionPanel,
|
||||
onDismiss = { showActionPanel = false },
|
||||
isEditing = isEditing,
|
||||
onEditClick = { isEditing = true },
|
||||
onSaveClick = { handleSave() },
|
||||
onCancelClick = handleCancel,
|
||||
onEditClick = {
|
||||
isEditing = true
|
||||
onEditStateChange?.invoke(true)
|
||||
onEditActionHandlersReady?.invoke(
|
||||
{ handleSave() },
|
||||
{ handleCancel() }
|
||||
)
|
||||
},
|
||||
|
||||
onStatisticsClick = onStatisticsClick,
|
||||
onMoveToCategoryClick = onMoveToCategoryClick,
|
||||
@@ -438,18 +495,15 @@ fun VocabularyCardPreview() {
|
||||
languageSecondId = R.string.language_2
|
||||
)
|
||||
val navController = NavController(LocalContext.current)
|
||||
VocabularyCard(
|
||||
VocabularyDisplayCard(
|
||||
vocabularyItem = item,
|
||||
navController = navController,
|
||||
exerciseMode = false,
|
||||
isFlipped = false,
|
||||
switchOrder = false,
|
||||
onStatisticsClick = {},
|
||||
onMoveToCategoryClick = {},
|
||||
onMoveToStageClick = {},
|
||||
onDeleteClick = {},
|
||||
userSpellingAnswer = null,
|
||||
isUserSpellingCorrect = null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -475,7 +529,7 @@ private fun FrequencyPill(zipfFrequency: Float?) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
.width(80.dp),
|
||||
.width(100.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
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).
|
||||
*/
|
||||
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
|
||||
val dataFlow = getStructuredDictionaryDataState(entry)
|
||||
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) {
|
||||
viewModelScope.launch {
|
||||
val allQuestions = exerciseRepository.getAllQuestionsFlow().first()
|
||||
|
||||
@@ -90,12 +90,20 @@ class ProgressViewModel @Inject constructor(
|
||||
private val _totalWordsInProgress = MutableStateFlow(0)
|
||||
val totalWordsInProgress: StateFlow<Int> = _totalWordsInProgress.asStateFlow()
|
||||
|
||||
private val _totalWords = MutableStateFlow(0)
|
||||
val totalWords: StateFlow<Int> = _totalWords.asStateFlow()
|
||||
|
||||
private val _weeklyActivityStats = MutableStateFlow<List<WeeklyActivityStat>>(emptyList())
|
||||
val weeklyActivityStats: StateFlow<List<WeeklyActivityStat>> = _weeklyActivityStats.asStateFlow()
|
||||
|
||||
private val _dailyVocabularyStats = MutableStateFlow<Map<LocalDate, Int>>(emptyMap())
|
||||
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 {
|
||||
viewModelScope.launch {
|
||||
@@ -255,6 +263,15 @@ class ProgressViewModel @Inject constructor(
|
||||
try {
|
||||
loadSelectedCategories()
|
||||
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 lastSevenDaysDeferred = viewModelScope.async { getLastSevenDays() }
|
||||
val streakDeferred = viewModelScope.async { calculateDailyStreak() }
|
||||
@@ -270,6 +287,8 @@ class ProgressViewModel @Inject constructor(
|
||||
.filter { it.stage != VocabularyStage.LEARNED && it.stage != VocabularyStage.NEW }
|
||||
.sumOf { it.itemCount }
|
||||
|
||||
_totalWords.value = stageList.sumOf { it.itemCount }
|
||||
|
||||
if (_selectedCategories.value.isEmpty() && progressList.isNotEmpty()) {
|
||||
val initialCategory = setOf(progressList.first().vocabularyCategory.id)
|
||||
_selectedCategories.value = initialCategory
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("HardCodedStringLiteral")
|
||||
|
||||
package eu.gaudian.translator.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
@@ -31,8 +33,8 @@ enum class ScreenState {
|
||||
}
|
||||
|
||||
data class ExerciseConfig(
|
||||
val shuffleCards: Boolean = false,
|
||||
val shuffleLanguages: Boolean = false,
|
||||
val shuffleCards: Boolean = true,
|
||||
val shuffleLanguages: Boolean = true,
|
||||
val trainingMode: Boolean = false,
|
||||
val dueTodayOnly: Boolean = false,
|
||||
val selectedExerciseTypes: Set<VocabularyExerciseType> = setOf(VocabularyExerciseType.GUESSING),
|
||||
@@ -90,6 +92,9 @@ class VocabularyExerciseViewModel @Inject constructor(
|
||||
|
||||
// Exercise configuration state
|
||||
private val _exerciseConfig = MutableStateFlow(ExerciseConfig())
|
||||
private val _pendingExerciseConfig = MutableStateFlow(ExerciseConfig())
|
||||
|
||||
val pendingExerciseConfig: StateFlow<ExerciseConfig> = _pendingExerciseConfig.asStateFlow()
|
||||
|
||||
// Exercise results state
|
||||
private val _exerciseResults = MutableStateFlow(ExerciseResults())
|
||||
@@ -106,6 +111,7 @@ class VocabularyExerciseViewModel @Inject constructor(
|
||||
types: Set<VocabularyExerciseType>,
|
||||
shuffleLanguages: Boolean
|
||||
) {
|
||||
Log.d("ExerciseVM", "startExercise called: items=${items.size}, types=$types, shuffleLanguages=$shuffleLanguages")
|
||||
// Reset counters for the new exercise session
|
||||
_correctAnswers.value = 0
|
||||
_wrongAnswers.value = 0
|
||||
@@ -158,6 +164,7 @@ class VocabularyExerciseViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun loadExercise() {
|
||||
Log.d("ExerciseVM", "loadExercise: index=$currentIndex, total=${currentItems.size}")
|
||||
if (currentIndex < currentItems.size) {
|
||||
// Ensure item categories align with exercise type by attempting a swap instead of replacement
|
||||
val randomType = exerciseTypes.random()
|
||||
@@ -273,8 +280,10 @@ class VocabularyExerciseViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
Log.d("ExerciseVM", "exerciseState set: type=$randomType, itemId=${itemToUse.id}")
|
||||
} else {
|
||||
_exerciseState.value = null // End of exercise
|
||||
Log.d("ExerciseVM", "loadExercise: end of exercise, state cleared")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,27 +399,33 @@ class VocabularyExerciseViewModel @Inject constructor(
|
||||
loadExercise()
|
||||
}
|
||||
|
||||
fun onTrainingModeChanged(value: Boolean) {
|
||||
_trainingMode.value = value
|
||||
}
|
||||
|
||||
fun startExerciseWithConfig(
|
||||
items: List<VocabularyItem>,
|
||||
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
|
||||
_pendingExerciseConfig.value = config
|
||||
_totalItems.value = items.size
|
||||
_originalItems.value = items
|
||||
startExercise(items, config.selectedExerciseTypes, config.shuffleLanguages)
|
||||
_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) {
|
||||
_exerciseResults.value = ExerciseResults(score, wrongAnswers)
|
||||
_screenState.value = ScreenState.RESULT
|
||||
}
|
||||
|
||||
fun resetExercise() {
|
||||
Log.d("ExerciseVM", "resetExercise called")
|
||||
_screenState.value = ScreenState.START
|
||||
_exerciseConfig.value = ExerciseConfig()
|
||||
_exerciseResults.value = ExerciseResults()
|
||||
@@ -420,6 +435,7 @@ class VocabularyExerciseViewModel @Inject constructor(
|
||||
_exerciseState.value = null
|
||||
_totalItems.value = 0
|
||||
_originalItems.value = emptyList()
|
||||
Log.d("ExerciseVM", "resetExercise completed; screenState=START")
|
||||
}
|
||||
|
||||
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) {
|
||||
val selectedSourceLanguage = languageRepository.loadSelectedSourceLanguage().first()
|
||||
val selectedTargetLanguage = languageRepository.loadSelectedTargetLanguage().first()
|
||||
@@ -664,6 +714,7 @@ class VocabularyViewModel @Inject constructor(
|
||||
languageIdsAsJson: String?,
|
||||
dailyOnly: Boolean = false
|
||||
) {
|
||||
Log.d("VocabularyVM", "prepareExercise: categories=$categoryIdsAsJson, stages=$stageNamesAsJson, languages=$languageIdsAsJson, dailyOnly=$dailyOnly")
|
||||
viewModelScope.launch {
|
||||
val categoryList = categoryIdsAsJson?.takeIf { it.isNotBlank() }
|
||||
?.split(",")
|
||||
@@ -682,6 +733,7 @@ class VocabularyViewModel @Inject constructor(
|
||||
allLangs.filter { it.nameResId in ids }
|
||||
} ?: emptyList()
|
||||
|
||||
Log.d("VocabularyVM", "prepareExercise parsed: categories=${categoryList.size}, stages=${stageList.size}, languages=${languageList.size}")
|
||||
loadCardSet(categoryList, stageList, languageList, dailyOnly)
|
||||
}
|
||||
}
|
||||
@@ -693,6 +745,7 @@ class VocabularyViewModel @Inject constructor(
|
||||
languages: List<Language>? = null,
|
||||
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}")
|
||||
viewModelScope.launch {
|
||||
statusService.showLoadingMessage("Loading card set")
|
||||
@@ -752,6 +805,8 @@ class VocabularyViewModel @Inject constructor(
|
||||
dueTodayOnly = dailyOnly
|
||||
).first()
|
||||
|
||||
Log.d(TAG, "loadCardSet: filterVocabularyItems returned ${filteredItems.size} items")
|
||||
|
||||
Log.d(TAG, "loadCardSet: Filtering completed, found ${filteredItems.size} items")
|
||||
|
||||
if (filteredItems.isNotEmpty()) {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
@@ -1,10 +1,23 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<resources>
|
||||
<string-array name="exercise_example_prompts">Exercise Example Prompts</string-array>
|
||||
<string-array name="changelog_entries">
|
||||
<item>Version 0.3.0 \n• CSV-Import für Vokabeln aktiviert\n• Option, für einige unterstützte Sprachen einen Übersetzungsserver statt KI-Modelle zu nutzen\n• UI-Fehlerbehebungen \n• Anzeige der Worthäufigkeit \n• Leistungsoptimierungen \n• Verbesserte Übersetzungen (Deutsch und Portugiesisch)</item>
|
||||
<item>Version 0.3.0 \n• CSV-Import für Vokabeln aktiviert\n• Option, für einige unterstützte Sprachen einen Übersetzungsserver, statt KI-Modelle zu nutzen\n• UI-Fehlerbehebungen \n• Anzeige der Worthäufigkeit \n• Leistungsoptimierungen \n• Verbesserte Übersetzungen (Deutsch und Portugiesisch)</item>
|
||||
<item>Version 0.4.0 \n• Wörterbuch-Download hinzugefügt (Beta) \n• Verbesserungen der Benutzeroberfläche \n• Fehlerbehebungen \n• Neugestaltete Vokabelkarte mit verbesserter UI \n• Mehr vorkonfigurierte Anbieter \n• Verbesserte Leistung</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="dictionary_content">
|
||||
<item>Wortart und Genus (bei Nomen)</item>
|
||||
<item>Deklination (bei Nomen)</item>
|
||||
<item>Aussprache (IPA) und Worttrennung</item>
|
||||
<item>Definition</item>
|
||||
<item>Herkunft</item>
|
||||
<item>Synonyme</item>
|
||||
<item>Antonyme</item>
|
||||
<item>Beispiele</item>
|
||||
<item>Konjugation (bei Verben)</item>
|
||||
<item>Redewendungen</item>
|
||||
<item>Grammatikalische Merkmale (bei Präpositionen)</item>
|
||||
</string-array>
|
||||
<!-- Stable, non-localized keys for dictionary content options. Order must match dictionary_content. -->
|
||||
<string-array name="dictionary_content_keys">
|
||||
<item>word_class_gender</item>
|
||||
@@ -19,35 +32,87 @@
|
||||
<item>idioms</item>
|
||||
<item>grammatical_features_prepositions</item>
|
||||
</string-array>
|
||||
<string-array name="dictionary_content">
|
||||
<item>Wortart und Genus (bei Nomen)</item>
|
||||
<item>Deklination (bei Nomen)</item>
|
||||
<item>Aussprache (IPA) und Worttrennung</item>
|
||||
<item>Definition</item>
|
||||
<item>Herkunft</item>
|
||||
<item>Synonyme</item>
|
||||
<item>Antonyme</item>
|
||||
<item>Beispiele</item>
|
||||
<item>Konjugation (bei Verben)</item>
|
||||
<item>Redewendungen</item>
|
||||
<item>Grammatikalische Merkmale (bei Präpositionen)</item>
|
||||
</string-array>
|
||||
<string-array name="example_prompts"><item>Alles übersetzen, ohne etwas hinzuzufügen.</item>
|
||||
|
||||
<string-array name="example_prompts">
|
||||
<item>Alles übersetzen, ohne etwas hinzuzufügen.</item>
|
||||
<item>Ersetze höfliche Pronomen Sie (formell) durch "du".</item>
|
||||
<item>Mach es sehr formell.</item>
|
||||
<item>Mach es informell und füge ein Emoji hinzu.</item>
|
||||
</string-array>
|
||||
<string-array name="vocabulary_hints"><item>Grundlegende Begrüßungen</item>
|
||||
<item>Unregelmäßige Verben</item>
|
||||
<item>Vokabular am Flughafen</item>
|
||||
<item>Wie man einen Kaffee bestellt</item>
|
||||
<item>Idiomatische Ausdrücke</item>
|
||||
</string-array>
|
||||
<string-array name="vocabulary_example_prompts"><item>Verwende lateinamerikanisches Spanisch</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="exercise_example_prompts">Exercise Example Prompts</string-array>
|
||||
|
||||
<string-array name="motivational_phrases">
|
||||
<item>Die Grenzen deiner Sprache sind die Grenzen deiner Welt. Lass uns den Käfig etwas größer machen.</item>
|
||||
<item>Du bist im Grunde ein Fleisch-Computer, der eine Sprachsimulation am Laufen hat. Zeit für ein Firmware-Upgrade.</item>
|
||||
<item>Eine andere Sprache zu sprechen heißt, eine zweite Seele zu besitzen. Oder zumindest einen ziemlich guten Party-Trick.</item>
|
||||
<item>Irgendwann wirst du deine existenzielle Angst in einem völlig anderen Dialekt formulieren.</item>
|
||||
<item>Neuroplastizität klingt nach einer schicken OP, ist aber nur dein Gehirn, das mal endlich schweres Heben macht.</item>
|
||||
<item>Jedes neue Wort, das du lernst, ist ein Konzept, das du dir aus der Leere zurückholst.</item>
|
||||
<item>Red weniger, sag mehr. Stopp, du lernst Vokabeln – rede schlecht, aber rede oft.</item>
|
||||
<item>Deine heutige Wortanzahl ist im Kosmos statistisch irrelevant, aber für dein Ego monumental.</item>
|
||||
<item>Descartes‘ Dualismus meint, Geist und Körper seien getrennt. Lass uns sicherstellen, dass der Geist heute auch mal was reißt.</item>
|
||||
<item>Fließend sein ist nur eine Reihe gut getarnter Fehler. Leg los und mach ein paar.</item>
|
||||
<item>Wissen ist das Einzige, was die Leere wirklich stillt. Fütter sie.</item>
|
||||
<item>Dein Kurzzeitgedächtnis ist ein Sieb. Wir werden diese Wörter durch reine Wiederholung in den Tresor des Langzeitgedächtnisses prügeln.</item>
|
||||
<item>Wahre Freiheit ist, genau das richtige, hochspezifische Adjektiv zu kennen, wenn du jemanden stumm verurteilst.</item>
|
||||
<item>Du bist ein Künstler und dein Medium sind leicht falsch ausgesprochene Nomen.</item>
|
||||
<item>Ein hungriger Magen knurrt. Ein hungriger Geist scrollt nur dumm durchs Internet. Lern lieber ein Wort.</item>
|
||||
<item>Spaced Repetition: weil dein Gehirn aggressiv versucht, alles zu löschen, was du nicht aktiv nutzt.</item>
|
||||
<item>Kuratiere deinen Wortschatz wie ein Museum, in dem die Hauptattraktion deine eigene Überlegenheit ist.</item>
|
||||
<item>Solipsismus ist der Glaube, dass nur dein Geist existiert. Wenn das so ist, solltest du ihm vielleicht einen besseren Wortschatz verpassen.</item>
|
||||
<item>Ein Mensch ohne Sprache ist ein Mensch ohne Land. Oder einfach ein sehr stiller Tourist.</item>
|
||||
<item>Baue ein Vokabel-Imperium auf, eine aggressiv missverstandene Redewendung nach der anderen.</item>
|
||||
<item>Die Eule der Minerva beginnt ihren Flug erst in der Dämmerung. Und nach einer richtigen Lerneinheit.</item>
|
||||
<item>Um dem Gravitationsfeld der Einsprachigkeit zu entkommen, braucht es ordentlich Schubkraft. Bleib am Ball.</item>
|
||||
<item>Sprache ist das Betriebssystem des Bewusstseins. Deins installiert gerade ein Update.</item>
|
||||
<item>Wir sind alle in der Gosse, aber einige von uns schauen in fremdsprachige Wörterbücher.</item>
|
||||
<item>Eine Sache zu benennen heißt, sie zu besitzen. Es wird Zeit, dass du zum absoluten Großgrundbesitzer der Realität wirst.</item>
|
||||
<item>Grammatik ist die Grundstruktur der Realität. Ignorier sie auf eigene ontologische Gefahr.</item>
|
||||
<item>Sogar Sokrates wusste, dass er nichts wusste. Aber er wusste wenigstens das Wort für \'nichts\' auf Griechisch.</item>
|
||||
<item>Die Reise von tausend Meilen beginnt mit einer unbeholfenen Kaffee-Bestellung in der falschen Zeitform.</item>
|
||||
<item>Dein Gehirn verbraucht etwa zwanzig Prozent der Energie deines Körpers. Lass es sich seinen Unterhalt verdienen.</item>
|
||||
<item>Motivation ist eine flüchtige Emotion. Gewohnheit ist eine unerbittliche Maschine. Sei die Maschine.</item>
|
||||
<item>Wenn Worte Waffen sind, ist dein Arsenal gerade eine Sammlung stumpfer Löffel. Lass uns sie schärfen.</item>
|
||||
<item>Lernen bedeutet, sich freiwillig vorübergehender Inkompetenz zu unterziehen. Leb mit der Peinlichkeit.</item>
|
||||
<item>Sprachenlernen ist der Mythos des Sisyphos, aber manchmal rollt der Stein den Berg runter und du verstehst tatsächlich einen Podcast.</item>
|
||||
<item>Dein zukünftiges Ich beurteilt gerade deine heutige Arbeitsmoral. Beweis ihm das Gegenteil.</item>
|
||||
<item>Entropie besagt, dass das Universum zum Chaos neigt. Vokabeln zu lernen ist deine tägliche Revolte gegen die Physik.</item>
|
||||
<item>10 Wörter am Tag sind 3.650 im Jahr. Immer noch nicht genug, um die menschliche Existenz zu erklären, aber ein ordentlicher Anfang.</item>
|
||||
<item>Zinseszins gilt auch für kognitive Werte. Also los, investier in ein paar Verben.</item>
|
||||
<item>Du bist die Summe deiner Erinnerungen. Lass uns sicherstellen, dass die nicht nur aus Popsongs und Peinlichkeiten bestehen.</item>
|
||||
<item>Englisch sind einfach drei verschiedene Sprachen, die in einem Mantel aufeinanders Schultern stehen. Wenn das so tun kann, kannst du das auch.</item>
|
||||
<item>Das französische Wort für 99 heißt wörtlich \'vier-zwanzig-zehn-neun\'. Deine aktuellen Lernprobleme sind nichts gegen deren Mathe.</item>
|
||||
<item>Im Mandarin macht der falsche Ton aus \'Mutter\' ein \'Pferd\'. Vermeiden wir heute mal ödipale Reitunfälle.</item>
|
||||
<item>Deutsch lässt dich Nomen zusammenknallen, bis du ein Wort hast, das lang genug ist, um deine ganz spezielle Form des Elends auszudrücken. Streb nach dieser Macht.</item>
|
||||
<item>Die Beherrschung des grammatikalischen Geschlechts bedeutet zu akzeptieren, dass das Universum willkürlich entschieden hat, dass ein Tisch weiblich und eine Brücke männlich ist. Füg dich dem Absurden.</item>
|
||||
<item>Japanisch hat spezielle Zählwörter, je nachdem, ob ein Objekt flach, zylindrisch oder ein kleines Tier ist. Präzision ist eine Tugend. Fang an zu üben.</item>
|
||||
<item>Spanischsprachige rasseln etwa 25% schneller Silben runter als Englischsprachige. Du musst nicht schlauer sein, du musst nur schneller denken.</item>
|
||||
<item>Finnisch hat 15 Fälle. Plötzlich ist Vokabeln lernen gar nicht mehr so schlimm, oder?</item>
|
||||
<item>Eine Sprache zu lernen bedeutet zu erkennen, dass Redewendungen nur kulturell sanktionierte, kollektive Halluzinationen sind. Lass uns halluzinieren.</item>
|
||||
<item>Geschriebenes Walisisch sieht aus, als wäre jemand auf einer vokalfaulen Tastatur eingeschlafen, aber es birgt eine dunkle, uralte Poesie. Find die Poesie in deiner Zielsprache.</item>
|
||||
<item>Ein robuster Wortschatz ist der sozial akzeptabelste Weg, ein herablassender Snob zu sein. Verdien dir deine Arroganz.</item>
|
||||
<item>Du kämpfst gerade gegen die Ebbinghaussche Vergessenskurve. Sie ist eine gnadenlose, unverzeihliche mathematische Realität. Wehr dich.</item>
|
||||
<item>Latein zu lernen ist nutzlos, es sei denn, du willst mit mittelalterlichen Mönchen diskutieren oder versehentlich irgendwas beschwören. So oder so, es ist eine gute Vorbereitung.</item>
|
||||
<item>Ohne Vokabeln ist Grammatik nur eine wunderschön strukturierte Stille. Gib ihr etwas Lärm.</item>
|
||||
<item>Irisch hat keine Wörter für \'ja\' oder \'nein\'. Du musst das Verb wiederholen. Bei Sprachen geht es darum, voll und ganz dabei zu sein.</item>
|
||||
<item>Das hawaiianische Alphabet hat nur 13 Buchstaben. Die Sprache, die du lernst, hat mehr. Hör auf zu jammern und lerne sie.</item>
|
||||
<item>Wortwörtlich zu übersetzen ist das reinste Hamsterrad. Jede Sprache bildet die Realität anders ab. Zeit, eine neue Karte zu lernen.</item>
|
||||
<item>Irgendwann kannst du einen fremdsprachigen Film gucken, ohne nur auf das untere Drittel des Bildschirms zu starren. Bleib dran.</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="vocabulary_example_prompts">
|
||||
<item>Verwende lateinamerikanisches Spanisch</item>
|
||||
<item>Vermeide lange Wörter</item>
|
||||
<item>Vermeide Sätze</item>
|
||||
<item>Enthält viele Verben und Adjektive</item>
|
||||
<item>Verwende informelle Sprache</item>
|
||||
</string-array>
|
||||
</string-array>
|
||||
<string-array name="vocabulary_hints">
|
||||
<item>Grundlegende Begrüßungen</item>
|
||||
<item>Unregelmäßige Verben</item>
|
||||
<item>Vokabular am Flughafen</item>
|
||||
<item>Wie man einen Kaffee bestellt</item>
|
||||
<item>Idiomatische Ausdrücke</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
<string name="title_show_success_message">Erfolgsmeldung anzeigen</string>
|
||||
<string name="label_add_category">Kategorie hinzufügen</string>
|
||||
<string name="title_settings">Einstellungen</string>
|
||||
<string name="title_dashboard">Dashboard</string>
|
||||
<string name="title_developer_options">Entwickleroptionen</string>
|
||||
<string name="title_multiple">Mehrere</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="info_imported_items_from">%1$d Vokabeln importiert.</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="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>
|
||||
@@ -83,7 +81,6 @@
|
||||
<string name="text_shuffle_languages">Sprachen mischen</string>
|
||||
<string name="text_training_mode">Trainingsmodus</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_translation">Gib die Übersetzung ein</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_show_loading">Laden anzeigen</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_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_reset_intro">Intro zurücksetzen</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_save_key">Schlüssel speichern</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_manual_vocabulary_list">Manuelle Vokabelliste</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_amount_2d_questions">Anzahl: %1$d Fragen</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_hint">Tipp</string>
|
||||
<string name="text_select_languages">Sprachen auswählen</string>
|
||||
@@ -226,19 +218,14 @@
|
||||
<string name="cd_target_met">Ziel erreicht</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_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_learned">Gelernt</string>
|
||||
<string name="remaining">Übrig</string>
|
||||
<string name="label_ai_model_and_prompt"><![CDATA[KI-Modell & Prompt]]></string>
|
||||
<string name="examples">Beispiele</string>
|
||||
<string name="vocabulary_settings">Vokabular-Einstellungen</string>
|
||||
<string name="label_learning_criteria">Lernkriterien</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="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="export_vocabulary_data">Vokabeldaten exportieren</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="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="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="shuffle_cards">Karten mischen</string>
|
||||
<string name="quit">Beenden</string>
|
||||
@@ -336,12 +323,11 @@
|
||||
<string name="last_incorrect">Zuletzt falsch: %1$s</string>
|
||||
<string name="correct_answers_">Richtige 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="statistics_are_loading">Statistiken werden geladen…</string>
|
||||
<string name="to_d">nach %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="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>
|
||||
@@ -367,7 +353,6 @@
|
||||
<string name="more_actions">Mehr Aktionen</string>
|
||||
<string name="select_all">Alle auswählen</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="label_category_2d">Kategorie: %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="translate_the_following_d">Übersetze Folgendes (%1$s):</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="primary_button">Primärer Button</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="label_close_selection_mode">Auswahlmodus schließen</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="generate_related_vocabulary_items">Verwandte Vokabeln generieren</string>
|
||||
<string name="dismiss">Verwerfen</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="word_type">Wortart</string>
|
||||
<string name="levels">Level</string>
|
||||
<string name="quick_word_pairs">Schnelle Wortpaare</string>
|
||||
<string name="stage_filter">Stufenfilter</string>
|
||||
<string name="language_pair">Sprachpaar</string>
|
||||
<string name="language_filter">Sprachfilter</string>
|
||||
@@ -549,10 +529,6 @@
|
||||
<string name="friendly">Freundlich</string>
|
||||
<string name="label_academic">Akademisch</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="settings_title_voice">Stimme</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="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_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_days">" Tage"</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_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_sample_word">Beispielwort</string>
|
||||
<string name="toggle_use_libretranslate">Übersetungs-Server verwenden</string>
|
||||
@@ -634,7 +600,6 @@
|
||||
<string name="label_language_none">Keine</string>
|
||||
<string name="text_no_data_available">Keine Daten verfügbar</string>
|
||||
<string name="label_grammar_inflections">Flexionen</string>
|
||||
<string name="label_more">Weniger</string>
|
||||
<string name="label_translations">Übersetzungen</string>
|
||||
<string name="label_show_examples">Beispiele anzeigen</string>
|
||||
<string name="label_grammar_hyphenation">Silbentrennung</string>
|
||||
@@ -684,13 +649,15 @@
|
||||
<string name="label_language_direction">Sprachenrichtung
|
||||
</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_multiple_choice_exercise">Multiple Choice</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_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_disabled_by_direction">Deaktiviere die Sprachrichtungs-Einstellung, um das Mischen zu aktivieren.</string>
|
||||
<string name="label_conjugation">Konjugation: %1$s</string>
|
||||
<string name="label_collapse">Einklappen</string>
|
||||
<string name="label_expand">Ausklappen</string>
|
||||
@@ -862,5 +829,92 @@
|
||||
<string name="duplicate">Duplikat</string>
|
||||
<string name="hint_scan_hint_title">Das richtige AI-Modell finden</string>
|
||||
<string name="hint_translate_how_it_works">Wie Übersetzung funktioniert</string>
|
||||
<string name="label_home">Startseite</string>
|
||||
<string name="label_more">Mehr</string>
|
||||
<string name="label_quit_app">App beenden</string>
|
||||
<string name="label_target_correct_answers_per_day">Ziel für richtige Antworten pro Tag</string>
|
||||
<string name="label_interval_settings_in_days">Intervall-Einstellungen</string>
|
||||
<string name="label_vocabulary_settings">Fortschritts-Einstellungen</string>
|
||||
<string name="label_no_category">Keine</string>
|
||||
<string name="text_search">Suche</string>
|
||||
<string name="text_language_settings_description">Stelle ein, welche Sprachen du in der App verwenden möchtest. Sprachen, die nicht aktiviert sind, werden in dieser App nicht angezeigt. Du kannst auch deine eigene Sprache zur Liste hinzufügen oder eine vorhandene Sprache (Region/Locale) ändern.</string>
|
||||
<string name="message_success_generic">Erfolg!</string>
|
||||
<string name="message_info_generic">Info</string>
|
||||
<string name="message_error_generic">Ein Fehler ist aufgetreten</string>
|
||||
<string name="message_loading_generic">Lädt…</string>
|
||||
<string name="message_error_language_not_selected">Quell- und Zielsprachen müssen ausgewählt sein.</string>
|
||||
<string name="message_error_no_words_found">Keine Wörter im bereitgestellten Text gefunden.</string>
|
||||
<string name="message_success_language_replaced">Sprach-ID für %1$d Elemente aktualisiert.</string>
|
||||
<string name="message_success_vocabulary_imported">Vokabeln erfolgreich importiert.</string>
|
||||
<string name="message_error_vocabulary_import_failed">Fehler beim Importieren der Vokabeln: %1$s</string>
|
||||
<string name="message_success_items_merged">Einträge zusammengeführt!</string>
|
||||
<string name="message_success_items_added">%1$d neue Vokabeln erfolgreich hinzugefügt.</string>
|
||||
<string name="message_error_items_add_failed">Fehler beim Hinzufügen der Einträge: %1$s</string>
|
||||
<string name="message_success_items_deleted">Vokabeln erfolgreich gelöscht.</string>
|
||||
<string name="message_error_items_delete_failed">Fehler beim Löschen: %1$s</string>
|
||||
<string name="message_error_no_cards_found">Keine Karten für den angegebenen Filter gefunden.</string>
|
||||
<string name="message_success_cards_loaded">Kartensatz erfolgreich geladen.</string>
|
||||
<string name="message_success_grammar_updated">Grammatikdetails aktualisiert!</string>
|
||||
<string name="message_error_grammar_fetch_failed">Konnte Grammatikdetails nicht abrufen.</string>
|
||||
<string name="message_loading_grammar_fetch">Grammatik wird für %1$d Elemente abgerufen…</string>
|
||||
<string name="message_success_file_saved">Datei unter %1$s gespeichert</string>
|
||||
<string name="message_error_file_save_failed">Fehler beim Speichern der Datei: %1$s</string>
|
||||
<string name="message_error_file_save_cancelled">Speichern der Datei abgebrochen oder fehlgeschlagen.</string>
|
||||
<string name="message_error_file_picker_not_initialized">Save File Launcher nicht initialisiert.</string>
|
||||
<string name="message_success_category_saved">Kategorie in %1$s gespeichert.</string>
|
||||
<string name="message_error_api_key_missing">Dein API-Schlüssel fehlt oder ist ungültig.</string>
|
||||
<string name="message_error_api_key_invalid">Dein API-Schlüssel fehlt oder ist ungültig.</string>
|
||||
<string name="message_loading_translating">Übersetze %1$d Wörter…</string>
|
||||
<string name="message_success_translation_completed">Übersetzung abgeschlossen.</string>
|
||||
<string name="message_error_translation_failed">Übersetzung fehlgeschlagen: %1$s</string>
|
||||
<string name="message_success_repository_wiped">Alle Repository-Daten gelöscht.</string>
|
||||
<string name="message_error_repository_wipe_failed">Fehler beim Löschen des Repositorys: %1$s</string>
|
||||
<string name="message_loading_card_set">Lade Kartensatz</string>
|
||||
<string name="message_success_stage_updated">Stufe erfolgreich aktualisiert.</string>
|
||||
<string name="message_error_stage_update_failed">Fehler beim Aktualisieren des Stages: %1$s</string>
|
||||
<string name="message_success_category_updated">Kategorie erfolgreich aktualisiert.</string>
|
||||
<string name="message_error_category_update_failed">Fehler beim Aktualisieren der Kategorie: %1$s</string>
|
||||
<string name="message_success_articles_removed">Artikel erfolgreich entfernt.</string>
|
||||
<string name="message_error_articles_remove_failed">Fehler beim Entfernen der Artikel: %1$s</string>
|
||||
<string name="message_success_synonyms_generated">Synonyme erfolgreich generiert.</string>
|
||||
<string name="message_error_synonyms_generation_failed">Fehler beim Generieren von Synonymen: %1$s</string>
|
||||
<string name="message_error_operation_failed">Operation fehlgeschlagen: %1$s</string>
|
||||
<string name="message_loading_operation_in_progress">Operation läuft…</string>
|
||||
<string name="message_test_info">Das ist eine allgemeine Infomeldung.</string>
|
||||
<string name="message_test_success">Das ist eine Erfolgsmeldung für den Test!</string>
|
||||
<string name="message_test_error">Hoppla, da ist etwas schiefgelaufen :(</string>
|
||||
<string name="label_stats">Statistiken</string>
|
||||
<string name="label_library">Sammlung</string>
|
||||
<string name="label_edit">Bearbeiten</string>
|
||||
<string name="label_new_words">Neue Wörter</string>
|
||||
<string name="desc_expand_your_vocabulary">Erweitere deinen Wortschatz</string>
|
||||
<string name="label_settings">Einstellungen</string>
|
||||
<string name="label_2d_days">%1$d Tage</string>
|
||||
<string name="label_current_streak">Aktuelle Streak</string>
|
||||
<string name="label_daily_goal">Tägliches Ziel</string>
|
||||
<string name="text_desc_no_activity_data_available">Keine Aktivitätsdaten verfügbar</string>
|
||||
<string name="label_see_history">Verlauf anzeigen</string>
|
||||
<string name="label_weekly_progress">Wöchentlicher Fortschritt</string>
|
||||
<string name="cd_go">Los</string>
|
||||
<string name="label_sort_by">Sortieren nach</string>
|
||||
<string name="label_reset">Zurücksetzen</string>
|
||||
<string name="label_filter_cards">Filter Cards</string>
|
||||
<string name="text_desc_organize_vocabulary_groups">Organisiere deinen Wortschatz in Gruppen</string>
|
||||
<string name="text_add_new_word_to_list">Extrahiere ein neues Wort in deine Liste</string>
|
||||
<string name="cd_scroll_to_top">Nach oben scrollen</string>
|
||||
<string name="cd_settings">Einstellungen</string>
|
||||
<string name="label_import_csv">CSV importieren</string>
|
||||
<string name="label_ai_generator">KI-Generator</string>
|
||||
<string name="label_new_wordss">Neue Wörter</string>
|
||||
<string name="label_recently_added">Kürzlich hinzugefügt</string>
|
||||
<string name="label_view_all">Alle anzeigen</string>
|
||||
<string name="text_explore_more_categories">Entdecke weitere Kategorien</string>
|
||||
<string name="cd_options">Optionen</string>
|
||||
<string name="cd_selected">Ausgewählt</string>
|
||||
<string name="label_all_cards">Alle Karten</string>
|
||||
<string name="cd_filter_options">Filteroptionen</string>
|
||||
<string name="cd_add">Hinzufügen</string>
|
||||
<string name="cd_searchh">Suche</string>
|
||||
<string name="label_learnedd">gelernt</string>
|
||||
</resources>
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user