Compare commits
1 Commits
64dcc5d0d5
...
glassmorph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d0bf4cb1c |
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-16T10:13:39.492968600Z">
|
<DropdownSelection timestamp="2026-02-15T19:51:37.987601800Z">
|
||||||
<Target type="DEFAULT_BOOT">
|
<Target type="DEFAULT_BOOT">
|
||||||
<handle>
|
<handle>
|
||||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=RFCW11ML1WV" />
|
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\jonas\.android\avd\Medium_Phone_28.avd" />
|
||||||
</handle>
|
</handle>
|
||||||
</Target>
|
</Target>
|
||||||
</DropdownSelection>
|
</DropdownSelection>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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")
|
||||||
@@ -61,8 +62,11 @@ 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
|
||||||
@@ -126,7 +130,8 @@ 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)
|
implementation(libs.androidx.compose.foundation.layout)
|
||||||
|
ksp(libs.room.compiler) // CHANGED: Use ksp instead of implementation
|
||||||
|
|
||||||
// Networking
|
// Networking
|
||||||
implementation(libs.retrofit)
|
implementation(libs.retrofit)
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ 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,6 +32,17 @@
|
|||||||
<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>
|
||||||
|
|
||||||
|
|||||||
45
app/src/main/java/eu/gaudian/translator/CorrectActivity.kt
Normal file
45
app/src/main/java/eu/gaudian/translator/CorrectActivity.kt
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
@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,3 +1,5 @@
|
|||||||
|
@file:Suppress("unused", "HardCodedStringLiteral")
|
||||||
|
|
||||||
package eu.gaudian.translator.di
|
package eu.gaudian.translator.di
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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)
|
||||||
@@ -22,6 +23,7 @@ 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,7 +56,6 @@ 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,6 +144,19 @@ 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,7 +76,6 @@ 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,6 +129,25 @@ 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,7 +10,6 @@ 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,12 +55,6 @@ 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,6 +75,7 @@ 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,6 +117,7 @@ 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"
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
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,6 +5,11 @@ 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,19 +253,7 @@ 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?.hierarchy?.any { destination ->
|
@Suppress("HardCodedStringLiteral") val isBottomBarHidden = currentDestination?.route?.startsWith("vocabulary_exercise") == true
|
||||||
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(
|
||||||
@@ -274,12 +262,6 @@ 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) {
|
||||||
@@ -292,11 +274,6 @@ 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) {
|
||||||
@@ -308,10 +285,6 @@ fun TranslatorApp(
|
|||||||
restoreState = false
|
restoreState = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
onPlayClicked = {
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
navController.navigate("start_exercise")
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,48 +26,27 @@ 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.NewWordReviewScreen
|
import eu.gaudian.translator.view.vocabulary.MainVocabularyScreen
|
||||||
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,
|
||||||
@@ -78,12 +57,11 @@ 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,
|
Screen.Home.route, // "home" or "main_translation" depending on which one you actually navigate to
|
||||||
Screen.Library.route,
|
"main_translation",
|
||||||
Screen.Stats.route,
|
"main_dictionary",
|
||||||
Screen.Translation.route,
|
"main_vocabulary",
|
||||||
Screen.Dictionary.route,
|
"main_exercise",
|
||||||
Screen.Exercises.route,
|
|
||||||
SettingsRoutes.LIST
|
SettingsRoutes.LIST
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -143,50 +121,77 @@ fun AppNavHost(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
composable(Screen.Home.route) {
|
composable(Screen.Home.route) {
|
||||||
HomeScreen(navController = navController)
|
TranslationScreen(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.homeGraph(navController: NavHostController) {
|
fun NavGraphBuilder.translationGraph(navController: NavHostController) {
|
||||||
navigation(
|
navigation(
|
||||||
startDestination = "main_home",
|
startDestination = "main_translation",
|
||||||
route = Screen.Home.route
|
route = Screen.Home.route
|
||||||
) {
|
) {
|
||||||
composable("main_home") {
|
composable("main_translation") {
|
||||||
HomeScreen(navController = navController)
|
TranslationScreen(navController = navController)
|
||||||
|
}
|
||||||
|
composable("custom_translation_prompt") {
|
||||||
|
TranslationSettingsScreen(navController = navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
|
@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
|
||||||
|
fun NavGraphBuilder.dictionaryGraph(navController: NavHostController) {
|
||||||
navigation(
|
navigation(
|
||||||
startDestination = "main_library",
|
startDestination = "main_dictionary",
|
||||||
route = Screen.Library.route
|
route = Screen.Dictionary.route
|
||||||
) {
|
) {
|
||||||
composable("main_library") {
|
composable("main_dictionary") {
|
||||||
LibraryScreen(navController = navController)
|
MainDictionaryScreen(navController = navController)
|
||||||
|
}
|
||||||
|
composable("dictionary_result/{entryId}") { backStackEntry ->
|
||||||
|
val entryId = backStackEntry.arguments?.getString("entryId")?.toIntOrNull()
|
||||||
|
if (entryId != null) {
|
||||||
|
DictionaryResultScreen(
|
||||||
|
entryId = entryId,
|
||||||
|
navController = navController,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text("Error: Invalid Entry ID")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
composable("dictionary_options") {
|
||||||
|
DictionaryOptionsScreen(navController = navController)
|
||||||
|
}
|
||||||
|
composable("etymology_result/{word}/{languageCode}") { backStackEntry ->
|
||||||
|
val word = backStackEntry.arguments?.getString("word") ?: ""
|
||||||
|
val languageCode = backStackEntry.arguments?.getString("languageCode")?.toIntOrNull() ?: 1
|
||||||
|
EtymologyResultScreen(
|
||||||
|
navController = navController,
|
||||||
|
word = word,
|
||||||
|
languageCode = languageCode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavGraphBuilder.vocabularyGraph(
|
||||||
|
navController: NavHostController,
|
||||||
|
) {
|
||||||
|
navigation(
|
||||||
|
startDestination = "main_vocabulary",
|
||||||
|
route = Screen.Vocabulary.route
|
||||||
|
) {
|
||||||
|
composable("main_vocabulary") {
|
||||||
|
MainVocabularyScreen(navController = navController)
|
||||||
}
|
}
|
||||||
composable("vocabulary_sorting") {
|
composable("vocabulary_sorting") {
|
||||||
VocabularySortingScreen(
|
VocabularySortingScreen(
|
||||||
@@ -219,7 +224,7 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
|
|||||||
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()
|
||||||
AllCardsListScreen(
|
VocabularyListScreen(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
showDueTodayOnly = showDueTodayOnly,
|
showDueTodayOnly = showDueTodayOnly,
|
||||||
categoryId = categoryId,
|
categoryId = categoryId,
|
||||||
@@ -236,7 +241,7 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
composable(NavigationRoutes.STATS_VOCABULARY_HEATMAP) {
|
composable("vocabulary_heatmap") {
|
||||||
VocabularyHeatmapScreen(
|
VocabularyHeatmapScreen(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
)
|
)
|
||||||
@@ -248,7 +253,7 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
|
|||||||
if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it)
|
if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
AllCardsListScreen(
|
VocabularyListScreen(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
showDueTodayOnly = showDueTodayOnly,
|
showDueTodayOnly = showDueTodayOnly,
|
||||||
stage = stage,
|
stage = stage,
|
||||||
@@ -371,159 +376,6 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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:Hint? = null,
|
hintContent: @Composable (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
@@ -142,15 +142,13 @@ fun AppAlertDialog(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (showBottomSheet) {
|
if (showBottomSheet) {
|
||||||
hintContent?.let {
|
|
||||||
HintBottomSheet(
|
HintBottomSheet(
|
||||||
onDismissRequest = { showBottomSheet = false },
|
onDismissRequest = { showBottomSheet = false },
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
content = it
|
content = hintContent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standard Dialog Header for BottomSheets.
|
* Standard Dialog Header for BottomSheets.
|
||||||
@@ -214,7 +212,7 @@ private fun DialogHeader(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun DialogTitleWithHint(
|
private fun DialogTitleWithHint(
|
||||||
title: @Composable () -> Unit,
|
title: @Composable () -> Unit,
|
||||||
hintContent: Hint? = null,
|
hintContent: @Composable (() -> Unit)?,
|
||||||
onHintClick: () -> Unit
|
onHintClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val showHints = LocalShowHints.current
|
val showHints = LocalShowHints.current
|
||||||
@@ -426,6 +424,7 @@ 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.") }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,6 +492,7 @@ 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,7 +25,6 @@ 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
|
||||||
@@ -551,55 +550,7 @@ 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,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ 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.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
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
|
||||||
@@ -159,14 +159,14 @@ private fun MenuItem(
|
|||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.shadow(elevation = 8.dp, shape = ComponentDefaults.DefaultShape)
|
.glassmorphic(shape = RoundedCornerShape(16.dp), alpha = 0.4f)
|
||||||
.clickable(
|
.clickable(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
indication = null
|
indication = null
|
||||||
),
|
),
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
color = MaterialTheme.colorScheme.surfaceContainer
|
color = Color.Transparent // Allow glassmorphic modifier to handle color
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
@@ -197,15 +197,3 @@ private fun MenuItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun MenuItemPreview() {
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
MenuItem(
|
|
||||||
text = "Menu Item",
|
|
||||||
imageVector = AppIcons.Add,
|
|
||||||
painter = null,
|
|
||||||
onClick = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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.ArrowBackIos
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
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,7 +81,6 @@ 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
|
||||||
@@ -136,7 +135,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.ArrowBackIos
|
val ArrowBack = Icons.AutoMirrored.Filled.ArrowBack
|
||||||
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
|
||||||
@@ -203,7 +202,6 @@ 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,15 +2,26 @@ 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(
|
||||||
@@ -47,3 +58,37 @@ 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,3 +1,5 @@
|
|||||||
|
@file:Suppress("HardCodedStringLiteral")
|
||||||
|
|
||||||
package eu.gaudian.translator.view.composable
|
package eu.gaudian.translator.view.composable
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
@@ -18,13 +20,9 @@ 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
|
||||||
@@ -38,7 +36,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.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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,55 +46,31 @@ 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)
|
||||||
|
|
||||||
Row(
|
BoxWithConstraints(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 8.dp, horizontal = 8.dp),
|
.padding(vertical = 8.dp, horizontal = 8.dp)
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
if (onNavigateBack != null) {
|
|
||||||
IconButton(
|
|
||||||
onClick = onNavigateBack,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(end = 8.dp)
|
|
||||||
.size(40.dp)
|
|
||||||
.background(
|
|
||||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
|
||||||
shape = CircleShape
|
|
||||||
),
|
|
||||||
colors = IconButtonDefaults.iconButtonColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
|
||||||
contentColor = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = AppIcons.ArrowBack,
|
|
||||||
contentDescription = stringResource(R.string.cd_navigate_back)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BoxWithConstraints(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.height(56.dp)
|
.height(56.dp)
|
||||||
.background(
|
// Replace background with glassmorphic extension
|
||||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
.glassmorphic(shape = ComponentDefaults.CardShape, alpha = 0.3f)
|
||||||
shape = ComponentDefaults.CardShape
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
val tabWidth = maxWidth / tabs.size
|
val tabWidth = maxWidth / tabs.size
|
||||||
|
|
||||||
@@ -114,7 +87,7 @@ fun <T : TabItem> AppTabLayout(
|
|||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
.background(
|
.background(
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f), // Glassy indicator
|
||||||
shape = RoundedCornerShape(12.dp)
|
shape = RoundedCornerShape(12.dp)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -165,9 +138,7 @@ fun <T : TabItem> AppTabLayout(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
@ThemePreviews
|
@ThemePreviews
|
||||||
@Composable
|
@Composable
|
||||||
fun ModernTabLayoutPreview() {
|
fun ModernTabLayoutPreview() {
|
||||||
|
|||||||
@@ -1,25 +1,20 @@
|
|||||||
@file:Suppress("HardCodedStringLiteral")
|
|
||||||
|
|
||||||
package eu.gaudian.translator.view.composable
|
package eu.gaudian.translator.view.composable
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.layout.Box
|
||||||
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
|
||||||
@@ -30,10 +25,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.Color
|
||||||
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
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
|
||||||
@@ -43,37 +38,43 @@ 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 = {},
|
||||||
colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),
|
colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
scrolledContainerColor = Color.Transparent
|
||||||
|
),
|
||||||
hintContent: Hint? = null
|
hintContent: Hint? = null
|
||||||
) {
|
) {
|
||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// Changed to CenterAlignedTopAppBar to perfectly match the design requirements
|
Surface(
|
||||||
CenterAlignedTopAppBar(
|
modifier = modifier.glassmorphic(shape = RectangleShape, alpha = 0.2f),
|
||||||
modifier = modifier.height(56.dp),
|
color = Color.Transparent
|
||||||
|
) {
|
||||||
|
TopAppBar(
|
||||||
|
modifier = Modifier.height(56.dp),
|
||||||
windowInsets = WindowInsets(0.dp),
|
windowInsets = WindowInsets(0.dp),
|
||||||
colors = colors,
|
colors = colors,
|
||||||
title = {
|
title = {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxHeight(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
val showHints = LocalShowHints.current
|
val showHints = LocalShowHints.current
|
||||||
if (showHints && hintContent != null) {
|
if (showHints && hintContent != null) {
|
||||||
// Simplified row: keeps the title and hint icon neatly centered together
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.Center
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
text = title,
|
title()
|
||||||
style = MaterialTheme.typography.titleLarge,
|
}
|
||||||
fontWeight = FontWeight.Bold,
|
Box {
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
|
||||||
)
|
|
||||||
IconButton(onClick = { showBottomSheet = true }) {
|
IconButton(onClick = { showBottomSheet = true }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = AppIcons.Help,
|
imageVector = AppIcons.Help,
|
||||||
@@ -82,55 +83,63 @@ fun AppTopAppBar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Text(
|
title()
|
||||||
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) {
|
||||||
IconButton(
|
Box(
|
||||||
onClick = onNavigateBack,
|
modifier = Modifier.fillMaxHeight(),
|
||||||
modifier = Modifier
|
contentAlignment = Alignment.Center
|
||||||
.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) {
|
||||||
hintContent?.let {
|
|
||||||
HintBottomSheet(
|
HintBottomSheet(
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
@Suppress("AssignedValueIsNeverRead")
|
@Suppress("AssignedValueIsNeverRead")
|
||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
},
|
},
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
content = it
|
content = {
|
||||||
|
hintContent?.Render()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A composable that acts as a TopAppBar, containing a back navigation icon
|
* A composable that acts as a TopAppBar, containing a back navigation icon
|
||||||
* and an [AppTabLayout].
|
* and an [AppTabLayout].
|
||||||
|
*
|
||||||
|
* @param T The type of the tab item, must implement [TabItem].
|
||||||
|
* @param tabs The list of tab items to display.
|
||||||
|
* @param selectedTab The currently selected tab item.
|
||||||
|
* @param onTabSelected Callback function when a tab is selected.
|
||||||
|
* @param onNavigateBack Callback function when the back arrow is clicked.
|
||||||
|
* @param modifier The modifier to be applied to the layout.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun <T : TabItem> TabbedTopAppBar(
|
fun <T : TabItem> TabbedTopAppBar(
|
||||||
@@ -140,6 +149,7 @@ 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,21 +158,20 @@ fun <T : TabItem> TabbedTopAppBar(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// Updated back icon here as well to keep your entire app consistent!
|
// Back navigation icon, similar to its usage in AppTopAppBar
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onNavigateBack,
|
onClick = onNavigateBack,
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(horizontal = 4.dp)
|
||||||
.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.primary
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The AppTabLayout, taking up the remaining space.
|
||||||
|
// Its appearance matches the provided image.
|
||||||
AppTabLayout(
|
AppTabLayout(
|
||||||
tabs = tabs,
|
tabs = tabs,
|
||||||
selectedTab = selectedTab,
|
selectedTab = selectedTab,
|
||||||
@@ -173,12 +182,11 @@ 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(
|
||||||
@@ -204,7 +212,7 @@ fun TabbedTopAppBarPreview() {
|
|||||||
@Composable
|
@Composable
|
||||||
fun AppTopAppBarPreview() {
|
fun AppTopAppBarPreview() {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = "Previwe Title"
|
title = { Text("Preview Title") }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +220,7 @@ fun AppTopAppBarPreview() {
|
|||||||
@Composable
|
@Composable
|
||||||
fun AppTopAppBarWithNavigationIconPreview() {
|
fun AppTopAppBarWithNavigationIconPreview() {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = "Preview Title",
|
title = { Text(stringResource(R.string.title_title_preview_title)) },
|
||||||
onNavigateBack = {}
|
onNavigateBack = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -221,13 +229,13 @@ fun AppTopAppBarWithNavigationIconPreview() {
|
|||||||
@Composable
|
@Composable
|
||||||
fun AppTopAppBarWithActionsPreview() {
|
fun AppTopAppBarWithActionsPreview() {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = "Preview Title",
|
title = { Text(stringResource(R.string.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 = {}) {
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = null)
|
AppIcons.ArrowBack
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@file:Suppress("HardCodedStringLiteral", "AssignedValueIsNeverRead")
|
@file:Suppress("HardCodedStringLiteral")
|
||||||
|
|
||||||
package eu.gaudian.translator.view.composable
|
package eu.gaudian.translator.view.composable
|
||||||
|
|
||||||
@@ -11,43 +11,24 @@ 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.shape.RoundedCornerShape
|
||||||
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.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
|
||||||
@@ -62,7 +43,6 @@ 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,
|
||||||
@@ -70,42 +50,34 @@ sealed class Screen(
|
|||||||
val selectedIcon: ImageVector,
|
val selectedIcon: ImageVector,
|
||||||
val unselectedIcon: ImageVector
|
val unselectedIcon: ImageVector
|
||||||
) {
|
) {
|
||||||
object Home : Screen("home", R.string.label_home, AppIcons.Home, AppIcons.Home)
|
object Home : Screen("home", R.string.label_translation, AppIcons.TranslateFilled, AppIcons.TranslateOutlined)
|
||||||
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> {
|
||||||
return listOf(Home, Library, Stats)
|
val screens = mutableListOf(Home, Dictionary, Vocabulary, Settings)
|
||||||
}
|
|
||||||
|
|
||||||
fun getMoreMenuItems(showExperimental: Boolean = false): List<Screen> {
|
|
||||||
val items = mutableListOf<Screen>()
|
|
||||||
items.add(Translation)
|
|
||||||
items.add(Dictionary)
|
|
||||||
items.add(Settings)
|
|
||||||
if (showExperimental) {
|
if (showExperimental) {
|
||||||
items.add(Exercises)
|
screens.add(2, Exercises)
|
||||||
}
|
}
|
||||||
return items
|
return screens
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun fromDestination(destination: NavDestination?): Screen {
|
fun fromDestination(destination: NavDestination?): Screen {
|
||||||
val showExperimental = LocalShowExperimentalFeatures.current
|
val showExperimental = LocalShowExperimentalFeatures.current
|
||||||
val allScreens = getAllScreens(showExperimental) + getMoreMenuItems(showExperimental) + More
|
return getAllScreens(showExperimental).find { screen ->
|
||||||
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(
|
||||||
@@ -114,32 +86,19 @@ 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 = spring(stiffness = Spring.StiffnessHigh),
|
animationSpec = androidx.compose.animation.core.tween(durationMillis = 220),
|
||||||
initialOffsetY = { it }
|
initialOffsetY = { it }
|
||||||
),
|
),
|
||||||
exit = slideOutVertically(
|
exit = slideOutVertically(
|
||||||
animationSpec = spring(stiffness = Spring.StiffnessHigh),
|
animationSpec = androidx.compose.animation.core.tween(durationMillis = 220),
|
||||||
targetOffsetY = { it }
|
targetOffsetY = { it }
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
@@ -148,49 +107,16 @@ fun BottomNavigationBar(
|
|||||||
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
|
||||||
|
|
||||||
// Outer Box height is purely determined by the NavigationBar now
|
|
||||||
Box(
|
|
||||||
modifier = modifier.fillMaxWidth(),
|
|
||||||
contentAlignment = Alignment.TopCenter
|
|
||||||
) {
|
|
||||||
|
|
||||||
// The actual Navigation Bar
|
|
||||||
NavigationBar(
|
NavigationBar(
|
||||||
modifier = Modifier.height(height),
|
modifier = modifier
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
.height(height)
|
||||||
tonalElevation = 8.dp,
|
// Apply glassmorphism on the top corners
|
||||||
|
.glassmorphic(shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), alpha = 0.35f),
|
||||||
|
containerColor = Color.Transparent, // Let the glass shine through
|
||||||
|
tonalElevation = 0.dp,
|
||||||
) {
|
) {
|
||||||
// Create a list of 5 items (2 left, 1 empty spacer, 2 right)
|
screens.forEach { screen ->
|
||||||
val allNavItems = buildList {
|
val isSelected = screen == selectedItem
|
||||||
addAll(screens.take(2))
|
|
||||||
add(null) // Empty spacer for Play Button gap
|
|
||||||
if (screens.size > 2) {
|
|
||||||
addAll(screens.drop(2))
|
|
||||||
}
|
|
||||||
add(moreScreen)
|
|
||||||
}
|
|
||||||
|
|
||||||
allNavItems.forEach { screen ->
|
|
||||||
if (screen == null) {
|
|
||||||
// Dummy item to create the gap
|
|
||||||
NavigationBarItem(
|
|
||||||
selected = false,
|
|
||||||
onClick = {},
|
|
||||||
enabled = false, // Disables ripples and clicks
|
|
||||||
icon = { Spacer(modifier = Modifier.size(24.dp)) },
|
|
||||||
label = if (showLabels) { { Spacer(modifier = Modifier.size(10.dp)) } } else null,
|
|
||||||
colors = NavigationBarItemDefaults.colors(
|
|
||||||
disabledIconColor = Color.Transparent,
|
|
||||||
disabledTextColor = Color.Transparent
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Regular or More items
|
|
||||||
val isSelected = if (screen == Screen.More) {
|
|
||||||
selectedItem is Screen.More || Screen.getMoreMenuItems(showExperimental).contains(selectedItem)
|
|
||||||
} else {
|
|
||||||
screen == selectedItem
|
|
||||||
}
|
|
||||||
val title = stringResource(id = screen.title)
|
val title = stringResource(id = screen.title)
|
||||||
|
|
||||||
val scale by animateFloatAsState(
|
val scale by animateFloatAsState(
|
||||||
@@ -207,7 +133,7 @@ fun BottomNavigationBar(
|
|||||||
onClick = {
|
onClick = {
|
||||||
if (!isSelected) {
|
if (!isSelected) {
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
if (screen == Screen.More) showMoreMenu = true else onItemSelected(screen)
|
onItemSelected(screen)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
label = if (showLabels) {
|
label = if (showLabels) {
|
||||||
@@ -231,7 +157,7 @@ fun BottomNavigationBar(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = NavigationBarItemDefaults.colors(
|
colors = NavigationBarItemDefaults.colors(
|
||||||
indicatorColor = MaterialTheme.colorScheme.primaryContainer,
|
indicatorColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f), // Glassy indicator
|
||||||
selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
selectedTextColor = MaterialTheme.colorScheme.primary,
|
selectedTextColor = MaterialTheme.colorScheme.primary,
|
||||||
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
@@ -241,147 +167,8 @@ fun BottomNavigationBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Glowing Play Button
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
// This negative offset pulls the button UP out of the bounding box
|
|
||||||
// without increasing the layout height of the parent Box.
|
|
||||||
.offset(y = -upwardOffset)
|
|
||||||
.size(playButtonSize + glowPadding),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
// Background radial glow
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.matchParentSize()
|
|
||||||
.background(
|
|
||||||
brush = Brush.radialGradient(
|
|
||||||
colors = listOf(
|
|
||||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.5f),
|
|
||||||
Color.Transparent
|
|
||||||
)
|
|
||||||
),
|
|
||||||
shape = CircleShape
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Actual clickable button
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(playButtonSize)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(MaterialTheme.colorScheme.primaryContainer)
|
|
||||||
.clickable {
|
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
|
||||||
onPlayClicked()
|
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.PlayArrow,
|
|
||||||
contentDescription = "Play",
|
|
||||||
tint = Color.White,
|
|
||||||
modifier = Modifier.size(32.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modal Bottom Sheet for More menu (Remains exactly the same)
|
|
||||||
if (showMoreMenu) {
|
|
||||||
ModalBottomSheet(
|
|
||||||
onDismissRequest = { showMoreMenu = false },
|
|
||||||
sheetState = sheetState
|
|
||||||
) {
|
|
||||||
MoreBottomSheetContent(
|
|
||||||
showExperimental = showExperimental,
|
|
||||||
onItemSelected = { screen ->
|
|
||||||
scope.launch {
|
|
||||||
sheetState.hide()
|
|
||||||
showMoreMenu = false
|
|
||||||
onItemSelected(screen)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MoreBottomSheetContent(
|
|
||||||
showExperimental: Boolean,
|
|
||||||
onItemSelected: (Screen) -> Unit
|
|
||||||
) {
|
|
||||||
val moreItems = remember(showExperimental) { Screen.getMoreMenuItems(showExperimental) }
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(bottom = 32.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.label_more),
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
fontWeight = FontWeight.Bold, // Added bold to match the new style
|
|
||||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Removed HorizontalDivider() for a cleaner look
|
|
||||||
|
|
||||||
moreItems.forEach { screen ->
|
|
||||||
MoreMenuItem(
|
|
||||||
screen = screen,
|
|
||||||
onClick = { onItemSelected(screen) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MoreMenuItem(
|
|
||||||
screen: Screen,
|
|
||||||
onClick: () -> Unit
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable { onClick() }
|
|
||||||
.padding(horizontal = 24.dp, vertical = 16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
// Circular Icon Background
|
|
||||||
Surface(
|
|
||||||
shape = CircleShape,
|
|
||||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
|
||||||
modifier = Modifier.size(52.dp)
|
|
||||||
) {
|
|
||||||
Box(contentAlignment = Alignment.Center) {
|
|
||||||
// Adjust this depending on whether your Screen uses ImageVector or Drawable Res
|
|
||||||
Icon(
|
|
||||||
imageVector = screen.selectedIcon,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier.size(28.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
|
|
||||||
// Title
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = screen.title), // Adjust to your actual string property
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ThemePreviews
|
@ThemePreviews
|
||||||
@Composable
|
@Composable
|
||||||
fun BottomNavigationBarPreview() {
|
fun BottomNavigationBarPreview() {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ package eu.gaudian.translator.view.composable
|
|||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -28,7 +30,6 @@ 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
|
||||||
@@ -37,7 +38,6 @@ 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
|
||||||
@@ -45,6 +45,7 @@ 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.composed
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
@@ -57,47 +58,46 @@ 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
|
||||||
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.hints.Hint
|
|
||||||
import eu.gaudian.translator.view.hints.HintBottomSheet
|
|
||||||
import eu.gaudian.translator.view.hints.LocalShowHints
|
|
||||||
|
|
||||||
|
|
||||||
object ComponentDefaults {
|
object ComponentDefaults {
|
||||||
// Sizing
|
|
||||||
val DefaultButtonHeight = 48.dp
|
val DefaultButtonHeight = 48.dp
|
||||||
val CardPadding = 8.dp
|
val CardPadding = 8.dp
|
||||||
|
|
||||||
// Elevation
|
|
||||||
val DefaultElevation = 0.dp
|
val DefaultElevation = 0.dp
|
||||||
val NoElevation = 0.dp
|
val NoElevation = 0.dp
|
||||||
|
|
||||||
// Borders
|
|
||||||
val DefaultBorderWidth = 1.dp
|
val DefaultBorderWidth = 1.dp
|
||||||
|
|
||||||
// Shapes
|
|
||||||
val DefaultCornerRadius = 16.dp
|
val DefaultCornerRadius = 16.dp
|
||||||
val CardClipRadius = 8.dp
|
val CardClipRadius = 16.dp // Increased slightly for softer glass look
|
||||||
val NoRounding = 0.dp
|
val NoRounding = 0.dp
|
||||||
val DefaultShape = RoundedCornerShape(DefaultCornerRadius)
|
val DefaultShape = RoundedCornerShape(DefaultCornerRadius)
|
||||||
val CardClipShape = RoundedCornerShape(CardClipRadius)
|
val CardClipShape = RoundedCornerShape(CardClipRadius)
|
||||||
val CardShape = RoundedCornerShape(DefaultCornerRadius)
|
val CardShape = RoundedCornerShape(DefaultCornerRadius)
|
||||||
val NoShape = RoundedCornerShape(NoRounding)
|
val NoShape = RoundedCornerShape(NoRounding)
|
||||||
|
|
||||||
// Opacity Levels
|
|
||||||
const val ALPHA_HIGH = 0.6f
|
const val ALPHA_HIGH = 0.6f
|
||||||
const val ALPHA_MEDIUM = 0.5f
|
const val ALPHA_MEDIUM = 0.4f
|
||||||
const val ALPHA_LOW = 0.3f
|
const val ALPHA_LOW = 0.2f // Adjusted for glass
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A styled card container for displaying content with a consistent floating look.
|
* Standard Glassmorphism Modifier
|
||||||
*
|
|
||||||
* @param modifier The modifier to be applied to the card.
|
|
||||||
* @param content The content to be displayed inside the card.
|
|
||||||
*/
|
*/
|
||||||
|
fun Modifier.glassmorphic(
|
||||||
|
shape: Shape = ComponentDefaults.DefaultShape,
|
||||||
|
alpha: Float = ComponentDefaults.ALPHA_LOW,
|
||||||
|
borderAlpha: Float = 0.15f
|
||||||
|
): Modifier = composed {
|
||||||
|
this
|
||||||
|
.shadow(elevation = 8.dp, shape = shape, spotColor = Color.Black.copy(alpha = 0.05f))
|
||||||
|
.clip(shape)
|
||||||
|
.background(MaterialTheme.colorScheme.surface.copy(alpha = alpha))
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = borderAlpha),
|
||||||
|
shape = shape
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppCard(
|
fun AppCard(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -106,68 +106,35 @@ fun AppCard(
|
|||||||
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,
|
||||||
label = "Chevron Rotation"
|
label = "Chevron Rotation"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check if we need to render the header row
|
|
||||||
// Updated to include icon in the check
|
|
||||||
val hasHeader = title != null || text != null || expandable || icon != null
|
val 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
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.shadow(
|
.glassmorphic(shape = ComponentDefaults.CardShape, alpha = 0.25f)
|
||||||
DefaultElevation,
|
|
||||||
shape = ComponentDefaults.CardShape
|
|
||||||
)
|
|
||||||
.clip(ComponentDefaults.CardClipShape)
|
|
||||||
// Animate height changes when expanding/collapsing
|
|
||||||
.animateContentSize(),
|
.animateContentSize(),
|
||||||
shape = ComponentDefaults.CardShape,
|
shape = ComponentDefaults.CardShape,
|
||||||
color = MaterialTheme.colorScheme.surfaceContainer,
|
color = Color.Transparent // Let glassmorphic handle the background
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
// --- Header Row ---
|
|
||||||
if (hasHeader) {
|
if (hasHeader) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(enabled = canClickHeader) {
|
.clickable(enabled = expandable) { isExpanded = !isExpanded }
|
||||||
if (expandable) {
|
|
||||||
isExpanded = !isExpanded
|
|
||||||
}
|
|
||||||
onClick?.invoke()
|
|
||||||
}
|
|
||||||
.padding(ComponentDefaults.CardPadding),
|
.padding(ComponentDefaults.CardPadding),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// 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,
|
||||||
@@ -177,7 +144,6 @@ fun AppCard(
|
|||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Title and Text Column
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
if (!title.isNullOrBlank()) {
|
if (!title.isNullOrBlank()) {
|
||||||
Text(
|
Text(
|
||||||
@@ -186,12 +152,9 @@ fun AppCard(
|
|||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only show spacer if both title and text exist
|
|
||||||
if (!title.isNullOrBlank() && !text.isNullOrBlank()) {
|
if (!title.isNullOrBlank() && !text.isNullOrBlank()) {
|
||||||
Spacer(Modifier.size(4.dp))
|
Spacer(Modifier.size(4.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!text.isNullOrBlank()) {
|
if (!text.isNullOrBlank()) {
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
@@ -201,17 +164,6 @@ fun AppCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showHints && hintContent != null) {
|
|
||||||
IconButton(onClick = { showBottomSheet = true }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = AppIcons.Help,
|
|
||||||
contentDescription = stringResource(R.string.show_hint),
|
|
||||||
tint = MaterialTheme.colorScheme.secondary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Expand Chevron (Far right)
|
|
||||||
if (expandable) {
|
if (expandable) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = AppIcons.ArrowDropDown,
|
imageVector = AppIcons.ArrowDropDown,
|
||||||
@@ -221,32 +173,18 @@ fun AppCard(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Content Area ---
|
|
||||||
if (!expandable || isExpanded) {
|
if (!expandable || isExpanded) {
|
||||||
val contentModifier = Modifier
|
Column(
|
||||||
.padding(
|
modifier = Modifier.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 no header (legacy behavior), keep the top padding.
|
|
||||||
top = if (hasHeader) 0.dp else ComponentDefaults.CardPadding
|
top = if (hasHeader) 0.dp else ComponentDefaults.CardPadding
|
||||||
)
|
),
|
||||||
|
|
||||||
if (!hasHeader && onClick != null) {
|
|
||||||
Column(
|
|
||||||
modifier = contentModifier.clickable { onClick() },
|
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
Column(
|
|
||||||
modifier = contentModifier,
|
|
||||||
content = content
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -354,31 +292,27 @@ fun AppButton(
|
|||||||
modifier: Modifier? = Modifier,
|
modifier: Modifier? = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
shape: Shape? = null,
|
shape: Shape? = null,
|
||||||
colors: ButtonColors = ButtonDefaults.buttonColors(),
|
colors: ButtonColors = ButtonDefaults.buttonColors(
|
||||||
elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
|
containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f) // Glassy primary
|
||||||
|
),
|
||||||
|
elevation: ButtonElevation? = ButtonDefaults.buttonElevation(defaultElevation = 0.dp),
|
||||||
border: BorderStroke? = null,
|
border: BorderStroke? = null,
|
||||||
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
|
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
|
||||||
interactionSource: MutableInteractionSource? = null,
|
interactionSource: MutableInteractionSource? = null,
|
||||||
content: @Composable RowScope.() -> Unit
|
content: @Composable RowScope.() -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val m = modifier ?: Modifier.height(ComponentDefaults.DefaultButtonHeight)
|
val m = modifier ?: Modifier.height(ComponentDefaults.DefaultButtonHeight)
|
||||||
val s = shape ?: ComponentDefaults.DefaultShape
|
val s = shape ?: ComponentDefaults.DefaultShape
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = m,
|
modifier = m.border(1.dp, MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.2f), s),
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
shape = s,
|
shape = s,
|
||||||
colors = colors,
|
colors = colors,
|
||||||
elevation = elevation,
|
elevation = elevation,
|
||||||
border = border,
|
border = border,
|
||||||
contentPadding = PaddingValues(
|
contentPadding = PaddingValues(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 8.dp),
|
||||||
start = 8.dp, // More horizontal padding
|
|
||||||
end = 8.dp,
|
|
||||||
top = 8.dp, // Default vertical padding
|
|
||||||
bottom = 8.dp
|
|
||||||
),
|
|
||||||
interactionSource = interactionSource
|
interactionSource = interactionSource
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
@@ -418,11 +352,7 @@ fun AppOutlinedButton(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun PrimaryButtonWithIconPreview() {
|
|
||||||
PrimaryButton(onClick = { }, text = stringResource(R.string.primary_with_icon), icon = AppIcons.Add)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The secondary button for less prominent actions.
|
* The secondary button for less prominent actions.
|
||||||
|
|||||||
@@ -56,8 +56,6 @@ 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,
|
||||||
) {
|
) {
|
||||||
@@ -70,13 +68,9 @@ 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, restrictToAlternateLanguages) {
|
val languages = remember(alternateLanguages, defaultLanguages) {
|
||||||
if (restrictToAlternateLanguages) {
|
|
||||||
alternateLanguages
|
|
||||||
} else {
|
|
||||||
alternateLanguages.ifEmpty { defaultLanguages }
|
alternateLanguages.ifEmpty { defaultLanguages }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val buttonText = when {
|
val buttonText = when {
|
||||||
enableMultipleSelection && selectedLanguagesCount > 0 -> stringResource(
|
enableMultipleSelection && selectedLanguagesCount > 0 -> stringResource(
|
||||||
@@ -96,7 +90,6 @@ 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
|
||||||
) {
|
) {
|
||||||
@@ -229,12 +222,7 @@ fun BaseLanguageDropDown(
|
|||||||
val isSearching = searchText.isNotBlank()
|
val isSearching = searchText.isNotBlank()
|
||||||
|
|
||||||
if (isSearching) {
|
if (isSearching) {
|
||||||
val searchBase = if (restrictToAlternateLanguages) {
|
val searchResults = (favoriteLanguages + languageHistory + languages)
|
||||||
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)
|
||||||
@@ -249,16 +237,6 @@ fun BaseLanguageDropDown(
|
|||||||
searchResults.forEach { language -> SingleSelectItem(language) }
|
searchResults.forEach { language -> SingleSelectItem(language) }
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (restrictToAlternateLanguages) {
|
|
||||||
val sortedAlternate = alternateLanguages.sortedBy { it.name }
|
|
||||||
if (enableMultipleSelection) {
|
|
||||||
DropdownHeader(text = stringResource(R.string.text_all_languages))
|
|
||||||
sortedAlternate.forEach { language -> MultiSelectItem(language) }
|
|
||||||
} else {
|
|
||||||
DropdownHeader(text = stringResource(R.string.text_all_languages))
|
|
||||||
sortedAlternate.forEach { language -> SingleSelectItem(language) }
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (alternateLanguages.isNotEmpty()) {
|
} else if (alternateLanguages.isNotEmpty()) {
|
||||||
val sortedAlternate = alternateLanguages.sortedBy { it.name }
|
val sortedAlternate = alternateLanguages.sortedBy { it.name }
|
||||||
if (enableMultipleSelection) {
|
if (enableMultipleSelection) {
|
||||||
@@ -480,9 +458,7 @@ 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()
|
||||||
|
|
||||||
@@ -501,10 +477,6 @@ 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,6 +6,8 @@ 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
|
||||||
@@ -27,6 +29,8 @@ fun CategorySelectionDialog(
|
|||||||
val activity = LocalContext.current.findActivity()
|
val activity = LocalContext.current.findActivity()
|
||||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
|
|
||||||
|
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
||||||
|
|
||||||
AppDialog(
|
AppDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
title = {
|
title = {
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
@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,6 +35,7 @@ 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
|
||||||
@@ -44,8 +45,10 @@ 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>() }
|
||||||
@@ -62,7 +65,7 @@ fun VocabularyReviewScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = stringResource(R.string.found_items),
|
title = { Text(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 (_: Exception) {
|
} catch (e: 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,6 +466,12 @@ 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,6 +10,8 @@ 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
|
||||||
@@ -26,6 +28,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.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
|
||||||
@@ -343,8 +346,28 @@ fun DictionarySimpleTopBar(
|
|||||||
onNavigateBack: () -> Unit
|
onNavigateBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = "TODO",
|
title = {
|
||||||
onNavigateBack = onNavigateBack
|
Column {
|
||||||
|
Text(
|
||||||
|
text = word ?: stringResource(R.string.text_loading_3d),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
languageName?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontStyle = FontStyle.Italic
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onNavigateBack) {
|
||||||
|
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
@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,6 +30,7 @@ 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
|
||||||
@@ -93,8 +94,27 @@ fun EtymologyResultScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = "TODO",
|
title = {
|
||||||
onNavigateBack = { navController.popBackStack() },
|
Column {
|
||||||
|
Text(
|
||||||
|
text = word,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
language?.name?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.popBackStack() }) {
|
||||||
|
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||||
|
}
|
||||||
|
},
|
||||||
actions = {
|
actions = {
|
||||||
etymologyData?.let { data ->
|
etymologyData?.let { data ->
|
||||||
if (isTtsAvailable) {
|
if (isTtsAvailable) {
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ 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
|
||||||
@@ -64,15 +63,7 @@ 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.AllCardsListScreen
|
import eu.gaudian.translator.view.vocabulary.VocabularyListScreen
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExerciseVocabularyScreen(
|
fun ExerciseVocabularyScreen(
|
||||||
@@ -24,7 +24,7 @@ fun ExerciseVocabularyScreen(
|
|||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(title =stringResource(R.string.text_new_vocabulary_for_this_exercise))
|
AppTopAppBar(title = { Text(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)) {
|
||||||
|
|
||||||
AllCardsListScreen(
|
VocabularyListScreen(
|
||||||
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,7 +38,6 @@ 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
|
||||||
@@ -77,15 +76,7 @@ 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)) {
|
||||||
|
|||||||
@@ -1,932 +0,0 @@
|
|||||||
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,6 +16,8 @@ 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
|
||||||
@@ -24,10 +26,12 @@ 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
|
||||||
@@ -57,8 +61,12 @@ fun YouTubeBrowserScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = "YouTube" ,
|
title = { Text("YouTube") },
|
||||||
onNavigateBack = { navController.popBackStack() }
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.popBackStack() }) {
|
||||||
|
Icon(AppIcons.ArrowBack, stringResource(R.string.cd_back))
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
|
|||||||
@@ -183,8 +183,14 @@ fun YouTubeExerciseScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = title,
|
title = { Text(title, maxLines = 1) },
|
||||||
onNavigateBack = { navController.popBackStack() },
|
navigationIcon = {
|
||||||
|
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),
|
||||||
VOCABULARY_GENERATE_AI("import_hint", R.string.hint_how_to_generate_vocabulary_with_ai),
|
IMPORT("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,6 +40,7 @@ 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,3 +1,5 @@
|
|||||||
|
@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: Hint,
|
content: @Composable (() -> Unit)?
|
||||||
) {
|
) {
|
||||||
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.Render()
|
content?.invoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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
|
||||||
@@ -15,6 +16,7 @@ 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
|
||||||
@@ -37,8 +39,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
|
||||||
@@ -67,16 +69,27 @@ fun WithHint(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showBottomSheet) {
|
if (showBottomSheet) {
|
||||||
hintContent?.let {
|
|
||||||
HintBottomSheet(
|
HintBottomSheet(
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
@Suppress("AssignedValueIsNeverRead")
|
@Suppress("AssignedValueIsNeverRead")
|
||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
},
|
},
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
content = it,
|
hintContent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun WithHintPreview() {
|
||||||
|
androidx.compose.runtime.CompositionLocalProvider(LocalShowHints provides true) {
|
||||||
|
WithHint(
|
||||||
|
hintContent = {
|
||||||
|
Text(stringResource(R.string.this_is_a_hint))
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.this_is_the_main_content))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,9 +5,15 @@ 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
|
||||||
|
|
||||||
@@ -24,8 +30,12 @@ fun HintScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = title,
|
title = { Text(title) },
|
||||||
onNavigateBack = { navController.popBackStack() }
|
navigationIcon = {
|
||||||
|
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.VOCABULARY_GENERATE_AI.hint()
|
val importHint = HintDefinition.IMPORT.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 = stringResource(R.string.hint_title_hints_overview)
|
title = { Text(stringResource(R.string.hint_title_hints_overview), style = MaterialTheme.typography.titleLarge) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ object MarkdownHintLoader {
|
|||||||
append(language.lowercase())
|
append(language.lowercase())
|
||||||
}
|
}
|
||||||
if (country.isNotEmpty()) {
|
if (country.isNotEmpty()) {
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
append("-r")
|
append("-r")
|
||||||
append(country.uppercase())
|
append(country.uppercase())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,433 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,730 +0,0 @@
|
|||||||
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 = {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,600 +0,0 @@
|
|||||||
@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,6 +17,7 @@ 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
|
||||||
@@ -72,8 +73,12 @@ fun AboutScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = stringResource(R.string.label_about),
|
title = { Text(stringResource(R.string.label_about)) },
|
||||||
onNavigateBack = { navController.popBackStack() }
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.popBackStack() }) {
|
||||||
|
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -134,8 +134,12 @@ fun AddModelScreen(navController: NavController, providerKey: String) {
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = providerName,
|
title = { Text(providerName) },
|
||||||
onNavigateBack = { navController.popBackStack() },
|
navigationIcon = {
|
||||||
|
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,8 +115,12 @@ fun ApiKeyScreen(navController: NavController) {
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = stringResource(R.string.label_ai_configuration),
|
title = { Text(stringResource(R.string.label_ai_configuration)) },
|
||||||
onNavigateBack = { navController.popBackStack() },
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.popBackStack() }) {
|
||||||
|
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||||
|
}
|
||||||
|
},
|
||||||
hintContent = HintDefinition.API_KEY.hint()
|
hintContent = HintDefinition.API_KEY.hint()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -133,7 +137,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,6 +5,9 @@ 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
|
||||||
@@ -19,6 +22,7 @@ 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
|
||||||
@@ -51,8 +55,12 @@ fun CustomVocabularyPromptScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = stringResource(R.string.text_vocabulary_prompt),
|
title = { Text(stringResource(R.string.text_vocabulary_prompt)) },
|
||||||
onNavigateBack = { navController.popBackStack() },
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.popBackStack() }) {
|
||||||
|
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||||
|
}
|
||||||
|
},
|
||||||
hintContent = null //TODO: Add hint
|
hintContent = null //TODO: Add hint
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ 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
|
||||||
@@ -28,6 +31,7 @@ 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
|
||||||
@@ -62,8 +66,12 @@ fun DictionaryOptionsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = stringResource(R.string.label_dictionary_options),
|
title = { Text(stringResource(R.string.label_dictionary_options)) },
|
||||||
onNavigateBack = { navController.popBackStack() },
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.popBackStack() }) {
|
||||||
|
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||||
|
}
|
||||||
|
},
|
||||||
hintContent = HintDefinition.DICTIONARY_OPTIONS.hint()
|
hintContent = HintDefinition.DICTIONARY_OPTIONS.hint()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ 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
|
||||||
@@ -29,6 +31,7 @@ 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
|
||||||
@@ -68,8 +71,12 @@ fun ExerciseSettingsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = stringResource(R.string.exercise_settings),
|
title = { Text(stringResource(R.string.exercise_settings)) },
|
||||||
onNavigateBack = { navController.popBackStack() },
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.popBackStack() }) {
|
||||||
|
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ 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
|
||||||
@@ -22,6 +24,7 @@ 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
|
||||||
@@ -38,8 +41,12 @@ fun GeneralSettingsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = stringResource(R.string.label_general),
|
title = { Text(stringResource(R.string.label_general)) },
|
||||||
onNavigateBack = { navController.popBackStack() },
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.popBackStack() }) {
|
||||||
|
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -61,8 +61,12 @@ fun LanguageOptionsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = stringResource(R.string.text_language_options),
|
title = { Text(stringResource(R.string.text_language_options)) },
|
||||||
onNavigateBack = { navController.popBackStack() },
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.popBackStack() }) {
|
||||||
|
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
@@ -128,7 +132,6 @@ fun LanguageOptionsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showAddLanguageDialog) {
|
if (showAddLanguageDialog) {
|
||||||
@Suppress("KotlinConstantConditions")
|
|
||||||
AddCustomLanguageDialog(
|
AddCustomLanguageDialog(
|
||||||
showDialog = showAddLanguageDialog,
|
showDialog = showAddLanguageDialog,
|
||||||
onDismiss = { showAddLanguageDialog = false },
|
onDismiss = { showAddLanguageDialog = false },
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ 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
|
||||||
@@ -96,11 +97,16 @@ 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 = stringResource(R.string.label_appearance),
|
title = { Text(stringResource(R.string.label_appearance)) },
|
||||||
onNavigateBack = { navController.popBackStack() },
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.popBackStack() }) {
|
||||||
|
Icon(AppIcons.ArrowBack, contentDescription = cdBack)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -101,8 +101,15 @@ fun LogsScreen(navController: NavController) {
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = stringResource(R.string.label_logs),
|
title = { Text(stringResource(R.string.label_logs)) },
|
||||||
onNavigateBack = { navController.popBackStack() },
|
navigationIcon = {
|
||||||
|
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,7 +27,6 @@ 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)
|
||||||
|
|
||||||
@@ -85,15 +84,7 @@ fun MainSettingsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title =stringResource(R.string.title_settings),
|
title = { Text(stringResource(R.string.title_settings), style = MaterialTheme.typography.titleLarge) }
|
||||||
onNavigateBack = {
|
|
||||||
if (!navController.popBackStack()) {
|
|
||||||
navController.navigate(Screen.Home.route) {
|
|
||||||
launchSingleTop = true
|
|
||||||
restoreState = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
@@ -105,7 +96,7 @@ fun MainSettingsScreen(
|
|||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
AppCard(
|
AppCard(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
modifier = Modifier.padding(horizontal = 8.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.VOCABULARY_GENERATE_AI)
|
HintScreen(navController, HintDefinition.IMPORT)
|
||||||
}
|
}
|
||||||
composable(SettingsRoutes.HINTS_SORTING) {
|
composable(SettingsRoutes.HINTS_SORTING) {
|
||||||
HintScreen(navController, HintDefinition.SORTING)
|
HintScreen(navController, HintDefinition.SORTING)
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ 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
|
||||||
@@ -84,8 +86,12 @@ fun TextToSpeechSettingsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = stringResource(R.string.settings_title_voice),
|
title = { Text(stringResource(R.string.settings_title_voice)) },
|
||||||
onNavigateBack = { navController.popBackStack() },
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.popBackStack() }) {
|
||||||
|
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ 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
|
||||||
@@ -24,6 +27,7 @@ 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
|
||||||
@@ -60,8 +64,12 @@ fun TranslationSettingsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = stringResource(R.string.label_translation_settings),
|
title = { Text(stringResource(R.string.label_translation_settings)) },
|
||||||
onNavigateBack = { navController.popBackStack() },
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.popBackStack() }) {
|
||||||
|
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||||
|
}
|
||||||
|
},
|
||||||
hintContent = null //TODO add hint
|
hintContent = null //TODO add hint
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ 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
|
||||||
@@ -77,8 +78,13 @@ fun VocabularyProgressOptionsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = stringResource(R.string.label_vocabulary_settings),
|
title = { Text(stringResource(R.string.vocabulary_settings)) },
|
||||||
onNavigateBack = { navController.popBackStack() },
|
navigationIcon = {
|
||||||
|
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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -91,28 +97,45 @@ fun VocabularyProgressOptionsScreen(
|
|||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
// 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.text_interval_settings_in_days),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
text = stringResource(R.string.text_customize_the_intervals),
|
||||||
) {
|
|
||||||
|
|
||||||
@Suppress("USELESS_ELVIS", "HardCodedStringLiteral")
|
|
||||||
SettingsSlider(
|
|
||||||
label = stringResource(R.string.label_target_correct_answers_per_day),
|
) {
|
||||||
value = dailyGoal ?: 10,
|
val intervals by settingsViewModel.intervals.collectAsStateWithLifecycle()
|
||||||
onValueChange = { settingsViewModel.setDailyGoal(it) },
|
Column(
|
||||||
valueRange = 10f..100f,
|
modifier = Modifier
|
||||||
steps = 17 // Allows snapping in steps of 5
|
.padding(16.dp)
|
||||||
)
|
.animateContentSize(),
|
||||||
Text(
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
text = stringResource(R.string.text_daily_goal_description),
|
) {
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
IntervalTimeline(intervals = intervals)
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
intervals.forEach { (stageKey, days) ->
|
||||||
|
val displayLabel = labelForStage(stageKey)
|
||||||
|
IntervalSlider(
|
||||||
|
label = displayLabel,
|
||||||
|
value = days,
|
||||||
|
onValueChange = { newValue ->
|
||||||
|
settingsViewModel.setInterval(stageKey, newValue)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
TextButton(onClick = { settingsViewModel.resetIntervalsToDefaults() }) {
|
||||||
|
Text(stringResource(R.string.reset_to_defaults))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Criteria Settings
|
// Criteria Settings
|
||||||
@@ -146,41 +169,30 @@ fun VocabularyProgressOptionsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interval Settings
|
// Daily Goal Settings
|
||||||
AppCard(
|
AppCard {
|
||||||
expandable = true,
|
val dailyGoal by settingsViewModel.dailyGoal.collectAsStateWithLifecycle()
|
||||||
initiallyExpanded = true,
|
|
||||||
title = stringResource(R.string.label_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)
|
Text(
|
||||||
intervals.forEach { (stageKey, days) ->
|
text = stringResource(R.string.daily_learning_goal),
|
||||||
val displayLabel = labelForStage(stageKey)
|
style = MaterialTheme.typography.titleMedium
|
||||||
IntervalSlider(
|
)
|
||||||
label = displayLabel,
|
@Suppress("USELESS_ELVIS", "HardCodedStringLiteral")
|
||||||
value = days,
|
SettingsSlider(
|
||||||
onValueChange = { newValue ->
|
label = stringResource(R.string.target_correct_answers_per_day),
|
||||||
settingsViewModel.setInterval(stageKey, newValue)
|
value = dailyGoal ?: 10,
|
||||||
}
|
onValueChange = { settingsViewModel.setDailyGoal(it) },
|
||||||
|
valueRange = 10f..100f,
|
||||||
|
steps = 17 // Allows snapping in steps of 5
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.text_daily_goal_description),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.End
|
|
||||||
) {
|
|
||||||
TextButton(onClick = { settingsViewModel.resetIntervalsToDefaults() }) {
|
|
||||||
Text(stringResource(R.string.reset_to_defaults))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ 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
|
||||||
@@ -42,6 +44,7 @@ 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
|
||||||
@@ -197,8 +200,12 @@ fun VocabularyRepositoryOptionsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = stringResource(R.string.vocabulary_repository),
|
title = { Text(stringResource(R.string.vocabulary_repository)) },
|
||||||
onNavigateBack = { navController.popBackStack() },
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.popBackStack() }) {
|
||||||
|
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -1,678 +0,0 @@
|
|||||||
@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,7 +37,6 @@ 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
|
||||||
|
|
||||||
@@ -68,23 +67,9 @@ fun ActionBar(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TopBarActions(
|
fun TopBarActions(languageViewModel: LanguageViewModel, onSettingsClick: () -> Unit, hintContent: (@Composable () -> Unit)? = null) {
|
||||||
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,14 +106,6 @@ 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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -127,7 +119,6 @@ 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()
|
||||||
@@ -176,8 +167,7 @@ private fun LoadedTranslationContent(
|
|||||||
TopBarActions(
|
TopBarActions(
|
||||||
languageViewModel = languageViewModel,
|
languageViewModel = languageViewModel,
|
||||||
onSettingsClick = onSettingsClick,
|
onSettingsClick = onSettingsClick,
|
||||||
onNavigateBack = onNavigateBack,
|
hintContent = { HintDefinition.TRANSLATION.Render() }
|
||||||
hintContent = HintDefinition.TRANSLATION.hint()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
AppCard(modifier = Modifier.padding(8.dp, end = 8.dp, bottom = 8.dp, top = 0.dp)) {
|
AppCard(modifier = Modifier.padding(8.dp, end = 8.dp, bottom = 8.dp, top = 0.dp)) {
|
||||||
|
|||||||
@@ -5,16 +5,14 @@ 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.height
|
import androidx.compose.foundation.layout.heightIn
|
||||||
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
|
||||||
@@ -32,8 +30,7 @@ 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.TextAlign
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
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
|
||||||
@@ -46,12 +43,10 @@ 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
|
||||||
@@ -94,6 +89,7 @@ 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
|
||||||
@@ -122,8 +118,30 @@ fun CategoryDetailScreen(
|
|||||||
modifier = Modifier.background(MaterialTheme.colorScheme.surface)
|
modifier = Modifier.background(MaterialTheme.colorScheme.surface)
|
||||||
) {
|
) {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = title,
|
title = {
|
||||||
onNavigateBack = { navController.popBackStack() },
|
Column {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBackClick) {
|
||||||
|
Icon(
|
||||||
|
AppIcons.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.cd_back)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = { showMenu = !showMenu }) {
|
IconButton(onClick = { showMenu = !showMenu }) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -132,58 +150,94 @@ 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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Category Header Card with Progress and Action Buttons
|
Row(
|
||||||
CategoryHeaderCard(
|
modifier = Modifier
|
||||||
subtitle = subtitle,
|
.fillMaxWidth()
|
||||||
categoryProgress = categoryProgress,
|
.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||||
onStartExerciseClick = {
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
if (categoryProgress != null) {
|
||||||
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
|
CategoryProgressCircle(
|
||||||
|
totalItems = categoryProgress.totalItems,
|
||||||
|
itemsCompleted = categoryProgress.itemsCompleted,
|
||||||
|
itemsInStages = categoryProgress.itemsInStages,
|
||||||
|
newItems = categoryProgress.newItems,
|
||||||
|
circleSize = 80.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
) {
|
||||||
|
PrimaryButton(
|
||||||
|
text = stringResource(R.string.label_start),
|
||||||
|
icon = AppIcons.Play,
|
||||||
|
onClick = {
|
||||||
val categories = listOf(category)
|
val categories = listOf(category)
|
||||||
val categoryIds = categories.joinToString(",") { it?.id.toString() }
|
val categoryIds = categories.joinToString(",") { it?.id.toString() }
|
||||||
navController.navigate("vocabulary_exercise/false?categories=$categoryIds")
|
navController.navigate("vocabulary_exercise/false?categories=$categoryIds")
|
||||||
},
|
},
|
||||||
onEditClick = {
|
modifier = Modifier.heightIn(max = 80.dp)
|
||||||
categoryViewModel.setShowEditCategoryDialog(true, categoryId)
|
|
||||||
},
|
|
||||||
onDeleteClick = {
|
|
||||||
categoryViewModel.setShowDeleteCategoryDialog(true, categoryId)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(modifier = Modifier.padding(paddingValues)) {
|
Column(modifier = Modifier.padding(paddingValues)) {
|
||||||
AllCardsListScreen(
|
VocabularyListScreen(
|
||||||
categoryId = categoryId,
|
categoryId = categoryId,
|
||||||
showDueTodayOnly = false,
|
showDueTodayOnly = false,
|
||||||
onNavigateToItem = onNavigateToItem,
|
onNavigateToItem = onNavigateToItem,
|
||||||
navController = navController,
|
navController = navController, // Pass the received navController here
|
||||||
isRemoveFromCategoryEnabled = category is TagCategory,
|
isRemoveFromCategoryEnabled = category is TagCategory,
|
||||||
showTopBar = false,
|
showTopBar = false,
|
||||||
enableNavigationButtons = true
|
enableNavigationButtons = true
|
||||||
@@ -212,131 +266,3 @@ 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,7 +100,13 @@ fun CategoryListScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = "TODO",
|
title = {
|
||||||
|
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 = {
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ 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
|
||||||
@@ -64,6 +65,7 @@ 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
|
||||||
@@ -528,7 +530,17 @@ 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 = stringResource(R.string.your_language_journey),
|
title = { Text(stringResource(R.string.your_language_journey)) },
|
||||||
onNavigateBack = { navController.popBackStack() }
|
onNavigateBack = { navController.popBackStack() }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,474 @@
|
|||||||
|
@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)
|
||||||
|
}
|
||||||
@@ -1,289 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,780 +0,0 @@
|
|||||||
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,6 +14,8 @@ 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
|
||||||
@@ -37,6 +39,7 @@ 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
|
||||||
@@ -54,7 +57,7 @@ fun NoGrammarItemsScreen(
|
|||||||
|
|
||||||
var showFetchGrammarDialog by remember { mutableStateOf(false) }
|
var showFetchGrammarDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@Suppress("UnusedVariable") val onClose = { navController.popBackStack() }
|
@Suppress("UnusedVariable", "unused", "HardCodedStringLiteral") val onClose = { navController.popBackStack() }
|
||||||
|
|
||||||
if (itemsWithoutGrammar.isEmpty() && !isGenerating) {
|
if (itemsWithoutGrammar.isEmpty() && !isGenerating) {
|
||||||
Column(
|
Column(
|
||||||
@@ -63,8 +66,12 @@ fun NoGrammarItemsScreen(
|
|||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = stringResource(R.string.title_items_without_grammar),
|
title = { Text(stringResource(R.string.title_items_without_grammar)) },
|
||||||
onNavigateBack = { navController.popBackStack() },
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.popBackStack() }) {
|
||||||
|
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -80,8 +87,8 @@ fun NoGrammarItemsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Use the generic AllCardsListScreen to display the items
|
// Use the generic VocabularyListScreen to display the items
|
||||||
AllCardsListScreen(
|
VocabularyListScreen(
|
||||||
itemsToShow = itemsWithoutGrammar,
|
itemsToShow = itemsWithoutGrammar,
|
||||||
onNavigateToItem = { item ->
|
onNavigateToItem = { item ->
|
||||||
@Suppress("HardCodedStringLiteral")
|
@Suppress("HardCodedStringLiteral")
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
@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,6 +2,9 @@ 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
|
||||||
@@ -14,6 +17,7 @@ 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
|
||||||
@@ -36,8 +40,15 @@ fun StageDetailScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = stringResource(R.string.due_today_, stage.toString()),
|
title = { Text(text = stringResource(R.string.due_today_, stage.toString())) },
|
||||||
onNavigateBack = { navController.popBackStack() },
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.popBackStack() }) {
|
||||||
|
Icon(
|
||||||
|
AppIcons.ArrowBack,
|
||||||
|
contentDescription =stringResource(R.string.cd_back)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
@@ -49,7 +60,7 @@ fun StageDetailScreen(
|
|||||||
onStageTapped = {},
|
onStageTapped = {},
|
||||||
)
|
)
|
||||||
|
|
||||||
AllCardsListScreen(
|
VocabularyListScreen(
|
||||||
categoryId = null,
|
categoryId = null,
|
||||||
showDueTodayOnly = true,
|
showDueTodayOnly = true,
|
||||||
stage = stage,
|
stage = stage,
|
||||||
|
|||||||
@@ -0,0 +1,467 @@
|
|||||||
|
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,23 +1,14 @@
|
|||||||
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
|
||||||
@@ -48,9 +39,10 @@ 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.VocabularyDisplayCard
|
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.VocabularyViewModel
|
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.time.ExperimentalTime
|
import kotlin.time.ExperimentalTime
|
||||||
@@ -67,6 +59,7 @@ 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()
|
||||||
@@ -78,18 +71,26 @@ 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 = stringResource(R.string.item_details),
|
title = {
|
||||||
onNavigateBack = { navController.popBackStack() },
|
if (navigationItems.isNotEmpty()) {
|
||||||
|
Text(stringResource(R.string.label_card_with_position, navigationPosition + 1, navigationItems.size))
|
||||||
|
} else {
|
||||||
|
Text(stringResource(R.string.item_details))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { onBackPressed?.invoke() }) {
|
||||||
|
Icon(
|
||||||
|
AppIcons.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.cd_back)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
actions = {
|
actions = {
|
||||||
if (!isEditing) {
|
|
||||||
// Previous button
|
// Previous button
|
||||||
if (navigationPosition > 0) {
|
if (navigationPosition > 0) {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
@@ -129,7 +130,6 @@ fun VocabularyCardHost(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
@@ -146,12 +146,8 @@ 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
|
||||||
@@ -203,45 +199,9 @@ fun VocabularyCardHost(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Main content
|
// Main content
|
||||||
Column(
|
VocabularyCard(
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
|
||||||
if (!exerciseMode && isEditing) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
OutlinedButton(
|
|
||||||
onClick = { onCancelEdit?.invoke() },
|
|
||||||
shape = RoundedCornerShape(16.dp)
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(R.string.label_cancel))
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Button(
|
|
||||||
onClick = { onSaveEdit?.invoke() },
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(R.string.label_save))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exerciseMode) {
|
|
||||||
VocabularyExerciseCard(
|
|
||||||
vocabularyItem = currentVocabularyItem,
|
|
||||||
switchOrder = switchOrder == true,
|
|
||||||
isFlipped = isFlipped,
|
|
||||||
navController = navController,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
VocabularyDisplayCard(
|
|
||||||
vocabularyItem = currentVocabularyItem,
|
vocabularyItem = currentVocabularyItem,
|
||||||
|
exerciseMode = exerciseMode,
|
||||||
switchOrder = switchOrder == true,
|
switchOrder = switchOrder == true,
|
||||||
isFlipped = isFlipped,
|
isFlipped = isFlipped,
|
||||||
onStatisticsClick = { showStatisticsDialog = true },
|
onStatisticsClick = { showStatisticsDialog = true },
|
||||||
@@ -249,20 +209,8 @@ fun VocabularyCardHost(
|
|||||||
onMoveToStageClick = { showStageDialog = true },
|
onMoveToStageClick = { showStageDialog = true },
|
||||||
onDeleteClick = { showDeleteDialog = true },
|
onDeleteClick = { showDeleteDialog = true },
|
||||||
navController = navController,
|
navController = navController,
|
||||||
onEditStateChange = { editing ->
|
isUserSpellingCorrect = false,
|
||||||
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) {
|
||||||
@@ -311,6 +259,16 @@ 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.VocabularyExerciseCard
|
import eu.gaudian.translator.view.vocabulary.card.VocabularyCard
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -141,10 +141,11 @@ fun GuessingExercise(
|
|||||||
navController: NavController,
|
navController: NavController,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
VocabularyExerciseCard(
|
VocabularyCard(
|
||||||
vocabularyItem = state.item,
|
vocabularyItem = state.item,
|
||||||
isFlipped = state.isRevealed,
|
isFlipped = state.isRevealed,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
|
exerciseMode = true,
|
||||||
switchOrder = state.isSwitched,
|
switchOrder = state.isSwitched,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -157,12 +158,13 @@ fun SpellingExercise(
|
|||||||
navController: NavController,
|
navController: NavController,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
VocabularyExerciseCard(
|
VocabularyCard(
|
||||||
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,5 +1,3 @@
|
|||||||
@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
|
||||||
@@ -21,6 +19,7 @@ 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
|
||||||
@@ -29,10 +28,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.utils.Log
|
import eu.gaudian.translator.model.Language
|
||||||
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.view.composable.Screen
|
import eu.gaudian.translator.viewmodel.ExerciseConfig
|
||||||
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
|
||||||
@@ -58,7 +57,14 @@ 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) }
|
||||||
@@ -70,59 +76,89 @@ fun VocabularyExerciseHostScreen(
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(categoryIdsAsJson, stageNamesAsJson, languageIdsAsJson, dailyOnly) {
|
LaunchedEffect(Unit) {
|
||||||
Log.d("ExerciseHost", "LaunchedEffect filters: categories=$categoryIdsAsJson, stages=$stageNamesAsJson, languages=$languageIdsAsJson, dailyOnly=$dailyOnly")
|
// Reset exercise state when starting fresh
|
||||||
// Only reset and prepare if the host is opened via explicit filters.
|
|
||||||
if (!categoryIdsAsJson.isNullOrBlank() || !stageNamesAsJson.isNullOrBlank() || !languageIdsAsJson.isNullOrBlank() || dailyOnly) {
|
|
||||||
Log.d("ExerciseHost", "Preparing exercise from filters")
|
|
||||||
exerciseViewModel.resetExercise()
|
exerciseViewModel.resetExercise()
|
||||||
|
|
||||||
vocabularyViewModel.prepareExercise(
|
vocabularyViewModel.prepareExercise(
|
||||||
categoryIdsAsJson,
|
categoryIdsAsJson,
|
||||||
stageNamesAsJson,
|
stageNamesAsJson,
|
||||||
languageIdsAsJson,
|
languageIdsAsJson,
|
||||||
dailyOnly = dailyOnly,
|
dailyOnly = dailyOnly,
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
Log.d("ExerciseHost", "No filters provided; skipping prepareExercise")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(cardSet, screenState, pendingConfig) {
|
if (cardSet == null && screenState != ScreenState.START) {
|
||||||
Log.d("ExerciseHost", "State update: screenState=$screenState, cardSet=${cardSet?.cards?.size ?: 0}, pendingCount=${pendingConfig.exerciseItemCount}")
|
|
||||||
if (cardSet != null && screenState == ScreenState.START) {
|
|
||||||
val items = cardSet?.cards.orEmpty()
|
|
||||||
if (items.isNotEmpty()) {
|
|
||||||
val selectedCount = pendingConfig.exerciseItemCount
|
|
||||||
.takeIf { it > 0 }
|
|
||||||
?: items.size
|
|
||||||
val finalItems = if (pendingConfig.shuffleCards) {
|
|
||||||
items.shuffled().take(selectedCount)
|
|
||||||
} else {
|
|
||||||
items.take(selectedCount)
|
|
||||||
}
|
|
||||||
Log.d("ExerciseHost", "Auto-starting exercise with ${finalItems.size} items")
|
|
||||||
exerciseViewModel.startExerciseWithConfig(
|
|
||||||
finalItems,
|
|
||||||
pendingConfig.copy(
|
|
||||||
exerciseItemCount = finalItems.size,
|
|
||||||
originalExerciseItems = finalItems
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Log.d("ExerciseHost", "CardSet present but empty")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
when (screenState) {
|
|
||||||
ScreenState.START -> {
|
|
||||||
Log.d("ExerciseHost", "Rendering START screen")
|
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
when (screenState) {
|
||||||
|
ScreenState.START -> {
|
||||||
|
StartScreen(
|
||||||
|
cardSet = cardSet,
|
||||||
|
onStartClicked = { finalItems ->
|
||||||
|
exerciseViewModel.startExerciseWithConfig(
|
||||||
|
finalItems,
|
||||||
|
ExerciseConfig(
|
||||||
|
shuffleCards = shuffleCards,
|
||||||
|
shuffleLanguages = shuffleLanguages,
|
||||||
|
trainingMode = trainingMode,
|
||||||
|
dueTodayOnly = dueTodayOnly,
|
||||||
|
selectedExerciseTypes = selectedExerciseTypes,
|
||||||
|
exerciseItemCount = finalItems.size,
|
||||||
|
originalExerciseItems = finalItems,
|
||||||
|
originLanguageId = selectedOriginLanguage?.nameResId,
|
||||||
|
targetLanguageId = selectedTargetLanguage?.nameResId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClose = onClose,
|
||||||
|
shuffleCards = shuffleCards,
|
||||||
|
onShuffleCardsChanged = {
|
||||||
|
@Suppress("AssignedValueIsNeverRead")
|
||||||
|
shuffleCards = it
|
||||||
|
},
|
||||||
|
shuffleLanguages = shuffleLanguages,
|
||||||
|
onShuffleLanguagesChanged = {
|
||||||
|
@Suppress("AssignedValueIsNeverRead")
|
||||||
|
shuffleLanguages = it
|
||||||
|
},
|
||||||
|
trainingMode = trainingMode,
|
||||||
|
onTrainingModeChanged = {
|
||||||
|
@Suppress("AssignedValueIsNeverRead")
|
||||||
|
trainingMode = it
|
||||||
|
exerciseViewModel.onTrainingModeChanged(it)
|
||||||
|
},
|
||||||
|
hideTodayOnlySwitch = dailyOnly,
|
||||||
|
dueTodayOnly = dueTodayOnly,
|
||||||
|
onDueTodayOnlyChanged = {
|
||||||
|
@Suppress("AssignedValueIsNeverRead")
|
||||||
|
dueTodayOnly = it
|
||||||
|
},
|
||||||
|
selectedExerciseTypes = selectedExerciseTypes,
|
||||||
|
onExerciseTypeSelected = { type ->
|
||||||
|
val currentTypes = selectedExerciseTypes.toMutableSet()
|
||||||
|
if (type in currentTypes) {
|
||||||
|
if (currentTypes.size > 1) currentTypes.remove(type)
|
||||||
|
} else {
|
||||||
|
currentTypes.add(type)
|
||||||
|
}
|
||||||
|
selectedExerciseTypes = currentTypes
|
||||||
|
},
|
||||||
|
selectedOriginLanguage = selectedOriginLanguage,
|
||||||
|
onOriginLanguageChanged = {
|
||||||
|
@Suppress("AssignedValueIsNeverRead")
|
||||||
|
selectedOriginLanguage = it
|
||||||
|
},
|
||||||
|
selectedTargetLanguage = selectedTargetLanguage,
|
||||||
|
onTargetLanguageChanged = {
|
||||||
|
@Suppress("AssignedValueIsNeverRead")
|
||||||
|
selectedTargetLanguage = it
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
ScreenState.EXERCISE -> {
|
ScreenState.EXERCISE -> {
|
||||||
Log.d("ExerciseHost", "Rendering EXERCISE screen")
|
|
||||||
ExerciseScreen(
|
ExerciseScreen(
|
||||||
viewModel = exerciseViewModel,
|
viewModel = exerciseViewModel,
|
||||||
onClose = onClose,
|
onClose = onClose,
|
||||||
@@ -137,7 +173,6 @@ fun VocabularyExerciseHostScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
ScreenState.RESULT -> {
|
ScreenState.RESULT -> {
|
||||||
Log.d("ExerciseHost", "Rendering RESULT screen")
|
|
||||||
val totalItems by exerciseViewModel.totalItems.collectAsState()
|
val totalItems by exerciseViewModel.totalItems.collectAsState()
|
||||||
val originalItems by exerciseViewModel.originalItems.collectAsState()
|
val originalItems by exerciseViewModel.originalItems.collectAsState()
|
||||||
ResultScreen(
|
ResultScreen(
|
||||||
@@ -151,16 +186,12 @@ fun VocabularyExerciseHostScreen(
|
|||||||
onRetryWrong = { _ ->
|
onRetryWrong = { _ ->
|
||||||
exerciseViewModel.retryWrongAnswers(originalItems)
|
exerciseViewModel.retryWrongAnswers(originalItems)
|
||||||
},
|
},
|
||||||
onClose = {
|
onClose = onClose
|
||||||
navController.navigate(Screen.Home.route) {
|
|
||||||
popUpTo(Screen.Home.route) { inclusive = true }
|
|
||||||
launchSingleTop = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ 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
|
||||||
@@ -51,7 +53,6 @@ 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
|
||||||
@@ -67,7 +68,6 @@ 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 = stringResource(R.string.label_vocabulary_activity),
|
title = { Text(stringResource(R.string.label_vocabulary_activity)) },
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -263,8 +263,7 @@ 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)
|
||||||
@@ -284,7 +283,7 @@ private fun MonthGrid(
|
|||||||
) {
|
) {
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
val locale = getDefault()
|
val locale = java.util.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)
|
||||||
@@ -386,7 +385,7 @@ private fun Legend(modifier: Modifier = Modifier) {
|
|||||||
Row(
|
Row(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.End
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.less),
|
text = stringResource(R.string.less),
|
||||||
@@ -468,11 +467,12 @@ fun StatsOverview(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun StatCard(title: String, value: String, modifier: Modifier = Modifier) {
|
private fun StatCard(title: String, value: String, modifier: Modifier = Modifier) {
|
||||||
AppCard(
|
Card(
|
||||||
modifier = modifier.padding(0.dp),
|
modifier = modifier,
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(8.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -5,13 +5,19 @@ 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
|
||||||
@@ -19,10 +25,14 @@ 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
|
||||||
@@ -33,6 +43,7 @@ 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
|
||||||
@@ -48,15 +59,20 @@ 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
|
||||||
@@ -68,10 +84,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
|
||||||
@@ -94,7 +110,7 @@ private data class VocabularyFilterState(
|
|||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AllCardsListScreen(
|
fun VocabularyListScreen(
|
||||||
categoryId: Int? = null,
|
categoryId: Int? = null,
|
||||||
showDueTodayOnly: Boolean? = null,
|
showDueTodayOnly: Boolean? = null,
|
||||||
stage: VocabularyStage? = null,
|
stage: VocabularyStage? = null,
|
||||||
@@ -229,6 +245,10 @@ fun AllCardsListScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
"search" -> SearchTopAppBar(
|
"search" -> SearchTopAppBar(
|
||||||
|
searchQuery = filterState.searchQuery,
|
||||||
|
onQueryChanged = { newQuery ->
|
||||||
|
filterState = filterState.copy(searchQuery = newQuery)
|
||||||
|
},
|
||||||
onCloseSearch = {
|
onCloseSearch = {
|
||||||
isSearchActive = false
|
isSearchActive = false
|
||||||
filterState = filterState.copy(searchQuery = "")
|
filterState = filterState.copy(searchQuery = "")
|
||||||
@@ -275,16 +295,50 @@ fun AllCardsListScreen(
|
|||||||
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)) {
|
||||||
AllCardsView(
|
if (vocabularyItems.isEmpty()) {
|
||||||
vocabularyItems = vocabularyItems,
|
Column(
|
||||||
allLanguages = allLanguages,
|
modifier = Modifier
|
||||||
selection = selection,
|
.fillMaxSize()
|
||||||
listState = lazyListState,
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier.size(200.dp),
|
||||||
|
painter = painterResource(id = R.drawable.ic_nothing_found),
|
||||||
|
contentDescription = stringResource(id = R.string.no_vocabulary_items_found_perhaps_try_changing_the_filters)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
|
|
||||||
|
Box(modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(8.dp), contentAlignment = Alignment.Center) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.no_vocabulary_items_found_perhaps_try_changing_the_filters),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
state = lazyListState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 8.dp),
|
.padding(horizontal = 8.dp),
|
||||||
onItemClick = { 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())
|
val isSelected = selection.contains(item.id.toLong())
|
||||||
|
VocabularyListItem(
|
||||||
|
item = item,
|
||||||
|
allLanguages = allLanguages,
|
||||||
|
isSelected = isSelected,
|
||||||
|
onItemClick = {
|
||||||
if (isInSelectionMode) {
|
if (isInSelectionMode) {
|
||||||
selection = if (isSelected) {
|
selection = if (isSelected) {
|
||||||
selection - item.id.toLong()
|
selection - item.id.toLong()
|
||||||
@@ -300,16 +354,20 @@ fun AllCardsListScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onItemLongClick = { item ->
|
onItemLongClick = {
|
||||||
if (!isInSelectionMode) {
|
if (!isInSelectionMode) {
|
||||||
selection = setOf(item.id.toLong())
|
selection = setOf(item.id.toLong())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDeleteClick = { item ->
|
onDeleteClick = {
|
||||||
vocabularyViewModel.deleteData(VocabularyViewModel.DeleteType.VOCABULARY_ITEM, item = item)
|
vocabularyViewModel.deleteData(VocabularyViewModel.DeleteType.VOCABULARY_ITEM, item = item)
|
||||||
}
|
},
|
||||||
|
modifier = Modifier.animateItem()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (showFilterSheet) {
|
if (showFilterSheet) {
|
||||||
@@ -324,7 +382,8 @@ fun AllCardsListScreen(
|
|||||||
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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,34 +417,21 @@ fun AllCardsListScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("Use AllCardsListScreen which renders AllCardsView")
|
@ThemePreviews
|
||||||
@Composable
|
@Composable
|
||||||
fun VocabularyListScreen(
|
fun VocabularyListScreenPreview() {
|
||||||
categoryId: Int? = null,
|
val navController = rememberNavController()
|
||||||
showDueTodayOnly: Boolean? = null,
|
VocabularyListScreen(
|
||||||
stage: VocabularyStage? = null,
|
categoryId = 1,
|
||||||
onNavigateToItem: (VocabularyItem) -> Unit?,
|
showDueTodayOnly = false,
|
||||||
onNavigateBack: (() -> Unit)? = null,
|
stage = VocabularyStage.NEW,
|
||||||
navController: NavHostController? = null,
|
onNavigateToItem = {},
|
||||||
itemsToShow: List<VocabularyItem> = emptyList(),
|
onNavigateBack = {},
|
||||||
isRemoveFromCategoryEnabled: Boolean = false,
|
navController = navController
|
||||||
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,
|
||||||
@@ -400,8 +446,25 @@ 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(
|
||||||
@@ -459,6 +522,8 @@ 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() }
|
||||||
@@ -469,7 +534,37 @@ private fun SearchTopAppBar(
|
|||||||
|
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
modifier = Modifier.height(56.dp),
|
modifier = Modifier.height(56.dp),
|
||||||
title = "TODO",
|
title = {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxHeight(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
BasicTextField(
|
||||||
|
value = searchQuery,
|
||||||
|
onValueChange = onQueryChanged,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.focusRequester(focusRequester),
|
||||||
|
textStyle = MaterialTheme.typography.bodyLarge.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
),
|
||||||
|
singleLine = true,
|
||||||
|
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||||
|
decorationBox = { innerTextField ->
|
||||||
|
Box(contentAlignment = Alignment.CenterStart) {
|
||||||
|
if (searchQuery.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.search_vocabulary),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
innerTextField()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onCloseSearch) {
|
IconButton(onClick = onCloseSearch) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -487,6 +582,8 @@ private fun SearchTopAppBar(
|
|||||||
@Composable
|
@Composable
|
||||||
fun SearchTopAppBarPreview() {
|
fun SearchTopAppBarPreview() {
|
||||||
SearchTopAppBar(
|
SearchTopAppBar(
|
||||||
|
searchQuery = stringResource(R.string.search_query),
|
||||||
|
onQueryChanged = {},
|
||||||
onCloseSearch = {}
|
onCloseSearch = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -508,7 +605,14 @@ private fun ContextualTopAppBar(
|
|||||||
|
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
modifier = modifier.height(56.dp),
|
modifier = modifier.height(56.dp),
|
||||||
title = stringResource(R.string.d_selected, selectionCount),
|
title = {
|
||||||
|
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))
|
||||||
@@ -589,6 +693,112 @@ 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(
|
||||||
@@ -598,7 +808,8 @@ 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) }
|
||||||
@@ -743,3 +954,20 @@ 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 = stringResource(R.string.sort_new_vocabulary),
|
title = { Text(stringResource(R.string.sort_new_vocabulary)) },
|
||||||
actions = {
|
actions = {
|
||||||
Box {
|
Box {
|
||||||
IconButton(onClick = { showFilterMenu = true }) {
|
IconButton(onClick = { showFilterMenu = true }) {
|
||||||
@@ -231,7 +231,11 @@ fun VocabularySortingScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onNavigateBack = { navController.popBackStack() },
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.popBackStack() }) {
|
||||||
|
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
||||||
|
}
|
||||||
|
},
|
||||||
hintContent = HintDefinition.SORTING.hint()
|
hintContent = HintDefinition.SORTING.hint()
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -295,6 +299,7 @@ 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()) }
|
||||||
@@ -309,6 +314,7 @@ 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,6 +66,8 @@ 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,
|
||||||
@@ -173,8 +175,13 @@ internal fun DraggableActionPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isEditing) {
|
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))
|
ActionItem(icon = AppIcons.Edit, label = stringResource(R.string.edit), isExtended = isExtended, onClick = actionClickHandler(onEditClick))
|
||||||
|
}
|
||||||
|
if (!isEditing) {
|
||||||
|
|
||||||
if (showAnalyzeGrammarButton) {
|
if (showAnalyzeGrammarButton) {
|
||||||
ActionItem(
|
ActionItem(
|
||||||
@@ -245,6 +252,8 @@ fun DraggableActionPanelPreview() {
|
|||||||
onDismiss = {},
|
onDismiss = {},
|
||||||
isEditing = false,
|
isEditing = false,
|
||||||
onEditClick = {},
|
onEditClick = {},
|
||||||
|
onSaveClick = {},
|
||||||
|
onCancelClick = {},
|
||||||
|
|
||||||
onStatisticsClick = {},
|
onStatisticsClick = {},
|
||||||
onMoveToCategoryClick = {},
|
onMoveToCategoryClick = {},
|
||||||
|
|||||||
@@ -87,58 +87,10 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VocabularyDisplayCard(
|
fun VocabularyCard(
|
||||||
vocabularyItem: VocabularyItem,
|
vocabularyItem: VocabularyItem,
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
isFlipped: Boolean,
|
exerciseMode: 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 = {},
|
||||||
@@ -147,8 +99,6 @@ private fun VocabularyCardContent(
|
|||||||
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()
|
||||||
@@ -251,7 +201,6 @@ private fun VocabularyCardContent(
|
|||||||
)
|
)
|
||||||
vocabularyViewModel.editVocabularyItem(updatedItem)
|
vocabularyViewModel.editVocabularyItem(updatedItem)
|
||||||
isEditing = false
|
isEditing = false
|
||||||
onEditStateChange?.invoke(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -264,7 +213,6 @@ private fun VocabularyCardContent(
|
|||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,13 +286,13 @@ private fun VocabularyCardContent(
|
|||||||
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 || isExerciseMode,
|
isRevealed = isFrontFace || exerciseMode,
|
||||||
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 = isExerciseMode,
|
isExerciseMode = exerciseMode,
|
||||||
vocabularyItem = item,
|
vocabularyItem = item,
|
||||||
onMoreClick = {
|
onMoreClick = {
|
||||||
@Suppress("HardCodedStringLiteral")
|
@Suppress("HardCodedStringLiteral")
|
||||||
@@ -369,7 +317,7 @@ private fun VocabularyCardContent(
|
|||||||
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 (!isExerciseMode && !isEditing && !isFlipped) {
|
if (!exerciseMode && !isFlipped) {
|
||||||
IconButton(onClick = { showActionPanel = true }) {
|
IconButton(onClick = { showActionPanel = true }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = AppIcons.MoreVert,
|
imageVector = AppIcons.MoreVert,
|
||||||
@@ -391,7 +339,7 @@ private fun VocabularyCardContent(
|
|||||||
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 && isExerciseMode),
|
isRevealed = !(!isFlipped && exerciseMode),
|
||||||
userSpellingAnswer = userSpellingAnswer,
|
userSpellingAnswer = userSpellingAnswer,
|
||||||
isUserSpellingCorrect = isUserSpellingCorrect,
|
isUserSpellingCorrect = isUserSpellingCorrect,
|
||||||
correctWord = if (switchOrder) item.wordFirst else item.wordSecond,
|
correctWord = if (switchOrder) item.wordFirst else item.wordSecond,
|
||||||
@@ -400,7 +348,7 @@ private fun VocabularyCardContent(
|
|||||||
@Suppress("HardCodedStringLiteral")
|
@Suppress("HardCodedStringLiteral")
|
||||||
showGrammarDialogFor = "second"
|
showGrammarDialogFor = "second"
|
||||||
},
|
},
|
||||||
isExerciseMode = isExerciseMode,
|
isExerciseMode = exerciseMode,
|
||||||
vocabularyItem = item,
|
vocabularyItem = item,
|
||||||
onMoreClick = {
|
onMoreClick = {
|
||||||
@Suppress("HardCodedStringLiteral")
|
@Suppress("HardCodedStringLiteral")
|
||||||
@@ -413,7 +361,7 @@ private fun VocabularyCardContent(
|
|||||||
|
|
||||||
|
|
||||||
!switchOrder
|
!switchOrder
|
||||||
if(isFlipped || !isExerciseMode)
|
if(isFlipped || !exerciseMode)
|
||||||
DraggableActionPanel(
|
DraggableActionPanel(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.CenterEnd)
|
.align(Alignment.CenterEnd)
|
||||||
@@ -421,14 +369,9 @@ private fun VocabularyCardContent(
|
|||||||
isOpen = showActionPanel,
|
isOpen = showActionPanel,
|
||||||
onDismiss = { showActionPanel = false },
|
onDismiss = { showActionPanel = false },
|
||||||
isEditing = isEditing,
|
isEditing = isEditing,
|
||||||
onEditClick = {
|
onEditClick = { isEditing = true },
|
||||||
isEditing = true
|
onSaveClick = { handleSave() },
|
||||||
onEditStateChange?.invoke(true)
|
onCancelClick = handleCancel,
|
||||||
onEditActionHandlersReady?.invoke(
|
|
||||||
{ handleSave() },
|
|
||||||
{ handleCancel() }
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
onStatisticsClick = onStatisticsClick,
|
onStatisticsClick = onStatisticsClick,
|
||||||
onMoveToCategoryClick = onMoveToCategoryClick,
|
onMoveToCategoryClick = onMoveToCategoryClick,
|
||||||
@@ -495,15 +438,18 @@ fun VocabularyCardPreview() {
|
|||||||
languageSecondId = R.string.language_2
|
languageSecondId = R.string.language_2
|
||||||
)
|
)
|
||||||
val navController = NavController(LocalContext.current)
|
val navController = NavController(LocalContext.current)
|
||||||
VocabularyDisplayCard(
|
VocabularyCard(
|
||||||
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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,7 +475,7 @@ private fun FrequencyPill(zipfFrequency: Float?) {
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 4.dp)
|
.padding(horizontal = 4.dp)
|
||||||
.width(100.dp),
|
.width(80.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
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> {
|
||||||
entry.word + "_" + entry.langCode
|
val key = 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,6 +209,13 @@ 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,20 +90,12 @@ 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 {
|
||||||
@@ -263,15 +255,6 @@ 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() }
|
||||||
@@ -287,8 +270,6 @@ 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,5 +1,3 @@
|
|||||||
@file:Suppress("HardCodedStringLiteral")
|
|
||||||
|
|
||||||
package eu.gaudian.translator.viewmodel
|
package eu.gaudian.translator.viewmodel
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
@@ -33,8 +31,8 @@ enum class ScreenState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class ExerciseConfig(
|
data class ExerciseConfig(
|
||||||
val shuffleCards: Boolean = true,
|
val shuffleCards: Boolean = false,
|
||||||
val shuffleLanguages: Boolean = true,
|
val shuffleLanguages: Boolean = false,
|
||||||
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),
|
||||||
@@ -92,9 +90,6 @@ 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())
|
||||||
@@ -111,7 +106,6 @@ 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
|
||||||
@@ -164,7 +158,6 @@ 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()
|
||||||
@@ -280,10 +273,8 @@ 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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,33 +390,27 @@ 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()
|
||||||
@@ -435,7 +420,6 @@ 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,56 +617,6 @@ 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()
|
||||||
@@ -714,7 +664,6 @@ 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(",")
|
||||||
@@ -733,7 +682,6 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -745,7 +693,6 @@ 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")
|
||||||
@@ -805,8 +752,6 @@ 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()) {
|
||||||
|
|||||||
BIN
app/src/main/res/drawable/ic_empty.png
Normal file
BIN
app/src/main/res/drawable/ic_empty.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@@ -1,23 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<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>
|
||||||
@@ -32,87 +19,35 @@
|
|||||||
<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">
|
||||||
<string-array name="example_prompts">
|
<item>Wortart und Genus (bei Nomen)</item>
|
||||||
<item>Alles übersetzen, ohne etwas hinzuzufügen.</item>
|
<item>Deklination (bei Nomen)</item>
|
||||||
|
<item>Aussprache (IPA) und Worttrennung</item>
|
||||||
|
<item>Definition</item>
|
||||||
|
<item>Herkunft</item>
|
||||||
|
<item>Synonyme</item>
|
||||||
|
<item>Antonyme</item>
|
||||||
|
<item>Beispiele</item>
|
||||||
|
<item>Konjugation (bei Verben)</item>
|
||||||
|
<item>Redewendungen</item>
|
||||||
|
<item>Grammatikalische Merkmale (bei Präpositionen)</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="example_prompts"><item>Alles übersetzen, ohne etwas hinzuzufügen.</item>
|
||||||
<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>
|
||||||
<string-array name="exercise_example_prompts">Exercise Example Prompts</string-array>
|
|
||||||
|
|
||||||
<string-array name="motivational_phrases">
|
|
||||||
<item>Die Grenzen deiner Sprache sind die Grenzen deiner Welt. Lass uns den Käfig etwas größer machen.</item>
|
|
||||||
<item>Du bist im Grunde ein Fleisch-Computer, der eine Sprachsimulation am Laufen hat. Zeit für ein Firmware-Upgrade.</item>
|
|
||||||
<item>Eine andere Sprache zu sprechen heißt, eine zweite Seele zu besitzen. Oder zumindest einen ziemlich guten Party-Trick.</item>
|
|
||||||
<item>Irgendwann wirst du deine existenzielle Angst in einem völlig anderen Dialekt formulieren.</item>
|
|
||||||
<item>Neuroplastizität klingt nach einer schicken OP, ist aber nur dein Gehirn, das mal endlich schweres Heben macht.</item>
|
|
||||||
<item>Jedes neue Wort, das du lernst, ist ein Konzept, das du dir aus der Leere zurückholst.</item>
|
|
||||||
<item>Red weniger, sag mehr. Stopp, du lernst Vokabeln – rede schlecht, aber rede oft.</item>
|
|
||||||
<item>Deine heutige Wortanzahl ist im Kosmos statistisch irrelevant, aber für dein Ego monumental.</item>
|
|
||||||
<item>Descartes‘ Dualismus meint, Geist und Körper seien getrennt. Lass uns sicherstellen, dass der Geist heute auch mal was reißt.</item>
|
|
||||||
<item>Fließend sein ist nur eine Reihe gut getarnter Fehler. Leg los und mach ein paar.</item>
|
|
||||||
<item>Wissen ist das Einzige, was die Leere wirklich stillt. Fütter sie.</item>
|
|
||||||
<item>Dein Kurzzeitgedächtnis ist ein Sieb. Wir werden diese Wörter durch reine Wiederholung in den Tresor des Langzeitgedächtnisses prügeln.</item>
|
|
||||||
<item>Wahre Freiheit ist, genau das richtige, hochspezifische Adjektiv zu kennen, wenn du jemanden stumm verurteilst.</item>
|
|
||||||
<item>Du bist ein Künstler und dein Medium sind leicht falsch ausgesprochene Nomen.</item>
|
|
||||||
<item>Ein hungriger Magen knurrt. Ein hungriger Geist scrollt nur dumm durchs Internet. Lern lieber ein Wort.</item>
|
|
||||||
<item>Spaced Repetition: weil dein Gehirn aggressiv versucht, alles zu löschen, was du nicht aktiv nutzt.</item>
|
|
||||||
<item>Kuratiere deinen Wortschatz wie ein Museum, in dem die Hauptattraktion deine eigene Überlegenheit ist.</item>
|
|
||||||
<item>Solipsismus ist der Glaube, dass nur dein Geist existiert. Wenn das so ist, solltest du ihm vielleicht einen besseren Wortschatz verpassen.</item>
|
|
||||||
<item>Ein Mensch ohne Sprache ist ein Mensch ohne Land. Oder einfach ein sehr stiller Tourist.</item>
|
|
||||||
<item>Baue ein Vokabel-Imperium auf, eine aggressiv missverstandene Redewendung nach der anderen.</item>
|
|
||||||
<item>Die Eule der Minerva beginnt ihren Flug erst in der Dämmerung. Und nach einer richtigen Lerneinheit.</item>
|
|
||||||
<item>Um dem Gravitationsfeld der Einsprachigkeit zu entkommen, braucht es ordentlich Schubkraft. Bleib am Ball.</item>
|
|
||||||
<item>Sprache ist das Betriebssystem des Bewusstseins. Deins installiert gerade ein Update.</item>
|
|
||||||
<item>Wir sind alle in der Gosse, aber einige von uns schauen in fremdsprachige Wörterbücher.</item>
|
|
||||||
<item>Eine Sache zu benennen heißt, sie zu besitzen. Es wird Zeit, dass du zum absoluten Großgrundbesitzer der Realität wirst.</item>
|
|
||||||
<item>Grammatik ist die Grundstruktur der Realität. Ignorier sie auf eigene ontologische Gefahr.</item>
|
|
||||||
<item>Sogar Sokrates wusste, dass er nichts wusste. Aber er wusste wenigstens das Wort für \'nichts\' auf Griechisch.</item>
|
|
||||||
<item>Die Reise von tausend Meilen beginnt mit einer unbeholfenen Kaffee-Bestellung in der falschen Zeitform.</item>
|
|
||||||
<item>Dein Gehirn verbraucht etwa zwanzig Prozent der Energie deines Körpers. Lass es sich seinen Unterhalt verdienen.</item>
|
|
||||||
<item>Motivation ist eine flüchtige Emotion. Gewohnheit ist eine unerbittliche Maschine. Sei die Maschine.</item>
|
|
||||||
<item>Wenn Worte Waffen sind, ist dein Arsenal gerade eine Sammlung stumpfer Löffel. Lass uns sie schärfen.</item>
|
|
||||||
<item>Lernen bedeutet, sich freiwillig vorübergehender Inkompetenz zu unterziehen. Leb mit der Peinlichkeit.</item>
|
|
||||||
<item>Sprachenlernen ist der Mythos des Sisyphos, aber manchmal rollt der Stein den Berg runter und du verstehst tatsächlich einen Podcast.</item>
|
|
||||||
<item>Dein zukünftiges Ich beurteilt gerade deine heutige Arbeitsmoral. Beweis ihm das Gegenteil.</item>
|
|
||||||
<item>Entropie besagt, dass das Universum zum Chaos neigt. Vokabeln zu lernen ist deine tägliche Revolte gegen die Physik.</item>
|
|
||||||
<item>10 Wörter am Tag sind 3.650 im Jahr. Immer noch nicht genug, um die menschliche Existenz zu erklären, aber ein ordentlicher Anfang.</item>
|
|
||||||
<item>Zinseszins gilt auch für kognitive Werte. Also los, investier in ein paar Verben.</item>
|
|
||||||
<item>Du bist die Summe deiner Erinnerungen. Lass uns sicherstellen, dass die nicht nur aus Popsongs und Peinlichkeiten bestehen.</item>
|
|
||||||
<item>Englisch sind einfach drei verschiedene Sprachen, die in einem Mantel aufeinanders Schultern stehen. Wenn das so tun kann, kannst du das auch.</item>
|
|
||||||
<item>Das französische Wort für 99 heißt wörtlich \'vier-zwanzig-zehn-neun\'. Deine aktuellen Lernprobleme sind nichts gegen deren Mathe.</item>
|
|
||||||
<item>Im Mandarin macht der falsche Ton aus \'Mutter\' ein \'Pferd\'. Vermeiden wir heute mal ödipale Reitunfälle.</item>
|
|
||||||
<item>Deutsch lässt dich Nomen zusammenknallen, bis du ein Wort hast, das lang genug ist, um deine ganz spezielle Form des Elends auszudrücken. Streb nach dieser Macht.</item>
|
|
||||||
<item>Die Beherrschung des grammatikalischen Geschlechts bedeutet zu akzeptieren, dass das Universum willkürlich entschieden hat, dass ein Tisch weiblich und eine Brücke männlich ist. Füg dich dem Absurden.</item>
|
|
||||||
<item>Japanisch hat spezielle Zählwörter, je nachdem, ob ein Objekt flach, zylindrisch oder ein kleines Tier ist. Präzision ist eine Tugend. Fang an zu üben.</item>
|
|
||||||
<item>Spanischsprachige rasseln etwa 25% schneller Silben runter als Englischsprachige. Du musst nicht schlauer sein, du musst nur schneller denken.</item>
|
|
||||||
<item>Finnisch hat 15 Fälle. Plötzlich ist Vokabeln lernen gar nicht mehr so schlimm, oder?</item>
|
|
||||||
<item>Eine Sprache zu lernen bedeutet zu erkennen, dass Redewendungen nur kulturell sanktionierte, kollektive Halluzinationen sind. Lass uns halluzinieren.</item>
|
|
||||||
<item>Geschriebenes Walisisch sieht aus, als wäre jemand auf einer vokalfaulen Tastatur eingeschlafen, aber es birgt eine dunkle, uralte Poesie. Find die Poesie in deiner Zielsprache.</item>
|
|
||||||
<item>Ein robuster Wortschatz ist der sozial akzeptabelste Weg, ein herablassender Snob zu sein. Verdien dir deine Arroganz.</item>
|
|
||||||
<item>Du kämpfst gerade gegen die Ebbinghaussche Vergessenskurve. Sie ist eine gnadenlose, unverzeihliche mathematische Realität. Wehr dich.</item>
|
|
||||||
<item>Latein zu lernen ist nutzlos, es sei denn, du willst mit mittelalterlichen Mönchen diskutieren oder versehentlich irgendwas beschwören. So oder so, es ist eine gute Vorbereitung.</item>
|
|
||||||
<item>Ohne Vokabeln ist Grammatik nur eine wunderschön strukturierte Stille. Gib ihr etwas Lärm.</item>
|
|
||||||
<item>Irisch hat keine Wörter für \'ja\' oder \'nein\'. Du musst das Verb wiederholen. Bei Sprachen geht es darum, voll und ganz dabei zu sein.</item>
|
|
||||||
<item>Das hawaiianische Alphabet hat nur 13 Buchstaben. Die Sprache, die du lernst, hat mehr. Hör auf zu jammern und lerne sie.</item>
|
|
||||||
<item>Wortwörtlich zu übersetzen ist das reinste Hamsterrad. Jede Sprache bildet die Realität anders ab. Zeit, eine neue Karte zu lernen.</item>
|
|
||||||
<item>Irgendwann kannst du einen fremdsprachigen Film gucken, ohne nur auf das untere Drittel des Bildschirms zu starren. Bleib dran.</item>
|
|
||||||
</string-array>
|
|
||||||
|
|
||||||
<string-array name="vocabulary_example_prompts">
|
|
||||||
<item>Verwende lateinamerikanisches Spanisch</item>
|
|
||||||
<item>Vermeide lange Wörter</item>
|
|
||||||
<item>Vermeide Sätze</item>
|
|
||||||
<item>Enthält viele Verben und Adjektive</item>
|
|
||||||
<item>Verwende informelle Sprache</item>
|
|
||||||
</string-array>
|
|
||||||
<string-array name="vocabulary_hints">
|
|
||||||
<item>Grundlegende Begrüßungen</item>
|
|
||||||
<item>Unregelmäßige Verben</item>
|
<item>Unregelmäßige Verben</item>
|
||||||
<item>Vokabular am Flughafen</item>
|
<item>Vokabular am Flughafen</item>
|
||||||
<item>Wie man einen Kaffee bestellt</item>
|
<item>Wie man einen Kaffee bestellt</item>
|
||||||
<item>Idiomatische Ausdrücke</item>
|
<item>Idiomatische Ausdrücke</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
<string-array name="vocabulary_example_prompts"><item>Verwende lateinamerikanisches Spanisch</item>
|
||||||
|
<item>Vermeide lange Wörter</item>
|
||||||
|
<item>Vermeide Sätze</item>
|
||||||
|
<item>Enthält viele Verben und Adjektive</item>
|
||||||
|
<item>Verwende informelle Sprache</item>
|
||||||
|
</string-array>
|
||||||
</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