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>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
<DropdownSelection timestamp="2026-02-15T19:51:37.987601800Z">
|
<DropdownSelection timestamp="2026-02-16T10:13:39.492968600Z">
|
||||||
<Target type="DEFAULT_BOOT">
|
<Target type="DEFAULT_BOOT">
|
||||||
<handle>
|
<handle>
|
||||||
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\jonas\.android\avd\Medium_Phone_28.avd" />
|
<DeviceId pluginId="PhysicalDevice" identifier="serial=RFCW11ML1WV" />
|
||||||
</handle>
|
</handle>
|
||||||
</Target>
|
</Target>
|
||||||
</DropdownSelection>
|
</DropdownSelection>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import java.util.Locale
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
|
||||||
alias(libs.plugins.kotlin.compose)
|
alias(libs.plugins.kotlin.compose)
|
||||||
alias(libs.plugins.hilt.android)
|
alias(libs.plugins.hilt.android)
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
@@ -62,11 +61,8 @@ android {
|
|||||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi"
|
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
kotlin {
|
|
||||||
compilerOptions {
|
|
||||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
viewBinding = false
|
viewBinding = false
|
||||||
@@ -130,7 +126,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.room.runtime) // ADDED: Explicitly add runtime
|
implementation(libs.androidx.room.runtime) // ADDED: Explicitly add runtime
|
||||||
implementation(libs.androidx.room.ktx)
|
implementation(libs.androidx.room.ktx)
|
||||||
implementation(libs.core.ktx)
|
implementation(libs.core.ktx)
|
||||||
ksp(libs.room.compiler) // CHANGED: Use ksp instead of implementation
|
ksp(libs.room.compiler)
|
||||||
|
|
||||||
// Networking
|
// Networking
|
||||||
implementation(libs.retrofit)
|
implementation(libs.retrofit)
|
||||||
|
|||||||
@@ -4,11 +4,8 @@ package eu.gaudian.translator
|
|||||||
|
|
||||||
object TestConfig {
|
object TestConfig {
|
||||||
// REPLACE with your actual API Key for the test
|
// REPLACE with your actual API Key for the test
|
||||||
const val API_KEY = "YOUR_REAL_API_KEY_HERE"
|
const val API_KEY = "YOUR_REAL_API_KEY_HERE"
|
||||||
|
|
||||||
// Set to true if you want to see full log output in Logcat
|
|
||||||
const val ENABLE_LOGGING = true
|
|
||||||
|
|
||||||
// Optional: If your ApiManager requires a specific provider (e.g., "Mistral", "OpenAI")
|
// Optional: If your ApiManager requires a specific provider (e.g., "Mistral", "OpenAI")
|
||||||
const val PROVIDER_NAME = "Mistral"
|
const val PROVIDER_NAME = "Mistral"
|
||||||
|
|
||||||
|
|||||||
@@ -32,17 +32,6 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name=".CorrectActivity"
|
|
||||||
android:exported="true">
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.SEND" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<data android:mimeType="text/plain" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
package eu.gaudian.translator.di
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import eu.gaudian.translator.R
|
|||||||
sealed class WidgetType(val id: String, @param:StringRes val titleRes: Int) {
|
sealed class WidgetType(val id: String, @param:StringRes val titleRes: Int) {
|
||||||
data object Status : WidgetType("status", R.string.label_status)
|
data object Status : WidgetType("status", R.string.label_status)
|
||||||
data object Streak : WidgetType("streak", R.string.title_widget_streak)
|
data object Streak : WidgetType("streak", R.string.title_widget_streak)
|
||||||
data object StartButtons : WidgetType("start_buttons", R.string.label_start_exercise)
|
|
||||||
data object AllVocabulary : WidgetType("all_vocabulary", R.string.label_all_vocabulary)
|
data object AllVocabulary : WidgetType("all_vocabulary", R.string.label_all_vocabulary)
|
||||||
data object DueToday : WidgetType("due_today", R.string.title_widget_due_today)
|
data object DueToday : WidgetType("due_today", R.string.title_widget_due_today)
|
||||||
data object CategoryProgress : WidgetType("category_progress", R.string.label_categories)
|
data object CategoryProgress : WidgetType("category_progress", R.string.label_categories)
|
||||||
@@ -23,7 +22,6 @@ sealed class WidgetType(val id: String, @param:StringRes val titleRes: Int) {
|
|||||||
val DEFAULT_ORDER = listOf(
|
val DEFAULT_ORDER = listOf(
|
||||||
Status,
|
Status,
|
||||||
Streak,
|
Streak,
|
||||||
StartButtons,
|
|
||||||
AllVocabulary,
|
AllVocabulary,
|
||||||
DueToday,
|
DueToday,
|
||||||
CategoryProgress ,
|
CategoryProgress ,
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ object LocalDictionaryMorphologyMapper {
|
|||||||
/**
|
/**
|
||||||
* Overload that uses parsed [DictionaryEntryData] instead of raw JSON.
|
* Overload that uses parsed [DictionaryEntryData] instead of raw JSON.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
fun parseMorphology(
|
fun parseMorphology(
|
||||||
langCode: String,
|
langCode: String,
|
||||||
pos: String?,
|
pos: String?,
|
||||||
|
|||||||
@@ -144,19 +144,6 @@ class ApiRepository(private val context: Context) {
|
|||||||
|
|
||||||
var configurationValid = true
|
var configurationValid = true
|
||||||
|
|
||||||
// (Helper function to reduce repetition)
|
|
||||||
fun checkAndFallback(current: LanguageModel?, setter: suspend (LanguageModel) -> Unit) {
|
|
||||||
val isValid = current != null && availableModels.any { it.modelId == current.modelId && it.providerKey == current.providerKey }
|
|
||||||
if (!isValid) {
|
|
||||||
val fallback = findFallbackModel(availableModels)
|
|
||||||
if (fallback != null) {
|
|
||||||
// We must use a blocking call or scope here because we can't easily pass a suspend function to a lambda
|
|
||||||
// But since we are inside a suspend function, we can just call the setter directly if we unroll the loop.
|
|
||||||
// For simplicity, I'll keep the unrolled logic below.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback checks
|
// Fallback checks
|
||||||
if (currentTrans == null || !availableModels.any { it.modelId == currentTrans.modelId && it.providerKey == currentTrans.providerKey }) {
|
if (currentTrans == null || !availableModels.any { it.modelId == currentTrans.modelId && it.providerKey == currentTrans.providerKey }) {
|
||||||
findFallbackModel(availableModels)?.let { setTranslationModel(it) } ?: run { configurationValid = false }
|
findFallbackModel(availableModels)?.let { setTranslationModel(it) } ?: run { configurationValid = false }
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ class LanguageRepository(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
suspend fun wipeHistoryAndFavorites() {
|
suspend fun wipeHistoryAndFavorites() {
|
||||||
clearLanguages(LanguageListType.HISTORY)
|
clearLanguages(LanguageListType.HISTORY)
|
||||||
clearLanguages(LanguageListType.FAVORITE)
|
clearLanguages(LanguageListType.FAVORITE)
|
||||||
|
|||||||
@@ -129,25 +129,6 @@ class JsonHelper {
|
|||||||
*/
|
*/
|
||||||
class JsonParsingException(message: String, cause: Throwable? = null) : Exception(message, cause)
|
class JsonParsingException(message: String, cause: Throwable? = null) : Exception(message, cause)
|
||||||
|
|
||||||
/**
|
|
||||||
* Legacy JsonHelper class for backward compatibility.
|
|
||||||
* @deprecated Use the enhanced JsonHelper class instead
|
|
||||||
*/
|
|
||||||
@Deprecated("Use the enhanced JsonHelper class instead")
|
|
||||||
class LegacyJsonHelper {
|
|
||||||
|
|
||||||
fun cleanJson(json: String): String {
|
|
||||||
val startIndex = json.indexOf('{')
|
|
||||||
val endIndex = json.lastIndexOf('}')
|
|
||||||
|
|
||||||
if (startIndex == -1 || endIndex == -1 || startIndex >= endIndex) {
|
|
||||||
throw IllegalArgumentException("Invalid JSON format")
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.substring(startIndex, endIndex + 1).trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object JsonCleanUtil {
|
object JsonCleanUtil {
|
||||||
private val jsonParser = Json { isLenient = true; ignoreUnknownKeys = true }
|
private val jsonParser = Json { isLenient = true; ignoreUnknownKeys = true }
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import timber.log.Timber
|
|||||||
* "HardcodedText" lint warning for log messages, which are for
|
* "HardcodedText" lint warning for log messages, which are for
|
||||||
* development purposes only.
|
* development purposes only.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
object Log {
|
object Log {
|
||||||
|
|
||||||
@SuppressLint("HardcodedText")
|
@SuppressLint("HardcodedText")
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ enum class StatusMessageId(
|
|||||||
ERROR_FILE_PICKER_NOT_INITIALIZED(R.string.message_error_file_picker_not_initialized, MessageDisplayType.ERROR, 5),
|
ERROR_FILE_PICKER_NOT_INITIALIZED(R.string.message_error_file_picker_not_initialized, MessageDisplayType.ERROR, 5),
|
||||||
SUCCESS_CATEGORY_SAVED(R.string.message_success_category_saved, MessageDisplayType.SUCCESS, 3),
|
SUCCESS_CATEGORY_SAVED(R.string.message_success_category_saved, MessageDisplayType.SUCCESS, 3),
|
||||||
ERROR_EXCEL_NOT_SUPPORTED(R.string.message_error_excel_not_supported, MessageDisplayType.ERROR, 5),
|
ERROR_EXCEL_NOT_SUPPORTED(R.string.message_error_excel_not_supported, MessageDisplayType.ERROR, 5),
|
||||||
|
ERROR_PARSING_TABLE(R.string.error_parsing_table, MessageDisplayType.ERROR, 5),
|
||||||
|
ERROR_PARSING_TABLE_WITH_REASON(R.string.error_parsing_table_with_reason, MessageDisplayType.ERROR, 5),
|
||||||
|
ERROR_SELECT_TWO_COLUMNS(R.string.error_select_two_columns, MessageDisplayType.ERROR, 5),
|
||||||
|
ERROR_SELECT_LANGUAGES(R.string.error_select_languages, MessageDisplayType.ERROR, 5),
|
||||||
|
ERROR_NO_ROWS_TO_IMPORT(R.string.error_no_rows_to_import, MessageDisplayType.ERROR, 5),
|
||||||
|
SUCCESS_ITEMS_IMPORTED(R.string.info_imported_items_from, MessageDisplayType.SUCCESS, 3),
|
||||||
|
|
||||||
|
|
||||||
// API Key related
|
// API Key related
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ object StatusMessageService {
|
|||||||
* @deprecated Use showMessageById() instead for internationalization support.
|
* @deprecated Use showMessageById() instead for internationalization support.
|
||||||
*/
|
*/
|
||||||
@Deprecated("Use showMessageById() for internationalization support", ReplaceWith("showMessageById(messageId)"))
|
@Deprecated("Use showMessageById() for internationalization support", ReplaceWith("showMessageById(messageId)"))
|
||||||
@Suppress("unused")
|
|
||||||
fun showSimpleMessage(text: String, type: MessageDisplayType = MessageDisplayType.INFO) {
|
fun showSimpleMessage(text: String, type: MessageDisplayType = MessageDisplayType.INFO) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
_actions.emit(StatusAction.ShowMessage(text, type, 5))
|
_actions.emit(StatusAction.ShowMessage(text, type, 5))
|
||||||
|
|||||||
@@ -117,7 +117,6 @@ class TranslationService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun translateSentence(sentence: String): Result<TranslationHistoryItem> = withContext(Dispatchers.IO) {
|
suspend fun translateSentence(sentence: String): Result<TranslationHistoryItem> = withContext(Dispatchers.IO) {
|
||||||
val statusMessageService = StatusMessageService
|
|
||||||
val additionalInstructions = settingsRepository.customPromptTranslation.flow.first()
|
val additionalInstructions = settingsRepository.customPromptTranslation.flow.first()
|
||||||
val selectedSource = languageRepository.loadSelectedSourceLanguage().first()
|
val selectedSource = languageRepository.loadSelectedSourceLanguage().first()
|
||||||
val sourceLangName = selectedSource?.englishName ?: "Auto"
|
val sourceLangName = selectedSource?.englishName ?: "Auto"
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
package eu.gaudian.translator.utils.dictionary
|
|
||||||
|
|
||||||
|
|
||||||
import eu.gaudian.translator.model.grammar.Inflection
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface for a language-specific inflection parser.
|
|
||||||
*/
|
|
||||||
interface InflectionParser {
|
|
||||||
fun parse(inflections: List<Inflection>): DisplayInflectionData
|
|
||||||
}
|
|
||||||
@@ -5,11 +5,6 @@ package eu.gaudian.translator.utils.dictionary
|
|||||||
* Either a simple list or a complex, grouped verb conjugation table.
|
* Either a simple list or a complex, grouped verb conjugation table.
|
||||||
*/
|
*/
|
||||||
sealed class DisplayInflectionData {
|
sealed class DisplayInflectionData {
|
||||||
data class VerbConjugation(
|
|
||||||
val gerund: String? = null,
|
|
||||||
val participle: String? = null,
|
|
||||||
val moods: List<DisplayMood>
|
|
||||||
) : DisplayInflectionData()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DisplayMood(
|
data class DisplayMood(
|
||||||
|
|||||||
@@ -253,7 +253,19 @@ fun TranslatorApp(
|
|||||||
val currentDestination = navBackStackEntry?.destination
|
val currentDestination = navBackStackEntry?.destination
|
||||||
val selectedScreen = Screen.fromDestination(currentDestination)
|
val selectedScreen = Screen.fromDestination(currentDestination)
|
||||||
|
|
||||||
@Suppress("HardCodedStringLiteral") val isBottomBarHidden = currentDestination?.route?.startsWith("vocabulary_exercise") == true
|
@Suppress("HardCodedStringLiteral") val isBottomBarHidden = currentDestination?.hierarchy?.any { destination ->
|
||||||
|
destination.route in setOf(
|
||||||
|
Screen.Translation.route,
|
||||||
|
Screen.Dictionary.route,
|
||||||
|
Screen.Exercises.route,
|
||||||
|
Screen.Settings.route
|
||||||
|
)
|
||||||
|
} == true || currentDestination?.route in setOf(
|
||||||
|
"start_exercise",
|
||||||
|
"new_word",
|
||||||
|
"new_word_review",
|
||||||
|
"vocabulary_detail/{itemId}"
|
||||||
|
)
|
||||||
val showBottomNavLabels by settingsViewModel.showBottomNavLabels.collectAsStateWithLifecycle(initialValue = false)
|
val showBottomNavLabels by settingsViewModel.showBottomNavLabels.collectAsStateWithLifecycle(initialValue = false)
|
||||||
|
|
||||||
BottomNavigationBar(
|
BottomNavigationBar(
|
||||||
@@ -262,6 +274,12 @@ fun TranslatorApp(
|
|||||||
showLabels = showBottomNavLabels,
|
showLabels = showBottomNavLabels,
|
||||||
onItemSelected = { screen ->
|
onItemSelected = { screen ->
|
||||||
val inSameSection = currentDestination?.hierarchy?.any { it.route == screen.route } == true
|
val inSameSection = currentDestination?.hierarchy?.any { it.route == screen.route } == true
|
||||||
|
val isMoreSection = screen in setOf(
|
||||||
|
Screen.Translation,
|
||||||
|
Screen.Dictionary,
|
||||||
|
Screen.Settings,
|
||||||
|
Screen.Exercises
|
||||||
|
)
|
||||||
|
|
||||||
// Always reset the selected section to its root and clear back stack between sections
|
// Always reset the selected section to its root and clear back stack between sections
|
||||||
if (inSameSection) {
|
if (inSameSection) {
|
||||||
@@ -274,6 +292,11 @@ fun TranslatorApp(
|
|||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
restoreState = false
|
restoreState = false
|
||||||
}
|
}
|
||||||
|
} else if (isMoreSection) {
|
||||||
|
navController.navigate(screen.route) {
|
||||||
|
launchSingleTop = true
|
||||||
|
restoreState = false
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Switching sections: clear entire back stack to start to avoid back navigation results
|
// Switching sections: clear entire back stack to start to avoid back navigation results
|
||||||
navController.navigate(screen.route) {
|
navController.navigate(screen.route) {
|
||||||
@@ -285,6 +308,10 @@ fun TranslatorApp(
|
|||||||
restoreState = false
|
restoreState = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onPlayClicked = {
|
||||||
|
@Suppress("HardCodedStringLiteral")
|
||||||
|
navController.navigate("start_exercise")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,27 +26,48 @@ import eu.gaudian.translator.view.dictionary.EtymologyResultScreen
|
|||||||
import eu.gaudian.translator.view.dictionary.MainDictionaryScreen
|
import eu.gaudian.translator.view.dictionary.MainDictionaryScreen
|
||||||
import eu.gaudian.translator.view.exercises.ExerciseSessionScreen
|
import eu.gaudian.translator.view.exercises.ExerciseSessionScreen
|
||||||
import eu.gaudian.translator.view.exercises.MainExerciseScreen
|
import eu.gaudian.translator.view.exercises.MainExerciseScreen
|
||||||
|
import eu.gaudian.translator.view.exercises.StartExerciseScreen
|
||||||
import eu.gaudian.translator.view.exercises.YouTubeBrowserScreen
|
import eu.gaudian.translator.view.exercises.YouTubeBrowserScreen
|
||||||
import eu.gaudian.translator.view.exercises.YouTubeExerciseScreen
|
import eu.gaudian.translator.view.exercises.YouTubeExerciseScreen
|
||||||
|
import eu.gaudian.translator.view.home.HomeScreen
|
||||||
|
import eu.gaudian.translator.view.library.LibraryScreen
|
||||||
import eu.gaudian.translator.view.settings.DictionaryOptionsScreen
|
import eu.gaudian.translator.view.settings.DictionaryOptionsScreen
|
||||||
import eu.gaudian.translator.view.settings.SettingsRoutes
|
import eu.gaudian.translator.view.settings.SettingsRoutes
|
||||||
import eu.gaudian.translator.view.settings.TranslationSettingsScreen
|
import eu.gaudian.translator.view.settings.TranslationSettingsScreen
|
||||||
import eu.gaudian.translator.view.settings.settingsGraph
|
import eu.gaudian.translator.view.settings.settingsGraph
|
||||||
|
import eu.gaudian.translator.view.stats.StatsScreen
|
||||||
import eu.gaudian.translator.view.translation.TranslationScreen
|
import eu.gaudian.translator.view.translation.TranslationScreen
|
||||||
|
import eu.gaudian.translator.view.vocabulary.AllCardsListScreen
|
||||||
import eu.gaudian.translator.view.vocabulary.CategoryDetailScreen
|
import eu.gaudian.translator.view.vocabulary.CategoryDetailScreen
|
||||||
import eu.gaudian.translator.view.vocabulary.CategoryListScreen
|
import eu.gaudian.translator.view.vocabulary.CategoryListScreen
|
||||||
import eu.gaudian.translator.view.vocabulary.LanguageProgressScreen
|
import eu.gaudian.translator.view.vocabulary.LanguageProgressScreen
|
||||||
import eu.gaudian.translator.view.vocabulary.MainVocabularyScreen
|
import eu.gaudian.translator.view.vocabulary.NewWordReviewScreen
|
||||||
|
import eu.gaudian.translator.view.vocabulary.NewWordScreen
|
||||||
import eu.gaudian.translator.view.vocabulary.NoGrammarItemsScreen
|
import eu.gaudian.translator.view.vocabulary.NoGrammarItemsScreen
|
||||||
import eu.gaudian.translator.view.vocabulary.StageDetailScreen
|
import eu.gaudian.translator.view.vocabulary.StageDetailScreen
|
||||||
import eu.gaudian.translator.view.vocabulary.VocabularyCardHost
|
import eu.gaudian.translator.view.vocabulary.VocabularyCardHost
|
||||||
import eu.gaudian.translator.view.vocabulary.VocabularyExerciseHostScreen
|
import eu.gaudian.translator.view.vocabulary.VocabularyExerciseHostScreen
|
||||||
import eu.gaudian.translator.view.vocabulary.VocabularyHeatmapScreen
|
import eu.gaudian.translator.view.vocabulary.VocabularyHeatmapScreen
|
||||||
import eu.gaudian.translator.view.vocabulary.VocabularyListScreen
|
|
||||||
import eu.gaudian.translator.view.vocabulary.VocabularySortingScreen
|
import eu.gaudian.translator.view.vocabulary.VocabularySortingScreen
|
||||||
|
|
||||||
private const val TRANSITION_DURATION = 300
|
private const val TRANSITION_DURATION = 300
|
||||||
|
|
||||||
|
object NavigationRoutes {
|
||||||
|
const val NEW_WORD = "new_word"
|
||||||
|
const val NEW_WORD_REVIEW = "new_word_review"
|
||||||
|
const val VOCABULARY_DETAIL = "vocabulary_detail"
|
||||||
|
const val START_EXERCISE = "start_exercise"
|
||||||
|
const val CATEGORY_DETAIL = "category_detail"
|
||||||
|
const val CATEGORY_LIST = "category_list_screen"
|
||||||
|
const val STATS_VOCABULARY_HEATMAP = "stats/vocabulary_heatmap"
|
||||||
|
const val STATS_LANGUAGE_PROGRESS = "stats/language_progress"
|
||||||
|
const val STATS_CATEGORY_DETAIL = "stats/category_detail"
|
||||||
|
const val STATS_CATEGORY_LIST = "stats/category_list_screen"
|
||||||
|
const val STATS_VOCABULARY_SORTING = "stats/vocabulary_sorting"
|
||||||
|
const val STATS_NO_GRAMMAR_ITEMS = "stats/no_grammar_items"
|
||||||
|
const val STATS_VOCABULARY_LIST = "stats/vocabulary_list"
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppNavHost(
|
fun AppNavHost(
|
||||||
navController: NavHostController,
|
navController: NavHostController,
|
||||||
@@ -57,11 +78,12 @@ fun AppNavHost(
|
|||||||
|
|
||||||
// 1. Define your Main Tab "Leaf" Routes (the actual start destinations of your graphs)
|
// 1. Define your Main Tab "Leaf" Routes (the actual start destinations of your graphs)
|
||||||
val mainTabRoutes = setOf(
|
val mainTabRoutes = setOf(
|
||||||
Screen.Home.route, // "home" or "main_translation" depending on which one you actually navigate to
|
Screen.Home.route,
|
||||||
"main_translation",
|
Screen.Library.route,
|
||||||
"main_dictionary",
|
Screen.Stats.route,
|
||||||
"main_vocabulary",
|
Screen.Translation.route,
|
||||||
"main_exercise",
|
Screen.Dictionary.route,
|
||||||
|
Screen.Exercises.route,
|
||||||
SettingsRoutes.LIST
|
SettingsRoutes.LIST
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -121,77 +143,50 @@ fun AppNavHost(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
composable(Screen.Home.route) {
|
composable(Screen.Home.route) {
|
||||||
TranslationScreen(navController = navController)
|
HomeScreen(navController = navController)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(NavigationRoutes.NEW_WORD) {
|
||||||
|
NewWordScreen(navController = navController)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(NavigationRoutes.NEW_WORD_REVIEW) {
|
||||||
|
NewWordReviewScreen(navController = navController)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(NavigationRoutes.START_EXERCISE) {
|
||||||
|
StartExerciseScreen(navController = navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define all other navigation graphs at the same top level.
|
// Define all other navigation graphs at the same top level.
|
||||||
|
homeGraph(navController)
|
||||||
|
libraryGraph(navController)
|
||||||
|
statsGraph(navController)
|
||||||
translationGraph(navController)
|
translationGraph(navController)
|
||||||
dictionaryGraph(navController)
|
dictionaryGraph(navController)
|
||||||
vocabularyGraph(navController)
|
|
||||||
exerciseGraph(navController)
|
exerciseGraph(navController)
|
||||||
settingsGraph(navController)
|
settingsGraph(navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
|
|
||||||
fun NavGraphBuilder.translationGraph(navController: NavHostController) {
|
fun NavGraphBuilder.homeGraph(navController: NavHostController) {
|
||||||
navigation(
|
navigation(
|
||||||
startDestination = "main_translation",
|
startDestination = "main_home",
|
||||||
route = Screen.Home.route
|
route = Screen.Home.route
|
||||||
) {
|
) {
|
||||||
composable("main_translation") {
|
composable("main_home") {
|
||||||
TranslationScreen(navController = navController)
|
HomeScreen(navController = navController)
|
||||||
}
|
|
||||||
composable("custom_translation_prompt") {
|
|
||||||
TranslationSettingsScreen(navController = navController)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
|
fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
|
||||||
fun NavGraphBuilder.dictionaryGraph(navController: NavHostController) {
|
|
||||||
navigation(
|
navigation(
|
||||||
startDestination = "main_dictionary",
|
startDestination = "main_library",
|
||||||
route = Screen.Dictionary.route
|
route = Screen.Library.route
|
||||||
) {
|
) {
|
||||||
composable("main_dictionary") {
|
composable("main_library") {
|
||||||
MainDictionaryScreen(navController = navController)
|
LibraryScreen(navController = navController)
|
||||||
}
|
|
||||||
composable("dictionary_result/{entryId}") { backStackEntry ->
|
|
||||||
val entryId = backStackEntry.arguments?.getString("entryId")?.toIntOrNull()
|
|
||||||
if (entryId != null) {
|
|
||||||
DictionaryResultScreen(
|
|
||||||
entryId = entryId,
|
|
||||||
navController = navController,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Text("Error: Invalid Entry ID")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
composable("dictionary_options") {
|
|
||||||
DictionaryOptionsScreen(navController = navController)
|
|
||||||
}
|
|
||||||
composable("etymology_result/{word}/{languageCode}") { backStackEntry ->
|
|
||||||
val word = backStackEntry.arguments?.getString("word") ?: ""
|
|
||||||
val languageCode = backStackEntry.arguments?.getString("languageCode")?.toIntOrNull() ?: 1
|
|
||||||
EtymologyResultScreen(
|
|
||||||
navController = navController,
|
|
||||||
word = word,
|
|
||||||
languageCode = languageCode
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun NavGraphBuilder.vocabularyGraph(
|
|
||||||
navController: NavHostController,
|
|
||||||
) {
|
|
||||||
navigation(
|
|
||||||
startDestination = "main_vocabulary",
|
|
||||||
route = Screen.Vocabulary.route
|
|
||||||
) {
|
|
||||||
composable("main_vocabulary") {
|
|
||||||
MainVocabularyScreen(navController = navController)
|
|
||||||
}
|
}
|
||||||
composable("vocabulary_sorting") {
|
composable("vocabulary_sorting") {
|
||||||
VocabularySortingScreen(
|
VocabularySortingScreen(
|
||||||
@@ -224,7 +219,7 @@ fun NavGraphBuilder.vocabularyGraph(
|
|||||||
composable("vocabulary_list/{showDueTodayOnly}/{categoryId}") { backStackEntry ->
|
composable("vocabulary_list/{showDueTodayOnly}/{categoryId}") { backStackEntry ->
|
||||||
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
|
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
|
||||||
val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull()
|
val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull()
|
||||||
VocabularyListScreen(
|
AllCardsListScreen(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
showDueTodayOnly = showDueTodayOnly,
|
showDueTodayOnly = showDueTodayOnly,
|
||||||
categoryId = categoryId,
|
categoryId = categoryId,
|
||||||
@@ -241,7 +236,7 @@ fun NavGraphBuilder.vocabularyGraph(
|
|||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
composable("vocabulary_heatmap") {
|
composable(NavigationRoutes.STATS_VOCABULARY_HEATMAP) {
|
||||||
VocabularyHeatmapScreen(
|
VocabularyHeatmapScreen(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
)
|
)
|
||||||
@@ -253,7 +248,7 @@ fun NavGraphBuilder.vocabularyGraph(
|
|||||||
if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it)
|
if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
VocabularyListScreen(
|
AllCardsListScreen(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
showDueTodayOnly = showDueTodayOnly,
|
showDueTodayOnly = showDueTodayOnly,
|
||||||
stage = stage,
|
stage = stage,
|
||||||
@@ -376,6 +371,159 @@ fun NavGraphBuilder.vocabularyGraph(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun NavGraphBuilder.statsGraph(
|
||||||
|
navController: NavHostController,
|
||||||
|
) {
|
||||||
|
navigation(
|
||||||
|
startDestination = "main_stats",
|
||||||
|
route = Screen.Stats.route
|
||||||
|
) {
|
||||||
|
composable("main_stats") {
|
||||||
|
StatsScreen(navController = navController)
|
||||||
|
}
|
||||||
|
composable("stats/vocabulary_sorting") {
|
||||||
|
VocabularySortingScreen(
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable("stats/vocabulary_list/{showDueTodayOnly}/{categoryId}") { backStackEntry ->
|
||||||
|
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
|
||||||
|
val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull()
|
||||||
|
AllCardsListScreen(
|
||||||
|
navController = navController,
|
||||||
|
showDueTodayOnly = showDueTodayOnly,
|
||||||
|
categoryId = categoryId,
|
||||||
|
onNavigateToItem = { item ->
|
||||||
|
navController.navigate("vocabulary_detail/${item.id}")
|
||||||
|
},
|
||||||
|
onNavigateBack = { navController.popBackStack() },
|
||||||
|
enableNavigationButtons = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable(NavigationRoutes.STATS_LANGUAGE_PROGRESS) {
|
||||||
|
LanguageProgressScreen(
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable(NavigationRoutes.STATS_VOCABULARY_HEATMAP) {
|
||||||
|
VocabularyHeatmapScreen(
|
||||||
|
navController = navController,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable("stats/vocabulary_list/{showDueTodayOnly}/{stage}") { backStackEntry ->
|
||||||
|
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
|
||||||
|
val stageString = backStackEntry.arguments?.getString("stage")
|
||||||
|
val stage = stageString?.let {
|
||||||
|
if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
AllCardsListScreen(
|
||||||
|
navController = navController,
|
||||||
|
showDueTodayOnly = showDueTodayOnly,
|
||||||
|
stage = stage,
|
||||||
|
onNavigateToItem = { item ->
|
||||||
|
navController.navigate("vocabulary_detail/${item.id}")
|
||||||
|
},
|
||||||
|
onNavigateBack = { navController.popBackStack() },
|
||||||
|
categoryId = 0,
|
||||||
|
enableNavigationButtons = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable("stats/category_detail/{categoryId}") { backStackEntry ->
|
||||||
|
val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull()
|
||||||
|
|
||||||
|
if (categoryId != null) {
|
||||||
|
CategoryDetailScreen(
|
||||||
|
categoryId = categoryId,
|
||||||
|
onBackClick = { navController.popBackStack() },
|
||||||
|
onNavigateToItem = { item ->
|
||||||
|
navController.navigate("vocabulary_detail/${item.id}")
|
||||||
|
},
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
composable("stats/category_list_screen") {
|
||||||
|
CategoryListScreen(
|
||||||
|
onNavigateBack = { navController.popBackStack() },
|
||||||
|
onCategoryClicked = { categoryId ->
|
||||||
|
navController.navigate("stats/category_detail/$categoryId")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable(
|
||||||
|
route = "stats/vocabulary_sorting?mode={mode}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("mode") {
|
||||||
|
type = NavType.StringType
|
||||||
|
nullable = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
VocabularySortingScreen(
|
||||||
|
navController = navController,
|
||||||
|
initialFilterMode = backStackEntry.arguments?.getString("mode")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable("stats/no_grammar_items") {
|
||||||
|
NoGrammarItemsScreen(
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
|
||||||
|
fun NavGraphBuilder.translationGraph(navController: NavHostController) {
|
||||||
|
navigation(
|
||||||
|
startDestination = "main_translation",
|
||||||
|
route = Screen.Translation.route
|
||||||
|
) {
|
||||||
|
composable("main_translation") {
|
||||||
|
TranslationScreen(navController = navController)
|
||||||
|
}
|
||||||
|
composable("custom_translation_prompt") {
|
||||||
|
TranslationSettingsScreen(navController = navController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
|
||||||
|
fun NavGraphBuilder.dictionaryGraph(navController: NavHostController) {
|
||||||
|
navigation(
|
||||||
|
startDestination = "main_dictionary",
|
||||||
|
route = Screen.Dictionary.route
|
||||||
|
) {
|
||||||
|
composable("main_dictionary") {
|
||||||
|
MainDictionaryScreen(navController = navController)
|
||||||
|
}
|
||||||
|
composable("dictionary_result/{entryId}") { backStackEntry ->
|
||||||
|
val entryId = backStackEntry.arguments?.getString("entryId")?.toIntOrNull()
|
||||||
|
if (entryId != null) {
|
||||||
|
DictionaryResultScreen(
|
||||||
|
entryId = entryId,
|
||||||
|
navController = navController,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text("Error: Invalid Entry ID")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
composable("dictionary_options") {
|
||||||
|
DictionaryOptionsScreen(navController = navController)
|
||||||
|
}
|
||||||
|
composable("etymology_result/{word}/{languageCode}") { backStackEntry ->
|
||||||
|
val word = backStackEntry.arguments?.getString("word") ?: ""
|
||||||
|
val languageCode = backStackEntry.arguments?.getString("languageCode")?.toIntOrNull() ?: 1
|
||||||
|
EtymologyResultScreen(
|
||||||
|
navController = navController,
|
||||||
|
word = word,
|
||||||
|
languageCode = languageCode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
|
@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
|
||||||
fun NavGraphBuilder.exerciseGraph(
|
fun NavGraphBuilder.exerciseGraph(
|
||||||
navController: NavHostController,
|
navController: NavHostController,
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ fun AppAlertDialog(
|
|||||||
title: @Composable (() -> Unit)? = null,
|
title: @Composable (() -> Unit)? = null,
|
||||||
text: @Composable (() -> Unit)? = null,
|
text: @Composable (() -> Unit)? = null,
|
||||||
properties: DialogProperties = DialogProperties(),
|
properties: DialogProperties = DialogProperties(),
|
||||||
hintContent: @Composable (() -> Unit)? = null,
|
hintContent:Hint? = null,
|
||||||
) {
|
) {
|
||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
@@ -142,11 +142,13 @@ fun AppAlertDialog(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (showBottomSheet) {
|
if (showBottomSheet) {
|
||||||
HintBottomSheet(
|
hintContent?.let {
|
||||||
onDismissRequest = { showBottomSheet = false },
|
HintBottomSheet(
|
||||||
sheetState = sheetState,
|
onDismissRequest = { showBottomSheet = false },
|
||||||
content = hintContent
|
sheetState = sheetState,
|
||||||
)
|
content = it
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +214,7 @@ private fun DialogHeader(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun DialogTitleWithHint(
|
private fun DialogTitleWithHint(
|
||||||
title: @Composable () -> Unit,
|
title: @Composable () -> Unit,
|
||||||
hintContent: @Composable (() -> Unit)?,
|
hintContent: Hint? = null,
|
||||||
onHintClick: () -> Unit
|
onHintClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val showHints = LocalShowHints.current
|
val showHints = LocalShowHints.current
|
||||||
@@ -424,7 +426,6 @@ fun AppAlertDialogPreview() {
|
|||||||
},
|
},
|
||||||
title = { Text("Alert Dialog Title") },
|
title = { Text("Alert Dialog Title") },
|
||||||
text = { Text("This is the alert dialog text.") },
|
text = { Text("This is the alert dialog text.") },
|
||||||
hintContent = { Text("This is a hint for the alert dialog.") }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,7 +493,6 @@ fun AppAlertDialogLongTextPreview() {
|
|||||||
Text("Third paragraph with additional information that users need to be aware of.")
|
Text("Third paragraph with additional information that users need to be aware of.")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hintContent = { Text("This hint explains the terms in more detail.") }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
@@ -550,7 +551,55 @@ fun AppDropdownMenu(
|
|||||||
// =========================================
|
// =========================================
|
||||||
// LEGACY COMPONENTS (UPDATED TO USE UNIFIED STYLES)
|
// LEGACY COMPONENTS (UPDATED TO USE UNIFIED STYLES)
|
||||||
// =========================================
|
// =========================================
|
||||||
|
@Composable
|
||||||
|
fun BottomSheetMenuItem(
|
||||||
|
icon: ImageVector,
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onClick() }
|
||||||
|
.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// Circular Icon Background
|
||||||
|
Surface(
|
||||||
|
shape = CircleShape,
|
||||||
|
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
modifier = Modifier.size(52.dp)
|
||||||
|
) {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.size(28.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
|
// Title and Subtitle Column
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@Composable
|
@Composable
|
||||||
fun LargeDropdownMenuItem(
|
fun LargeDropdownMenuItem(
|
||||||
text: String,
|
text: String,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
|||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.Icons.Default
|
import androidx.compose.material.icons.Icons.Default
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBackIos
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||||
import androidx.compose.material.icons.automirrored.filled.DriveFileMove
|
import androidx.compose.material.icons.automirrored.filled.DriveFileMove
|
||||||
import androidx.compose.material.icons.automirrored.filled.ExitToApp
|
import androidx.compose.material.icons.automirrored.filled.ExitToApp
|
||||||
@@ -81,6 +81,7 @@ import androidx.compose.material.icons.filled.MenuBook
|
|||||||
import androidx.compose.material.icons.filled.Merge
|
import androidx.compose.material.icons.filled.Merge
|
||||||
import androidx.compose.material.icons.filled.ModelTraining
|
import androidx.compose.material.icons.filled.ModelTraining
|
||||||
import androidx.compose.material.icons.filled.MonitorHeart
|
import androidx.compose.material.icons.filled.MonitorHeart
|
||||||
|
import androidx.compose.material.icons.filled.MoreHoriz
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material.icons.filled.NoteAdd
|
import androidx.compose.material.icons.filled.NoteAdd
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
@@ -135,7 +136,7 @@ object AppIcons {
|
|||||||
val AI = Default.AutoAwesome
|
val AI = Default.AutoAwesome
|
||||||
val Appearance = Icons.Filled.ColorLens
|
val Appearance = Icons.Filled.ColorLens
|
||||||
val ApiKey = Default.Key
|
val ApiKey = Default.Key
|
||||||
val ArrowBack = Icons.AutoMirrored.Filled.ArrowBack
|
val ArrowBack = Icons.AutoMirrored.Filled.ArrowBackIos
|
||||||
val ArrowCircleUp = Icons.Filled.ArrowCircleUp
|
val ArrowCircleUp = Icons.Filled.ArrowCircleUp
|
||||||
val ArrowDropDown = Icons.Filled.KeyboardArrowDown
|
val ArrowDropDown = Icons.Filled.KeyboardArrowDown
|
||||||
val ArrowDropUp = Icons.Filled.KeyboardArrowUp
|
val ArrowDropUp = Icons.Filled.KeyboardArrowUp
|
||||||
@@ -202,6 +203,7 @@ object AppIcons {
|
|||||||
val Merge = Icons.Filled.Merge
|
val Merge = Icons.Filled.Merge
|
||||||
val ModelTraining = Icons.Filled.ModelTraining
|
val ModelTraining = Icons.Filled.ModelTraining
|
||||||
val More = Default.MoreVert
|
val More = Default.MoreVert
|
||||||
|
val MoreHorizontal = Icons.Filled.MoreHoriz
|
||||||
val MoreVert = Default.MoreVert
|
val MoreVert = Default.MoreVert
|
||||||
val MoveTo = Icons.AutoMirrored.Filled.DriveFileMove
|
val MoveTo = Icons.AutoMirrored.Filled.DriveFileMove
|
||||||
val Paste = Default.ContentPaste
|
val Paste = Default.ContentPaste
|
||||||
|
|||||||
@@ -2,26 +2,15 @@ package eu.gaudian.translator.view.composable
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.AccountCircle
|
|
||||||
import androidx.compose.material.icons.filled.Search
|
|
||||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
|
||||||
import androidx.compose.material3.FabPosition
|
import androidx.compose.material3.FabPosition
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.ScaffoldDefaults
|
import androidx.compose.material3.ScaffoldDefaults
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.material3.contentColorFor
|
import androidx.compose.material3.contentColorFor
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.gaudian.translator.R
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppScaffold(
|
fun AppScaffold(
|
||||||
@@ -58,37 +47,3 @@ fun AppScaffold(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ParrotTopBar() {
|
|
||||||
val navyBlue = Color(0xFF1A237E) // The color from your mockup
|
|
||||||
|
|
||||||
CenterAlignedTopAppBar(
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = "ParrotPal",
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
color = Color.White
|
|
||||||
)
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
// Your new parrot logo icon
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(id = R.drawable.ic_level_parrot),
|
|
||||||
contentDescription = "Logo",
|
|
||||||
modifier = Modifier.size(32.dp),
|
|
||||||
tint = Color.Unspecified // Keeps the logo's original colors
|
|
||||||
)
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
IconButton(onClick = { /* Search */ }) {
|
|
||||||
Icon(Icons.Default.Search, contentDescription = "Search", tint = Color.White)
|
|
||||||
}
|
|
||||||
IconButton(onClick = { /* Profile */ }) {
|
|
||||||
Icon(Icons.Default.AccountCircle, contentDescription = "Profile", tint = Color.White)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
|
||||||
containerColor = navyBlue
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
@file:Suppress("HardCodedStringLiteral")
|
|
||||||
|
|
||||||
package eu.gaudian.translator.view.composable
|
package eu.gaudian.translator.view.composable
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
@@ -20,9 +18,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -36,6 +38,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,35 +49,57 @@ interface TabItem {
|
|||||||
val title: String
|
val title: String
|
||||||
val icon: ImageVector
|
val icon: ImageVector
|
||||||
}
|
}
|
||||||
|
@SuppressLint("UnusedBoxWithConstraintsScope", "LocalContextResourcesRead", "DiscouragedApi",
|
||||||
/**
|
"SuspiciousIndentation"
|
||||||
* A generic, reusable tab layout composable.
|
)
|
||||||
* @param T The type of the tab item, which must implement the TabItem interface.
|
|
||||||
* @param tabs A list of all tab items to display.
|
|
||||||
* @param selectedTab The currently selected tab item.
|
|
||||||
* @param onTabSelected A lambda function to be invoked when a tab is clicked.
|
|
||||||
*/
|
|
||||||
@SuppressLint("UnusedBoxWithConstraintsScope", "LocalContextResourcesRead", "DiscouragedApi")
|
|
||||||
@Composable
|
@Composable
|
||||||
fun <T : TabItem> AppTabLayout(
|
fun <T : TabItem> AppTabLayout(
|
||||||
tabs: List<T>,
|
tabs: List<T>,
|
||||||
selectedTab: T,
|
selectedTab: T,
|
||||||
onTabSelected: (T) -> Unit,
|
onTabSelected: (T) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier,
|
||||||
|
onNavigateBack: (() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
val selectedIndex = tabs.indexOf(selectedTab)
|
val selectedIndex = tabs.indexOf(selectedTab)
|
||||||
|
|
||||||
BoxWithConstraints(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 8.dp, horizontal = 8.dp)
|
.padding(vertical = 8.dp, horizontal = 8.dp),
|
||||||
.height(56.dp)
|
verticalAlignment = Alignment.CenterVertically
|
||||||
.background(
|
|
||||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
|
||||||
shape = ComponentDefaults.CardShape
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
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(
|
val indicatorOffset by animateDpAsState(
|
||||||
targetValue = tabWidth * selectedIndex,
|
targetValue = tabWidth * selectedIndex,
|
||||||
@@ -82,58 +107,59 @@ fun <T : TabItem> AppTabLayout(
|
|||||||
label = "IndicatorOffset"
|
label = "IndicatorOffset"
|
||||||
)
|
)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.offset(x = indicatorOffset)
|
.offset(x = indicatorOffset)
|
||||||
.width(tabWidth)
|
.width(tabWidth)
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
.background(
|
.background(
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
shape = RoundedCornerShape(12.dp)
|
shape = RoundedCornerShape(12.dp)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
tabs.forEach { tab ->
|
tabs.forEach { tab ->
|
||||||
val isSelected = tab == selectedTab
|
val isSelected = tab == selectedTab
|
||||||
val contentColor by animateColorAsState(
|
val contentColor by animateColorAsState(
|
||||||
targetValue = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
|
targetValue = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
animationSpec = spring(stiffness = Spring.StiffnessLow)
|
animationSpec = spring(stiffness = Spring.StiffnessLow)
|
||||||
)
|
)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.clickable(
|
.clickable(
|
||||||
onClick = { onTabSelected(tab) },
|
onClick = { onTabSelected(tab) },
|
||||||
indication = null,
|
indication = null,
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.Center
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
val resolvedTitle = run {
|
Row(
|
||||||
val resId = context.resources.getIdentifier(tab.title, "string", context.packageName)
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
if (resId != 0) stringResource(resId) else tab.title
|
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
|
@ThemePreviews
|
||||||
@Composable
|
@Composable
|
||||||
fun ModernTabLayoutPreview() {
|
fun ModernTabLayoutPreview() {
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
|
@file:Suppress("HardCodedStringLiteral")
|
||||||
|
|
||||||
package eu.gaudian.translator.view.composable
|
package eu.gaudian.translator.view.composable
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBackIosNew
|
||||||
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LocalContentColor
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.material3.TopAppBarColors
|
import androidx.compose.material3.TopAppBarColors
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
@@ -25,8 +30,10 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||||
@@ -36,8 +43,8 @@ import eu.gaudian.translator.view.hints.LocalShowHints
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppTopAppBar(
|
fun AppTopAppBar(
|
||||||
title: @Composable () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
title: String,
|
||||||
onNavigateBack: (() -> Unit)? = null,
|
onNavigateBack: (() -> Unit)? = null,
|
||||||
navigationIcon: @Composable (() -> Unit)? = null,
|
navigationIcon: @Composable (() -> Unit)? = null,
|
||||||
actions: @Composable RowScope.() -> Unit = {},
|
actions: @Composable RowScope.() -> Unit = {},
|
||||||
@@ -47,89 +54,83 @@ fun AppTopAppBar(
|
|||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
TopAppBar(
|
// Changed to CenterAlignedTopAppBar to perfectly match the design requirements
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
modifier = modifier.height(56.dp),
|
modifier = modifier.height(56.dp),
|
||||||
windowInsets = WindowInsets(0.dp),
|
windowInsets = WindowInsets(0.dp),
|
||||||
colors = colors,
|
colors = colors,
|
||||||
title = {
|
title = {
|
||||||
Box(
|
val showHints = LocalShowHints.current
|
||||||
modifier = Modifier.fillMaxHeight(),
|
if (showHints && hintContent != null) {
|
||||||
contentAlignment = Alignment.Center
|
// Simplified row: keeps the title and hint icon neatly centered together
|
||||||
) {
|
Row(
|
||||||
val showHints = LocalShowHints.current
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
if (showHints && hintContent != null) {
|
horizontalArrangement = Arrangement.Center
|
||||||
Row(
|
) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
Text(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
text = title,
|
||||||
) {
|
style = MaterialTheme.typography.titleLarge,
|
||||||
Box(modifier = Modifier.weight(1f)) {
|
fontWeight = FontWeight.Bold,
|
||||||
title()
|
modifier = Modifier.weight(1f),
|
||||||
}
|
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||||
Box {
|
)
|
||||||
IconButton(onClick = { showBottomSheet = true }) {
|
IconButton(onClick = { showBottomSheet = true }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = AppIcons.Help,
|
imageVector = AppIcons.Help,
|
||||||
contentDescription = stringResource(R.string.show_hint),
|
contentDescription = stringResource(R.string.show_hint),
|
||||||
tint = MaterialTheme.colorScheme.secondary
|
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 = {
|
navigationIcon = {
|
||||||
if (onNavigateBack != null) {
|
if (onNavigateBack != null) {
|
||||||
Box(
|
IconButton(
|
||||||
modifier = Modifier.fillMaxHeight(),
|
onClick = onNavigateBack,
|
||||||
contentAlignment = Alignment.Center
|
modifier = Modifier
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = onNavigateBack) {
|
Icon(
|
||||||
Icon(
|
imageVector = Icons.Default.ArrowBackIosNew,
|
||||||
AppIcons.ArrowBack,
|
contentDescription = "Back",
|
||||||
contentDescription = stringResource(R.string.cd_navigate_back),
|
modifier = Modifier.size(18.dp),
|
||||||
tint = LocalContentColor.current
|
tint = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (navigationIcon != null) {
|
} else if (navigationIcon != null) {
|
||||||
Box(modifier = Modifier.fillMaxHeight(), contentAlignment = Alignment.Center) {
|
navigationIcon()
|
||||||
navigationIcon()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No navigation icon
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = actions
|
actions = actions
|
||||||
)
|
)
|
||||||
|
|
||||||
if (showBottomSheet) {
|
if (showBottomSheet) {
|
||||||
HintBottomSheet(
|
hintContent?.let {
|
||||||
onDismissRequest = {
|
HintBottomSheet(
|
||||||
@Suppress("AssignedValueIsNeverRead")
|
onDismissRequest = {
|
||||||
showBottomSheet = false
|
@Suppress("AssignedValueIsNeverRead")
|
||||||
},
|
showBottomSheet = false
|
||||||
sheetState = sheetState,
|
},
|
||||||
content = {
|
sheetState = sheetState,
|
||||||
hintContent?.Render()
|
content = it
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A composable that acts as a TopAppBar, containing a back navigation icon
|
* A composable that acts as a TopAppBar, containing a back navigation icon
|
||||||
* and an [AppTabLayout].
|
* and an [AppTabLayout].
|
||||||
*
|
|
||||||
* @param T The type of the tab item, must implement [TabItem].
|
|
||||||
* @param tabs The list of tab items to display.
|
|
||||||
* @param selectedTab The currently selected tab item.
|
|
||||||
* @param onTabSelected Callback function when a tab is selected.
|
|
||||||
* @param onNavigateBack Callback function when the back arrow is clicked.
|
|
||||||
* @param modifier The modifier to be applied to the layout.
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun <T : TabItem> TabbedTopAppBar(
|
fun <T : TabItem> TabbedTopAppBar(
|
||||||
@@ -139,7 +140,6 @@ fun <T : TabItem> TabbedTopAppBar(
|
|||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
// Use a Surface to provide background color and context for the app bar
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
color = MaterialTheme.colorScheme.surface
|
color = MaterialTheme.colorScheme.surface
|
||||||
@@ -148,20 +148,21 @@ fun <T : TabItem> TabbedTopAppBar(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// Back navigation icon, similar to its usage in AppTopAppBar
|
// Updated back icon here as well to keep your entire app consistent!
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onNavigateBack,
|
onClick = onNavigateBack,
|
||||||
modifier = Modifier.padding(horizontal = 4.dp)
|
modifier = Modifier
|
||||||
|
.padding(start = 8.dp, end = 4.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = AppIcons.ArrowBack,
|
imageVector = AppIcons.ArrowBack,
|
||||||
contentDescription = stringResource(R.string.cd_navigate_back),
|
contentDescription = stringResource(R.string.cd_navigate_back),
|
||||||
tint = MaterialTheme.colorScheme.onSurface
|
tint = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The AppTabLayout, taking up the remaining space.
|
|
||||||
// Its appearance matches the provided image.
|
|
||||||
AppTabLayout(
|
AppTabLayout(
|
||||||
tabs = tabs,
|
tabs = tabs,
|
||||||
selectedTab = selectedTab,
|
selectedTab = selectedTab,
|
||||||
@@ -172,11 +173,12 @@ fun <T : TabItem> TabbedTopAppBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ... [Previews remain exactly the same below]
|
||||||
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
@Suppress("HardCodedStringLiteral")
|
||||||
@ThemePreviews
|
@ThemePreviews
|
||||||
@Composable
|
@Composable
|
||||||
fun TabbedTopAppBarPreview() {
|
fun TabbedTopAppBarPreview() {
|
||||||
// Sample data for preview, similar to ModernTabLayoutPreview
|
|
||||||
data class SampleTabItem(override val title: String, override val icon: ImageVector) : TabItem
|
data class SampleTabItem(override val title: String, override val icon: ImageVector) : TabItem
|
||||||
|
|
||||||
val tabs = listOf(
|
val tabs = listOf(
|
||||||
@@ -202,7 +204,7 @@ fun TabbedTopAppBarPreview() {
|
|||||||
@Composable
|
@Composable
|
||||||
fun AppTopAppBarPreview() {
|
fun AppTopAppBarPreview() {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text("Preview Title") }
|
title = "Previwe Title"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +212,7 @@ fun AppTopAppBarPreview() {
|
|||||||
@Composable
|
@Composable
|
||||||
fun AppTopAppBarWithNavigationIconPreview() {
|
fun AppTopAppBarWithNavigationIconPreview() {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.title_title_preview_title)) },
|
title = "Preview Title",
|
||||||
onNavigateBack = {}
|
onNavigateBack = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -219,13 +221,13 @@ fun AppTopAppBarWithNavigationIconPreview() {
|
|||||||
@Composable
|
@Composable
|
||||||
fun AppTopAppBarWithActionsPreview() {
|
fun AppTopAppBarWithActionsPreview() {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.title_title_preview_title)) },
|
title = "Preview Title",
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = {}) {
|
IconButton(onClick = {}) {
|
||||||
Icon(AppIcons.Settings, contentDescription = stringResource(R.string.title_settings))
|
Icon(AppIcons.Settings, contentDescription = stringResource(R.string.title_settings))
|
||||||
}
|
}
|
||||||
IconButton(onClick = {}) {
|
IconButton(onClick = {}) {
|
||||||
AppIcons.ArrowBack
|
Icon(AppIcons.ArrowBack, contentDescription = null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@file:Suppress("HardCodedStringLiteral")
|
@file:Suppress("HardCodedStringLiteral", "AssignedValueIsNeverRead")
|
||||||
|
|
||||||
package eu.gaudian.translator.view.composable
|
package eu.gaudian.translator.view.composable
|
||||||
|
|
||||||
@@ -11,23 +11,44 @@ import androidx.compose.animation.core.spring
|
|||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.NavigationBar
|
import androidx.compose.material3.NavigationBar
|
||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
import androidx.compose.material3.NavigationBarItemDefaults
|
import androidx.compose.material3.NavigationBarItemDefaults
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
@@ -41,6 +62,7 @@ import androidx.navigation.NavDestination.Companion.hierarchy
|
|||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||||
import eu.gaudian.translator.view.LocalShowExperimentalFeatures
|
import eu.gaudian.translator.view.LocalShowExperimentalFeatures
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
sealed class Screen(
|
sealed class Screen(
|
||||||
val route: String,
|
val route: String,
|
||||||
@@ -48,34 +70,42 @@ sealed class Screen(
|
|||||||
val selectedIcon: ImageVector,
|
val selectedIcon: ImageVector,
|
||||||
val unselectedIcon: ImageVector
|
val unselectedIcon: ImageVector
|
||||||
) {
|
) {
|
||||||
object Home : Screen("home", R.string.label_translation, AppIcons.TranslateFilled, AppIcons.TranslateOutlined)
|
object Home : Screen("home", R.string.label_home, AppIcons.Home, AppIcons.Home)
|
||||||
|
object Stats : Screen("stats", R.string.label_stats, AppIcons.Statistics, AppIcons.Statistics)
|
||||||
|
object Translation : Screen("translation", R.string.label_translation, AppIcons.TranslateFilled, AppIcons.TranslateOutlined)
|
||||||
|
object Library : Screen("library", R.string.label_library, AppIcons.VocabularyFilled, AppIcons.VocabularyOutlined)
|
||||||
|
object Settings : Screen("settings", R.string.title_settings, AppIcons.SettingsFilled, AppIcons.SettingsOutlined)
|
||||||
|
object More : Screen("more", R.string.label_more, AppIcons.MoreVert, AppIcons.MoreHorizontal)
|
||||||
object Dictionary : Screen("dictionary", R.string.label_dictionary, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined)
|
object Dictionary : Screen("dictionary", R.string.label_dictionary, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined)
|
||||||
object Exercises : Screen("exercises", R.string.label_exercises, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined)
|
object Exercises : Screen("exercises", R.string.label_exercises, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined)
|
||||||
object Vocabulary : Screen("vocabulary", R.string.label_vocabulary, AppIcons.VocabularyFilled, AppIcons.VocabularyOutlined)
|
|
||||||
object Settings : Screen("settings", R.string.title_settings, AppIcons.SettingsFilled, AppIcons.SettingsOutlined)
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getAllScreens(showExperimental: Boolean = false): List<Screen> {
|
fun getAllScreens(showExperimental: Boolean = false): List<Screen> {
|
||||||
val screens = mutableListOf(Home, Dictionary, Vocabulary, Settings)
|
return listOf(Home, Library, Stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMoreMenuItems(showExperimental: Boolean = false): List<Screen> {
|
||||||
|
val items = mutableListOf<Screen>()
|
||||||
|
items.add(Translation)
|
||||||
|
items.add(Dictionary)
|
||||||
|
items.add(Settings)
|
||||||
if (showExperimental) {
|
if (showExperimental) {
|
||||||
screens.add(2, Exercises)
|
items.add(Exercises)
|
||||||
}
|
}
|
||||||
return screens
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun fromDestination(destination: NavDestination?): Screen {
|
fun fromDestination(destination: NavDestination?): Screen {
|
||||||
val showExperimental = LocalShowExperimentalFeatures.current
|
val showExperimental = LocalShowExperimentalFeatures.current
|
||||||
return getAllScreens(showExperimental).find { screen ->
|
val allScreens = getAllScreens(showExperimental) + getMoreMenuItems(showExperimental) + More
|
||||||
|
return allScreens.find { screen ->
|
||||||
destination?.hierarchy?.any { it.route == screen.route } == true
|
destination?.hierarchy?.any { it.route == screen.route } == true
|
||||||
} ?: Home
|
} ?: Home
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A modernized Material 3 bottom navigation bar with spring animations and haptic feedback.
|
|
||||||
*/
|
|
||||||
@SuppressLint("UnusedBoxWithConstraintsScope")
|
@SuppressLint("UnusedBoxWithConstraintsScope")
|
||||||
@Composable
|
@Composable
|
||||||
fun BottomNavigationBar(
|
fun BottomNavigationBar(
|
||||||
@@ -84,89 +114,274 @@ fun BottomNavigationBar(
|
|||||||
showLabels: Boolean,
|
showLabels: Boolean,
|
||||||
onItemSelected: (Screen) -> Unit,
|
onItemSelected: (Screen) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
onPlayClicked: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val showExperimental = LocalShowExperimentalFeatures.current
|
val showExperimental = LocalShowExperimentalFeatures.current
|
||||||
val screens = remember(showExperimental) { Screen.getAllScreens(showExperimental) }
|
val screens = remember(showExperimental) { Screen.getAllScreens(showExperimental) }
|
||||||
|
val moreScreen = remember { Screen.More }
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
|
var showMoreMenu by remember { mutableStateOf(false) }
|
||||||
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
// Configuration for the play button
|
||||||
|
val playButtonSize = 56.dp
|
||||||
|
val glowPadding = 12.dp // Total extra space for the glow (16dp on each side)
|
||||||
|
|
||||||
|
// This dictates how far up the button shifts.
|
||||||
|
// Setting it to around half the button size centers it on the top border.
|
||||||
|
val upwardOffset = 16.dp
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isVisible,
|
visible = isVisible,
|
||||||
enter = slideInVertically(
|
enter = slideInVertically(
|
||||||
animationSpec = androidx.compose.animation.core.tween(durationMillis = 220),
|
animationSpec = spring(stiffness = Spring.StiffnessHigh),
|
||||||
initialOffsetY = { it }
|
initialOffsetY = { it }
|
||||||
),
|
),
|
||||||
exit = slideOutVertically(
|
exit = slideOutVertically(
|
||||||
animationSpec = androidx.compose.animation.core.tween(durationMillis = 220),
|
animationSpec = spring(stiffness = Spring.StiffnessHigh),
|
||||||
targetOffsetY = { it }
|
targetOffsetY = { it }
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val baseHeight = if (showLabels) 80.dp else 56.dp
|
val baseHeight = if (showLabels) 80.dp else 56.dp
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val navBarDp = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() }
|
val navBarDp = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() }
|
||||||
val height = baseHeight + navBarDp
|
val height = baseHeight + navBarDp
|
||||||
|
|
||||||
NavigationBar(
|
// Outer Box height is purely determined by the NavigationBar now
|
||||||
modifier = modifier.height(height),
|
Box(
|
||||||
containerColor = MaterialTheme.colorScheme.surface, // Cleaner look than surfaceVariant
|
modifier = modifier.fillMaxWidth(),
|
||||||
tonalElevation = 8.dp, // Slight elevation for depth
|
contentAlignment = Alignment.TopCenter
|
||||||
) {
|
) {
|
||||||
screens.forEach { screen ->
|
|
||||||
val isSelected = screen == selectedItem
|
|
||||||
val title = stringResource(id = screen.title)
|
|
||||||
|
|
||||||
// 1. Spring Animation for the Icon Scale
|
// The actual Navigation Bar
|
||||||
val scale by animateFloatAsState(
|
NavigationBar(
|
||||||
targetValue = if (isSelected) 1.2f else 1.0f, // Subtle pop effect
|
modifier = Modifier.height(height),
|
||||||
animationSpec = spring(
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
tonalElevation = 8.dp,
|
||||||
stiffness = Spring.StiffnessLow
|
) {
|
||||||
),
|
// Create a list of 5 items (2 left, 1 empty spacer, 2 right)
|
||||||
label = "iconScale"
|
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(
|
allNavItems.forEach { screen ->
|
||||||
selected = isSelected,
|
if (screen == null) {
|
||||||
onClick = {
|
// Dummy item to create the gap
|
||||||
if (!isSelected) {
|
NavigationBarItem(
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress) // 2. Tactile feedback
|
selected = false,
|
||||||
onItemSelected(screen)
|
onClick = {},
|
||||||
}
|
enabled = false, // Disables ripples and clicks
|
||||||
},
|
icon = { Spacer(modifier = Modifier.size(24.dp)) },
|
||||||
label = if (showLabels) {
|
label = if (showLabels) { { Spacer(modifier = Modifier.size(10.dp)) } } else null,
|
||||||
{
|
colors = NavigationBarItemDefaults.colors(
|
||||||
Text(
|
disabledIconColor = Color.Transparent,
|
||||||
text = title,
|
disabledTextColor = Color.Transparent
|
||||||
maxLines = 1,
|
|
||||||
fontSize = 10.sp,
|
|
||||||
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
|
|
||||||
color = if(isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
} 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,
|
val title = stringResource(id = screen.title)
|
||||||
icon = {
|
|
||||||
// 3. Crossfade between Outlined and Filled icons
|
val scale by animateFloatAsState(
|
||||||
Crossfade(targetState = isSelected, label = "iconFade") { selected ->
|
targetValue = if (isSelected) 1.2f else 1.0f,
|
||||||
Icon(
|
animationSpec = spring(
|
||||||
imageVector = if (selected) screen.selectedIcon else screen.unselectedIcon,
|
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||||
contentDescription = title,
|
stiffness = Spring.StiffnessLow
|
||||||
modifier = Modifier.scale(scale) // Apply the spring scale
|
),
|
||||||
|
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
|
@ThemePreviews
|
||||||
@Composable
|
@Composable
|
||||||
fun BottomNavigationBarPreview() {
|
fun BottomNavigationBarPreview() {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import androidx.compose.material3.CardDefaults
|
|||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.CheckboxDefaults
|
import androidx.compose.material3.CheckboxDefaults
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.OutlinedCard
|
import androidx.compose.material3.OutlinedCard
|
||||||
@@ -36,6 +37,7 @@ import androidx.compose.material3.Switch
|
|||||||
import androidx.compose.material3.SwitchDefaults
|
import androidx.compose.material3.SwitchDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -56,6 +58,9 @@ import eu.gaudian.translator.R
|
|||||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||||
import eu.gaudian.translator.ui.theme.semanticColors
|
import eu.gaudian.translator.ui.theme.semanticColors
|
||||||
import eu.gaudian.translator.view.composable.ComponentDefaults.DefaultElevation
|
import eu.gaudian.translator.view.composable.ComponentDefaults.DefaultElevation
|
||||||
|
import eu.gaudian.translator.view.hints.Hint
|
||||||
|
import eu.gaudian.translator.view.hints.HintBottomSheet
|
||||||
|
import eu.gaudian.translator.view.hints.LocalShowHints
|
||||||
|
|
||||||
|
|
||||||
object ComponentDefaults {
|
object ComponentDefaults {
|
||||||
@@ -97,13 +102,16 @@ object ComponentDefaults {
|
|||||||
fun AppCard(
|
fun AppCard(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
title: String? = null,
|
title: String? = null,
|
||||||
icon: ImageVector? = null, // New optional icon parameter
|
icon: ImageVector? = null,
|
||||||
text: String? = null,
|
text: String? = null,
|
||||||
expandable: Boolean = false,
|
expandable: Boolean = false,
|
||||||
initiallyExpanded: Boolean = false,
|
initiallyExpanded: Boolean = false,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
hintContent : Hint? = null,
|
||||||
content: @Composable ColumnScope.() -> Unit,
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
var isExpanded by remember { mutableStateOf(initiallyExpanded) }
|
var isExpanded by remember { mutableStateOf(initiallyExpanded) }
|
||||||
|
val showHints = LocalShowHints.current
|
||||||
|
|
||||||
val rotationState by animateFloatAsState(
|
val rotationState by animateFloatAsState(
|
||||||
targetValue = if (isExpanded) 180f else 0f,
|
targetValue = if (isExpanded) 180f else 0f,
|
||||||
@@ -113,6 +121,21 @@ fun AppCard(
|
|||||||
// Check if we need to render the header row
|
// Check if we need to render the header row
|
||||||
// Updated to include icon in the check
|
// Updated to include icon in the check
|
||||||
val hasHeader = title != null || text != null || expandable || icon != null
|
val hasHeader = title != null || text != null || expandable || icon != null
|
||||||
|
val canClickHeader = expandable || onClick != null
|
||||||
|
|
||||||
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (showBottomSheet) {
|
||||||
|
hintContent?.let {
|
||||||
|
HintBottomSheet(
|
||||||
|
onDismissRequest = { showBottomSheet = false },
|
||||||
|
content = it,
|
||||||
|
sheetState = rememberModalBottomSheetState(
|
||||||
|
skipPartiallyExpanded = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -125,7 +148,7 @@ fun AppCard(
|
|||||||
// Animate height changes when expanding/collapsing
|
// Animate height changes when expanding/collapsing
|
||||||
.animateContentSize(),
|
.animateContentSize(),
|
||||||
shape = ComponentDefaults.CardShape,
|
shape = ComponentDefaults.CardShape,
|
||||||
color = MaterialTheme.colorScheme.surfaceContainer
|
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
// --- Header Row ---
|
// --- Header Row ---
|
||||||
@@ -133,12 +156,18 @@ fun AppCard(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(enabled = expandable) { isExpanded = !isExpanded }
|
.clickable(enabled = canClickHeader) {
|
||||||
|
if (expandable) {
|
||||||
|
isExpanded = !isExpanded
|
||||||
|
}
|
||||||
|
onClick?.invoke()
|
||||||
|
}
|
||||||
.padding(ComponentDefaults.CardPadding),
|
.padding(ComponentDefaults.CardPadding),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// 1. Optional Icon on the left
|
// 1. Optional Icon on the left
|
||||||
if (icon != null) {
|
if (icon != null) {
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
@@ -172,6 +201,16 @@ fun AppCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showHints && hintContent != null) {
|
||||||
|
IconButton(onClick = { showBottomSheet = true }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = AppIcons.Help,
|
||||||
|
contentDescription = stringResource(R.string.show_hint),
|
||||||
|
tint = MaterialTheme.colorScheme.secondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Expand Chevron (Far right)
|
// 3. Expand Chevron (Far right)
|
||||||
if (expandable) {
|
if (expandable) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -182,21 +221,32 @@ fun AppCard(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Content Area ---
|
// --- Content Area ---
|
||||||
if (!expandable || isExpanded) {
|
if (!expandable || isExpanded) {
|
||||||
Column(
|
val contentModifier = Modifier
|
||||||
modifier = Modifier.padding(
|
.padding(
|
||||||
start = ComponentDefaults.CardPadding,
|
start = ComponentDefaults.CardPadding,
|
||||||
end = ComponentDefaults.CardPadding,
|
end = ComponentDefaults.CardPadding,
|
||||||
bottom = ComponentDefaults.CardPadding,
|
bottom = ComponentDefaults.CardPadding,
|
||||||
// If we have a header, remove the top padding so content sits closer to the title.
|
// If we have a header, remove the top padding so content sits closer to the title.
|
||||||
// If no header (legacy behavior), keep the top padding.
|
// If no header (legacy behavior), keep the top padding.
|
||||||
top = if (hasHeader) 0.dp else ComponentDefaults.CardPadding
|
top = if (hasHeader) 0.dp else ComponentDefaults.CardPadding
|
||||||
),
|
)
|
||||||
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,
|
enableMultipleSelection: Boolean = false,
|
||||||
onLanguagesSelected: (List<Language>) -> Unit = {},
|
onLanguagesSelected: (List<Language>) -> Unit = {},
|
||||||
alternateLanguages: List<Language> = emptyList(),
|
alternateLanguages: List<Language> = emptyList(),
|
||||||
|
restrictToAlternateLanguages: Boolean = false,
|
||||||
|
enabled: Boolean = true,
|
||||||
iconEnabled: Boolean = true,
|
iconEnabled: Boolean = true,
|
||||||
noBorder: Boolean = false,
|
noBorder: Boolean = false,
|
||||||
) {
|
) {
|
||||||
@@ -68,8 +70,12 @@ fun BaseLanguageDropDown(
|
|||||||
var tempSelection by remember { mutableStateOf<List<Language>>(emptyList()) }
|
var tempSelection by remember { mutableStateOf<List<Language>>(emptyList()) }
|
||||||
var selectedLanguagesCount by remember { mutableIntStateOf(if (enableMultipleSelection) tempSelection.size else 0) }
|
var selectedLanguagesCount by remember { mutableIntStateOf(if (enableMultipleSelection) tempSelection.size else 0) }
|
||||||
|
|
||||||
val languages = remember(alternateLanguages, defaultLanguages) {
|
val languages = remember(alternateLanguages, defaultLanguages, restrictToAlternateLanguages) {
|
||||||
alternateLanguages.ifEmpty { defaultLanguages }
|
if (restrictToAlternateLanguages) {
|
||||||
|
alternateLanguages
|
||||||
|
} else {
|
||||||
|
alternateLanguages.ifEmpty { defaultLanguages }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val buttonText = when {
|
val buttonText = when {
|
||||||
@@ -90,6 +96,7 @@ fun BaseLanguageDropDown(
|
|||||||
AppOutlinedButton(
|
AppOutlinedButton(
|
||||||
shape = RoundedCornerShape(8.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
onClick = { expanded = true },
|
onClick = { expanded = true },
|
||||||
|
enabled = enabled,
|
||||||
contentPadding = if (!iconEnabled) PaddingValues(0.dp) else PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
contentPadding = if (!iconEnabled) PaddingValues(0.dp) else PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
||||||
borderColor = if (noBorder) Color.Unspecified else null
|
borderColor = if (noBorder) Color.Unspecified else null
|
||||||
) {
|
) {
|
||||||
@@ -221,8 +228,13 @@ fun BaseLanguageDropDown(
|
|||||||
) {
|
) {
|
||||||
val isSearching = searchText.isNotBlank()
|
val isSearching = searchText.isNotBlank()
|
||||||
|
|
||||||
if (isSearching) {
|
if (isSearching) {
|
||||||
val searchResults = (favoriteLanguages + languageHistory + languages)
|
val searchBase = if (restrictToAlternateLanguages) {
|
||||||
|
alternateLanguages
|
||||||
|
} else {
|
||||||
|
favoriteLanguages + languageHistory + languages
|
||||||
|
}
|
||||||
|
val searchResults = searchBase
|
||||||
.distinctBy { it.nameResId }
|
.distinctBy { it.nameResId }
|
||||||
.filter { language ->
|
.filter { language ->
|
||||||
val matchesName = language.name.contains(searchText, ignoreCase = true)
|
val matchesName = language.name.contains(searchText, ignoreCase = true)
|
||||||
@@ -237,8 +249,18 @@ fun BaseLanguageDropDown(
|
|||||||
searchResults.forEach { language -> SingleSelectItem(language) }
|
searchResults.forEach { language -> SingleSelectItem(language) }
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (alternateLanguages.isNotEmpty()) {
|
} else if (restrictToAlternateLanguages) {
|
||||||
val sortedAlternate = alternateLanguages.sortedBy { it.name }
|
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) {
|
if (enableMultipleSelection) {
|
||||||
DropdownHeader(text = stringResource(R.string.text_all_languages))
|
DropdownHeader(text = stringResource(R.string.text_all_languages))
|
||||||
sortedAlternate.forEach { language -> MultiSelectItem(language) }
|
sortedAlternate.forEach { language -> MultiSelectItem(language) }
|
||||||
@@ -458,7 +480,9 @@ fun SingleLanguageDropDown(
|
|||||||
onAutoSelected: () -> Unit = {},
|
onAutoSelected: () -> Unit = {},
|
||||||
showNoneOption: Boolean = false,
|
showNoneOption: Boolean = false,
|
||||||
onNoneSelected: () -> Unit = {},
|
onNoneSelected: () -> Unit = {},
|
||||||
alternateLanguages: List<Language> = emptyList()
|
alternateLanguages: List<Language> = emptyList(),
|
||||||
|
restrictToAlternateLanguages: Boolean = false,
|
||||||
|
enabled: Boolean = true
|
||||||
) {
|
) {
|
||||||
val languageHistory by languageViewModel.languageHistory.collectAsState()
|
val languageHistory by languageViewModel.languageHistory.collectAsState()
|
||||||
|
|
||||||
@@ -477,6 +501,10 @@ fun SingleLanguageDropDown(
|
|||||||
showNoneOption = showNoneOption,
|
showNoneOption = showNoneOption,
|
||||||
onNoneSelected = onNoneSelected,
|
onNoneSelected = onNoneSelected,
|
||||||
enableMultipleSelection = false,
|
enableMultipleSelection = false,
|
||||||
alternateLanguages = alternateLanguages
|
alternateLanguages = alternateLanguages,
|
||||||
|
restrictToAlternateLanguages = restrictToAlternateLanguages,
|
||||||
|
enabled = enabled,
|
||||||
|
iconEnabled = enabled,
|
||||||
|
noBorder = !enabled
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -6,8 +6,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -28,8 +26,6 @@ fun CategorySelectionDialog(
|
|||||||
) {
|
) {
|
||||||
val activity = LocalContext.current.findActivity()
|
val activity = LocalContext.current.findActivity()
|
||||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
|
|
||||||
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
|
||||||
|
|
||||||
AppDialog(
|
AppDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
|||||||
@@ -1,219 +0,0 @@
|
|||||||
package eu.gaudian.translator.view.dialogs
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringArrayResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.compose.NavHost
|
|
||||||
import androidx.navigation.compose.composable
|
|
||||||
import androidx.navigation.compose.rememberNavController
|
|
||||||
import eu.gaudian.translator.R
|
|
||||||
import eu.gaudian.translator.utils.findActivity
|
|
||||||
import eu.gaudian.translator.view.composable.AppDialog
|
|
||||||
import eu.gaudian.translator.view.composable.AppSlider
|
|
||||||
import eu.gaudian.translator.view.composable.DialogButton
|
|
||||||
import eu.gaudian.translator.view.composable.InspiringSearchField
|
|
||||||
import eu.gaudian.translator.view.composable.SourceLanguageDropdown
|
|
||||||
import eu.gaudian.translator.view.composable.TargetLanguageDropdown
|
|
||||||
import eu.gaudian.translator.view.hints.HintDefinition
|
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
|
||||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ImportVocabularyDialog(
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
languageViewModel: LanguageViewModel,
|
|
||||||
vocabularyViewModel : VocabularyViewModel,
|
|
||||||
optionalDescription: String? = null,
|
|
||||||
optionalSearchTerm: String? = null
|
|
||||||
) {
|
|
||||||
|
|
||||||
val navController = rememberNavController()
|
|
||||||
NavHost(navController = navController, startDestination = "import") {
|
|
||||||
composable("import") {
|
|
||||||
ImportDialogContent(
|
|
||||||
navController = navController,
|
|
||||||
onDismiss = onDismiss,
|
|
||||||
languageViewModel = languageViewModel,
|
|
||||||
optionalDescription = optionalDescription,
|
|
||||||
optionalSearchTerm = optionalSearchTerm
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
composable("review") {
|
|
||||||
Dialog(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
|
||||||
) {
|
|
||||||
// Full-screen surface to ensure the dialog covers content and stays above the main FAB/menu
|
|
||||||
Surface(modifier = Modifier.fillMaxSize()) {
|
|
||||||
VocabularyReviewScreen(
|
|
||||||
onConfirm = { selectedItems, categoryIds ->
|
|
||||||
vocabularyViewModel.addVocabularyItems(selectedItems, categoryIds)
|
|
||||||
onDismiss()
|
|
||||||
},
|
|
||||||
onCancel = onDismiss
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ImportDialogContent(
|
|
||||||
navController: NavController,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
languageViewModel: LanguageViewModel,
|
|
||||||
optionalDescription: String? = null,
|
|
||||||
optionalSearchTerm: String? = null
|
|
||||||
) {
|
|
||||||
val activity = LocalContext.current.findActivity()
|
|
||||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
|
||||||
var category by remember { mutableStateOf(optionalSearchTerm ?: "") }
|
|
||||||
var amount by remember { mutableFloatStateOf(1f) }
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
val descriptionText = optionalDescription ?: stringResource(R.string.text_let_ai_find_vocabulary_for_you)
|
|
||||||
val isGenerating by vocabularyViewModel.isGenerating.collectAsState()
|
|
||||||
|
|
||||||
AppDialog(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
title = { Text(descriptionText) },
|
|
||||||
hintContent = HintDefinition.IMPORT.hint(),
|
|
||||||
content = {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(4.dp)
|
|
||||||
) {
|
|
||||||
if (isGenerating) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.text_search_term),
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
|
||||||
)
|
|
||||||
// Modern rotating field using XML resource array
|
|
||||||
InspiringSearchField(
|
|
||||||
value = category,
|
|
||||||
hints = stringArrayResource(R.array.vocabulary_hints),
|
|
||||||
onValueChange = { category = it }
|
|
||||||
)
|
|
||||||
|
|
||||||
// The "Dica" string has been removed to keep the interface clean
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.text_select_languages),
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(1.dp)
|
|
||||||
) {
|
|
||||||
SourceLanguageDropdown(languageViewModel = languageViewModel)
|
|
||||||
}
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(1.dp)
|
|
||||||
) {
|
|
||||||
TargetLanguageDropdown(languageViewModel = languageViewModel)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.text_select_amount),
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
|
||||||
)
|
|
||||||
AppSlider(
|
|
||||||
value = amount,
|
|
||||||
onValueChange = { amount = it },
|
|
||||||
valueRange = 1f..25f,
|
|
||||||
steps = 24,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.text_amount_2d, amount.toInt()),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.End
|
|
||||||
) {
|
|
||||||
DialogButton(
|
|
||||||
onClick = onDismiss,
|
|
||||||
content = { Text(stringResource(R.string.label_cancel)) }
|
|
||||||
)
|
|
||||||
if (category.isNotBlank() && !isGenerating) {
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
DialogButton(onClick = {
|
|
||||||
coroutineScope.launch {
|
|
||||||
vocabularyViewModel.generateVocabularyItems(category, amount.toInt())
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
navController.navigate("review")
|
|
||||||
}
|
|
||||||
}) { Text(stringResource(R.string.text_generate)) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun ImportDialogContentPreview() {
|
|
||||||
val activity = LocalContext.current.findActivity()
|
|
||||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
|
||||||
|
|
||||||
ImportDialogContent(
|
|
||||||
navController = rememberNavController(),
|
|
||||||
onDismiss = {},
|
|
||||||
languageViewModel = languageViewModel,
|
|
||||||
optionalDescription = "Let AI find vocabulary for you",
|
|
||||||
optionalSearchTerm = "Travel"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
package eu.gaudian.translator.view.dialogs
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|
||||||
import eu.gaudian.translator.R
|
|
||||||
import eu.gaudian.translator.model.Language
|
|
||||||
import eu.gaudian.translator.model.VocabularyCategory
|
|
||||||
import eu.gaudian.translator.model.VocabularyStage
|
|
||||||
import eu.gaudian.translator.utils.findActivity
|
|
||||||
import eu.gaudian.translator.view.composable.AppDialog
|
|
||||||
import eu.gaudian.translator.view.composable.MultipleLanguageDropdown
|
|
||||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
|
||||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun StartExerciseDialog(
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
onConfirm: (
|
|
||||||
categories: List<VocabularyCategory>,
|
|
||||||
stages: List<VocabularyStage>,
|
|
||||||
languageIds: List<Int>
|
|
||||||
) -> Unit
|
|
||||||
) {
|
|
||||||
val activity = LocalContext.current.findActivity()
|
|
||||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
|
||||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
|
||||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
|
||||||
|
|
||||||
var lids by remember { mutableStateOf<List<Int>>(emptyList()) }
|
|
||||||
var languages by remember { mutableStateOf<List<Language>>(emptyList()) }
|
|
||||||
// Map displayed Language to its DB id (lid) using position mapping from load
|
|
||||||
var languageIdMap by remember { mutableStateOf<Map<Language, Int>>(emptyMap()) }
|
|
||||||
var selectedLanguages by remember { mutableStateOf<List<Language>>(emptyList()) }
|
|
||||||
var selectedStages by remember { mutableStateOf<List<VocabularyStage>>(emptyList()) }
|
|
||||||
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory>>(emptyList()) }
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
coroutineScope.launch {
|
|
||||||
lids = vocabularyViewModel.getAllLanguagesIdsPresent().filterNotNull().toList()
|
|
||||||
languages = lids.map { lid ->
|
|
||||||
languageViewModel.getLanguageById(lid)
|
|
||||||
}
|
|
||||||
// build reverse map
|
|
||||||
languageIdMap = languages.mapIndexed { index, lang -> lang to lids.getOrNull(index) }.filter { it.second != null }.associate { it.first to it.second!! }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppDialog(onDismissRequest = onDismiss, title = { Text(stringResource(R.string.label_start_exercise)) }) {
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(24.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
MultipleLanguageDropdown(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
languageViewModel = languageViewModel,
|
|
||||||
onLanguagesSelected = { langs ->
|
|
||||||
selectedLanguages = langs
|
|
||||||
},
|
|
||||||
languages
|
|
||||||
)
|
|
||||||
CategoryDropdown(
|
|
||||||
onCategorySelected = { cats ->
|
|
||||||
selectedCategories = cats.filterIsInstance<VocabularyCategory>()
|
|
||||||
},
|
|
||||||
multipleSelectable = true,
|
|
||||||
onlyLists = false, // Show both filters and lists
|
|
||||||
addCategory = false,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
VocabularyStageDropDown(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
preselectedStages = selectedStages,
|
|
||||||
onStageSelected = { stages ->
|
|
||||||
@Suppress("FilterIsInstanceResultIsAlwaysEmpty")
|
|
||||||
selectedStages = stages.filterIsInstance<VocabularyStage>()
|
|
||||||
},
|
|
||||||
multipleSelectable = true
|
|
||||||
)
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 16.dp),
|
|
||||||
horizontalArrangement = Arrangement.End
|
|
||||||
) {
|
|
||||||
TextButton(
|
|
||||||
onClick = onDismiss,
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.label_cancel))
|
|
||||||
}
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
run {
|
|
||||||
val ids = selectedLanguages.mapNotNull { languageIdMap[it] }
|
|
||||||
onConfirm(selectedCategories, selectedStages, ids)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.label_start_exercise))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
@file:Suppress("AssignedValueIsNeverRead")
|
|
||||||
|
|
||||||
package eu.gaudian.translator.view.dialogs
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|
||||||
import eu.gaudian.translator.R
|
|
||||||
import eu.gaudian.translator.utils.findActivity
|
|
||||||
import eu.gaudian.translator.view.composable.AppFabMenu
|
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
|
||||||
import eu.gaudian.translator.view.composable.FabMenuItem
|
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
|
||||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun VocabularyMenu(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
showFabText : Boolean = true
|
|
||||||
) {
|
|
||||||
val activity = LocalContext.current.findActivity()
|
|
||||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
|
||||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
|
||||||
var showAddVocabularyDialog by remember { mutableStateOf(false) }
|
|
||||||
var showImportVocabularyDialog by remember { mutableStateOf(false) }
|
|
||||||
var showAddCategoryDialog by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val menuItems = listOf(
|
|
||||||
FabMenuItem(
|
|
||||||
text = stringResource(R.string.label_add_vocabulary),
|
|
||||||
imageVector = AppIcons.Add,
|
|
||||||
onClick = { showAddVocabularyDialog = true }
|
|
||||||
),
|
|
||||||
FabMenuItem(
|
|
||||||
text = stringResource(R.string.menu_import_vocabulary),
|
|
||||||
imageVector = AppIcons.AI,
|
|
||||||
onClick = { showImportVocabularyDialog = true }
|
|
||||||
),
|
|
||||||
FabMenuItem(
|
|
||||||
text = stringResource(R.string.label_add_category),
|
|
||||||
imageVector = AppIcons.Add,
|
|
||||||
onClick = { showAddCategoryDialog = true }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
AppFabMenu(items = menuItems, modifier = modifier, title = stringResource(R.string.label_add_vocabulary), showFabText = showFabText)
|
|
||||||
|
|
||||||
if (showAddVocabularyDialog) {
|
|
||||||
AddVocabularyDialog(
|
|
||||||
onDismissRequest = { showAddVocabularyDialog = false }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showImportVocabularyDialog) {
|
|
||||||
ImportVocabularyDialog(
|
|
||||||
languageViewModel = languageViewModel,
|
|
||||||
vocabularyViewModel = vocabularyViewModel,
|
|
||||||
onDismiss = { showImportVocabularyDialog = false }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showAddCategoryDialog) {
|
|
||||||
AddCategoryDialog(
|
|
||||||
onDismiss = { showAddCategoryDialog = false }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -35,7 +35,6 @@ import eu.gaudian.translator.view.composable.AppCheckbox
|
|||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.view.hints.HintDefinition
|
import eu.gaudian.translator.view.hints.HintDefinition
|
||||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
|
||||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -45,11 +44,9 @@ fun VocabularyReviewScreen(
|
|||||||
) {
|
) {
|
||||||
val activity = LocalContext.current.findActivity()
|
val activity = LocalContext.current.findActivity()
|
||||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(activity)
|
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(activity)
|
||||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
|
||||||
|
|
||||||
val generatedItems: List<VocabularyItem> by vocabularyViewModel.generatedVocabularyItems.collectAsState()
|
val generatedItems: List<VocabularyItem> by vocabularyViewModel.generatedVocabularyItems.collectAsState()
|
||||||
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
|
||||||
|
|
||||||
val selectedItems = remember { mutableStateListOf<VocabularyItem>() }
|
val selectedItems = remember { mutableStateListOf<VocabularyItem>() }
|
||||||
val duplicates = remember { mutableStateListOf<Boolean>() }
|
val duplicates = remember { mutableStateListOf<Boolean>() }
|
||||||
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory?>>(emptyList()) }
|
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory?>>(emptyList()) }
|
||||||
@@ -65,7 +62,7 @@ fun VocabularyReviewScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.found_items)) },
|
title = stringResource(R.string.found_items),
|
||||||
hintContent = HintDefinition.REVIEW.hint()
|
hintContent = HintDefinition.REVIEW.hint()
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ fun DefinitionPart(part: EntryPart) {
|
|||||||
// Fallback for JsonObject or other top-level types
|
// Fallback for JsonObject or other top-level types
|
||||||
else -> contentElement.toString()
|
else -> contentElement.toString()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
// Ultimate fallback if something else goes wrong during parsing
|
// Ultimate fallback if something else goes wrong during parsing
|
||||||
part.content.toString()
|
part.content.toString()
|
||||||
}
|
}
|
||||||
@@ -466,12 +466,6 @@ fun DefinitionPartPreview() {
|
|||||||
DefinitionPart(part = mockPart)
|
DefinitionPart(part = mockPart)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data classes for the refactored components
|
|
||||||
data class EntryData(
|
|
||||||
val entry: DictionaryEntry,
|
|
||||||
val language: Language?
|
|
||||||
)
|
|
||||||
|
|
||||||
data class BreadcrumbItem(
|
data class BreadcrumbItem(
|
||||||
val word: String,
|
val word: String,
|
||||||
val entryId: Int
|
val entryId: Int
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -28,7 +26,6 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
@@ -346,28 +343,8 @@ fun DictionarySimpleTopBar(
|
|||||||
onNavigateBack: () -> Unit
|
onNavigateBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = {
|
title = "TODO",
|
||||||
Column {
|
onNavigateBack = onNavigateBack
|
||||||
Text(
|
|
||||||
text = word ?: stringResource(R.string.text_loading_3d),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
languageName?.let {
|
|
||||||
Text(
|
|
||||||
text = it,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
fontStyle = FontStyle.Italic
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = onNavigateBack) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("SameParameterValue")
|
||||||
|
|
||||||
package eu.gaudian.translator.view.dictionary
|
package eu.gaudian.translator.view.dictionary
|
||||||
|
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
@@ -94,27 +93,8 @@ fun EtymologyResultScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = {
|
title = "TODO",
|
||||||
Column {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
Text(
|
|
||||||
text = word,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
language?.name?.let {
|
|
||||||
Text(
|
|
||||||
text = it,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
actions = {
|
||||||
etymologyData?.let { data ->
|
etymologyData?.let { data ->
|
||||||
if (isTtsAvailable) {
|
if (isTtsAvailable) {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import eu.gaudian.translator.view.LocalConnectionConfigured
|
|||||||
import eu.gaudian.translator.view.NoConnectionScreen
|
import eu.gaudian.translator.view.NoConnectionScreen
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
import eu.gaudian.translator.view.composable.AppIcons
|
||||||
import eu.gaudian.translator.view.composable.AppTabLayout
|
import eu.gaudian.translator.view.composable.AppTabLayout
|
||||||
|
import eu.gaudian.translator.view.composable.Screen
|
||||||
import eu.gaudian.translator.view.composable.TabItem
|
import eu.gaudian.translator.view.composable.TabItem
|
||||||
import eu.gaudian.translator.view.settings.SettingsRoutes
|
import eu.gaudian.translator.view.settings.SettingsRoutes
|
||||||
import eu.gaudian.translator.viewmodel.CorrectionViewModel
|
import eu.gaudian.translator.viewmodel.CorrectionViewModel
|
||||||
@@ -63,7 +64,15 @@ fun MainDictionaryScreen(
|
|||||||
AppTabLayout(
|
AppTabLayout(
|
||||||
tabs = dictionaryTabs,
|
tabs = dictionaryTabs,
|
||||||
selectedTab = selectedTab,
|
selectedTab = selectedTab,
|
||||||
onTabSelected = { selectedTab = it }
|
onTabSelected = { selectedTab = it },
|
||||||
|
onNavigateBack = {
|
||||||
|
if (!navController.popBackStack()) {
|
||||||
|
navController.navigate(Screen.Home.route) {
|
||||||
|
launchSingleTop = true
|
||||||
|
restoreState = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
when (selectedTab) {
|
when (selectedTab) {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import androidx.navigation.NavHostController
|
|||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.view.composable.AppButton
|
import eu.gaudian.translator.view.composable.AppButton
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.view.vocabulary.VocabularyListScreen
|
import eu.gaudian.translator.view.vocabulary.AllCardsListScreen
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExerciseVocabularyScreen(
|
fun ExerciseVocabularyScreen(
|
||||||
@@ -24,7 +24,7 @@ fun ExerciseVocabularyScreen(
|
|||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(title = { Text(stringResource(R.string.text_new_vocabulary_for_this_exercise)) })
|
AppTopAppBar(title =stringResource(R.string.text_new_vocabulary_for_this_exercise))
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
Surface(shadowElevation = 8.dp) {
|
Surface(shadowElevation = 8.dp) {
|
||||||
@@ -41,7 +41,7 @@ fun ExerciseVocabularyScreen(
|
|||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Box(modifier = Modifier.padding(paddingValues)) {
|
Box(modifier = Modifier.padding(paddingValues)) {
|
||||||
|
|
||||||
VocabularyListScreen(
|
AllCardsListScreen(
|
||||||
navController = navController as NavHostController?,
|
navController = navController as NavHostController?,
|
||||||
onNavigateToItem = { item ->
|
onNavigateToItem = { item ->
|
||||||
// Navigate to the detail screen for a specific vocabulary item
|
// Navigate to the detail screen for a specific vocabulary item
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import eu.gaudian.translator.view.composable.AppIcons
|
|||||||
import eu.gaudian.translator.view.composable.AppOutlinedCard
|
import eu.gaudian.translator.view.composable.AppOutlinedCard
|
||||||
import eu.gaudian.translator.view.composable.AppTabLayout
|
import eu.gaudian.translator.view.composable.AppTabLayout
|
||||||
import eu.gaudian.translator.view.composable.DialogButton
|
import eu.gaudian.translator.view.composable.DialogButton
|
||||||
|
import eu.gaudian.translator.view.composable.Screen
|
||||||
import eu.gaudian.translator.view.composable.TabItem
|
import eu.gaudian.translator.view.composable.TabItem
|
||||||
import eu.gaudian.translator.viewmodel.AiGenerationState
|
import eu.gaudian.translator.viewmodel.AiGenerationState
|
||||||
import eu.gaudian.translator.viewmodel.ExerciseViewModel
|
import eu.gaudian.translator.viewmodel.ExerciseViewModel
|
||||||
@@ -76,7 +77,15 @@ fun MainExerciseScreen(
|
|||||||
AppTabLayout(
|
AppTabLayout(
|
||||||
tabs = ExerciseTab.entries,
|
tabs = ExerciseTab.entries,
|
||||||
selectedTab = selectedTab,
|
selectedTab = selectedTab,
|
||||||
onTabSelected = { selectedTab = it }
|
onTabSelected = { selectedTab = it },
|
||||||
|
onNavigateBack = {
|
||||||
|
if (!navController.popBackStack()) {
|
||||||
|
navController.navigate(Screen.Home.route) {
|
||||||
|
launchSingleTop = true
|
||||||
|
restoreState = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
Box(modifier = Modifier.weight(1f)) {
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
|
|||||||
@@ -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.foundation.layout.padding
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -26,12 +24,10 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import eu.gaudian.translator.R
|
|
||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
import eu.gaudian.translator.view.composable.AppIcons
|
||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
@@ -61,12 +57,8 @@ fun YouTubeBrowserScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text("YouTube") },
|
title = "YouTube" ,
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() }
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
|
|||||||
@@ -183,14 +183,8 @@ fun YouTubeExerciseScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(title, maxLines = 1) },
|
title = title,
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(
|
|
||||||
R.string.cd_back
|
|
||||||
))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { onFinishVideo() },
|
onClick = { onFinishVideo() },
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ enum class HintDefinition(
|
|||||||
CATEGORY("category_hint", R.string.category_hint_intro),
|
CATEGORY("category_hint", R.string.category_hint_intro),
|
||||||
DICTIONARY_OPTIONS("dictionary_hint", R.string.label_dictionary_options),
|
DICTIONARY_OPTIONS("dictionary_hint", R.string.label_dictionary_options),
|
||||||
EXERCISE("exercise_hint", R.string.label_exercise),
|
EXERCISE("exercise_hint", R.string.label_exercise),
|
||||||
IMPORT("import_hint", R.string.hint_how_to_generate_vocabulary_with_ai),
|
VOCABULARY_GENERATE_AI("import_hint", R.string.hint_how_to_generate_vocabulary_with_ai),
|
||||||
LEARNING_STAGES("learning_stages_hint", R.string.learning_stages_title),
|
LEARNING_STAGES("learning_stages_hint", R.string.learning_stages_title),
|
||||||
REVIEW("review_hint", R.string.review_intro),
|
REVIEW("review_hint", R.string.review_intro),
|
||||||
SORTING("sorting_hint", R.string.sorting_hint_title),
|
SORTING("sorting_hint", R.string.sorting_hint_title),
|
||||||
@@ -40,7 +40,6 @@ enum class HintDefinition(
|
|||||||
@Composable
|
@Composable
|
||||||
fun hint(definition: HintDefinition): Hint = definition.hint()
|
fun hint(definition: HintDefinition): Hint = definition.hint()
|
||||||
|
|
||||||
@Composable fun HintContent(definition: HintDefinition) = definition.Render()
|
|
||||||
@Composable fun HintScreen(navController: NavController, definition: HintDefinition) = HintScreen(
|
@Composable fun HintScreen(navController: NavController, definition: HintDefinition) = HintScreen(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
title = stringResource(definition.titleRes),
|
title = stringResource(definition.titleRes),
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
@file:Suppress("HardCodedStringLiteral")
|
|
||||||
|
|
||||||
package eu.gaudian.translator.view.hints
|
package eu.gaudian.translator.view.hints
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import kotlinx.coroutines.launch
|
|||||||
fun HintBottomSheet(
|
fun HintBottomSheet(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
sheetState: SheetState,
|
sheetState: SheetState,
|
||||||
content: @Composable (() -> Unit)?
|
content: Hint,
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
@@ -50,7 +50,7 @@ fun HintBottomSheet(
|
|||||||
.weight(1f, fill = false)
|
.weight(1f, fill = false)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
content?.invoke()
|
content.Render()
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
@@ -16,7 +15,6 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
import eu.gaudian.translator.view.composable.AppIcons
|
||||||
@@ -39,8 +37,8 @@ val LocalShowHints = compositionLocalOf { false }
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun WithHint(
|
fun WithHint(
|
||||||
hintContent: @Composable () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
hintContent: Hint? = null,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val showHints = LocalShowHints.current
|
val showHints = LocalShowHints.current
|
||||||
@@ -69,27 +67,16 @@ fun WithHint(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showBottomSheet) {
|
if (showBottomSheet) {
|
||||||
HintBottomSheet(
|
hintContent?.let {
|
||||||
onDismissRequest = {
|
HintBottomSheet(
|
||||||
@Suppress("AssignedValueIsNeverRead")
|
onDismissRequest = {
|
||||||
showBottomSheet = false
|
@Suppress("AssignedValueIsNeverRead")
|
||||||
},
|
showBottomSheet = false
|
||||||
sheetState = sheetState,
|
},
|
||||||
hintContent
|
sheetState = sheetState,
|
||||||
)
|
content = it,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun WithHintPreview() {
|
|
||||||
androidx.compose.runtime.CompositionLocalProvider(LocalShowHints provides true) {
|
|
||||||
WithHint(
|
|
||||||
hintContent = {
|
|
||||||
Text(stringResource(R.string.this_is_a_hint))
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.this_is_the_main_content))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,15 +5,9 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import eu.gaudian.translator.R
|
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
|
||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
|
|
||||||
@@ -30,12 +24,8 @@ fun HintScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(title) },
|
title = title,
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() }
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ fun HintsOverviewScreen(
|
|||||||
val showExperimental = LocalShowExperimentalFeatures.current
|
val showExperimental = LocalShowExperimentalFeatures.current
|
||||||
|
|
||||||
// Get hints using the new function-based approach
|
// Get hints using the new function-based approach
|
||||||
val importHint = HintDefinition.IMPORT.hint()
|
val importHint = HintDefinition.VOCABULARY_GENERATE_AI.hint()
|
||||||
val addModelScanHint = HintDefinition.ADD_MODEL_SCAN.hint()
|
val addModelScanHint = HintDefinition.ADD_MODEL_SCAN.hint()
|
||||||
val dictionaryOptionsHint = HintDefinition.DICTIONARY_OPTIONS.hint()
|
val dictionaryOptionsHint = HintDefinition.DICTIONARY_OPTIONS.hint()
|
||||||
val translationScreenHint = HintDefinition.TRANSLATION.hint()
|
val translationScreenHint = HintDefinition.TRANSLATION.hint()
|
||||||
@@ -77,7 +77,7 @@ fun HintsOverviewScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.hint_title_hints_overview), style = MaterialTheme.typography.titleLarge) }
|
title = stringResource(R.string.hint_title_hints_overview)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ object MarkdownHintLoader {
|
|||||||
append(language.lowercase())
|
append(language.lowercase())
|
||||||
}
|
}
|
||||||
if (country.isNotEmpty()) {
|
if (country.isNotEmpty()) {
|
||||||
|
@Suppress("HardCodedStringLiteral")
|
||||||
append("-r")
|
append("-r")
|
||||||
append(country.uppercase())
|
append(country.uppercase())
|
||||||
}
|
}
|
||||||
|
|||||||
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.foundation.verticalScroll
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -73,12 +72,8 @@ fun AboutScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.label_about)) },
|
title = stringResource(R.string.label_about),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() }
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -134,12 +134,8 @@ fun AddModelScreen(navController: NavController, providerKey: String) {
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(providerName) },
|
title = providerName,
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
hintContent = HintDefinition.ADD_MODEL_SCAN.hint()
|
hintContent = HintDefinition.ADD_MODEL_SCAN.hint()
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -115,12 +115,8 @@ fun ApiKeyScreen(navController: NavController) {
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.label_ai_configuration)) },
|
title = stringResource(R.string.label_ai_configuration),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
hintContent = HintDefinition.API_KEY.hint()
|
hintContent = HintDefinition.API_KEY.hint()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -137,7 +133,7 @@ fun ApiKeyScreen(navController: NavController) {
|
|||||||
AppTabLayout(
|
AppTabLayout(
|
||||||
tabs = apiTabs,
|
tabs = apiTabs,
|
||||||
selectedTab = selectedTab,
|
selectedTab = selectedTab,
|
||||||
onTabSelected = { selectedTab = it }
|
onTabSelected = { selectedTab = it },
|
||||||
)
|
)
|
||||||
|
|
||||||
// Tab Content
|
// Tab Content
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -22,7 +19,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
|
||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.viewmodel.ApiViewModel
|
import eu.gaudian.translator.viewmodel.ApiViewModel
|
||||||
@@ -55,12 +51,8 @@ fun CustomVocabularyPromptScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.text_vocabulary_prompt)) },
|
title = stringResource(R.string.text_vocabulary_prompt),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
hintContent = null //TODO: Add hint
|
hintContent = null //TODO: Add hint
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -31,7 +28,6 @@ import eu.gaudian.translator.R
|
|||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.LocalShowExperimentalFeatures
|
import eu.gaudian.translator.view.LocalShowExperimentalFeatures
|
||||||
import eu.gaudian.translator.view.composable.AppCard
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
|
||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.view.composable.OptionItemSwitch
|
import eu.gaudian.translator.view.composable.OptionItemSwitch
|
||||||
@@ -66,12 +62,8 @@ fun DictionaryOptionsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.label_dictionary_options)) },
|
title = stringResource(R.string.label_dictionary_options),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
hintContent = HintDefinition.DICTIONARY_OPTIONS.hint()
|
hintContent = HintDefinition.DICTIONARY_OPTIONS.hint()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -31,7 +29,6 @@ import eu.gaudian.translator.R
|
|||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.ApiModelDropDown
|
import eu.gaudian.translator.view.composable.ApiModelDropDown
|
||||||
import eu.gaudian.translator.view.composable.AppCard
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
|
||||||
import eu.gaudian.translator.view.composable.AppOutlinedTextField
|
import eu.gaudian.translator.view.composable.AppOutlinedTextField
|
||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
@@ -71,12 +68,8 @@ fun ExerciseSettingsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.exercise_settings)) },
|
title = stringResource(R.string.exercise_settings),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -24,7 +22,6 @@ import androidx.navigation.NavController
|
|||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppCard
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
|
||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppSwitch
|
import eu.gaudian.translator.view.composable.AppSwitch
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
@@ -41,12 +38,8 @@ fun GeneralSettingsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.label_general)) },
|
title = stringResource(R.string.label_general),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -61,12 +61,8 @@ fun LanguageOptionsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.text_language_options)) },
|
title = stringResource(R.string.text_language_options),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
@@ -132,6 +128,7 @@ fun LanguageOptionsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showAddLanguageDialog) {
|
if (showAddLanguageDialog) {
|
||||||
|
@Suppress("KotlinConstantConditions")
|
||||||
AddCustomLanguageDialog(
|
AddCustomLanguageDialog(
|
||||||
showDialog = showAddLanguageDialog,
|
showDialog = showAddLanguageDialog,
|
||||||
onDismiss = { showAddLanguageDialog = false },
|
onDismiss = { showAddLanguageDialog = false },
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import androidx.compose.foundation.shape.CircleShape
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
@@ -97,16 +96,11 @@ fun LayoutOptionsScreen(navController: NavController) {
|
|||||||
val selectedFontName by settingsViewModel.fontPreference.collectAsStateWithLifecycle()
|
val selectedFontName by settingsViewModel.fontPreference.collectAsStateWithLifecycle()
|
||||||
val showBottomNavLabels by settingsViewModel.showBottomNavLabels.collectAsStateWithLifecycle()
|
val showBottomNavLabels by settingsViewModel.showBottomNavLabels.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val cdBack = stringResource(R.string.cd_back)
|
|
||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.label_appearance)) },
|
title = stringResource(R.string.label_appearance),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = cdBack)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -101,15 +101,8 @@ fun LogsScreen(navController: NavController) {
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.label_logs)) },
|
title = stringResource(R.string.label_logs),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = AppIcons.ArrowBack,
|
|
||||||
contentDescription = stringResource(R.string.cd_back)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
actions = {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
settingsViewModel.clearApiLogs()
|
settingsViewModel.clearApiLogs()
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import eu.gaudian.translator.view.composable.AppIcons
|
|||||||
import eu.gaudian.translator.view.composable.AppOutlinedCard
|
import eu.gaudian.translator.view.composable.AppOutlinedCard
|
||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
|
import eu.gaudian.translator.view.composable.Screen
|
||||||
|
|
||||||
private data class Setting(val titleRes: Int, val icon: ImageVector, val route: String)
|
private data class Setting(val titleRes: Int, val icon: ImageVector, val route: String)
|
||||||
|
|
||||||
@@ -84,7 +85,15 @@ fun MainSettingsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.title_settings), style = MaterialTheme.typography.titleLarge) }
|
title =stringResource(R.string.title_settings),
|
||||||
|
onNavigateBack = {
|
||||||
|
if (!navController.popBackStack()) {
|
||||||
|
navController.navigate(Screen.Home.route) {
|
||||||
|
launchSingleTop = true
|
||||||
|
restoreState = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
@@ -96,7 +105,7 @@ fun MainSettingsScreen(
|
|||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
AppCard(
|
AppCard(
|
||||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp),
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
settings.forEachIndexed { index, setting ->
|
settings.forEachIndexed { index, setting ->
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ fun NavGraphBuilder.settingsGraph(navController: NavController) {
|
|||||||
HintScreen(navController, HintDefinition.DICTIONARY_OPTIONS)
|
HintScreen(navController, HintDefinition.DICTIONARY_OPTIONS)
|
||||||
}
|
}
|
||||||
composable(SettingsRoutes.HINTS_IMPORT) {
|
composable(SettingsRoutes.HINTS_IMPORT) {
|
||||||
HintScreen(navController, HintDefinition.IMPORT)
|
HintScreen(navController, HintDefinition.VOCABULARY_GENERATE_AI)
|
||||||
}
|
}
|
||||||
composable(SettingsRoutes.HINTS_SORTING) {
|
composable(SettingsRoutes.HINTS_SORTING) {
|
||||||
HintScreen(navController, HintDefinition.SORTING)
|
HintScreen(navController, HintDefinition.SORTING)
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -86,12 +84,8 @@ fun TextToSpeechSettingsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.settings_title_voice)) },
|
title = stringResource(R.string.settings_title_voice),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -27,7 +24,6 @@ import androidx.navigation.NavController
|
|||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppCard
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
|
||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.view.composable.OptionItemSwitch
|
import eu.gaudian.translator.view.composable.OptionItemSwitch
|
||||||
@@ -64,12 +60,8 @@ fun TranslationSettingsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.label_translation_settings)) },
|
title = stringResource(R.string.label_translation_settings),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
hintContent = null //TODO add hint
|
hintContent = null //TODO add hint
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import androidx.compose.foundation.shape.CircleShape
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
@@ -78,13 +77,8 @@ fun VocabularyProgressOptionsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.vocabulary_settings)) },
|
title = stringResource(R.string.label_vocabulary_settings),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Here is the new hint content
|
|
||||||
hintContent = HintDefinition.VOCABULARY_PROGRESS.hint()
|
hintContent = HintDefinition.VOCABULARY_PROGRESS.hint()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -97,44 +91,27 @@ fun VocabularyProgressOptionsScreen(
|
|||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
// Interval Settings
|
// Daily Goal Settings
|
||||||
AppCard(
|
AppCard {
|
||||||
expandable = true,
|
val dailyGoal by settingsViewModel.dailyGoal.collectAsStateWithLifecycle()
|
||||||
initiallyExpanded = true,
|
|
||||||
title = stringResource(R.string.text_interval_settings_in_days),
|
|
||||||
text = stringResource(R.string.text_customize_the_intervals),
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
) {
|
|
||||||
val intervals by settingsViewModel.intervals.collectAsStateWithLifecycle()
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(16.dp),
|
||||||
.padding(16.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
.animateContentSize(),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
) {
|
||||||
IntervalTimeline(intervals = intervals)
|
|
||||||
intervals.forEach { (stageKey, days) ->
|
|
||||||
val displayLabel = labelForStage(stageKey)
|
|
||||||
IntervalSlider(
|
|
||||||
label = displayLabel,
|
|
||||||
value = days,
|
|
||||||
onValueChange = { newValue ->
|
|
||||||
settingsViewModel.setInterval(stageKey, newValue)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(Modifier.height(8.dp))
|
@Suppress("USELESS_ELVIS", "HardCodedStringLiteral")
|
||||||
Row(
|
SettingsSlider(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
label = stringResource(R.string.label_target_correct_answers_per_day),
|
||||||
horizontalArrangement = Arrangement.End
|
value = dailyGoal ?: 10,
|
||||||
) {
|
onValueChange = { settingsViewModel.setDailyGoal(it) },
|
||||||
TextButton(onClick = { settingsViewModel.resetIntervalsToDefaults() }) {
|
valueRange = 10f..100f,
|
||||||
Text(stringResource(R.string.reset_to_defaults))
|
steps = 17 // Allows snapping in steps of 5
|
||||||
}
|
)
|
||||||
}
|
Text(
|
||||||
|
text = stringResource(R.string.text_daily_goal_description),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,30 +146,41 @@ fun VocabularyProgressOptionsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Daily Goal Settings
|
// Interval Settings
|
||||||
AppCard {
|
AppCard(
|
||||||
val dailyGoal by settingsViewModel.dailyGoal.collectAsStateWithLifecycle()
|
expandable = true,
|
||||||
Column(
|
initiallyExpanded = true,
|
||||||
modifier = Modifier.padding(16.dp),
|
title = stringResource(R.string.label_interval_settings_in_days),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
text = stringResource(R.string.text_customize_the_intervals),
|
||||||
) {
|
) {
|
||||||
Text(
|
val intervals by settingsViewModel.intervals.collectAsStateWithLifecycle()
|
||||||
text = stringResource(R.string.daily_learning_goal),
|
Column(
|
||||||
style = MaterialTheme.typography.titleMedium
|
modifier = Modifier
|
||||||
)
|
.padding(16.dp)
|
||||||
@Suppress("USELESS_ELVIS", "HardCodedStringLiteral")
|
.animateContentSize(),
|
||||||
SettingsSlider(
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
label = stringResource(R.string.target_correct_answers_per_day),
|
) {
|
||||||
value = dailyGoal ?: 10,
|
IntervalTimeline(intervals = intervals)
|
||||||
onValueChange = { settingsViewModel.setDailyGoal(it) },
|
intervals.forEach { (stageKey, days) ->
|
||||||
valueRange = 10f..100f,
|
val displayLabel = labelForStage(stageKey)
|
||||||
steps = 17 // Allows snapping in steps of 5
|
IntervalSlider(
|
||||||
)
|
label = displayLabel,
|
||||||
Text(
|
value = days,
|
||||||
text = stringResource(R.string.text_daily_goal_description),
|
onValueChange = { newValue ->
|
||||||
style = MaterialTheme.typography.bodySmall,
|
settingsViewModel.setInterval(stageKey, newValue)
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
TextButton(onClick = { settingsViewModel.resetIntervalsToDefaults() }) {
|
||||||
|
Text(stringResource(R.string.reset_to_defaults))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ import androidx.compose.material3.ButtonDefaults
|
|||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
@@ -44,7 +42,6 @@ import eu.gaudian.translator.utils.StatusMessageService
|
|||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppButton
|
import eu.gaudian.translator.view.composable.AppButton
|
||||||
import eu.gaudian.translator.view.composable.AppCard
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
|
||||||
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
@@ -200,12 +197,8 @@ fun VocabularyRepositoryOptionsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.vocabulary_repository)) },
|
title = stringResource(R.string.vocabulary_repository),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -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.AppIcons
|
||||||
import eu.gaudian.translator.view.composable.SourceLanguageDropdown
|
import eu.gaudian.translator.view.composable.SourceLanguageDropdown
|
||||||
import eu.gaudian.translator.view.composable.TargetLanguageDropdown
|
import eu.gaudian.translator.view.composable.TargetLanguageDropdown
|
||||||
|
import eu.gaudian.translator.view.hints.Hint
|
||||||
import eu.gaudian.translator.view.hints.WithHint
|
import eu.gaudian.translator.view.hints.WithHint
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||||
|
|
||||||
@@ -67,9 +68,23 @@ fun ActionBar(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TopBarActions(languageViewModel: LanguageViewModel, onSettingsClick: () -> Unit, hintContent: (@Composable () -> Unit)? = null) {
|
fun TopBarActions(
|
||||||
|
languageViewModel: LanguageViewModel,
|
||||||
|
onSettingsClick: () -> Unit,
|
||||||
|
onNavigateBack: (() -> Unit)? = null,
|
||||||
|
hintContent: Hint? = null
|
||||||
|
) {
|
||||||
|
|
||||||
ActionBar(modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp)) {
|
ActionBar(modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp)) {
|
||||||
|
if (onNavigateBack != null) {
|
||||||
|
IconButton(onClick = onNavigateBack) {
|
||||||
|
Icon(
|
||||||
|
imageVector = AppIcons.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.cd_navigate_back)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (hintContent != null) {
|
if (hintContent != null) {
|
||||||
WithHint(hintContent = hintContent) {
|
WithHint(hintContent = hintContent) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,14 @@ fun TranslationScreen(
|
|||||||
settingsViewModel = settingsViewModel,
|
settingsViewModel = settingsViewModel,
|
||||||
onHistoryClick = onHistoryClick,
|
onHistoryClick = onHistoryClick,
|
||||||
onSettingsClick = onSettingsClick,
|
onSettingsClick = onSettingsClick,
|
||||||
|
onNavigateBack = {
|
||||||
|
if (!navController.popBackStack()) {
|
||||||
|
navController.navigate(eu.gaudian.translator.view.composable.Screen.Home.route) {
|
||||||
|
launchSingleTop = true
|
||||||
|
restoreState = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
context = context
|
context = context
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -119,6 +127,7 @@ private fun LoadedTranslationContent(
|
|||||||
settingsViewModel: SettingsViewModel,
|
settingsViewModel: SettingsViewModel,
|
||||||
onHistoryClick: () -> Unit,
|
onHistoryClick: () -> Unit,
|
||||||
onSettingsClick: () -> Unit,
|
onSettingsClick: () -> Unit,
|
||||||
|
onNavigateBack: () -> Unit,
|
||||||
context: Context
|
context: Context
|
||||||
) {
|
) {
|
||||||
val inputText by translationViewModel.inputText.collectAsState()
|
val inputText by translationViewModel.inputText.collectAsState()
|
||||||
@@ -167,7 +176,8 @@ private fun LoadedTranslationContent(
|
|||||||
TopBarActions(
|
TopBarActions(
|
||||||
languageViewModel = languageViewModel,
|
languageViewModel = languageViewModel,
|
||||||
onSettingsClick = onSettingsClick,
|
onSettingsClick = onSettingsClick,
|
||||||
hintContent = { HintDefinition.TRANSLATION.Render() }
|
onNavigateBack = onNavigateBack,
|
||||||
|
hintContent = HintDefinition.TRANSLATION.hint()
|
||||||
)
|
)
|
||||||
|
|
||||||
AppCard(modifier = Modifier.padding(8.dp, end = 8.dp, bottom = 8.dp, top = 0.dp)) {
|
AppCard(modifier = Modifier.padding(8.dp, end = 8.dp, bottom = 8.dp, top = 0.dp)) {
|
||||||
|
|||||||
@@ -5,14 +5,16 @@ package eu.gaudian.translator.view.vocabulary
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -30,7 +32,8 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
@@ -43,10 +46,12 @@ import eu.gaudian.translator.view.composable.AppIcons
|
|||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.view.composable.PrimaryButton
|
import eu.gaudian.translator.view.composable.PrimaryButton
|
||||||
|
import eu.gaudian.translator.view.composable.SecondaryButton
|
||||||
import eu.gaudian.translator.view.dialogs.DeleteCategoryDialog
|
import eu.gaudian.translator.view.dialogs.DeleteCategoryDialog
|
||||||
import eu.gaudian.translator.view.dialogs.DeleteItemsDialog
|
import eu.gaudian.translator.view.dialogs.DeleteItemsDialog
|
||||||
import eu.gaudian.translator.view.dialogs.EditCategoryDialog
|
import eu.gaudian.translator.view.dialogs.EditCategoryDialog
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressCircle
|
import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressCircle
|
||||||
|
import eu.gaudian.translator.viewmodel.CategoryProgress
|
||||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||||
import eu.gaudian.translator.viewmodel.ProgressViewModel
|
import eu.gaudian.translator.viewmodel.ProgressViewModel
|
||||||
@@ -89,11 +94,10 @@ fun CategoryDetailScreen(
|
|||||||
if (!hasLangList && !hasPair && !hasStages) {
|
if (!hasLangList && !hasPair && !hasStages) {
|
||||||
append(stringResource(R.string.text_filter_all_items))
|
append(stringResource(R.string.text_filter_all_items))
|
||||||
} else {
|
} else {
|
||||||
//append(stringResource(R.string.filter))
|
|
||||||
append(" ")
|
append(" ")
|
||||||
if (hasPair) {
|
if (hasPair) {
|
||||||
val (a,b) = cat.languagePairs
|
val (a, b) = cat.languagePairs
|
||||||
append("[${languages.value.find{ it.nameResId == a }?.name} - ${languages.value.find{ it.nameResId == b }?.name}]")
|
append("[${languages.value.find { it.nameResId == a }?.name} - ${languages.value.find { it.nameResId == b }?.name}]")
|
||||||
} else if (hasLangList) {
|
} else if (hasLangList) {
|
||||||
append(cat.languages.joinToString(", ") { langId -> languages.value.find { it.nameResId == langId }?.name.toString() })
|
append(cat.languages.joinToString(", ") { langId -> languages.value.find { it.nameResId == langId }?.name.toString() })
|
||||||
} else {
|
} else {
|
||||||
@@ -118,30 +122,8 @@ fun CategoryDetailScreen(
|
|||||||
modifier = Modifier.background(MaterialTheme.colorScheme.surface)
|
modifier = Modifier.background(MaterialTheme.colorScheme.surface)
|
||||||
) {
|
) {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = {
|
title = title,
|
||||||
Column {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = subtitle,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = onBackClick) {
|
|
||||||
Icon(
|
|
||||||
AppIcons.ArrowBack,
|
|
||||||
contentDescription = stringResource(R.string.cd_back)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = { showMenu = !showMenu }) {
|
IconButton(onClick = { showMenu = !showMenu }) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -150,94 +132,58 @@ fun CategoryDetailScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
|
|
||||||
expanded = showMenu,
|
expanded = showMenu,
|
||||||
onDismissRequest = { showMenu = false },
|
onDismissRequest = { showMenu = false },
|
||||||
modifier = Modifier.width(220.dp)
|
modifier = Modifier.width(220.dp)
|
||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(R.string.text_edit_category)) },
|
|
||||||
onClick = {
|
|
||||||
categoryViewModel.setShowEditCategoryDialog(true, categoryId)
|
|
||||||
showMenu = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(stringResource(R.string.text_export_category)) },
|
text = { Text(stringResource(R.string.text_export_category)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
vocabularyViewModel.saveCategory(categoryId)
|
vocabularyViewModel.saveCategory(categoryId)
|
||||||
showMenu = false
|
showMenu = false
|
||||||
}
|
},
|
||||||
|
leadingIcon = { Icon(AppIcons.Share, contentDescription = null) }
|
||||||
)
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(stringResource(R.string.delete_items_category)) },
|
text = { Text(stringResource(R.string.delete_items_category)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
categoryViewModel.setShowDeleteItemsDialog(true, categoryId)
|
categoryViewModel.setShowDeleteItemsDialog(true, categoryId)
|
||||||
showMenu = false
|
showMenu = false
|
||||||
}
|
},
|
||||||
)
|
leadingIcon = { Icon(AppIcons.Delete, contentDescription = null) }
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(R.string.text_delete_category)) },
|
|
||||||
onClick = {
|
|
||||||
categoryViewModel.setShowDeleteCategoryDialog(true, categoryId)
|
|
||||||
showMenu = false
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
// TODO: Review this
|
|
||||||
containerColor = MaterialTheme.colorScheme.surface
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(
|
// Category Header Card with Progress and Action Buttons
|
||||||
modifier = Modifier
|
CategoryHeaderCard(
|
||||||
.fillMaxWidth()
|
subtitle = subtitle,
|
||||||
.padding(vertical = 8.dp, horizontal = 16.dp),
|
categoryProgress = categoryProgress,
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
onStartExerciseClick = {
|
||||||
verticalAlignment = Alignment.CenterVertically
|
val categories = listOf(category)
|
||||||
) {
|
val categoryIds = categories.joinToString(",") { it?.id.toString() }
|
||||||
if (categoryProgress != null) {
|
navController.navigate("vocabulary_exercise/false?categories=$categoryIds")
|
||||||
Box(modifier = Modifier.weight(1f)) {
|
},
|
||||||
CategoryProgressCircle(
|
onEditClick = {
|
||||||
totalItems = categoryProgress.totalItems,
|
categoryViewModel.setShowEditCategoryDialog(true, categoryId)
|
||||||
itemsCompleted = categoryProgress.itemsCompleted,
|
},
|
||||||
itemsInStages = categoryProgress.itemsInStages,
|
onDeleteClick = {
|
||||||
newItems = categoryProgress.newItems,
|
categoryViewModel.setShowDeleteCategoryDialog(true, categoryId)
|
||||||
circleSize = 80.dp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(start = 8.dp)
|
|
||||||
) {
|
|
||||||
PrimaryButton(
|
|
||||||
text = stringResource(R.string.label_start),
|
|
||||||
icon = AppIcons.Play,
|
|
||||||
onClick = {
|
|
||||||
val categories = listOf(category)
|
|
||||||
val categoryIds = categories.joinToString(",") { it?.id.toString() }
|
|
||||||
navController.navigate("vocabulary_exercise/false?categories=$categoryIds")
|
|
||||||
},
|
|
||||||
modifier = Modifier.heightIn(max = 80.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(modifier = Modifier.padding(paddingValues)) {
|
Column(modifier = Modifier.padding(paddingValues)) {
|
||||||
VocabularyListScreen(
|
AllCardsListScreen(
|
||||||
categoryId = categoryId,
|
categoryId = categoryId,
|
||||||
showDueTodayOnly = false,
|
showDueTodayOnly = false,
|
||||||
onNavigateToItem = onNavigateToItem,
|
onNavigateToItem = onNavigateToItem,
|
||||||
navController = navController, // Pass the received navController here
|
navController = navController,
|
||||||
isRemoveFromCategoryEnabled = category is TagCategory,
|
isRemoveFromCategoryEnabled = category is TagCategory,
|
||||||
showTopBar = false,
|
showTopBar = false,
|
||||||
enableNavigationButtons = true
|
enableNavigationButtons = true
|
||||||
@@ -265,4 +211,132 @@ 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,16 +100,10 @@ fun CategoryListScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = {
|
title = "TODO",
|
||||||
if (isSelectionMode && selectedCategories.isNotEmpty()) {
|
|
||||||
Text(stringResource(R.string.text_2d_categories_selected, selectedCategories.size))
|
|
||||||
} else {
|
|
||||||
Text(stringResource(R.string.label_categories))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
if (isSelectionMode) {
|
if (isSelectionMode) {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
isSelectionMode = false
|
isSelectionMode = false
|
||||||
selectedCategories = emptySet()
|
selectedCategories = emptySet()
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ import androidx.navigation.compose.rememberNavController
|
|||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.model.VocabularyStage
|
import eu.gaudian.translator.model.VocabularyStage
|
||||||
import eu.gaudian.translator.model.WidgetType
|
import eu.gaudian.translator.model.WidgetType
|
||||||
import eu.gaudian.translator.utils.Log
|
|
||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppCard
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
import eu.gaudian.translator.view.composable.AppIcons
|
||||||
@@ -65,7 +64,6 @@ import eu.gaudian.translator.view.vocabulary.widgets.AllVocabularyWidget
|
|||||||
import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressWidget
|
import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressWidget
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.DueTodayWidget
|
import eu.gaudian.translator.view.vocabulary.widgets.DueTodayWidget
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.LevelWidget
|
import eu.gaudian.translator.view.vocabulary.widgets.LevelWidget
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.ModernStartButtons
|
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.StatusWidget
|
import eu.gaudian.translator.view.vocabulary.widgets.StatusWidget
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.StreakWidget
|
import eu.gaudian.translator.view.vocabulary.widgets.StreakWidget
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget
|
import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget
|
||||||
@@ -530,17 +528,7 @@ private fun LazyWidget(
|
|||||||
onMissingLanguage: (Int) -> Unit
|
onMissingLanguage: (Int) -> Unit
|
||||||
) {
|
) {
|
||||||
when (widgetType) {
|
when (widgetType) {
|
||||||
WidgetType.StartButtons -> ModernStartButtons(
|
|
||||||
onCustomClick = onShowCustomExerciseDialog,
|
|
||||||
onDailyClick = { isSpelling ->
|
|
||||||
if (isSpelling) {
|
|
||||||
onShowWordPairExerciseDialog()
|
|
||||||
} else {
|
|
||||||
startDailyExercise(true)
|
|
||||||
Log.d("DailyExercise", "Starting daily exercise")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
WidgetType.Status -> LazyStatusWidget(
|
WidgetType.Status -> LazyStatusWidget(
|
||||||
vocabularyViewModel = vocabularyViewModel,
|
vocabularyViewModel = vocabularyViewModel,
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ fun LanguageProgressScreen(navController: NavController) {
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.your_language_journey)) },
|
title = stringResource(R.string.your_language_journey),
|
||||||
onNavigateBack = { navController.popBackStack() }
|
onNavigateBack = { navController.popBackStack() }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -39,7 +37,6 @@ import androidx.navigation.NavController
|
|||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppButton
|
import eu.gaudian.translator.view.composable.AppButton
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
|
||||||
import eu.gaudian.translator.view.composable.AppSlider
|
import eu.gaudian.translator.view.composable.AppSlider
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||||
@@ -57,7 +54,7 @@ fun NoGrammarItemsScreen(
|
|||||||
|
|
||||||
var showFetchGrammarDialog by remember { mutableStateOf(false) }
|
var showFetchGrammarDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@Suppress("UnusedVariable", "unused", "HardCodedStringLiteral") val onClose = { navController.popBackStack() }
|
@Suppress("UnusedVariable") val onClose = { navController.popBackStack() }
|
||||||
|
|
||||||
if (itemsWithoutGrammar.isEmpty() && !isGenerating) {
|
if (itemsWithoutGrammar.isEmpty() && !isGenerating) {
|
||||||
Column(
|
Column(
|
||||||
@@ -66,12 +63,8 @@ fun NoGrammarItemsScreen(
|
|||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.title_items_without_grammar)) },
|
title = stringResource(R.string.title_items_without_grammar),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -87,8 +80,8 @@ fun NoGrammarItemsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Use the generic VocabularyListScreen to display the items
|
// Use the generic AllCardsListScreen to display the items
|
||||||
VocabularyListScreen(
|
AllCardsListScreen(
|
||||||
itemsToShow = itemsWithoutGrammar,
|
itemsToShow = itemsWithoutGrammar,
|
||||||
onNavigateToItem = { item ->
|
onNavigateToItem = { item ->
|
||||||
@Suppress("HardCodedStringLiteral")
|
@Suppress("HardCodedStringLiteral")
|
||||||
|
|||||||
@@ -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.Column
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -17,7 +14,6 @@ import androidx.navigation.NavHostController
|
|||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.model.VocabularyStage
|
import eu.gaudian.translator.model.VocabularyStage
|
||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
|
||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.DetailedStageProgressBar
|
import eu.gaudian.translator.view.vocabulary.widgets.DetailedStageProgressBar
|
||||||
@@ -40,15 +36,8 @@ fun StageDetailScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(text = stringResource(R.string.due_today_, stage.toString())) },
|
title = stringResource(R.string.due_today_, stage.toString()),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(
|
|
||||||
AppIcons.ArrowBack,
|
|
||||||
contentDescription =stringResource(R.string.cd_back)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
@@ -60,7 +49,7 @@ fun StageDetailScreen(
|
|||||||
onStageTapped = {},
|
onStageTapped = {},
|
||||||
)
|
)
|
||||||
|
|
||||||
VocabularyListScreen(
|
AllCardsListScreen(
|
||||||
categoryId = null,
|
categoryId = null,
|
||||||
showDueTodayOnly = true,
|
showDueTodayOnly = true,
|
||||||
stage = stage,
|
stage = stage,
|
||||||
|
|||||||
@@ -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
|
package eu.gaudian.translator.view.vocabulary
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -39,10 +48,9 @@ import eu.gaudian.translator.view.composable.AppIcons
|
|||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.view.dialogs.CategorySelectionDialog
|
import eu.gaudian.translator.view.dialogs.CategorySelectionDialog
|
||||||
import eu.gaudian.translator.view.dialogs.ImportVocabularyDialog
|
|
||||||
import eu.gaudian.translator.view.dialogs.StageSelectionDialog
|
import eu.gaudian.translator.view.dialogs.StageSelectionDialog
|
||||||
import eu.gaudian.translator.view.vocabulary.card.VocabularyCard
|
import eu.gaudian.translator.view.vocabulary.card.VocabularyDisplayCard
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
import eu.gaudian.translator.view.vocabulary.card.VocabularyExerciseCard
|
||||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.time.ExperimentalTime
|
import kotlin.time.ExperimentalTime
|
||||||
@@ -59,7 +67,6 @@ fun VocabularyCardHost(
|
|||||||
) {
|
) {
|
||||||
val activity = LocalContext.current.findActivity()
|
val activity = LocalContext.current.findActivity()
|
||||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
val navigationItems by vocabularyViewModel.currentNavigationItems.collectAsState()
|
val navigationItems by vocabularyViewModel.currentNavigationItems.collectAsState()
|
||||||
@@ -71,62 +78,55 @@ fun VocabularyCardHost(
|
|||||||
vocabularyItem = vocabularyViewModel.getVocabularyItemById(itemId)
|
vocabularyItem = vocabularyViewModel.getVocabularyItemById(itemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isEditing by remember { mutableStateOf(false) }
|
||||||
|
var onSaveEdit by remember { mutableStateOf<(() -> Unit)?>(null) }
|
||||||
|
var onCancelEdit by remember { mutableStateOf<(() -> Unit)?>(null) }
|
||||||
|
|
||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
modifier = Modifier.height(56.dp),
|
modifier = Modifier.height(56.dp),
|
||||||
title = {
|
title = stringResource(R.string.item_details),
|
||||||
if (navigationItems.isNotEmpty()) {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
Text(stringResource(R.string.label_card_with_position, navigationPosition + 1, navigationItems.size))
|
|
||||||
} else {
|
|
||||||
Text(stringResource(R.string.item_details))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = { onBackPressed?.invoke() }) {
|
|
||||||
Icon(
|
|
||||||
AppIcons.ArrowBack,
|
|
||||||
contentDescription = stringResource(R.string.cd_back)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
actions = {
|
||||||
// Previous button
|
if (!isEditing) {
|
||||||
if (navigationPosition > 0) {
|
// Previous button
|
||||||
IconButton(onClick = {
|
if (navigationPosition > 0) {
|
||||||
if (vocabularyViewModel.navigateToPreviousItem()) {
|
IconButton(onClick = {
|
||||||
val prevItem = navigationItems[navigationPosition - 1]
|
if (vocabularyViewModel.navigateToPreviousItem()) {
|
||||||
scope.launch {
|
val prevItem = navigationItems[navigationPosition - 1]
|
||||||
|
scope.launch {
|
||||||
|
|
||||||
@Suppress("AssignedValueIsNeverRead")
|
@Suppress("AssignedValueIsNeverRead")
|
||||||
vocabularyItem = vocabularyViewModel.getVocabularyItemById(prevItem.id)
|
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
|
// Next button
|
||||||
if (navigationPosition < navigationItems.size - 1) {
|
if (navigationPosition < navigationItems.size - 1) {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
if (vocabularyViewModel.navigateToNextItem()) {
|
if (vocabularyViewModel.navigateToNextItem()) {
|
||||||
val nextItem = navigationItems[navigationPosition + 1]
|
val nextItem = navigationItems[navigationPosition + 1]
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@Suppress("AssignedValueIsNeverRead")
|
@Suppress("AssignedValueIsNeverRead")
|
||||||
vocabularyItem = vocabularyViewModel.getVocabularyItemById(nextItem.id)
|
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 showStatisticsDialog by remember { mutableStateOf(false) }
|
||||||
var showCategoryDialog by remember { mutableStateOf(false) }
|
var showCategoryDialog by remember { mutableStateOf(false) }
|
||||||
var showStageDialog by remember { mutableStateOf(false) }
|
var showStageDialog by remember { mutableStateOf(false) }
|
||||||
var showImportDialog by remember { mutableStateOf(false) }
|
|
||||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||||
|
LaunchedEffect(currentVocabularyItem.id) {
|
||||||
|
isEditing = false
|
||||||
|
onSaveEdit = null
|
||||||
|
onCancelEdit = null
|
||||||
|
}
|
||||||
|
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
val stats by vocabularyViewModel
|
val stats by vocabularyViewModel
|
||||||
@@ -199,18 +203,66 @@ fun VocabularyCardHost(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Main content
|
// Main content
|
||||||
VocabularyCard(
|
Column(
|
||||||
vocabularyItem = currentVocabularyItem,
|
modifier = Modifier.fillMaxSize(),
|
||||||
exerciseMode = exerciseMode,
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
switchOrder = switchOrder == true,
|
) {
|
||||||
isFlipped = isFlipped,
|
if (!exerciseMode && isEditing) {
|
||||||
onStatisticsClick = { showStatisticsDialog = true },
|
Row(
|
||||||
onMoveToCategoryClick = { showCategoryDialog = true },
|
modifier = Modifier.fillMaxWidth(),
|
||||||
onMoveToStageClick = { showStageDialog = true },
|
horizontalArrangement = Arrangement.Center,
|
||||||
onDeleteClick = { showDeleteDialog = true },
|
verticalAlignment = Alignment.CenterVertically
|
||||||
navController = navController,
|
) {
|
||||||
isUserSpellingCorrect = false,
|
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
|
// Dialogs are unaffected by the layout change
|
||||||
if (showQuitDialog) {
|
if (showQuitDialog) {
|
||||||
@@ -259,16 +311,6 @@ fun VocabularyCardHost(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showImportDialog) {
|
|
||||||
ImportVocabularyDialog(
|
|
||||||
onDismiss = { showImportDialog = false },
|
|
||||||
languageViewModel = languageViewModel,
|
|
||||||
optionalDescription = stringResource(R.string.generate_related_vocabulary_items),
|
|
||||||
optionalSearchTerm = currentVocabularyItem.wordFirst,
|
|
||||||
vocabularyViewModel = vocabularyViewModel
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(spellingMode) {
|
LaunchedEffect(spellingMode) {
|
||||||
@Suppress("ControlFlowWithEmptyBody")
|
@Suppress("ControlFlowWithEmptyBody")
|
||||||
if (spellingMode) {
|
if (spellingMode) {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ import eu.gaudian.translator.ui.theme.semanticColors
|
|||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppButton
|
import eu.gaudian.translator.view.composable.AppButton
|
||||||
import eu.gaudian.translator.view.composable.ComponentDefaults
|
import eu.gaudian.translator.view.composable.ComponentDefaults
|
||||||
import eu.gaudian.translator.view.vocabulary.card.VocabularyCard
|
import eu.gaudian.translator.view.vocabulary.card.VocabularyExerciseCard
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -141,11 +141,10 @@ fun GuessingExercise(
|
|||||||
navController: NavController,
|
navController: NavController,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
VocabularyCard(
|
VocabularyExerciseCard(
|
||||||
vocabularyItem = state.item,
|
vocabularyItem = state.item,
|
||||||
isFlipped = state.isRevealed,
|
isFlipped = state.isRevealed,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
exerciseMode = true,
|
|
||||||
switchOrder = state.isSwitched,
|
switchOrder = state.isSwitched,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -158,13 +157,12 @@ fun SpellingExercise(
|
|||||||
navController: NavController,
|
navController: NavController,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
VocabularyCard(
|
VocabularyExerciseCard(
|
||||||
vocabularyItem = state.item,
|
vocabularyItem = state.item,
|
||||||
isFlipped = state.isRevealed,
|
isFlipped = state.isRevealed,
|
||||||
userSpellingAnswer = state.userAnswer,
|
userSpellingAnswer = state.userAnswer,
|
||||||
isUserSpellingCorrect = state.isCorrect,
|
isUserSpellingCorrect = state.isCorrect,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
exerciseMode = true,
|
|
||||||
switchOrder = state.isSwitched,
|
switchOrder = state.isSwitched,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("HardCodedStringLiteral")
|
||||||
|
|
||||||
package eu.gaudian.translator.view.vocabulary
|
package eu.gaudian.translator.view.vocabulary
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
@@ -19,7 +21,6 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -28,10 +29,10 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.model.Language
|
import eu.gaudian.translator.utils.Log
|
||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppAlertDialog
|
import eu.gaudian.translator.view.composable.AppAlertDialog
|
||||||
import eu.gaudian.translator.viewmodel.ExerciseConfig
|
import eu.gaudian.translator.view.composable.Screen
|
||||||
import eu.gaudian.translator.viewmodel.ScreenState
|
import eu.gaudian.translator.viewmodel.ScreenState
|
||||||
import eu.gaudian.translator.viewmodel.VocabularyExerciseViewModel
|
import eu.gaudian.translator.viewmodel.VocabularyExerciseViewModel
|
||||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||||
@@ -57,14 +58,7 @@ fun VocabularyExerciseHostScreen(
|
|||||||
|
|
||||||
val cardSet by vocabularyViewModel.cardSet.collectAsState()
|
val cardSet by vocabularyViewModel.cardSet.collectAsState()
|
||||||
val screenState by exerciseViewModel.screenState.collectAsState()
|
val screenState by exerciseViewModel.screenState.collectAsState()
|
||||||
|
val pendingConfig by exerciseViewModel.pendingExerciseConfig.collectAsState()
|
||||||
var shuffleCards by rememberSaveable { mutableStateOf(false) }
|
|
||||||
var shuffleLanguages by remember { mutableStateOf(false) }
|
|
||||||
var trainingMode by remember { mutableStateOf(false) }
|
|
||||||
var dueTodayOnly by remember { mutableStateOf(false) }
|
|
||||||
var selectedExerciseTypes by remember { mutableStateOf(setOf(VocabularyExerciseType.GUESSING)) }
|
|
||||||
var selectedOriginLanguage by remember { mutableStateOf<Language?>(null) }
|
|
||||||
var selectedTargetLanguage by remember { mutableStateOf<Language?>(null) }
|
|
||||||
|
|
||||||
var finalScore by remember { mutableIntStateOf(0) }
|
var finalScore by remember { mutableIntStateOf(0) }
|
||||||
var finalWrongAnswers by remember { mutableIntStateOf(0) }
|
var finalWrongAnswers by remember { mutableIntStateOf(0) }
|
||||||
@@ -76,119 +70,94 @@ fun VocabularyExerciseHostScreen(
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(categoryIdsAsJson, stageNamesAsJson, languageIdsAsJson, dailyOnly) {
|
||||||
// Reset exercise state when starting fresh
|
Log.d("ExerciseHost", "LaunchedEffect filters: categories=$categoryIdsAsJson, stages=$stageNamesAsJson, languages=$languageIdsAsJson, dailyOnly=$dailyOnly")
|
||||||
exerciseViewModel.resetExercise()
|
// Only reset and prepare if the host is opened via explicit filters.
|
||||||
|
if (!categoryIdsAsJson.isNullOrBlank() || !stageNamesAsJson.isNullOrBlank() || !languageIdsAsJson.isNullOrBlank() || dailyOnly) {
|
||||||
vocabularyViewModel.prepareExercise(
|
Log.d("ExerciseHost", "Preparing exercise from filters")
|
||||||
categoryIdsAsJson,
|
exerciseViewModel.resetExercise()
|
||||||
stageNamesAsJson,
|
vocabularyViewModel.prepareExercise(
|
||||||
languageIdsAsJson,
|
categoryIdsAsJson,
|
||||||
dailyOnly = dailyOnly,
|
stageNamesAsJson,
|
||||||
)
|
languageIdsAsJson,
|
||||||
|
dailyOnly = dailyOnly,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Log.d("ExerciseHost", "No filters provided; skipping prepareExercise")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cardSet == null && screenState != ScreenState.START) {
|
LaunchedEffect(cardSet, screenState, pendingConfig) {
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Log.d("ExerciseHost", "State update: screenState=$screenState, cardSet=${cardSet?.cards?.size ?: 0}, pendingCount=${pendingConfig.exerciseItemCount}")
|
||||||
CircularProgressIndicator()
|
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 -> {
|
when (screenState) {
|
||||||
StartScreen(
|
ScreenState.START -> {
|
||||||
cardSet = cardSet,
|
Log.d("ExerciseHost", "Rendering START screen")
|
||||||
onStartClicked = { finalItems ->
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
exerciseViewModel.startExerciseWithConfig(
|
CircularProgressIndicator()
|
||||||
finalItems,
|
}
|
||||||
ExerciseConfig(
|
}
|
||||||
shuffleCards = shuffleCards,
|
ScreenState.EXERCISE -> {
|
||||||
shuffleLanguages = shuffleLanguages,
|
Log.d("ExerciseHost", "Rendering EXERCISE screen")
|
||||||
trainingMode = trainingMode,
|
ExerciseScreen(
|
||||||
dueTodayOnly = dueTodayOnly,
|
viewModel = exerciseViewModel,
|
||||||
selectedExerciseTypes = selectedExerciseTypes,
|
onClose = onClose,
|
||||||
exerciseItemCount = finalItems.size,
|
onFinish = { score, wrong ->
|
||||||
originalExerciseItems = finalItems,
|
@Suppress("AssignedValueIsNeverRead")
|
||||||
originLanguageId = selectedOriginLanguage?.nameResId,
|
finalScore = score
|
||||||
targetLanguageId = selectedTargetLanguage?.nameResId
|
@Suppress("AssignedValueIsNeverRead")
|
||||||
)
|
finalWrongAnswers = wrong
|
||||||
)
|
exerciseViewModel.finishExercise(score, wrong)
|
||||||
},
|
},
|
||||||
onClose = onClose,
|
navController = navController
|
||||||
shuffleCards = shuffleCards,
|
)
|
||||||
onShuffleCardsChanged = {
|
}
|
||||||
@Suppress("AssignedValueIsNeverRead")
|
ScreenState.RESULT -> {
|
||||||
shuffleCards = it
|
Log.d("ExerciseHost", "Rendering RESULT screen")
|
||||||
},
|
val totalItems by exerciseViewModel.totalItems.collectAsState()
|
||||||
shuffleLanguages = shuffleLanguages,
|
val originalItems by exerciseViewModel.originalItems.collectAsState()
|
||||||
onShuffleLanguagesChanged = {
|
ResultScreen(
|
||||||
@Suppress("AssignedValueIsNeverRead")
|
score = finalScore,
|
||||||
shuffleLanguages = it
|
wrongAnswers = finalWrongAnswers,
|
||||||
},
|
totalItems = totalItems,
|
||||||
trainingMode = trainingMode,
|
onRestart = {
|
||||||
onTrainingModeChanged = {
|
vocabularyViewModel.clearCardSet()
|
||||||
@Suppress("AssignedValueIsNeverRead")
|
exerciseViewModel.resetExercise()
|
||||||
trainingMode = it
|
},
|
||||||
exerciseViewModel.onTrainingModeChanged(it)
|
onRetryWrong = { _ ->
|
||||||
},
|
exerciseViewModel.retryWrongAnswers(originalItems)
|
||||||
hideTodayOnlySwitch = dailyOnly,
|
},
|
||||||
dueTodayOnly = dueTodayOnly,
|
onClose = {
|
||||||
onDueTodayOnlyChanged = {
|
navController.navigate(Screen.Home.route) {
|
||||||
@Suppress("AssignedValueIsNeverRead")
|
popUpTo(Screen.Home.route) { inclusive = true }
|
||||||
dueTodayOnly = it
|
launchSingleTop = true
|
||||||
},
|
|
||||||
selectedExerciseTypes = selectedExerciseTypes,
|
|
||||||
onExerciseTypeSelected = { type ->
|
|
||||||
val currentTypes = selectedExerciseTypes.toMutableSet()
|
|
||||||
if (type in currentTypes) {
|
|
||||||
if (currentTypes.size > 1) currentTypes.remove(type)
|
|
||||||
} else {
|
|
||||||
currentTypes.add(type)
|
|
||||||
}
|
|
||||||
selectedExerciseTypes = currentTypes
|
|
||||||
},
|
|
||||||
selectedOriginLanguage = selectedOriginLanguage,
|
|
||||||
onOriginLanguageChanged = {
|
|
||||||
@Suppress("AssignedValueIsNeverRead")
|
|
||||||
selectedOriginLanguage = it
|
|
||||||
},
|
|
||||||
selectedTargetLanguage = selectedTargetLanguage,
|
|
||||||
onTargetLanguageChanged = {
|
|
||||||
@Suppress("AssignedValueIsNeverRead")
|
|
||||||
selectedTargetLanguage = it
|
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
)
|
||||||
ScreenState.EXERCISE -> {
|
|
||||||
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.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
@@ -53,6 +51,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
import eu.gaudian.translator.view.composable.AppIcons
|
||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
@@ -68,6 +67,7 @@ import kotlinx.datetime.plus
|
|||||||
import kotlinx.datetime.todayIn
|
import kotlinx.datetime.todayIn
|
||||||
import java.time.YearMonth
|
import java.time.YearMonth
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.Locale.getDefault
|
||||||
import kotlin.math.log2
|
import kotlin.math.log2
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
import kotlin.time.ExperimentalTime
|
import kotlin.time.ExperimentalTime
|
||||||
@@ -95,7 +95,7 @@ fun VocabularyHeatmapScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.label_vocabulary_activity)) },
|
title = stringResource(R.string.label_vocabulary_activity),
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -263,7 +263,8 @@ private fun MonthHeader(
|
|||||||
Icon(AppIcons.ArrowLeft, contentDescription = stringResource(R.string.previous_month))
|
Icon(AppIcons.ArrowLeft, contentDescription = stringResource(R.string.previous_month))
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = month.format(formatter),
|
text = month.format(formatter)
|
||||||
|
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(getDefault()) else it.toString() },
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
modifier = Modifier.padding(horizontal = 16.dp)
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
@@ -283,7 +284,7 @@ private fun MonthGrid(
|
|||||||
) {
|
) {
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
val locale = java.util.Locale.getDefault()
|
val locale = getDefault()
|
||||||
// Generate localized short weekday labels for Monday to Sunday.
|
// Generate localized short weekday labels for Monday to Sunday.
|
||||||
val dayFormatter = remember(locale) {
|
val dayFormatter = remember(locale) {
|
||||||
DateTimeFormatter.ofPattern("EEEEE", locale)
|
DateTimeFormatter.ofPattern("EEEEE", locale)
|
||||||
@@ -385,7 +386,7 @@ private fun Legend(modifier: Modifier = Modifier) {
|
|||||||
Row(
|
Row(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.End
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.less),
|
text = stringResource(R.string.less),
|
||||||
@@ -467,12 +468,11 @@ fun StatsOverview(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun StatCard(title: String, value: String, modifier: Modifier = Modifier) {
|
private fun StatCard(title: String, value: String, modifier: Modifier = Modifier) {
|
||||||
Card(
|
AppCard(
|
||||||
modifier = modifier,
|
modifier = modifier.padding(0.dp),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(8.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -5,19 +5,13 @@ package eu.gaudian.translator.view.vocabulary
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.animation.animateContentSize
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.combinedClickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -25,14 +19,10 @@ import androidx.compose.foundation.layout.navigationBarsPadding
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.FabPosition
|
import androidx.compose.material3.FabPosition
|
||||||
@@ -43,7 +33,6 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.OutlinedCard
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
@@ -59,20 +48,15 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.rememberNavController
|
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.model.Language
|
import eu.gaudian.translator.model.Language
|
||||||
import eu.gaudian.translator.model.VocabularyItem
|
import eu.gaudian.translator.model.VocabularyItem
|
||||||
@@ -84,10 +68,10 @@ import eu.gaudian.translator.view.composable.AppScaffold
|
|||||||
import eu.gaudian.translator.view.composable.AppSwitch
|
import eu.gaudian.translator.view.composable.AppSwitch
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.view.composable.MultipleLanguageDropdown
|
import eu.gaudian.translator.view.composable.MultipleLanguageDropdown
|
||||||
import eu.gaudian.translator.view.composable.insertBreakOpportunities
|
|
||||||
import eu.gaudian.translator.view.dialogs.CategoryDropdown
|
import eu.gaudian.translator.view.dialogs.CategoryDropdown
|
||||||
import eu.gaudian.translator.view.dialogs.CategorySelectionDialog
|
import eu.gaudian.translator.view.dialogs.CategorySelectionDialog
|
||||||
import eu.gaudian.translator.view.dialogs.StageSelectionDialog
|
import eu.gaudian.translator.view.dialogs.StageSelectionDialog
|
||||||
|
import eu.gaudian.translator.view.library.AllCardsView
|
||||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||||
import eu.gaudian.translator.viewmodel.LanguageConfigViewModel
|
import eu.gaudian.translator.viewmodel.LanguageConfigViewModel
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||||
@@ -110,7 +94,7 @@ private data class VocabularyFilterState(
|
|||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VocabularyListScreen(
|
fun AllCardsListScreen(
|
||||||
categoryId: Int? = null,
|
categoryId: Int? = null,
|
||||||
showDueTodayOnly: Boolean? = null,
|
showDueTodayOnly: Boolean? = null,
|
||||||
stage: VocabularyStage? = null,
|
stage: VocabularyStage? = null,
|
||||||
@@ -245,11 +229,7 @@ fun VocabularyListScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
"search" -> SearchTopAppBar(
|
"search" -> SearchTopAppBar(
|
||||||
searchQuery = filterState.searchQuery,
|
onCloseSearch = {
|
||||||
onQueryChanged = { newQuery ->
|
|
||||||
filterState = filterState.copy(searchQuery = newQuery)
|
|
||||||
},
|
|
||||||
onCloseSearch = {
|
|
||||||
isSearchActive = false
|
isSearchActive = false
|
||||||
filterState = filterState.copy(searchQuery = "")
|
filterState = filterState.copy(searchQuery = "")
|
||||||
}
|
}
|
||||||
@@ -295,78 +275,40 @@ fun VocabularyListScreen(
|
|||||||
floatingActionButtonPosition = FabPosition.Center
|
floatingActionButtonPosition = FabPosition.Center
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(modifier = Modifier.padding(paddingValues).padding(bottom = 0.dp)) {
|
Column(modifier = Modifier.padding(paddingValues).padding(bottom = 0.dp)) {
|
||||||
if (vocabularyItems.isEmpty()) {
|
AllCardsView(
|
||||||
Column(
|
vocabularyItems = vocabularyItems,
|
||||||
modifier = Modifier
|
allLanguages = allLanguages,
|
||||||
.fillMaxSize()
|
selection = selection,
|
||||||
.padding(16.dp),
|
listState = lazyListState,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
modifier = Modifier
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
modifier = Modifier.size(200.dp),
|
|
||||||
painter = painterResource(id = R.drawable.ic_nothing_found),
|
|
||||||
contentDescription = stringResource(id = R.string.no_vocabulary_items_found_perhaps_try_changing_the_filters)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.size(16.dp))
|
|
||||||
|
|
||||||
Box(modifier = Modifier
|
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(8.dp), contentAlignment = Alignment.Center) {
|
.padding(horizontal = 8.dp),
|
||||||
Text(
|
onItemClick = { item ->
|
||||||
text = stringResource(R.string.no_vocabulary_items_found_perhaps_try_changing_the_filters),
|
val isSelected = selection.contains(item.id.toLong())
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
if (isInSelectionMode) {
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
selection = if (isSelected) {
|
||||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
selection - item.id.toLong()
|
||||||
)
|
} else {
|
||||||
}
|
selection + item.id.toLong()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
if (navController != null && enableNavigationButtons) {
|
||||||
state = lazyListState,
|
vocabularyViewModel.setNavigationContext(vocabularyItems, item.id)
|
||||||
modifier = Modifier
|
navController.navigate("vocabulary_detail/${item.id}")
|
||||||
.fillMaxSize()
|
} else {
|
||||||
.padding(horizontal = 8.dp),
|
onNavigateToItem(item)
|
||||||
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()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
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,
|
languageViewModel = languageViewModel,
|
||||||
languagesPresent = allLanguages.filter { it.nameResId in languagesPresent },
|
languagesPresent = allLanguages.filter { it.nameResId in languagesPresent },
|
||||||
hideCategory = categoryId != null && categoryId != 0,
|
hideCategory = categoryId != null && categoryId != 0,
|
||||||
hideStage = stage != null,
|
hideStage = stage != null
|
||||||
categoryViewModel = categoryViewModel
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,21 +358,34 @@ fun VocabularyListScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ThemePreviews
|
@Deprecated("Use AllCardsListScreen which renders AllCardsView")
|
||||||
@Composable
|
@Composable
|
||||||
fun VocabularyListScreenPreview() {
|
fun VocabularyListScreen(
|
||||||
val navController = rememberNavController()
|
categoryId: Int? = null,
|
||||||
VocabularyListScreen(
|
showDueTodayOnly: Boolean? = null,
|
||||||
categoryId = 1,
|
stage: VocabularyStage? = null,
|
||||||
showDueTodayOnly = false,
|
onNavigateToItem: (VocabularyItem) -> Unit?,
|
||||||
stage = VocabularyStage.NEW,
|
onNavigateBack: (() -> Unit)? = null,
|
||||||
onNavigateToItem = {},
|
navController: NavHostController? = null,
|
||||||
onNavigateBack = {},
|
itemsToShow: List<VocabularyItem> = emptyList(),
|
||||||
navController = navController
|
isRemoveFromCategoryEnabled: Boolean = false,
|
||||||
|
showTopBar: Boolean = true,
|
||||||
|
enableNavigationButtons: Boolean = false
|
||||||
|
) {
|
||||||
|
AllCardsListScreen(
|
||||||
|
categoryId = categoryId,
|
||||||
|
showDueTodayOnly = showDueTodayOnly,
|
||||||
|
stage = stage,
|
||||||
|
onNavigateToItem = onNavigateToItem,
|
||||||
|
onNavigateBack = onNavigateBack,
|
||||||
|
navController = navController,
|
||||||
|
itemsToShow = itemsToShow,
|
||||||
|
isRemoveFromCategoryEnabled = isRemoveFromCategoryEnabled,
|
||||||
|
showTopBar = showTopBar,
|
||||||
|
enableNavigationButtons = enableNavigationButtons
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DefaultTopAppBar(
|
private fun DefaultTopAppBar(
|
||||||
title: String,
|
title: String,
|
||||||
@@ -446,25 +400,8 @@ private fun DefaultTopAppBar(
|
|||||||
var showSortMenu by remember { mutableStateOf(false) }
|
var showSortMenu by remember { mutableStateOf(false) }
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
modifier = Modifier.height(56.dp),
|
modifier = Modifier.height(56.dp),
|
||||||
title = {
|
title = title,
|
||||||
|
onNavigateBack = onNavigateBack,
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxHeight(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(title)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
onNavigateBack?.let {
|
|
||||||
IconButton(onClick = it) {
|
|
||||||
Icon(
|
|
||||||
AppIcons.ArrowBack,
|
|
||||||
contentDescription = "stringResource(R.string.navigate_back)"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = onSearchClick) {
|
IconButton(onClick = onSearchClick) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -522,8 +459,6 @@ private fun DefaultTopAppBar(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SearchTopAppBar(
|
private fun SearchTopAppBar(
|
||||||
searchQuery: String,
|
|
||||||
onQueryChanged: (String) -> Unit,
|
|
||||||
onCloseSearch: () -> Unit
|
onCloseSearch: () -> Unit
|
||||||
) {
|
) {
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
@@ -534,37 +469,7 @@ private fun SearchTopAppBar(
|
|||||||
|
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
modifier = Modifier.height(56.dp),
|
modifier = Modifier.height(56.dp),
|
||||||
title = {
|
title = "TODO",
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxHeight(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
BasicTextField(
|
|
||||||
value = searchQuery,
|
|
||||||
onValueChange = onQueryChanged,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.focusRequester(focusRequester),
|
|
||||||
textStyle = MaterialTheme.typography.bodyLarge.copy(
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
),
|
|
||||||
singleLine = true,
|
|
||||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
|
||||||
decorationBox = { innerTextField ->
|
|
||||||
Box(contentAlignment = Alignment.CenterStart) {
|
|
||||||
if (searchQuery.isEmpty()) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.search_vocabulary),
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
innerTextField()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onCloseSearch) {
|
IconButton(onClick = onCloseSearch) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -582,8 +487,6 @@ private fun SearchTopAppBar(
|
|||||||
@Composable
|
@Composable
|
||||||
fun SearchTopAppBarPreview() {
|
fun SearchTopAppBarPreview() {
|
||||||
SearchTopAppBar(
|
SearchTopAppBar(
|
||||||
searchQuery = stringResource(R.string.search_query),
|
|
||||||
onQueryChanged = {},
|
|
||||||
onCloseSearch = {}
|
onCloseSearch = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -605,14 +508,7 @@ private fun ContextualTopAppBar(
|
|||||||
|
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
modifier = modifier.height(56.dp),
|
modifier = modifier.height(56.dp),
|
||||||
title = {
|
title = stringResource(R.string.d_selected, selectionCount),
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxHeight(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.d_selected, selectionCount))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onCloseClick) {
|
IconButton(onClick = onCloseClick) {
|
||||||
Icon(imageVector = AppIcons.Close, contentDescription = stringResource(R.string.label_close_selection_mode))
|
Icon(imageVector = AppIcons.Close, contentDescription = stringResource(R.string.label_close_selection_mode))
|
||||||
@@ -693,112 +589,6 @@ fun ContextualTopAppBarPreview() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun VocabularyListItem(
|
|
||||||
item: VocabularyItem,
|
|
||||||
allLanguages: List<Language>,
|
|
||||||
isSelected: Boolean,
|
|
||||||
onItemClick: () -> Unit,
|
|
||||||
onItemLongClick: () -> Unit,
|
|
||||||
onDeleteClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val languageMap = remember(allLanguages) { allLanguages.associateBy { it.nameResId } }
|
|
||||||
val langFirst = item.languageFirstId?.let { languageMap[it]?.name } ?: ""
|
|
||||||
val langSecond = item.languageSecondId?.let { languageMap[it]?.name } ?: ""
|
|
||||||
|
|
||||||
OutlinedCard(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(CardDefaults.shape)
|
|
||||||
.combinedClickable(
|
|
||||||
onClick = onItemClick,
|
|
||||||
onLongClick = onItemLongClick
|
|
||||||
)
|
|
||||||
.animateContentSize(),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = if (isSelected) 4.dp else 0.dp),
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer
|
|
||||||
else MaterialTheme.colorScheme.surface
|
|
||||||
),
|
|
||||||
border = BorderStroke(
|
|
||||||
width = if (isSelected) 1.5.dp else 1.dp,
|
|
||||||
color = if (isSelected) MaterialTheme.colorScheme.primary
|
|
||||||
else MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(horizontal = 4.dp, vertical = 6.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
|
||||||
) {
|
|
||||||
LanguageRow(word = item.wordFirst, language = langFirst)
|
|
||||||
HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f))
|
|
||||||
LanguageRow(word = item.wordSecond, language = langSecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.padding(4.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Crossfade(targetState = isSelected, label = "action-icon-fade") { selected ->
|
|
||||||
if (selected) {
|
|
||||||
Icon(
|
|
||||||
imageVector = AppIcons.Check,
|
|
||||||
contentDescription = "Selected",
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier.size(24.dp)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
IconButton(onClick = onDeleteClick) {
|
|
||||||
Icon(
|
|
||||||
imageVector = AppIcons.Delete,
|
|
||||||
contentDescription = stringResource(id = R.string.label_delete),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun LanguageRow(word: String, language: String) {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 2.dp)) {
|
|
||||||
Text(
|
|
||||||
text = insertBreakOpportunities(word),
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
modifier = Modifier.weight(0.7f)
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(8.dp))
|
|
||||||
Text(
|
|
||||||
text = language,
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
maxLines = 1,
|
|
||||||
modifier = Modifier.weight(0.3f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ThemePreviews
|
|
||||||
@Composable
|
|
||||||
fun LanguageRowPreview() {
|
|
||||||
LanguageRow(
|
|
||||||
word = "Hello",
|
|
||||||
language = "English"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun FilterSortBottomSheet(
|
private fun FilterSortBottomSheet(
|
||||||
@@ -808,8 +598,7 @@ private fun FilterSortBottomSheet(
|
|||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onApplyFilters: (VocabularyFilterState) -> Unit,
|
onApplyFilters: (VocabularyFilterState) -> Unit,
|
||||||
hideCategory: Boolean = false,
|
hideCategory: Boolean = false,
|
||||||
hideStage: Boolean = false,
|
hideStage: Boolean = false
|
||||||
categoryViewModel: CategoryViewModel
|
|
||||||
) {
|
) {
|
||||||
var selectedStage by rememberSaveable { mutableStateOf(currentFilterState.selectedStage) }
|
var selectedStage by rememberSaveable { mutableStateOf(currentFilterState.selectedStage) }
|
||||||
var dueTodayOnly by rememberSaveable { mutableStateOf(currentFilterState.dueTodayOnly) }
|
var dueTodayOnly by rememberSaveable { mutableStateOf(currentFilterState.dueTodayOnly) }
|
||||||
@@ -954,20 +743,3 @@ private fun FilterSortBottomSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ThemePreviews
|
|
||||||
@Composable
|
|
||||||
fun FilterSortBottomSheetPreview() {
|
|
||||||
val activity = LocalContext.current.findActivity()
|
|
||||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
|
||||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
|
||||||
FilterSortBottomSheet(
|
|
||||||
currentFilterState = VocabularyFilterState(),
|
|
||||||
languageViewModel = languageViewModel,
|
|
||||||
languagesPresent = emptyList(),
|
|
||||||
onDismiss = {},
|
|
||||||
onApplyFilters = {},
|
|
||||||
hideCategory = false,
|
|
||||||
hideStage = false,
|
|
||||||
categoryViewModel = categoryViewModel
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ fun VocabularySortingScreen(
|
|||||||
var showFilterMenu by remember { mutableStateOf(false) }
|
var showFilterMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.sort_new_vocabulary)) },
|
title = stringResource(R.string.sort_new_vocabulary),
|
||||||
actions = {
|
actions = {
|
||||||
Box {
|
Box {
|
||||||
IconButton(onClick = { showFilterMenu = true }) {
|
IconButton(onClick = { showFilterMenu = true }) {
|
||||||
@@ -231,11 +231,7 @@ fun VocabularySortingScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
hintContent = HintDefinition.SORTING.hint()
|
hintContent = HintDefinition.SORTING.hint()
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -299,7 +295,6 @@ fun VocabularySortingItem(
|
|||||||
val activity = LocalContext.current.findActivity()
|
val activity = LocalContext.current.findActivity()
|
||||||
val languageConfigViewModel: LanguageConfigViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
val languageConfigViewModel: LanguageConfigViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
|
||||||
var wordFirst by remember { mutableStateOf(item.wordFirst) }
|
var wordFirst by remember { mutableStateOf(item.wordFirst) }
|
||||||
var wordSecond by remember { mutableStateOf(item.wordSecond) }
|
var wordSecond by remember { mutableStateOf(item.wordSecond) }
|
||||||
var selectedCategories by remember { mutableStateOf<List<Int>>(emptyList()) }
|
var selectedCategories by remember { mutableStateOf<List<Int>>(emptyList()) }
|
||||||
@@ -314,7 +309,6 @@ fun VocabularySortingItem(
|
|||||||
var articlesLangSecond by remember { mutableStateOf(emptySet<String>()) }
|
var articlesLangSecond by remember { mutableStateOf(emptySet<String>()) }
|
||||||
|
|
||||||
var showDuplicateDialog by remember { mutableStateOf(false) }
|
var showDuplicateDialog by remember { mutableStateOf(false) }
|
||||||
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
|
||||||
|
|
||||||
// NEW: Calculate if the item is valid for the "Done" button in faulty mode
|
// NEW: Calculate if the item is valid for the "Done" button in faulty mode
|
||||||
val isItemNowValid by remember(wordFirst, wordSecond, langFirst, langSecond) {
|
val isItemNowValid by remember(wordFirst, wordSecond, langFirst, langSecond) {
|
||||||
|
|||||||
@@ -66,8 +66,6 @@ internal fun DraggableActionPanel(
|
|||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
isEditing: Boolean,
|
isEditing: Boolean,
|
||||||
onEditClick: () -> Unit,
|
onEditClick: () -> Unit,
|
||||||
onSaveClick: () -> Unit,
|
|
||||||
onCancelClick: () -> Unit,
|
|
||||||
onStatisticsClick: () -> Unit,
|
onStatisticsClick: () -> Unit,
|
||||||
onMoveToCategoryClick: () -> Unit,
|
onMoveToCategoryClick: () -> Unit,
|
||||||
onMoveToStageClick: () -> Unit,
|
onMoveToStageClick: () -> Unit,
|
||||||
@@ -175,13 +173,8 @@ internal fun DraggableActionPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEditing) {
|
|
||||||
ActionItem(icon = AppIcons.Check, label = stringResource(R.string.label_save), isExtended = isExtended, onClick = actionClickHandler(onSaveClick))
|
|
||||||
ActionItem(icon = AppIcons.Close, stringResource(R.string.label_cancel), isExtended = isExtended, onClick = actionClickHandler(onCancelClick))
|
|
||||||
} else {
|
|
||||||
ActionItem(icon = AppIcons.Edit, label = stringResource(R.string.edit), isExtended = isExtended, onClick = actionClickHandler(onEditClick))
|
|
||||||
}
|
|
||||||
if (!isEditing) {
|
if (!isEditing) {
|
||||||
|
ActionItem(icon = AppIcons.Edit, label = stringResource(R.string.edit), isExtended = isExtended, onClick = actionClickHandler(onEditClick))
|
||||||
|
|
||||||
if (showAnalyzeGrammarButton) {
|
if (showAnalyzeGrammarButton) {
|
||||||
ActionItem(
|
ActionItem(
|
||||||
@@ -252,8 +245,6 @@ fun DraggableActionPanelPreview() {
|
|||||||
onDismiss = {},
|
onDismiss = {},
|
||||||
isEditing = false,
|
isEditing = false,
|
||||||
onEditClick = {},
|
onEditClick = {},
|
||||||
onSaveClick = {},
|
|
||||||
onCancelClick = {},
|
|
||||||
|
|
||||||
onStatisticsClick = {},
|
onStatisticsClick = {},
|
||||||
onMoveToCategoryClick = {},
|
onMoveToCategoryClick = {},
|
||||||
|
|||||||
@@ -87,10 +87,58 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VocabularyCard(
|
fun VocabularyDisplayCard(
|
||||||
vocabularyItem: VocabularyItem,
|
vocabularyItem: VocabularyItem,
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
exerciseMode: Boolean,
|
isFlipped: Boolean,
|
||||||
|
switchOrder: Boolean,
|
||||||
|
onStatisticsClick: () -> Unit = {},
|
||||||
|
onMoveToCategoryClick: () -> Unit = {},
|
||||||
|
onMoveToStageClick: () -> Unit = {},
|
||||||
|
onDeleteClick: () -> Unit = {},
|
||||||
|
onEditStateChange: ((Boolean) -> Unit)? = null,
|
||||||
|
onEditActionHandlersReady: ((onSave: () -> Unit, onCancel: () -> Unit) -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
VocabularyCardContent(
|
||||||
|
vocabularyItem = vocabularyItem,
|
||||||
|
navController = navController,
|
||||||
|
isExerciseMode = false,
|
||||||
|
isFlipped = isFlipped,
|
||||||
|
switchOrder = switchOrder,
|
||||||
|
onStatisticsClick = onStatisticsClick,
|
||||||
|
onMoveToCategoryClick = onMoveToCategoryClick,
|
||||||
|
onMoveToStageClick = onMoveToStageClick,
|
||||||
|
onDeleteClick = onDeleteClick,
|
||||||
|
onEditStateChange = onEditStateChange,
|
||||||
|
onEditActionHandlersReady = onEditActionHandlersReady,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VocabularyExerciseCard(
|
||||||
|
vocabularyItem: VocabularyItem,
|
||||||
|
navController: NavController,
|
||||||
|
isFlipped: Boolean,
|
||||||
|
switchOrder: Boolean,
|
||||||
|
userSpellingAnswer: String? = null,
|
||||||
|
isUserSpellingCorrect: Boolean? = null,
|
||||||
|
) {
|
||||||
|
VocabularyCardContent(
|
||||||
|
vocabularyItem = vocabularyItem,
|
||||||
|
navController = navController,
|
||||||
|
isExerciseMode = true,
|
||||||
|
isFlipped = isFlipped,
|
||||||
|
switchOrder = switchOrder,
|
||||||
|
userSpellingAnswer = userSpellingAnswer,
|
||||||
|
isUserSpellingCorrect = isUserSpellingCorrect,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun VocabularyCardContent(
|
||||||
|
vocabularyItem: VocabularyItem,
|
||||||
|
navController: NavController,
|
||||||
|
isExerciseMode: Boolean,
|
||||||
isFlipped: Boolean,
|
isFlipped: Boolean,
|
||||||
switchOrder: Boolean,
|
switchOrder: Boolean,
|
||||||
onStatisticsClick: () -> Unit = {},
|
onStatisticsClick: () -> Unit = {},
|
||||||
@@ -99,6 +147,8 @@ fun VocabularyCard(
|
|||||||
onDeleteClick: () -> Unit = {},
|
onDeleteClick: () -> Unit = {},
|
||||||
userSpellingAnswer: String? = null,
|
userSpellingAnswer: String? = null,
|
||||||
isUserSpellingCorrect: Boolean? = null,
|
isUserSpellingCorrect: Boolean? = null,
|
||||||
|
onEditStateChange: ((Boolean) -> Unit)? = null,
|
||||||
|
onEditActionHandlersReady: ((onSave: () -> Unit, onCancel: () -> Unit) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val activity = LocalContext.current.findActivity()
|
val activity = LocalContext.current.findActivity()
|
||||||
@@ -201,6 +251,7 @@ fun VocabularyCard(
|
|||||||
)
|
)
|
||||||
vocabularyViewModel.editVocabularyItem(updatedItem)
|
vocabularyViewModel.editVocabularyItem(updatedItem)
|
||||||
isEditing = false
|
isEditing = false
|
||||||
|
onEditStateChange?.invoke(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,6 +264,7 @@ fun VocabularyCard(
|
|||||||
editedLangSecondId = item.languageSecondId
|
editedLangSecondId = item.languageSecondId
|
||||||
editedFeatures = item.features?.let { jsonParser.decodeFromString<VocabularyFeatures>(it) } ?: VocabularyFeatures()
|
editedFeatures = item.features?.let { jsonParser.decodeFromString<VocabularyFeatures>(it) } ?: VocabularyFeatures()
|
||||||
isEditing = false
|
isEditing = false
|
||||||
|
onEditStateChange?.invoke(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,13 +338,13 @@ fun VocabularyCard(
|
|||||||
onWordChange = { if (!switchOrder) editedWordFirst = it else editedWordSecond = it },
|
onWordChange = { if (!switchOrder) editedWordFirst = it else editedWordSecond = it },
|
||||||
language = if (!switchOrder) languageFirst else languageSecond,
|
language = if (!switchOrder) languageFirst else languageSecond,
|
||||||
onLanguageIdChange = { if (!switchOrder) editedLangFirstId = it else editedLangSecondId = it },
|
onLanguageIdChange = { if (!switchOrder) editedLangFirstId = it else editedLangSecondId = it },
|
||||||
isRevealed = isFrontFace || exerciseMode,
|
isRevealed = isFrontFace || isExerciseMode,
|
||||||
userSpellingAnswer = userSpellingAnswer,
|
userSpellingAnswer = userSpellingAnswer,
|
||||||
isUserSpellingCorrect = isUserSpellingCorrect,
|
isUserSpellingCorrect = isUserSpellingCorrect,
|
||||||
correctWord = if (switchOrder) item.wordFirst else item.wordSecond,
|
correctWord = if (switchOrder) item.wordFirst else item.wordSecond,
|
||||||
wordDetails = if (!switchOrder) editedFeatures.first else editedFeatures.second,
|
wordDetails = if (!switchOrder) editedFeatures.first else editedFeatures.second,
|
||||||
onEditGrammarClick = { showGrammarDialogFor = "first" },
|
onEditGrammarClick = { showGrammarDialogFor = "first" },
|
||||||
isExerciseMode = exerciseMode,
|
isExerciseMode = isExerciseMode,
|
||||||
vocabularyItem = item,
|
vocabularyItem = item,
|
||||||
onMoreClick = {
|
onMoreClick = {
|
||||||
@Suppress("HardCodedStringLiteral")
|
@Suppress("HardCodedStringLiteral")
|
||||||
@@ -317,7 +369,7 @@ fun VocabularyCard(
|
|||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
||||||
)
|
)
|
||||||
if (!exerciseMode && !isFlipped) {
|
if (!isExerciseMode && !isEditing && !isFlipped) {
|
||||||
IconButton(onClick = { showActionPanel = true }) {
|
IconButton(onClick = { showActionPanel = true }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = AppIcons.MoreVert,
|
imageVector = AppIcons.MoreVert,
|
||||||
@@ -339,7 +391,7 @@ fun VocabularyCard(
|
|||||||
onWordChange = { if (switchOrder) editedWordFirst = it else editedWordSecond = it },
|
onWordChange = { if (switchOrder) editedWordFirst = it else editedWordSecond = it },
|
||||||
language = if (switchOrder) languageFirst else languageSecond,
|
language = if (switchOrder) languageFirst else languageSecond,
|
||||||
onLanguageIdChange = { if (switchOrder) editedLangFirstId = it else editedLangSecondId = it },
|
onLanguageIdChange = { if (switchOrder) editedLangFirstId = it else editedLangSecondId = it },
|
||||||
isRevealed = !(!isFlipped && exerciseMode),
|
isRevealed = !(!isFlipped && isExerciseMode),
|
||||||
userSpellingAnswer = userSpellingAnswer,
|
userSpellingAnswer = userSpellingAnswer,
|
||||||
isUserSpellingCorrect = isUserSpellingCorrect,
|
isUserSpellingCorrect = isUserSpellingCorrect,
|
||||||
correctWord = if (switchOrder) item.wordFirst else item.wordSecond,
|
correctWord = if (switchOrder) item.wordFirst else item.wordSecond,
|
||||||
@@ -348,7 +400,7 @@ fun VocabularyCard(
|
|||||||
@Suppress("HardCodedStringLiteral")
|
@Suppress("HardCodedStringLiteral")
|
||||||
showGrammarDialogFor = "second"
|
showGrammarDialogFor = "second"
|
||||||
},
|
},
|
||||||
isExerciseMode = exerciseMode,
|
isExerciseMode = isExerciseMode,
|
||||||
vocabularyItem = item,
|
vocabularyItem = item,
|
||||||
onMoreClick = {
|
onMoreClick = {
|
||||||
@Suppress("HardCodedStringLiteral")
|
@Suppress("HardCodedStringLiteral")
|
||||||
@@ -361,7 +413,7 @@ fun VocabularyCard(
|
|||||||
|
|
||||||
|
|
||||||
!switchOrder
|
!switchOrder
|
||||||
if(isFlipped || !exerciseMode)
|
if(isFlipped || !isExerciseMode)
|
||||||
DraggableActionPanel(
|
DraggableActionPanel(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.CenterEnd)
|
.align(Alignment.CenterEnd)
|
||||||
@@ -369,9 +421,14 @@ fun VocabularyCard(
|
|||||||
isOpen = showActionPanel,
|
isOpen = showActionPanel,
|
||||||
onDismiss = { showActionPanel = false },
|
onDismiss = { showActionPanel = false },
|
||||||
isEditing = isEditing,
|
isEditing = isEditing,
|
||||||
onEditClick = { isEditing = true },
|
onEditClick = {
|
||||||
onSaveClick = { handleSave() },
|
isEditing = true
|
||||||
onCancelClick = handleCancel,
|
onEditStateChange?.invoke(true)
|
||||||
|
onEditActionHandlersReady?.invoke(
|
||||||
|
{ handleSave() },
|
||||||
|
{ handleCancel() }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
onStatisticsClick = onStatisticsClick,
|
onStatisticsClick = onStatisticsClick,
|
||||||
onMoveToCategoryClick = onMoveToCategoryClick,
|
onMoveToCategoryClick = onMoveToCategoryClick,
|
||||||
@@ -438,18 +495,15 @@ fun VocabularyCardPreview() {
|
|||||||
languageSecondId = R.string.language_2
|
languageSecondId = R.string.language_2
|
||||||
)
|
)
|
||||||
val navController = NavController(LocalContext.current)
|
val navController = NavController(LocalContext.current)
|
||||||
VocabularyCard(
|
VocabularyDisplayCard(
|
||||||
vocabularyItem = item,
|
vocabularyItem = item,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
exerciseMode = false,
|
|
||||||
isFlipped = false,
|
isFlipped = false,
|
||||||
switchOrder = false,
|
switchOrder = false,
|
||||||
onStatisticsClick = {},
|
onStatisticsClick = {},
|
||||||
onMoveToCategoryClick = {},
|
onMoveToCategoryClick = {},
|
||||||
onMoveToStageClick = {},
|
onMoveToStageClick = {},
|
||||||
onDeleteClick = {},
|
onDeleteClick = {},
|
||||||
userSpellingAnswer = null,
|
|
||||||
isUserSpellingCorrect = null
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,7 +529,7 @@ private fun FrequencyPill(zipfFrequency: Float?) {
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 4.dp)
|
.padding(horizontal = 4.dp)
|
||||||
.width(80.dp),
|
.width(100.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
|
|||||||
@@ -1,200 +0,0 @@
|
|||||||
package eu.gaudian.translator.view.vocabulary.widgets
|
|
||||||
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedButton
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.scale
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import eu.gaudian.translator.R
|
|
||||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A modern, visually appealing set of start buttons for exercises.
|
|
||||||
* The public signature is identical to the original for drop-in replacement.
|
|
||||||
*
|
|
||||||
* @param onCustomClick Lambda for the primary custom exercise action.
|
|
||||||
* @param onDailyClick Lambda for daily exercises. It's called with `false` for a
|
|
||||||
* normal daily exercise and `true` for a daily spelling exercise.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun ModernStartButtons(
|
|
||||||
onCustomClick: () -> Unit,
|
|
||||||
onDailyClick: (isSpelling: Boolean) -> Unit
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 24.dp, vertical = 16.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(20.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
// A large, prominent "feature button" for the main call to action.
|
|
||||||
FeatureButton(
|
|
||||||
text = stringResource(R.string.text_custom_exercise),
|
|
||||||
icon = AppIcons.PlayCircleFilled,
|
|
||||||
onClick = onCustomClick,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
|
|
||||||
// A column for the two secondary "daily" actions.
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
SecondaryButton(
|
|
||||||
text = stringResource(R.string.text_daily_exercise),
|
|
||||||
icon = AppIcons.Today,
|
|
||||||
onClick = { onDailyClick(false) }
|
|
||||||
)
|
|
||||||
|
|
||||||
SecondaryButton(
|
|
||||||
text = stringResource(R.string.quick_word_pairs),
|
|
||||||
icon = AppIcons.SwapHoriz,
|
|
||||||
onClick = { onDailyClick(true) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A visually rich feature button with a gradient background and a subtle
|
|
||||||
* press animation. Designed to be the primary call to action.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun FeatureButton(
|
|
||||||
text: String,
|
|
||||||
icon: ImageVector,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
|
||||||
val isPressed by interactionSource.collectIsPressedAsState()
|
|
||||||
@Suppress("HardCodedStringLiteral") val scale by animateFloatAsState(targetValue = if (isPressed) 0.95f else 1f, label = "label_scale"
|
|
||||||
)
|
|
||||||
|
|
||||||
Card(
|
|
||||||
modifier = modifier
|
|
||||||
.aspectRatio(1f)
|
|
||||||
.scale(scale)
|
|
||||||
.clickable(
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
indication = null,
|
|
||||||
onClick = onClick
|
|
||||||
),
|
|
||||||
shape = MaterialTheme.shapes.extraLarge,
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(
|
|
||||||
Brush.linearGradient(
|
|
||||||
colors = listOf(
|
|
||||||
MaterialTheme.colorScheme.primaryContainer,
|
|
||||||
MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
modifier = Modifier.padding(16.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(48.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.onPrimary
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
style = MaterialTheme.typography.titleMedium.copy(
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontSize = 14.sp
|
|
||||||
),
|
|
||||||
color = MaterialTheme.colorScheme.onPrimary,
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A clean and simple OutlinedButton for secondary actions, with an icon and text.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun SecondaryButton(
|
|
||||||
text: String,
|
|
||||||
icon: ImageVector,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
OutlinedButton(
|
|
||||||
onClick = onClick,
|
|
||||||
modifier = modifier.fillMaxWidth(),
|
|
||||||
shape = MaterialTheme.shapes.large,
|
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp),
|
|
||||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.5f))
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier.size(20.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.size(8.dp))
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@ThemePreviews
|
|
||||||
@Composable
|
|
||||||
private fun ModernStartButtonsPreview() {
|
|
||||||
ModernStartButtons(
|
|
||||||
onCustomClick = {},
|
|
||||||
onDailyClick = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -995,7 +995,7 @@ class DictionaryViewModel @Inject constructor(
|
|||||||
* Returns true if data is still loading (null).
|
* Returns true if data is still loading (null).
|
||||||
*/
|
*/
|
||||||
fun getStructuredDictionaryDataLoading(entry: DictionaryWordEntry): StateFlow<Boolean> {
|
fun getStructuredDictionaryDataLoading(entry: DictionaryWordEntry): StateFlow<Boolean> {
|
||||||
val key = entry.word + "_" + entry.langCode
|
entry.word + "_" + entry.langCode
|
||||||
// Create a derived flow that emits true when data is null
|
// Create a derived flow that emits true when data is null
|
||||||
val dataFlow = getStructuredDictionaryDataState(entry)
|
val dataFlow = getStructuredDictionaryDataState(entry)
|
||||||
val loadingFlow = MutableStateFlow(true)
|
val loadingFlow = MutableStateFlow(true)
|
||||||
|
|||||||
@@ -209,13 +209,6 @@ class ExerciseViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startAdHocExercise(exercise: Exercise, questions: List<Question>) {
|
|
||||||
_exerciseSessionState.value = ExerciseSessionState(
|
|
||||||
exercise = exercise,
|
|
||||||
questions = questions
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startExercise(exercise: Exercise) {
|
fun startExercise(exercise: Exercise) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val allQuestions = exerciseRepository.getAllQuestionsFlow().first()
|
val allQuestions = exerciseRepository.getAllQuestionsFlow().first()
|
||||||
|
|||||||
@@ -90,12 +90,20 @@ class ProgressViewModel @Inject constructor(
|
|||||||
private val _totalWordsInProgress = MutableStateFlow(0)
|
private val _totalWordsInProgress = MutableStateFlow(0)
|
||||||
val totalWordsInProgress: StateFlow<Int> = _totalWordsInProgress.asStateFlow()
|
val totalWordsInProgress: StateFlow<Int> = _totalWordsInProgress.asStateFlow()
|
||||||
|
|
||||||
|
private val _totalWords = MutableStateFlow(0)
|
||||||
|
val totalWords: StateFlow<Int> = _totalWords.asStateFlow()
|
||||||
|
|
||||||
private val _weeklyActivityStats = MutableStateFlow<List<WeeklyActivityStat>>(emptyList())
|
private val _weeklyActivityStats = MutableStateFlow<List<WeeklyActivityStat>>(emptyList())
|
||||||
val weeklyActivityStats: StateFlow<List<WeeklyActivityStat>> = _weeklyActivityStats.asStateFlow()
|
val weeklyActivityStats: StateFlow<List<WeeklyActivityStat>> = _weeklyActivityStats.asStateFlow()
|
||||||
|
|
||||||
private val _dailyVocabularyStats = MutableStateFlow<Map<LocalDate, Int>>(emptyMap())
|
private val _dailyVocabularyStats = MutableStateFlow<Map<LocalDate, Int>>(emptyMap())
|
||||||
val dailyVocabularyStats: StateFlow<Map<LocalDate, Int>> = _dailyVocabularyStats.asStateFlow()
|
val dailyVocabularyStats: StateFlow<Map<LocalDate, Int>> = _dailyVocabularyStats.asStateFlow()
|
||||||
|
|
||||||
|
private val _dailyGoal = MutableStateFlow(10)
|
||||||
|
val dailyGoal: StateFlow<Int> = _dailyGoal.asStateFlow()
|
||||||
|
|
||||||
|
private val _todayCompletedCount = MutableStateFlow(0)
|
||||||
|
val todayCompletedCount: StateFlow<Int> = _todayCompletedCount.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -255,6 +263,15 @@ class ProgressViewModel @Inject constructor(
|
|||||||
try {
|
try {
|
||||||
loadSelectedCategories()
|
loadSelectedCategories()
|
||||||
try {
|
try {
|
||||||
|
// Load daily goal setting
|
||||||
|
val dailyGoalValue = settingsRepository.dailyGoal.flow.first()
|
||||||
|
_dailyGoal.value = dailyGoalValue
|
||||||
|
|
||||||
|
// Get today's completed count
|
||||||
|
val today = kotlin.time.Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
|
||||||
|
val todayCompleted = vocabularyRepository.getCorrectAnswerCountForDate(today)
|
||||||
|
_todayCompletedCount.value = todayCompleted
|
||||||
|
|
||||||
val progressDeferred = viewModelScope.async { vocabularyRepository.calculateCategoryProgress() }
|
val progressDeferred = viewModelScope.async { vocabularyRepository.calculateCategoryProgress() }
|
||||||
val lastSevenDaysDeferred = viewModelScope.async { getLastSevenDays() }
|
val lastSevenDaysDeferred = viewModelScope.async { getLastSevenDays() }
|
||||||
val streakDeferred = viewModelScope.async { calculateDailyStreak() }
|
val streakDeferred = viewModelScope.async { calculateDailyStreak() }
|
||||||
@@ -270,6 +287,8 @@ class ProgressViewModel @Inject constructor(
|
|||||||
.filter { it.stage != VocabularyStage.LEARNED && it.stage != VocabularyStage.NEW }
|
.filter { it.stage != VocabularyStage.LEARNED && it.stage != VocabularyStage.NEW }
|
||||||
.sumOf { it.itemCount }
|
.sumOf { it.itemCount }
|
||||||
|
|
||||||
|
_totalWords.value = stageList.sumOf { it.itemCount }
|
||||||
|
|
||||||
if (_selectedCategories.value.isEmpty() && progressList.isNotEmpty()) {
|
if (_selectedCategories.value.isEmpty() && progressList.isNotEmpty()) {
|
||||||
val initialCategory = setOf(progressList.first().vocabularyCategory.id)
|
val initialCategory = setOf(progressList.first().vocabularyCategory.id)
|
||||||
_selectedCategories.value = initialCategory
|
_selectedCategories.value = initialCategory
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("HardCodedStringLiteral")
|
||||||
|
|
||||||
package eu.gaudian.translator.viewmodel
|
package eu.gaudian.translator.viewmodel
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
@@ -31,8 +33,8 @@ enum class ScreenState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class ExerciseConfig(
|
data class ExerciseConfig(
|
||||||
val shuffleCards: Boolean = false,
|
val shuffleCards: Boolean = true,
|
||||||
val shuffleLanguages: Boolean = false,
|
val shuffleLanguages: Boolean = true,
|
||||||
val trainingMode: Boolean = false,
|
val trainingMode: Boolean = false,
|
||||||
val dueTodayOnly: Boolean = false,
|
val dueTodayOnly: Boolean = false,
|
||||||
val selectedExerciseTypes: Set<VocabularyExerciseType> = setOf(VocabularyExerciseType.GUESSING),
|
val selectedExerciseTypes: Set<VocabularyExerciseType> = setOf(VocabularyExerciseType.GUESSING),
|
||||||
@@ -90,6 +92,9 @@ class VocabularyExerciseViewModel @Inject constructor(
|
|||||||
|
|
||||||
// Exercise configuration state
|
// Exercise configuration state
|
||||||
private val _exerciseConfig = MutableStateFlow(ExerciseConfig())
|
private val _exerciseConfig = MutableStateFlow(ExerciseConfig())
|
||||||
|
private val _pendingExerciseConfig = MutableStateFlow(ExerciseConfig())
|
||||||
|
|
||||||
|
val pendingExerciseConfig: StateFlow<ExerciseConfig> = _pendingExerciseConfig.asStateFlow()
|
||||||
|
|
||||||
// Exercise results state
|
// Exercise results state
|
||||||
private val _exerciseResults = MutableStateFlow(ExerciseResults())
|
private val _exerciseResults = MutableStateFlow(ExerciseResults())
|
||||||
@@ -106,6 +111,7 @@ class VocabularyExerciseViewModel @Inject constructor(
|
|||||||
types: Set<VocabularyExerciseType>,
|
types: Set<VocabularyExerciseType>,
|
||||||
shuffleLanguages: Boolean
|
shuffleLanguages: Boolean
|
||||||
) {
|
) {
|
||||||
|
Log.d("ExerciseVM", "startExercise called: items=${items.size}, types=$types, shuffleLanguages=$shuffleLanguages")
|
||||||
// Reset counters for the new exercise session
|
// Reset counters for the new exercise session
|
||||||
_correctAnswers.value = 0
|
_correctAnswers.value = 0
|
||||||
_wrongAnswers.value = 0
|
_wrongAnswers.value = 0
|
||||||
@@ -158,6 +164,7 @@ class VocabularyExerciseViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun loadExercise() {
|
private fun loadExercise() {
|
||||||
|
Log.d("ExerciseVM", "loadExercise: index=$currentIndex, total=${currentItems.size}")
|
||||||
if (currentIndex < currentItems.size) {
|
if (currentIndex < currentItems.size) {
|
||||||
// Ensure item categories align with exercise type by attempting a swap instead of replacement
|
// Ensure item categories align with exercise type by attempting a swap instead of replacement
|
||||||
val randomType = exerciseTypes.random()
|
val randomType = exerciseTypes.random()
|
||||||
@@ -273,8 +280,10 @@ class VocabularyExerciseViewModel @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Log.d("ExerciseVM", "exerciseState set: type=$randomType, itemId=${itemToUse.id}")
|
||||||
} else {
|
} else {
|
||||||
_exerciseState.value = null // End of exercise
|
_exerciseState.value = null // End of exercise
|
||||||
|
Log.d("ExerciseVM", "loadExercise: end of exercise, state cleared")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,27 +399,33 @@ class VocabularyExerciseViewModel @Inject constructor(
|
|||||||
loadExercise()
|
loadExercise()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTrainingModeChanged(value: Boolean) {
|
|
||||||
_trainingMode.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startExerciseWithConfig(
|
fun startExerciseWithConfig(
|
||||||
items: List<VocabularyItem>,
|
items: List<VocabularyItem>,
|
||||||
config: ExerciseConfig
|
config: ExerciseConfig
|
||||||
) {
|
) {
|
||||||
|
Log.d("ExerciseVM", "startExerciseWithConfig called: items=${items.size}, configCount=${config.exerciseItemCount}, shuffleCards=${config.shuffleCards}, shuffleLanguages=${config.shuffleLanguages}, trainingMode=${config.trainingMode}, dueTodayOnly=${config.dueTodayOnly}, types=${config.selectedExerciseTypes}")
|
||||||
_exerciseConfig.value = config
|
_exerciseConfig.value = config
|
||||||
|
_pendingExerciseConfig.value = config
|
||||||
_totalItems.value = items.size
|
_totalItems.value = items.size
|
||||||
_originalItems.value = items
|
_originalItems.value = items
|
||||||
startExercise(items, config.selectedExerciseTypes, config.shuffleLanguages)
|
startExercise(items, config.selectedExerciseTypes, config.shuffleLanguages)
|
||||||
_screenState.value = ScreenState.EXERCISE
|
_screenState.value = ScreenState.EXERCISE
|
||||||
|
Log.d("ExerciseVM", "screenState set to EXERCISE; totalItems=${_totalItems.value}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updatePendingExerciseConfig(config: ExerciseConfig) {
|
||||||
|
Log.d("ExerciseVM", "updatePendingExerciseConfig: count=${config.exerciseItemCount}, shuffleCards=${config.shuffleCards}, shuffleLanguages=${config.shuffleLanguages}, trainingMode=${config.trainingMode}, dueTodayOnly=${config.dueTodayOnly}, types=${config.selectedExerciseTypes}")
|
||||||
|
_pendingExerciseConfig.value = config
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun finishExercise(score: Int, wrongAnswers: Int) {
|
fun finishExercise(score: Int, wrongAnswers: Int) {
|
||||||
_exerciseResults.value = ExerciseResults(score, wrongAnswers)
|
_exerciseResults.value = ExerciseResults(score, wrongAnswers)
|
||||||
_screenState.value = ScreenState.RESULT
|
_screenState.value = ScreenState.RESULT
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetExercise() {
|
fun resetExercise() {
|
||||||
|
Log.d("ExerciseVM", "resetExercise called")
|
||||||
_screenState.value = ScreenState.START
|
_screenState.value = ScreenState.START
|
||||||
_exerciseConfig.value = ExerciseConfig()
|
_exerciseConfig.value = ExerciseConfig()
|
||||||
_exerciseResults.value = ExerciseResults()
|
_exerciseResults.value = ExerciseResults()
|
||||||
@@ -420,6 +435,7 @@ class VocabularyExerciseViewModel @Inject constructor(
|
|||||||
_exerciseState.value = null
|
_exerciseState.value = null
|
||||||
_totalItems.value = 0
|
_totalItems.value = 0
|
||||||
_originalItems.value = emptyList()
|
_originalItems.value = emptyList()
|
||||||
|
Log.d("ExerciseVM", "resetExercise completed; screenState=START")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun retryWrongAnswers(originalItems: List<VocabularyItem>) {
|
fun retryWrongAnswers(originalItems: List<VocabularyItem>) {
|
||||||
|
|||||||
@@ -617,6 +617,56 @@ class VocabularyViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun filterVocabularyItemsByPairs(
|
||||||
|
languagePairs: List<Pair<Int, Int>>?,
|
||||||
|
query: String?,
|
||||||
|
categoryIds: List<Int>?,
|
||||||
|
stages: List<VocabularyStage>?,
|
||||||
|
wordClass: String? = null,
|
||||||
|
dueTodayOnly: Boolean = false,
|
||||||
|
sortOrder: SortOrder
|
||||||
|
): Flow<List<VocabularyItem>> {
|
||||||
|
val baseFlow = filterVocabularyItems(
|
||||||
|
languages = null,
|
||||||
|
query = query,
|
||||||
|
categoryIds = categoryIds,
|
||||||
|
stage = null,
|
||||||
|
wordClass = wordClass,
|
||||||
|
dueTodayOnly = dueTodayOnly,
|
||||||
|
sortOrder = sortOrder
|
||||||
|
)
|
||||||
|
|
||||||
|
val normalizedPairs = languagePairs
|
||||||
|
?.map { pair -> if (pair.first < pair.second) pair else pair.second to pair.first }
|
||||||
|
?.toSet()
|
||||||
|
|
||||||
|
return combine(baseFlow, stageMapping) { items, stageMap ->
|
||||||
|
var filteredItems = items
|
||||||
|
|
||||||
|
if (!normalizedPairs.isNullOrEmpty()) {
|
||||||
|
filteredItems = filteredItems.filter { item ->
|
||||||
|
val firstId = item.languageFirstId
|
||||||
|
val secondId = item.languageSecondId
|
||||||
|
if (firstId == null || secondId == null) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
val normalizedPair = if (firstId < secondId) firstId to secondId else secondId to firstId
|
||||||
|
normalizedPairs.contains(normalizedPair)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stages.isNullOrEmpty()) {
|
||||||
|
filteredItems = filteredItems.filter { item ->
|
||||||
|
val stage = stageMap[item.id] ?: VocabularyStage.NEW
|
||||||
|
stage in stages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredItems
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun generateVocabularyItems(category: String, amount: Int) {
|
suspend fun generateVocabularyItems(category: String, amount: Int) {
|
||||||
val selectedSourceLanguage = languageRepository.loadSelectedSourceLanguage().first()
|
val selectedSourceLanguage = languageRepository.loadSelectedSourceLanguage().first()
|
||||||
val selectedTargetLanguage = languageRepository.loadSelectedTargetLanguage().first()
|
val selectedTargetLanguage = languageRepository.loadSelectedTargetLanguage().first()
|
||||||
@@ -664,6 +714,7 @@ class VocabularyViewModel @Inject constructor(
|
|||||||
languageIdsAsJson: String?,
|
languageIdsAsJson: String?,
|
||||||
dailyOnly: Boolean = false
|
dailyOnly: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
Log.d("VocabularyVM", "prepareExercise: categories=$categoryIdsAsJson, stages=$stageNamesAsJson, languages=$languageIdsAsJson, dailyOnly=$dailyOnly")
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val categoryList = categoryIdsAsJson?.takeIf { it.isNotBlank() }
|
val categoryList = categoryIdsAsJson?.takeIf { it.isNotBlank() }
|
||||||
?.split(",")
|
?.split(",")
|
||||||
@@ -682,6 +733,7 @@ class VocabularyViewModel @Inject constructor(
|
|||||||
allLangs.filter { it.nameResId in ids }
|
allLangs.filter { it.nameResId in ids }
|
||||||
} ?: emptyList()
|
} ?: emptyList()
|
||||||
|
|
||||||
|
Log.d("VocabularyVM", "prepareExercise parsed: categories=${categoryList.size}, stages=${stageList.size}, languages=${languageList.size}")
|
||||||
loadCardSet(categoryList, stageList, languageList, dailyOnly)
|
loadCardSet(categoryList, stageList, languageList, dailyOnly)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -693,6 +745,7 @@ class VocabularyViewModel @Inject constructor(
|
|||||||
languages: List<Language>? = null,
|
languages: List<Language>? = null,
|
||||||
dailyOnly: Boolean = false
|
dailyOnly: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
Log.d(TAG, "loadCardSet invoked: categories=${categories?.size ?: 0}, stages=${stages?.size ?: 0}, languages=${languages?.map { it.nameResId }}, dailyOnly=$dailyOnly")
|
||||||
Log.d(TAG, "Loading card set with languages: $languages, categories: ${categories?.size}, stages: ${stages?.size}")
|
Log.d(TAG, "Loading card set with languages: $languages, categories: ${categories?.size}, stages: ${stages?.size}")
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
statusService.showLoadingMessage("Loading card set")
|
statusService.showLoadingMessage("Loading card set")
|
||||||
@@ -752,6 +805,8 @@ class VocabularyViewModel @Inject constructor(
|
|||||||
dueTodayOnly = dailyOnly
|
dueTodayOnly = dailyOnly
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
|
Log.d(TAG, "loadCardSet: filterVocabularyItems returned ${filteredItems.size} items")
|
||||||
|
|
||||||
Log.d(TAG, "loadCardSet: Filtering completed, found ${filteredItems.size} items")
|
Log.d(TAG, "loadCardSet: Filtering completed, found ${filteredItems.size} items")
|
||||||
|
|
||||||
if (filteredItems.isNotEmpty()) {
|
if (filteredItems.isNotEmpty()) {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
@@ -1,10 +1,23 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string-array name="exercise_example_prompts">Exercise Example Prompts</string-array>
|
|
||||||
<string-array name="changelog_entries">
|
<string-array name="changelog_entries">
|
||||||
<item>Version 0.3.0 \n• CSV-Import für Vokabeln aktiviert\n• Option, für einige unterstützte Sprachen einen Übersetzungsserver statt KI-Modelle zu nutzen\n• UI-Fehlerbehebungen \n• Anzeige der Worthäufigkeit \n• Leistungsoptimierungen \n• Verbesserte Übersetzungen (Deutsch und Portugiesisch)</item>
|
<item>Version 0.3.0 \n• CSV-Import für Vokabeln aktiviert\n• Option, für einige unterstützte Sprachen einen Übersetzungsserver, statt KI-Modelle zu nutzen\n• UI-Fehlerbehebungen \n• Anzeige der Worthäufigkeit \n• Leistungsoptimierungen \n• Verbesserte Übersetzungen (Deutsch und Portugiesisch)</item>
|
||||||
<item>Version 0.4.0 \n• Wörterbuch-Download hinzugefügt (Beta) \n• Verbesserungen der Benutzeroberfläche \n• Fehlerbehebungen \n• Neugestaltete Vokabelkarte mit verbesserter UI \n• Mehr vorkonfigurierte Anbieter \n• Verbesserte Leistung</item>
|
<item>Version 0.4.0 \n• Wörterbuch-Download hinzugefügt (Beta) \n• Verbesserungen der Benutzeroberfläche \n• Fehlerbehebungen \n• Neugestaltete Vokabelkarte mit verbesserter UI \n• Mehr vorkonfigurierte Anbieter \n• Verbesserte Leistung</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="dictionary_content">
|
||||||
|
<item>Wortart und Genus (bei Nomen)</item>
|
||||||
|
<item>Deklination (bei Nomen)</item>
|
||||||
|
<item>Aussprache (IPA) und Worttrennung</item>
|
||||||
|
<item>Definition</item>
|
||||||
|
<item>Herkunft</item>
|
||||||
|
<item>Synonyme</item>
|
||||||
|
<item>Antonyme</item>
|
||||||
|
<item>Beispiele</item>
|
||||||
|
<item>Konjugation (bei Verben)</item>
|
||||||
|
<item>Redewendungen</item>
|
||||||
|
<item>Grammatikalische Merkmale (bei Präpositionen)</item>
|
||||||
|
</string-array>
|
||||||
<!-- Stable, non-localized keys for dictionary content options. Order must match dictionary_content. -->
|
<!-- Stable, non-localized keys for dictionary content options. Order must match dictionary_content. -->
|
||||||
<string-array name="dictionary_content_keys">
|
<string-array name="dictionary_content_keys">
|
||||||
<item>word_class_gender</item>
|
<item>word_class_gender</item>
|
||||||
@@ -19,35 +32,87 @@
|
|||||||
<item>idioms</item>
|
<item>idioms</item>
|
||||||
<item>grammatical_features_prepositions</item>
|
<item>grammatical_features_prepositions</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="dictionary_content">
|
|
||||||
<item>Wortart und Genus (bei Nomen)</item>
|
<string-array name="example_prompts">
|
||||||
<item>Deklination (bei Nomen)</item>
|
<item>Alles übersetzen, ohne etwas hinzuzufügen.</item>
|
||||||
<item>Aussprache (IPA) und Worttrennung</item>
|
|
||||||
<item>Definition</item>
|
|
||||||
<item>Herkunft</item>
|
|
||||||
<item>Synonyme</item>
|
|
||||||
<item>Antonyme</item>
|
|
||||||
<item>Beispiele</item>
|
|
||||||
<item>Konjugation (bei Verben)</item>
|
|
||||||
<item>Redewendungen</item>
|
|
||||||
<item>Grammatikalische Merkmale (bei Präpositionen)</item>
|
|
||||||
</string-array>
|
|
||||||
<string-array name="example_prompts"><item>Alles übersetzen, ohne etwas hinzuzufügen.</item>
|
|
||||||
<item>Ersetze höfliche Pronomen Sie (formell) durch "du".</item>
|
<item>Ersetze höfliche Pronomen Sie (formell) durch "du".</item>
|
||||||
<item>Mach es sehr formell.</item>
|
<item>Mach es sehr formell.</item>
|
||||||
<item>Mach es informell und füge ein Emoji hinzu.</item>
|
<item>Mach es informell und füge ein Emoji hinzu.</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="vocabulary_hints"><item>Grundlegende Begrüßungen</item>
|
|
||||||
<item>Unregelmäßige Verben</item>
|
<string-array name="exercise_example_prompts">Exercise Example Prompts</string-array>
|
||||||
<item>Vokabular am Flughafen</item>
|
|
||||||
<item>Wie man einen Kaffee bestellt</item>
|
<string-array name="motivational_phrases">
|
||||||
<item>Idiomatische Ausdrücke</item>
|
<item>Die Grenzen deiner Sprache sind die Grenzen deiner Welt. Lass uns den Käfig etwas größer machen.</item>
|
||||||
</string-array>
|
<item>Du bist im Grunde ein Fleisch-Computer, der eine Sprachsimulation am Laufen hat. Zeit für ein Firmware-Upgrade.</item>
|
||||||
<string-array name="vocabulary_example_prompts"><item>Verwende lateinamerikanisches Spanisch</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 lange Wörter</item>
|
||||||
<item>Vermeide Sätze</item>
|
<item>Vermeide Sätze</item>
|
||||||
<item>Enthält viele Verben und Adjektive</item>
|
<item>Enthält viele Verben und Adjektive</item>
|
||||||
<item>Verwende informelle Sprache</item>
|
<item>Verwende informelle Sprache</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
<string-array name="vocabulary_hints">
|
||||||
|
<item>Grundlegende Begrüßungen</item>
|
||||||
|
<item>Unregelmäßige Verben</item>
|
||||||
|
<item>Vokabular am Flughafen</item>
|
||||||
|
<item>Wie man einen Kaffee bestellt</item>
|
||||||
|
<item>Idiomatische Ausdrücke</item>
|
||||||
|
</string-array>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,6 @@
|
|||||||
<string name="title_show_success_message">Erfolgsmeldung anzeigen</string>
|
<string name="title_show_success_message">Erfolgsmeldung anzeigen</string>
|
||||||
<string name="label_add_category">Kategorie hinzufügen</string>
|
<string name="label_add_category">Kategorie hinzufügen</string>
|
||||||
<string name="title_settings">Einstellungen</string>
|
<string name="title_settings">Einstellungen</string>
|
||||||
<string name="title_dashboard">Dashboard</string>
|
|
||||||
<string name="title_developer_options">Entwickleroptionen</string>
|
<string name="title_developer_options">Entwickleroptionen</string>
|
||||||
<string name="title_multiple">Mehrere</string>
|
<string name="title_multiple">Mehrere</string>
|
||||||
<string name="label_translation_settings">Übersetzung</string>
|
<string name="label_translation_settings">Übersetzung</string>
|
||||||
@@ -61,7 +60,6 @@
|
|||||||
<string name="error_no_rows_to_import">Keine Zeilen zum Importieren. Bitte Spalten und Kopfzeile prüfen.</string>
|
<string name="error_no_rows_to_import">Keine Zeilen zum Importieren. Bitte Spalten und Kopfzeile prüfen.</string>
|
||||||
<string name="info_imported_items_from">%1$d Vokabeln importiert.</string>
|
<string name="info_imported_items_from">%1$d Vokabeln importiert.</string>
|
||||||
<string name="label_import">Importieren</string>
|
<string name="label_import">Importieren</string>
|
||||||
<string name="menu_import_vocabulary">Vokabular mit KI erstellen</string>
|
|
||||||
<string name="menu_create_youtube_exercise">YouTube-Übung erstellen</string>
|
<string name="menu_create_youtube_exercise">YouTube-Übung erstellen</string>
|
||||||
<string name="text_youtube_link">YouTube-Link</string>
|
<string name="text_youtube_link">YouTube-Link</string>
|
||||||
<string name="text_customize_the_intervals">Passe die Intervalle und Kriterien für das Verschieben von Vokabeln an. Karten in niedrigeren Stufen werden öfter abgefragt.</string>
|
<string name="text_customize_the_intervals">Passe die Intervalle und Kriterien für das Verschieben von Vokabeln an. Karten in niedrigeren Stufen werden öfter abgefragt.</string>
|
||||||
@@ -83,7 +81,6 @@
|
|||||||
<string name="text_shuffle_languages">Sprachen mischen</string>
|
<string name="text_shuffle_languages">Sprachen mischen</string>
|
||||||
<string name="text_training_mode">Trainingsmodus</string>
|
<string name="text_training_mode">Trainingsmodus</string>
|
||||||
<string name="text_amount_of_cards">Anzahl der Karten</string>
|
<string name="text_amount_of_cards">Anzahl der Karten</string>
|
||||||
<string name="text_interval_settings_in_days">Intervall-Einstellungen (in Tagen)</string>
|
|
||||||
<string name="text_label_word">Gib ein Wort ein</string>
|
<string name="text_label_word">Gib ein Wort ein</string>
|
||||||
<string name="text_translation">Gib die Übersetzung ein</string>
|
<string name="text_translation">Gib die Übersetzung ein</string>
|
||||||
<string name="text_due_today_only">Nur heute fällige</string>
|
<string name="text_due_today_only">Nur heute fällige</string>
|
||||||
@@ -95,10 +92,7 @@
|
|||||||
<string name="text_loading_3d">Laden…</string>
|
<string name="text_loading_3d">Laden…</string>
|
||||||
<string name="text_show_loading">Laden anzeigen</string>
|
<string name="text_show_loading">Laden anzeigen</string>
|
||||||
<string name="text_cancel_loading">Laden abbrechen</string>
|
<string name="text_cancel_loading">Laden abbrechen</string>
|
||||||
<string name="text_sentence_this_is_an_info_message">Dies ist eine Info-Nachricht.</string>
|
|
||||||
<string name="text_show_info_message">Info-Nachricht anzeigen</string>
|
<string name="text_show_info_message">Info-Nachricht anzeigen</string>
|
||||||
<string name="text_success_em">Erfolg!</string>
|
|
||||||
<string name="text_sentence_oops_something_went_wrong">Hoppla! Etwas ist schiefgegangen.</string>
|
|
||||||
<string name="text_show_error_message">Fehlermeldung anzeigen</string>
|
<string name="text_show_error_message">Fehlermeldung anzeigen</string>
|
||||||
<string name="text_reset_intro">Intro zurücksetzen</string>
|
<string name="text_reset_intro">Intro zurücksetzen</string>
|
||||||
<string name="text_sentenc_version_information_not_available">Versionsinformation nicht verfügbar.</string>
|
<string name="text_sentenc_version_information_not_available">Versionsinformation nicht verfügbar.</string>
|
||||||
@@ -128,7 +122,6 @@
|
|||||||
<string name="text_enter_api_key">API-Schlüssel eingeben</string>
|
<string name="text_enter_api_key">API-Schlüssel eingeben</string>
|
||||||
<string name="text_save_key">Schlüssel speichern</string>
|
<string name="text_save_key">Schlüssel speichern</string>
|
||||||
<string name="text_select_model">Modell auswählen</string>
|
<string name="text_select_model">Modell auswählen</string>
|
||||||
<string name="title_title_preview_title">Vorschau-Titel</string>
|
|
||||||
<string name="text_none">Keine</string>
|
<string name="text_none">Keine</string>
|
||||||
<string name="text_manual_vocabulary_list">Manuelle Vokabelliste</string>
|
<string name="text_manual_vocabulary_list">Manuelle Vokabelliste</string>
|
||||||
<string name="text_filter_all_items">Filter: Alle Einträge</string>
|
<string name="text_filter_all_items">Filter: Alle Einträge</string>
|
||||||
@@ -194,7 +187,6 @@
|
|||||||
<string name="text_difficulty_2d">Schwierigkeit: %1$s</string>
|
<string name="text_difficulty_2d">Schwierigkeit: %1$s</string>
|
||||||
<string name="text_amount_2d_questions">Anzahl: %1$d Fragen</string>
|
<string name="text_amount_2d_questions">Anzahl: %1$d Fragen</string>
|
||||||
<string name="text_generate">Erstellen</string>
|
<string name="text_generate">Erstellen</string>
|
||||||
<string name="text_let_ai_find_vocabulary_for_you">Lass die KI Vokabeln für dich finden!</string>
|
|
||||||
<string name="text_search_term">Suchbegriff</string>
|
<string name="text_search_term">Suchbegriff</string>
|
||||||
<string name="text_hint">Tipp</string>
|
<string name="text_hint">Tipp</string>
|
||||||
<string name="text_select_languages">Sprachen auswählen</string>
|
<string name="text_select_languages">Sprachen auswählen</string>
|
||||||
@@ -226,19 +218,14 @@
|
|||||||
<string name="cd_target_met">Ziel erreicht</string>
|
<string name="cd_target_met">Ziel erreicht</string>
|
||||||
<string name="text_no_vocabulary_due_today">Heute keine Vokabeln fällig</string>
|
<string name="text_no_vocabulary_due_today">Heute keine Vokabeln fällig</string>
|
||||||
<string name="text_view_all">Alle ansehen</string>
|
<string name="text_view_all">Alle ansehen</string>
|
||||||
<string name="text_custom_exercise">Eigene Übung</string>
|
|
||||||
<string name="text_daily_exercise">Tägliche Übung</string>
|
|
||||||
<string name="label_total_words">Wörter gesamt</string>
|
<string name="label_total_words">Wörter gesamt</string>
|
||||||
<string name="label_learned">Gelernt</string>
|
<string name="label_learned">Gelernt</string>
|
||||||
<string name="remaining">Übrig</string>
|
<string name="remaining">Übrig</string>
|
||||||
<string name="label_ai_model_and_prompt"><![CDATA[KI-Modell & Prompt]]></string>
|
<string name="label_ai_model_and_prompt"><![CDATA[KI-Modell & Prompt]]></string>
|
||||||
<string name="examples">Beispiele</string>
|
<string name="examples">Beispiele</string>
|
||||||
<string name="vocabulary_settings">Vokabular-Einstellungen</string>
|
|
||||||
<string name="label_learning_criteria">Lernkriterien</string>
|
<string name="label_learning_criteria">Lernkriterien</string>
|
||||||
<string name="min_correct_to_advance">Min. richtig zum Aufsteigen</string>
|
<string name="min_correct_to_advance">Min. richtig zum Aufsteigen</string>
|
||||||
<string name="max_wrong_to_demote">Max. falsch zum Absteigen</string>
|
<string name="max_wrong_to_demote">Max. falsch zum Absteigen</string>
|
||||||
<string name="daily_learning_goal">Tägliches Lernziel</string>
|
|
||||||
<string name="target_correct_answers_per_day">Ziel: Richtige Antworten pro Tag</string>
|
|
||||||
<string name="label_backup_and_restore">Sicherung & Wiederherstellung</string>
|
<string name="label_backup_and_restore">Sicherung & Wiederherstellung</string>
|
||||||
<string name="export_vocabulary_data">Vokabeldaten exportieren</string>
|
<string name="export_vocabulary_data">Vokabeldaten exportieren</string>
|
||||||
<string name="import_vocabulary_data">Vokabeldaten importieren</string>
|
<string name="import_vocabulary_data">Vokabeldaten importieren</string>
|
||||||
@@ -295,7 +282,7 @@
|
|||||||
<string name="label_start_exercise_2d">Übung starten (%1$d)</string>
|
<string name="label_start_exercise_2d">Übung starten (%1$d)</string>
|
||||||
<string name="number_of_cards">Anzahl der Karten: %1$d / %2$d</string>
|
<string name="number_of_cards">Anzahl der Karten: %1$d / %2$d</string>
|
||||||
<string name="no_cards_found_for_the_selected_filters">Keine Karten für die gewählten Filter gefunden.</string>
|
<string name="no_cards_found_for_the_selected_filters">Keine Karten für die gewählten Filter gefunden.</string>
|
||||||
<string name="label_choose_exercise_types">Übungstypen wählen</string>
|
<string name="label_choose_exercise_types">Die richtige Antwort wählen</string>
|
||||||
<string name="options">Optionen</string>
|
<string name="options">Optionen</string>
|
||||||
<string name="shuffle_cards">Karten mischen</string>
|
<string name="shuffle_cards">Karten mischen</string>
|
||||||
<string name="quit">Beenden</string>
|
<string name="quit">Beenden</string>
|
||||||
@@ -336,12 +323,11 @@
|
|||||||
<string name="last_incorrect">Zuletzt falsch: %1$s</string>
|
<string name="last_incorrect">Zuletzt falsch: %1$s</string>
|
||||||
<string name="correct_answers_">Richtige Antworten: %1$d</string>
|
<string name="correct_answers_">Richtige Antworten: %1$d</string>
|
||||||
<string name="incorrect_answers">Falsche Antworten: %1$d</string>
|
<string name="incorrect_answers">Falsche Antworten: %1$d</string>
|
||||||
<string name="label_card_with_position">Karte (%1$d/%2$d)</string>
|
|
||||||
<string name="item_id">Eintrags-ID: %1$d</string>
|
<string name="item_id">Eintrags-ID: %1$d</string>
|
||||||
<string name="statistics_are_loading">Statistiken werden geladen…</string>
|
<string name="statistics_are_loading">Statistiken werden geladen…</string>
|
||||||
<string name="to_d">nach %1$s</string>
|
<string name="to_d">nach %1$s</string>
|
||||||
<string name="label_translate_from_2d">Übersetze von %1$s</string>
|
<string name="label_translate_from_2d">Übersetze von %1$s</string>
|
||||||
<string name="text_assemble_the_word_here">Bilde das Wort hier</string>
|
<string name="text_assemble_the_word_here">Bringe die Buchstaben in Reihenfolge</string>
|
||||||
<string name="correct_answer">Richtige Antwort: %1$s</string>
|
<string name="correct_answer">Richtige Antwort: %1$s</string>
|
||||||
<string name="label_quit_exercise_qm">Übung beenden?</string>
|
<string name="label_quit_exercise_qm">Übung beenden?</string>
|
||||||
<string name="text_are_you_sure_you_want_to_quit_your_progress_in_this_session_will_be_lost">Möchtest du wirklich beenden? Dein Fortschritt in dieser Sitzung geht verloren.</string>
|
<string name="text_are_you_sure_you_want_to_quit_your_progress_in_this_session_will_be_lost">Möchtest du wirklich beenden? Dein Fortschritt in dieser Sitzung geht verloren.</string>
|
||||||
@@ -367,7 +353,6 @@
|
|||||||
<string name="more_actions">Mehr Aktionen</string>
|
<string name="more_actions">Mehr Aktionen</string>
|
||||||
<string name="select_all">Alle auswählen</string>
|
<string name="select_all">Alle auswählen</string>
|
||||||
<string name="deselect_all">Auswahl aufheben</string>
|
<string name="deselect_all">Auswahl aufheben</string>
|
||||||
<string name="search_vocabulary">Vokabular suchen…</string>
|
|
||||||
<string name="no_vocabulary_items_found_perhaps_try_changing_the_filters">Keine Vokabeln gefunden. Vielleicht die Filter ändern?</string>
|
<string name="no_vocabulary_items_found_perhaps_try_changing_the_filters">Keine Vokabeln gefunden. Vielleicht die Filter ändern?</string>
|
||||||
<string name="label_category_2d">Kategorie: %1$s</string>
|
<string name="label_category_2d">Kategorie: %1$s</string>
|
||||||
<string name="repository_state_imported_from">Repository-Status importiert von %1$s</string>
|
<string name="repository_state_imported_from">Repository-Status importiert von %1$s</string>
|
||||||
@@ -472,8 +457,6 @@
|
|||||||
<string name="text_assign_these_items_2d">Ordne diese Elemente zu:</string>
|
<string name="text_assign_these_items_2d">Ordne diese Elemente zu:</string>
|
||||||
<string name="translate_the_following_d">Übersetze Folgendes (%1$s):</string>
|
<string name="translate_the_following_d">Übersetze Folgendes (%1$s):</string>
|
||||||
<string name="label_your_translation">Deine Übersetzung</string>
|
<string name="label_your_translation">Deine Übersetzung</string>
|
||||||
<string name="this_is_a_hint">Dies ist ein Hinweis.</string>
|
|
||||||
<string name="this_is_the_main_content">Dies ist der Hauptinhalt.</string>
|
|
||||||
<string name="this_is_the_content_inside_the_card">Dies ist der Inhalt in der Karte.</string>
|
<string name="this_is_the_content_inside_the_card">Dies ist der Inhalt in der Karte.</string>
|
||||||
<string name="primary_button">Primärer Button</string>
|
<string name="primary_button">Primärer Button</string>
|
||||||
<string name="primary_with_icon">Primär mit Icon</string>
|
<string name="primary_with_icon">Primär mit Icon</string>
|
||||||
@@ -491,15 +474,12 @@
|
|||||||
<string name="text_base_url_and_example">Basis-URL (z.B. \'http://192.168.0.99:1234/\')</string>
|
<string name="text_base_url_and_example">Basis-URL (z.B. \'http://192.168.0.99:1234/\')</string>
|
||||||
<string name="label_close_selection_mode">Auswahlmodus schließen</string>
|
<string name="label_close_selection_mode">Auswahlmodus schließen</string>
|
||||||
<string name="d_selected">%1$d ausgewählt</string>
|
<string name="d_selected">%1$d ausgewählt</string>
|
||||||
<string name="search_query">Suchanfrage</string>
|
|
||||||
<string name="label_close_search">Suche schließen</string>
|
<string name="label_close_search">Suche schließen</string>
|
||||||
<string name="generate_related_vocabulary_items">Verwandte Vokabeln generieren</string>
|
|
||||||
<string name="dismiss">Verwerfen</string>
|
<string name="dismiss">Verwerfen</string>
|
||||||
<string name="edit_features_for">Merkmale für \'%1$s\' bearbeiten</string>
|
<string name="edit_features_for">Merkmale für \'%1$s\' bearbeiten</string>
|
||||||
<string name="no_grammar_configuration_found_for_this_language">Keine Grammatikkonfiguration für diese Sprache gefunden.</string>
|
<string name="no_grammar_configuration_found_for_this_language">Keine Grammatikkonfiguration für diese Sprache gefunden.</string>
|
||||||
<string name="word_type">Wortart</string>
|
<string name="word_type">Wortart</string>
|
||||||
<string name="levels">Level</string>
|
<string name="levels">Level</string>
|
||||||
<string name="quick_word_pairs">Schnelle Wortpaare</string>
|
|
||||||
<string name="stage_filter">Stufenfilter</string>
|
<string name="stage_filter">Stufenfilter</string>
|
||||||
<string name="language_pair">Sprachpaar</string>
|
<string name="language_pair">Sprachpaar</string>
|
||||||
<string name="language_filter">Sprachfilter</string>
|
<string name="language_filter">Sprachfilter</string>
|
||||||
@@ -549,10 +529,6 @@
|
|||||||
<string name="friendly">Freundlich</string>
|
<string name="friendly">Freundlich</string>
|
||||||
<string name="label_academic">Akademisch</string>
|
<string name="label_academic">Akademisch</string>
|
||||||
<string name="creative">Kreativ</string>
|
<string name="creative">Kreativ</string>
|
||||||
<string name="editing_text">Text bearbeiten: %1$s</string>
|
|
||||||
<string name="no_text_received">Kein Text empfangen!</string>
|
|
||||||
<string name="error_no_text_to_edit">Fehler: Kein Text zum Bearbeiten</string>
|
|
||||||
<string name="not_launched_with_text_to_edit">Nicht mit zu bearbeitendem Text gestartet</string>
|
|
||||||
<string name="text_a_simple_list_to">Eine einfache Liste, um deine Vokabeln manuell zu sortieren</string>
|
<string name="text_a_simple_list_to">Eine einfache Liste, um deine Vokabeln manuell zu sortieren</string>
|
||||||
<string name="settings_title_voice">Stimme</string>
|
<string name="settings_title_voice">Stimme</string>
|
||||||
<string name="default_value">Standard</string>
|
<string name="default_value">Standard</string>
|
||||||
@@ -593,21 +569,11 @@
|
|||||||
<string name="intro_if_you_need_help_you">Wenn du Hilfe brauchst, findest du Hinweise in allen Bereichen der App.</string>
|
<string name="intro_if_you_need_help_you">Wenn du Hilfe brauchst, findest du Hinweise in allen Bereichen der App.</string>
|
||||||
<string name="text_navigation_bar_labels">Navigationsleisten-Beschriftungen</string>
|
<string name="text_navigation_bar_labels">Navigationsleisten-Beschriftungen</string>
|
||||||
<string name="text_show_text_labels_on_the_main_navigation_bar">Textbeschriftungen in der Hauptnavigationsleiste anzeigen.</string>
|
<string name="text_show_text_labels_on_the_main_navigation_bar">Textbeschriftungen in der Hauptnavigationsleiste anzeigen.</string>
|
||||||
<string name="text_word_pair_settings">Wortpaar-Einstellungen</string>
|
|
||||||
<string name="text_amount_of_questions_2d">Anzahl der Fragen: %1$d</string>
|
|
||||||
<string name="text_shuffle_questions">Fragen mischen</string>
|
|
||||||
<string name="tetx_training_mode">Trainingsmodus</string>
|
|
||||||
<string name="text_match_the_pairs">Bilde die Paare</string>
|
|
||||||
<string name="text_word_pair_exercise">Wortpaar-Übung</string>
|
|
||||||
<string name="text_training_mode_description">Trainingsmodus ist aktiv: Antworten beeinflussen den Fortschritt nicht.</string>
|
<string name="text_training_mode_description">Trainingsmodus ist aktiv: Antworten beeinflussen den Fortschritt nicht.</string>
|
||||||
<string name="text_days">" Tage"</string>
|
<string name="text_days">" Tage"</string>
|
||||||
<string name="label_add_vocabulary">Vokabel hinzufügen</string>
|
<string name="label_add_vocabulary">Vokabel hinzufügen</string>
|
||||||
<string name="label_create_vocabulary_with_ai">Vokabular mit KI erstellen</string>
|
|
||||||
<string name="text_vocab_empty">Keine Vokabeln gefunden. Jetzt hinzufügen?</string>
|
|
||||||
<string name="text_this_will_remove_all">Dadurch werden alle konfigurierten API-Anbieter, Modelle und gespeicherten API-Schlüssel entfernt. Diese Aktion kann nicht rückgängig gemacht werden.</string>
|
<string name="text_this_will_remove_all">Dadurch werden alle konfigurierten API-Anbieter, Modelle und gespeicherten API-Schlüssel entfernt. Diese Aktion kann nicht rückgängig gemacht werden.</string>
|
||||||
<string name="text_delete_all_providers_and_models_qm">Alle Anbieter und Modelle löschen?</string>
|
<string name="text_delete_all_providers_and_models_qm">Alle Anbieter und Modelle löschen?</string>
|
||||||
<string name="text_swap_sides">Seiten tauschen</string>
|
|
||||||
<string name="text_no_progress">Kein Fortschritt</string>
|
|
||||||
<string name="text_theme_preview">Theme-Vorschau</string>
|
<string name="text_theme_preview">Theme-Vorschau</string>
|
||||||
<string name="text_sample_word">Beispielwort</string>
|
<string name="text_sample_word">Beispielwort</string>
|
||||||
<string name="toggle_use_libretranslate">Übersetungs-Server verwenden</string>
|
<string name="toggle_use_libretranslate">Übersetungs-Server verwenden</string>
|
||||||
@@ -634,7 +600,6 @@
|
|||||||
<string name="label_language_none">Keine</string>
|
<string name="label_language_none">Keine</string>
|
||||||
<string name="text_no_data_available">Keine Daten verfügbar</string>
|
<string name="text_no_data_available">Keine Daten verfügbar</string>
|
||||||
<string name="label_grammar_inflections">Flexionen</string>
|
<string name="label_grammar_inflections">Flexionen</string>
|
||||||
<string name="label_more">Weniger</string>
|
|
||||||
<string name="label_translations">Übersetzungen</string>
|
<string name="label_translations">Übersetzungen</string>
|
||||||
<string name="label_show_examples">Beispiele anzeigen</string>
|
<string name="label_show_examples">Beispiele anzeigen</string>
|
||||||
<string name="label_grammar_hyphenation">Silbentrennung</string>
|
<string name="label_grammar_hyphenation">Silbentrennung</string>
|
||||||
@@ -684,13 +649,15 @@
|
|||||||
<string name="label_language_direction">Sprachenrichtung
|
<string name="label_language_direction">Sprachenrichtung
|
||||||
</string>
|
</string>
|
||||||
<string name="text_language_direction_explanation">Du kannst eine optionale Einstellung vornehmen, welche Sprache zuerst oder zweit kommen soll.</string>
|
<string name="text_language_direction_explanation">Du kannst eine optionale Einstellung vornehmen, welche Sprache zuerst oder zweit kommen soll.</string>
|
||||||
<string name="label_guessing_exercise">Vermutung</string>
|
<string name="text_language_direction_disabled_with_pairs">Entferne die Sprachpaar-Auswahl, um eine Richtung zu wählen.</string>
|
||||||
|
<string name="label_guessing_exercise">Raten</string>
|
||||||
<string name="label_spelling_exercise">Rechtschreibung</string>
|
<string name="label_spelling_exercise">Rechtschreibung</string>
|
||||||
<string name="label_multiple_choice_exercise">Multiple Choice</string>
|
<string name="label_multiple_choice_exercise">Multiple Choice</string>
|
||||||
<string name="label_word_jumble_exercise">Wortwirrwarr</string>
|
<string name="label_word_jumble_exercise">Wortwirrwarr</string>
|
||||||
<string name="text_due_today_only_description">Nur Karten anzeigen, die heute fällig sind.</string>
|
<string name="text_due_today_only_description">Nur Karten anzeigen, die heute fällig sind.</string>
|
||||||
<string name="text_shuffle_card_order_description">Kartenmischung</string>
|
<string name="text_shuffle_card_order_description">Kartenmischung</string>
|
||||||
<string name="text_shuffle_languages_description">Mische die Reihenfolge der Sprachen. Beeinflusst nicht deine Sprachrichtungs-Einstellungen.</string>
|
<string name="text_shuffle_languages_description">Mische die Reihenfolge der Sprachen. Beeinflusst nicht deine Sprachrichtungs-Einstellungen.</string>
|
||||||
|
<string name="text_shuffle_languages_disabled_by_direction">Deaktiviere die Sprachrichtungs-Einstellung, um das Mischen zu aktivieren.</string>
|
||||||
<string name="label_conjugation">Konjugation: %1$s</string>
|
<string name="label_conjugation">Konjugation: %1$s</string>
|
||||||
<string name="label_collapse">Einklappen</string>
|
<string name="label_collapse">Einklappen</string>
|
||||||
<string name="label_expand">Ausklappen</string>
|
<string name="label_expand">Ausklappen</string>
|
||||||
@@ -862,5 +829,92 @@
|
|||||||
<string name="duplicate">Duplikat</string>
|
<string name="duplicate">Duplikat</string>
|
||||||
<string name="hint_scan_hint_title">Das richtige AI-Modell finden</string>
|
<string name="hint_scan_hint_title">Das richtige AI-Modell finden</string>
|
||||||
<string name="hint_translate_how_it_works">Wie Übersetzung funktioniert</string>
|
<string name="hint_translate_how_it_works">Wie Übersetzung funktioniert</string>
|
||||||
|
<string name="label_home">Startseite</string>
|
||||||
|
<string name="label_more">Mehr</string>
|
||||||
|
<string name="label_quit_app">App beenden</string>
|
||||||
|
<string name="label_target_correct_answers_per_day">Ziel für richtige Antworten pro Tag</string>
|
||||||
|
<string name="label_interval_settings_in_days">Intervall-Einstellungen</string>
|
||||||
|
<string name="label_vocabulary_settings">Fortschritts-Einstellungen</string>
|
||||||
|
<string name="label_no_category">Keine</string>
|
||||||
|
<string name="text_search">Suche</string>
|
||||||
|
<string name="text_language_settings_description">Stelle ein, welche Sprachen du in der App verwenden möchtest. Sprachen, die nicht aktiviert sind, werden in dieser App nicht angezeigt. Du kannst auch deine eigene Sprache zur Liste hinzufügen oder eine vorhandene Sprache (Region/Locale) ändern.</string>
|
||||||
|
<string name="message_success_generic">Erfolg!</string>
|
||||||
|
<string name="message_info_generic">Info</string>
|
||||||
|
<string name="message_error_generic">Ein Fehler ist aufgetreten</string>
|
||||||
|
<string name="message_loading_generic">Lädt…</string>
|
||||||
|
<string name="message_error_language_not_selected">Quell- und Zielsprachen müssen ausgewählt sein.</string>
|
||||||
|
<string name="message_error_no_words_found">Keine Wörter im bereitgestellten Text gefunden.</string>
|
||||||
|
<string name="message_success_language_replaced">Sprach-ID für %1$d Elemente aktualisiert.</string>
|
||||||
|
<string name="message_success_vocabulary_imported">Vokabeln erfolgreich importiert.</string>
|
||||||
|
<string name="message_error_vocabulary_import_failed">Fehler beim Importieren der Vokabeln: %1$s</string>
|
||||||
|
<string name="message_success_items_merged">Einträge zusammengeführt!</string>
|
||||||
|
<string name="message_success_items_added">%1$d neue Vokabeln erfolgreich hinzugefügt.</string>
|
||||||
|
<string name="message_error_items_add_failed">Fehler beim Hinzufügen der Einträge: %1$s</string>
|
||||||
|
<string name="message_success_items_deleted">Vokabeln erfolgreich gelöscht.</string>
|
||||||
|
<string name="message_error_items_delete_failed">Fehler beim Löschen: %1$s</string>
|
||||||
|
<string name="message_error_no_cards_found">Keine Karten für den angegebenen Filter gefunden.</string>
|
||||||
|
<string name="message_success_cards_loaded">Kartensatz erfolgreich geladen.</string>
|
||||||
|
<string name="message_success_grammar_updated">Grammatikdetails aktualisiert!</string>
|
||||||
|
<string name="message_error_grammar_fetch_failed">Konnte Grammatikdetails nicht abrufen.</string>
|
||||||
|
<string name="message_loading_grammar_fetch">Grammatik wird für %1$d Elemente abgerufen…</string>
|
||||||
|
<string name="message_success_file_saved">Datei unter %1$s gespeichert</string>
|
||||||
|
<string name="message_error_file_save_failed">Fehler beim Speichern der Datei: %1$s</string>
|
||||||
|
<string name="message_error_file_save_cancelled">Speichern der Datei abgebrochen oder fehlgeschlagen.</string>
|
||||||
|
<string name="message_error_file_picker_not_initialized">Save File Launcher nicht initialisiert.</string>
|
||||||
|
<string name="message_success_category_saved">Kategorie in %1$s gespeichert.</string>
|
||||||
|
<string name="message_error_api_key_missing">Dein API-Schlüssel fehlt oder ist ungültig.</string>
|
||||||
|
<string name="message_error_api_key_invalid">Dein API-Schlüssel fehlt oder ist ungültig.</string>
|
||||||
|
<string name="message_loading_translating">Übersetze %1$d Wörter…</string>
|
||||||
|
<string name="message_success_translation_completed">Übersetzung abgeschlossen.</string>
|
||||||
|
<string name="message_error_translation_failed">Übersetzung fehlgeschlagen: %1$s</string>
|
||||||
|
<string name="message_success_repository_wiped">Alle Repository-Daten gelöscht.</string>
|
||||||
|
<string name="message_error_repository_wipe_failed">Fehler beim Löschen des Repositorys: %1$s</string>
|
||||||
|
<string name="message_loading_card_set">Lade Kartensatz</string>
|
||||||
|
<string name="message_success_stage_updated">Stufe erfolgreich aktualisiert.</string>
|
||||||
|
<string name="message_error_stage_update_failed">Fehler beim Aktualisieren des Stages: %1$s</string>
|
||||||
|
<string name="message_success_category_updated">Kategorie erfolgreich aktualisiert.</string>
|
||||||
|
<string name="message_error_category_update_failed">Fehler beim Aktualisieren der Kategorie: %1$s</string>
|
||||||
|
<string name="message_success_articles_removed">Artikel erfolgreich entfernt.</string>
|
||||||
|
<string name="message_error_articles_remove_failed">Fehler beim Entfernen der Artikel: %1$s</string>
|
||||||
|
<string name="message_success_synonyms_generated">Synonyme erfolgreich generiert.</string>
|
||||||
|
<string name="message_error_synonyms_generation_failed">Fehler beim Generieren von Synonymen: %1$s</string>
|
||||||
|
<string name="message_error_operation_failed">Operation fehlgeschlagen: %1$s</string>
|
||||||
|
<string name="message_loading_operation_in_progress">Operation läuft…</string>
|
||||||
|
<string name="message_test_info">Das ist eine allgemeine Infomeldung.</string>
|
||||||
|
<string name="message_test_success">Das ist eine Erfolgsmeldung für den Test!</string>
|
||||||
|
<string name="message_test_error">Hoppla, da ist etwas schiefgelaufen :(</string>
|
||||||
|
<string name="label_stats">Statistiken</string>
|
||||||
|
<string name="label_library">Sammlung</string>
|
||||||
|
<string name="label_edit">Bearbeiten</string>
|
||||||
|
<string name="label_new_words">Neue Wörter</string>
|
||||||
|
<string name="desc_expand_your_vocabulary">Erweitere deinen Wortschatz</string>
|
||||||
|
<string name="label_settings">Einstellungen</string>
|
||||||
|
<string name="label_2d_days">%1$d Tage</string>
|
||||||
|
<string name="label_current_streak">Aktuelle Streak</string>
|
||||||
|
<string name="label_daily_goal">Tägliches Ziel</string>
|
||||||
|
<string name="text_desc_no_activity_data_available">Keine Aktivitätsdaten verfügbar</string>
|
||||||
|
<string name="label_see_history">Verlauf anzeigen</string>
|
||||||
|
<string name="label_weekly_progress">Wöchentlicher Fortschritt</string>
|
||||||
|
<string name="cd_go">Los</string>
|
||||||
|
<string name="label_sort_by">Sortieren nach</string>
|
||||||
|
<string name="label_reset">Zurücksetzen</string>
|
||||||
|
<string name="label_filter_cards">Filter Cards</string>
|
||||||
|
<string name="text_desc_organize_vocabulary_groups">Organisiere deinen Wortschatz in Gruppen</string>
|
||||||
|
<string name="text_add_new_word_to_list">Extrahiere ein neues Wort in deine Liste</string>
|
||||||
|
<string name="cd_scroll_to_top">Nach oben scrollen</string>
|
||||||
|
<string name="cd_settings">Einstellungen</string>
|
||||||
|
<string name="label_import_csv">CSV importieren</string>
|
||||||
|
<string name="label_ai_generator">KI-Generator</string>
|
||||||
|
<string name="label_new_wordss">Neue Wörter</string>
|
||||||
|
<string name="label_recently_added">Kürzlich hinzugefügt</string>
|
||||||
|
<string name="label_view_all">Alle anzeigen</string>
|
||||||
|
<string name="text_explore_more_categories">Entdecke weitere Kategorien</string>
|
||||||
|
<string name="cd_options">Optionen</string>
|
||||||
|
<string name="cd_selected">Ausgewählt</string>
|
||||||
|
<string name="label_all_cards">Alle Karten</string>
|
||||||
|
<string name="cd_filter_options">Filteroptionen</string>
|
||||||
|
<string name="cd_add">Hinzufügen</string>
|
||||||
|
<string name="cd_searchh">Suche</string>
|
||||||
|
<string name="label_learnedd">gelernt</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user