Compare commits
18 Commits
glassmorph
...
059e5d9d3f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
059e5d9d3f | ||
|
|
3e3d6d9cd1 | ||
|
|
a7c83bb846 | ||
|
|
70e416d5e1 | ||
|
|
84cad31810 | ||
|
|
89ac7cd9eb | ||
|
|
47d7e01f7f | ||
|
|
eae37715cd | ||
|
|
6c669ac310 | ||
|
|
af78bd316d | ||
|
|
24cebc4b15 | ||
|
|
cd5a53ff5f | ||
|
|
972b2226d0 | ||
|
|
5ae96d1f5c | ||
|
|
ef90df2150 | ||
|
|
d2d2f53b59 | ||
|
|
7fccda7f77 | ||
|
|
801b6f6404 |
4
.idea/deploymentTargetSelector.xml
generated
4
.idea/deploymentTargetSelector.xml
generated
@@ -4,10 +4,10 @@
|
|||||||
<selectionStates>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
<DropdownSelection timestamp="2026-02-15T19:51:37.987601800Z">
|
<DropdownSelection timestamp="2026-02-16T10:13:39.492968600Z">
|
||||||
<Target type="DEFAULT_BOOT">
|
<Target type="DEFAULT_BOOT">
|
||||||
<handle>
|
<handle>
|
||||||
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\jonas\.android\avd\Medium_Phone_28.avd" />
|
<DeviceId pluginId="PhysicalDevice" identifier="serial=RFCW11ML1WV" />
|
||||||
</handle>
|
</handle>
|
||||||
</Target>
|
</Target>
|
||||||
</DropdownSelection>
|
</DropdownSelection>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import java.util.Locale
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
|
||||||
alias(libs.plugins.kotlin.compose)
|
alias(libs.plugins.kotlin.compose)
|
||||||
alias(libs.plugins.hilt.android)
|
alias(libs.plugins.hilt.android)
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
@@ -62,11 +61,8 @@ android {
|
|||||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi"
|
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
kotlin {
|
|
||||||
compilerOptions {
|
|
||||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
viewBinding = false
|
viewBinding = false
|
||||||
|
|||||||
@@ -32,17 +32,6 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name=".CorrectActivity"
|
|
||||||
android:exported="true">
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.SEND" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<data android:mimeType="text/plain" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
@file:Suppress("HardCodedStringLiteral")
|
|
||||||
|
|
||||||
package eu.gaudian.translator
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.activity.compose.setContent
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import eu.gaudian.translator.utils.Log
|
|
||||||
|
|
||||||
class CorrectActivity : ComponentActivity() {
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
val intent = intent
|
|
||||||
val action = intent.action
|
|
||||||
val type = intent.type
|
|
||||||
|
|
||||||
if (Intent.ACTION_SEND == action && type == "text/plain") {
|
|
||||||
val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT)
|
|
||||||
if (sharedText != null) {
|
|
||||||
Log.d("EditActivity", "Received text: $sharedText")
|
|
||||||
setContent {
|
|
||||||
Text(stringResource(R.string.editing_text, sharedText))
|
|
||||||
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.e("EditActivity", getString(R.string.no_text_received))
|
|
||||||
setContent {
|
|
||||||
Text(stringResource(R.string.error_no_text_to_edit))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.d("EditActivity", "Not launched with ACTION_SEND")
|
|
||||||
setContent {
|
|
||||||
Text(stringResource(R.string.not_launched_with_text_to_edit))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -253,7 +253,15 @@ fun TranslatorApp(
|
|||||||
val currentDestination = navBackStackEntry?.destination
|
val currentDestination = navBackStackEntry?.destination
|
||||||
val selectedScreen = Screen.fromDestination(currentDestination)
|
val selectedScreen = Screen.fromDestination(currentDestination)
|
||||||
|
|
||||||
@Suppress("HardCodedStringLiteral") val isBottomBarHidden = currentDestination?.route?.startsWith("vocabulary_exercise") == true
|
val isBottomBarHidden = currentDestination?.hierarchy?.any { destination ->
|
||||||
|
destination.route in setOf(
|
||||||
|
Screen.Translation.route,
|
||||||
|
Screen.Vocabulary.route,
|
||||||
|
Screen.Dictionary.route,
|
||||||
|
Screen.Exercises.route,
|
||||||
|
Screen.Settings.route
|
||||||
|
)
|
||||||
|
} == true || currentDestination?.route == "start_exercise"
|
||||||
val showBottomNavLabels by settingsViewModel.showBottomNavLabels.collectAsStateWithLifecycle(initialValue = false)
|
val showBottomNavLabels by settingsViewModel.showBottomNavLabels.collectAsStateWithLifecycle(initialValue = false)
|
||||||
|
|
||||||
BottomNavigationBar(
|
BottomNavigationBar(
|
||||||
@@ -262,6 +270,13 @@ 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.Vocabulary,
|
||||||
|
Screen.Dictionary,
|
||||||
|
Screen.Settings,
|
||||||
|
Screen.Exercises
|
||||||
|
)
|
||||||
|
|
||||||
// Always reset the selected section to its root and clear back stack between sections
|
// Always reset the selected section to its root and clear back stack between sections
|
||||||
if (inSameSection) {
|
if (inSameSection) {
|
||||||
@@ -274,6 +289,11 @@ fun TranslatorApp(
|
|||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
restoreState = false
|
restoreState = false
|
||||||
}
|
}
|
||||||
|
} else if (isMoreSection) {
|
||||||
|
navController.navigate(screen.route) {
|
||||||
|
launchSingleTop = true
|
||||||
|
restoreState = false
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Switching sections: clear entire back stack to start to avoid back navigation results
|
// Switching sections: clear entire back stack to start to avoid back navigation results
|
||||||
navController.navigate(screen.route) {
|
navController.navigate(screen.route) {
|
||||||
@@ -285,6 +305,9 @@ fun TranslatorApp(
|
|||||||
restoreState = false
|
restoreState = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onPlayClicked = {
|
||||||
|
navController.navigate("start_exercise")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,17 +26,23 @@ 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.CategoryDetailScreen
|
import eu.gaudian.translator.view.vocabulary.CategoryDetailScreen
|
||||||
import eu.gaudian.translator.view.vocabulary.CategoryListScreen
|
import eu.gaudian.translator.view.vocabulary.CategoryListScreen
|
||||||
import eu.gaudian.translator.view.vocabulary.LanguageProgressScreen
|
import eu.gaudian.translator.view.vocabulary.LanguageProgressScreen
|
||||||
import eu.gaudian.translator.view.vocabulary.MainVocabularyScreen
|
import eu.gaudian.translator.view.vocabulary.MainVocabularyScreen
|
||||||
|
import eu.gaudian.translator.view.vocabulary.NewWordReviewScreen
|
||||||
|
import eu.gaudian.translator.view.vocabulary.NewWordScreen
|
||||||
import eu.gaudian.translator.view.vocabulary.NoGrammarItemsScreen
|
import eu.gaudian.translator.view.vocabulary.NoGrammarItemsScreen
|
||||||
import eu.gaudian.translator.view.vocabulary.StageDetailScreen
|
import eu.gaudian.translator.view.vocabulary.StageDetailScreen
|
||||||
import eu.gaudian.translator.view.vocabulary.VocabularyCardHost
|
import eu.gaudian.translator.view.vocabulary.VocabularyCardHost
|
||||||
@@ -57,11 +63,13 @@ fun AppNavHost(
|
|||||||
|
|
||||||
// 1. Define your Main Tab "Leaf" Routes (the actual start destinations of your graphs)
|
// 1. Define your Main Tab "Leaf" Routes (the actual start destinations of your graphs)
|
||||||
val mainTabRoutes = setOf(
|
val mainTabRoutes = setOf(
|
||||||
Screen.Home.route, // "home" or "main_translation" depending on which one you actually navigate to
|
Screen.Home.route,
|
||||||
"main_translation",
|
Screen.Library.route,
|
||||||
"main_dictionary",
|
Screen.Stats.route,
|
||||||
"main_vocabulary",
|
Screen.Translation.route,
|
||||||
"main_exercise",
|
Screen.Vocabulary.route,
|
||||||
|
Screen.Dictionary.route,
|
||||||
|
Screen.Exercises.route,
|
||||||
SettingsRoutes.LIST
|
SettingsRoutes.LIST
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -121,10 +129,20 @@ fun AppNavHost(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
composable(Screen.Home.route) {
|
composable(Screen.Home.route) {
|
||||||
TranslationScreen(navController = navController)
|
HomeScreen(navController = navController)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(Screen.Library.route) {
|
||||||
|
LibraryScreen(navController = navController)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable("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)
|
||||||
|
statsGraph(navController)
|
||||||
translationGraph(navController)
|
translationGraph(navController)
|
||||||
dictionaryGraph(navController)
|
dictionaryGraph(navController)
|
||||||
vocabularyGraph(navController)
|
vocabularyGraph(navController)
|
||||||
@@ -132,11 +150,131 @@ fun AppNavHost(
|
|||||||
settingsGraph(navController)
|
settingsGraph(navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun NavGraphBuilder.homeGraph(navController: NavHostController) {
|
||||||
|
navigation(
|
||||||
|
startDestination = "main_home",
|
||||||
|
route = Screen.Home.route
|
||||||
|
) {
|
||||||
|
composable("main_home") {
|
||||||
|
HomeScreen(navController = navController)
|
||||||
|
}
|
||||||
|
composable("new_word") {
|
||||||
|
NewWordScreen(navController = navController)
|
||||||
|
}
|
||||||
|
composable("new_word_review") {
|
||||||
|
NewWordReviewScreen(navController = navController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
VocabularyListScreen(
|
||||||
|
navController = navController,
|
||||||
|
showDueTodayOnly = showDueTodayOnly,
|
||||||
|
categoryId = categoryId,
|
||||||
|
onNavigateToItem = { item ->
|
||||||
|
navController.navigate("vocabulary_detail/${item.id}")
|
||||||
|
},
|
||||||
|
onNavigateBack = { navController.popBackStack() },
|
||||||
|
enableNavigationButtons = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable("stats/language_progress") {
|
||||||
|
LanguageProgressScreen(
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable("stats/vocabulary_heatmap") {
|
||||||
|
VocabularyHeatmapScreen(
|
||||||
|
navController = navController,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable("stats/vocabulary_list/{showDueTodayOnly}/{stage}") { backStackEntry ->
|
||||||
|
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
|
||||||
|
val stageString = backStackEntry.arguments?.getString("stage")
|
||||||
|
val stage = stageString?.let {
|
||||||
|
if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
VocabularyListScreen(
|
||||||
|
navController = navController,
|
||||||
|
showDueTodayOnly = showDueTodayOnly,
|
||||||
|
stage = stage,
|
||||||
|
onNavigateToItem = { item ->
|
||||||
|
navController.navigate("vocabulary_detail/${item.id}")
|
||||||
|
},
|
||||||
|
onNavigateBack = { navController.popBackStack() },
|
||||||
|
categoryId = 0,
|
||||||
|
enableNavigationButtons = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable("stats/category_detail/{categoryId}") { backStackEntry ->
|
||||||
|
val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull()
|
||||||
|
|
||||||
|
if (categoryId != null) {
|
||||||
|
CategoryDetailScreen(
|
||||||
|
categoryId = categoryId,
|
||||||
|
onBackClick = { navController.popBackStack() },
|
||||||
|
onNavigateToItem = { item ->
|
||||||
|
navController.navigate("vocabulary_detail/${item.id}")
|
||||||
|
},
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
composable("stats/category_list_screen") {
|
||||||
|
CategoryListScreen(
|
||||||
|
onNavigateBack = { navController.popBackStack() },
|
||||||
|
onCategoryClicked = { categoryId ->
|
||||||
|
navController.navigate("stats/category_detail/$categoryId")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable(
|
||||||
|
route = "stats/vocabulary_sorting?mode={mode}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("mode") {
|
||||||
|
type = NavType.StringType
|
||||||
|
nullable = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
VocabularySortingScreen(
|
||||||
|
navController = navController,
|
||||||
|
initialFilterMode = backStackEntry.arguments?.getString("mode")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable("stats/no_grammar_items") {
|
||||||
|
NoGrammarItemsScreen(
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
|
@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
|
||||||
fun NavGraphBuilder.translationGraph(navController: NavHostController) {
|
fun NavGraphBuilder.translationGraph(navController: NavHostController) {
|
||||||
navigation(
|
navigation(
|
||||||
startDestination = "main_translation",
|
startDestination = "main_translation",
|
||||||
route = Screen.Home.route
|
route = Screen.Translation.route
|
||||||
) {
|
) {
|
||||||
composable("main_translation") {
|
composable("main_translation") {
|
||||||
TranslationScreen(navController = navController)
|
TranslationScreen(navController = navController)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
|||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.Icons.Default
|
import androidx.compose.material.icons.Icons.Default
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBackIos
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||||
import androidx.compose.material.icons.automirrored.filled.DriveFileMove
|
import androidx.compose.material.icons.automirrored.filled.DriveFileMove
|
||||||
import androidx.compose.material.icons.automirrored.filled.ExitToApp
|
import androidx.compose.material.icons.automirrored.filled.ExitToApp
|
||||||
@@ -135,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.ArrowBack
|
val ArrowBack = Icons.AutoMirrored.Filled.ArrowBackIos
|
||||||
val ArrowCircleUp = Icons.Filled.ArrowCircleUp
|
val ArrowCircleUp = Icons.Filled.ArrowCircleUp
|
||||||
val ArrowDropDown = Icons.Filled.KeyboardArrowDown
|
val ArrowDropDown = Icons.Filled.KeyboardArrowDown
|
||||||
val ArrowDropUp = Icons.Filled.KeyboardArrowUp
|
val ArrowDropUp = Icons.Filled.KeyboardArrowUp
|
||||||
|
|||||||
@@ -20,9 +20,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -37,6 +41,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||||
|
import eu.gaudian.translator.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An interface that defines the required properties for any item
|
* An interface that defines the required properties for any item
|
||||||
@@ -60,14 +65,42 @@ fun <T : TabItem> AppTabLayout(
|
|||||||
tabs: List<T>,
|
tabs: List<T>,
|
||||||
selectedTab: T,
|
selectedTab: T,
|
||||||
onTabSelected: (T) -> Unit,
|
onTabSelected: (T) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier,
|
||||||
|
onNavigateBack: (() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
val selectedIndex = tabs.indexOf(selectedTab)
|
val selectedIndex = tabs.indexOf(selectedTab)
|
||||||
|
|
||||||
BoxWithConstraints(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 8.dp, horizontal = 8.dp)
|
.padding(vertical = 8.dp, horizontal = 8.dp),
|
||||||
|
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(
|
.background(
|
||||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||||
@@ -139,6 +172,7 @@ fun <T : TabItem> AppTabLayout(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ThemePreviews
|
@ThemePreviews
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
|
@file:Suppress("HardCodedStringLiteral")
|
||||||
|
|
||||||
package eu.gaudian.translator.view.composable
|
package eu.gaudian.translator.view.composable
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBackIosNew
|
||||||
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LocalContentColor
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.material3.TopAppBarColors
|
import androidx.compose.material3.TopAppBarColors
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
@@ -25,8 +30,10 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||||
@@ -36,7 +43,8 @@ import eu.gaudian.translator.view.hints.LocalShowHints
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppTopAppBar(
|
fun AppTopAppBar(
|
||||||
title: @Composable () -> Unit,
|
title: String,
|
||||||
|
additionalContent: @Composable () -> Unit = {},
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onNavigateBack: (() -> Unit)? = null,
|
onNavigateBack: (() -> Unit)? = null,
|
||||||
navigationIcon: @Composable (() -> Unit)? = null,
|
navigationIcon: @Composable (() -> Unit)? = null,
|
||||||
@@ -47,25 +55,26 @@ fun AppTopAppBar(
|
|||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
TopAppBar(
|
// Changed to CenterAlignedTopAppBar to perfectly match the design requirements
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
modifier = modifier.height(56.dp),
|
modifier = modifier.height(56.dp),
|
||||||
windowInsets = WindowInsets(0.dp),
|
windowInsets = WindowInsets(0.dp),
|
||||||
colors = colors,
|
colors = colors,
|
||||||
title = {
|
title = {
|
||||||
Box(
|
|
||||||
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(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Box(modifier = Modifier.weight(1f)) {
|
Text(
|
||||||
title()
|
text = title,
|
||||||
}
|
style = MaterialTheme.typography.titleLarge,
|
||||||
Box {
|
fontWeight = FontWeight.Bold,
|
||||||
|
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,
|
||||||
@@ -74,33 +83,33 @@ fun AppTopAppBar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
title()
|
Text(
|
||||||
}
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
if (onNavigateBack != null) {
|
if (onNavigateBack != null) {
|
||||||
Box(
|
IconButton(
|
||||||
modifier = Modifier.fillMaxHeight(),
|
onClick = onNavigateBack,
|
||||||
contentAlignment = Alignment.Center
|
modifier = Modifier
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = onNavigateBack) {
|
|
||||||
Icon(
|
Icon(
|
||||||
AppIcons.ArrowBack,
|
imageVector = Icons.Default.ArrowBackIosNew,
|
||||||
contentDescription = stringResource(R.string.cd_navigate_back),
|
contentDescription = "Back",
|
||||||
tint = LocalContentColor.current
|
modifier = Modifier.size(18.dp),
|
||||||
|
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
|
||||||
)
|
)
|
||||||
@@ -119,17 +128,9 @@ fun AppTopAppBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A composable that acts as a TopAppBar, containing a back navigation icon
|
* A composable that acts as a TopAppBar, containing a back navigation icon
|
||||||
* and an [AppTabLayout].
|
* and an [AppTabLayout].
|
||||||
*
|
|
||||||
* @param T The type of the tab item, must implement [TabItem].
|
|
||||||
* @param tabs The list of tab items to display.
|
|
||||||
* @param selectedTab The currently selected tab item.
|
|
||||||
* @param onTabSelected Callback function when a tab is selected.
|
|
||||||
* @param onNavigateBack Callback function when the back arrow is clicked.
|
|
||||||
* @param modifier The modifier to be applied to the layout.
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun <T : TabItem> TabbedTopAppBar(
|
fun <T : TabItem> TabbedTopAppBar(
|
||||||
@@ -139,7 +140,6 @@ fun <T : TabItem> TabbedTopAppBar(
|
|||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
// Use a Surface to provide background color and context for the app bar
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
color = MaterialTheme.colorScheme.surface
|
color = MaterialTheme.colorScheme.surface
|
||||||
@@ -148,20 +148,21 @@ fun <T : TabItem> TabbedTopAppBar(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// Back navigation icon, similar to its usage in AppTopAppBar
|
// Updated back icon here as well to keep your entire app consistent!
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onNavigateBack,
|
onClick = onNavigateBack,
|
||||||
modifier = Modifier.padding(horizontal = 4.dp)
|
modifier = Modifier
|
||||||
|
.padding(start = 8.dp, end = 4.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = AppIcons.ArrowBack,
|
imageVector = AppIcons.ArrowBack,
|
||||||
contentDescription = stringResource(R.string.cd_navigate_back),
|
contentDescription = stringResource(R.string.cd_navigate_back),
|
||||||
tint = MaterialTheme.colorScheme.onSurface
|
tint = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The AppTabLayout, taking up the remaining space.
|
|
||||||
// Its appearance matches the provided image.
|
|
||||||
AppTabLayout(
|
AppTabLayout(
|
||||||
tabs = tabs,
|
tabs = tabs,
|
||||||
selectedTab = selectedTab,
|
selectedTab = selectedTab,
|
||||||
@@ -172,11 +173,12 @@ fun <T : TabItem> TabbedTopAppBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ... [Previews remain exactly the same below]
|
||||||
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
@Suppress("HardCodedStringLiteral")
|
||||||
@ThemePreviews
|
@ThemePreviews
|
||||||
@Composable
|
@Composable
|
||||||
fun TabbedTopAppBarPreview() {
|
fun TabbedTopAppBarPreview() {
|
||||||
// Sample data for preview, similar to ModernTabLayoutPreview
|
|
||||||
data class SampleTabItem(override val title: String, override val icon: ImageVector) : TabItem
|
data class SampleTabItem(override val title: String, override val icon: ImageVector) : TabItem
|
||||||
|
|
||||||
val tabs = listOf(
|
val tabs = listOf(
|
||||||
@@ -202,7 +204,7 @@ fun TabbedTopAppBarPreview() {
|
|||||||
@Composable
|
@Composable
|
||||||
fun AppTopAppBarPreview() {
|
fun AppTopAppBarPreview() {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text("Preview Title") }
|
title = "Previwe Title"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +212,7 @@ fun AppTopAppBarPreview() {
|
|||||||
@Composable
|
@Composable
|
||||||
fun AppTopAppBarWithNavigationIconPreview() {
|
fun AppTopAppBarWithNavigationIconPreview() {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.title_title_preview_title)) },
|
title = "Preview Title",
|
||||||
onNavigateBack = {}
|
onNavigateBack = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -219,13 +221,13 @@ fun AppTopAppBarWithNavigationIconPreview() {
|
|||||||
@Composable
|
@Composable
|
||||||
fun AppTopAppBarWithActionsPreview() {
|
fun AppTopAppBarWithActionsPreview() {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.title_title_preview_title)) },
|
title = "Preview Title",
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = {}) {
|
IconButton(onClick = {}) {
|
||||||
Icon(AppIcons.Settings, contentDescription = stringResource(R.string.title_settings))
|
Icon(AppIcons.Settings, contentDescription = stringResource(R.string.title_settings))
|
||||||
}
|
}
|
||||||
IconButton(onClick = {}) {
|
IconButton(onClick = {}) {
|
||||||
AppIcons.ArrowBack
|
Icon(AppIcons.ArrowBack, contentDescription = null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,23 +11,44 @@ import androidx.compose.animation.core.spring
|
|||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.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.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
@@ -41,6 +62,7 @@ import androidx.navigation.NavDestination.Companion.hierarchy
|
|||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||||
import eu.gaudian.translator.view.LocalShowExperimentalFeatures
|
import eu.gaudian.translator.view.LocalShowExperimentalFeatures
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
sealed class Screen(
|
sealed class Screen(
|
||||||
val route: String,
|
val route: String,
|
||||||
@@ -48,34 +70,44 @@ sealed class Screen(
|
|||||||
val selectedIcon: ImageVector,
|
val selectedIcon: ImageVector,
|
||||||
val unselectedIcon: ImageVector
|
val unselectedIcon: ImageVector
|
||||||
) {
|
) {
|
||||||
object Home : Screen("home", R.string.label_translation, AppIcons.TranslateFilled, AppIcons.TranslateOutlined)
|
object Home : Screen("home", R.string.label_home, AppIcons.Home, AppIcons.Home)
|
||||||
|
object Stats : Screen("stats", R.string.label_stats, AppIcons.Statistics, AppIcons.Statistics)
|
||||||
|
object Translation : Screen("translation", R.string.label_translation, AppIcons.TranslateFilled, AppIcons.TranslateOutlined)
|
||||||
|
object Library : Screen("library", R.string.label_library, AppIcons.VocabularyFilled, AppIcons.VocabularyOutlined)
|
||||||
|
object Vocabulary : Screen("vocabulary", R.string.label_legacy_vocabulary, AppIcons.VocabularyFilled, AppIcons.VocabularyOutlined)
|
||||||
|
object Settings : Screen("settings", R.string.title_settings, AppIcons.SettingsFilled, AppIcons.SettingsOutlined)
|
||||||
|
object More : Screen("more", R.string.label_more, AppIcons.MoreVert, AppIcons.MoreVert)
|
||||||
object Dictionary : Screen("dictionary", R.string.label_dictionary, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined)
|
object Dictionary : Screen("dictionary", R.string.label_dictionary, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined)
|
||||||
object Exercises : Screen("exercises", R.string.label_exercises, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined)
|
object Exercises : Screen("exercises", R.string.label_exercises, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined)
|
||||||
object Vocabulary : Screen("vocabulary", R.string.label_vocabulary, AppIcons.VocabularyFilled, AppIcons.VocabularyOutlined)
|
|
||||||
object Settings : Screen("settings", R.string.title_settings, AppIcons.SettingsFilled, AppIcons.SettingsOutlined)
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getAllScreens(showExperimental: Boolean = false): List<Screen> {
|
fun getAllScreens(showExperimental: Boolean = false): List<Screen> {
|
||||||
val screens = mutableListOf(Home, Dictionary, Vocabulary, Settings)
|
return listOf(Home, Library, Stats)
|
||||||
if (showExperimental) {
|
|
||||||
screens.add(2, Exercises)
|
|
||||||
}
|
}
|
||||||
return screens
|
|
||||||
|
fun getMoreMenuItems(showExperimental: Boolean = false): List<Screen> {
|
||||||
|
val items = mutableListOf<Screen>()
|
||||||
|
items.add(Translation)
|
||||||
|
items.add(Vocabulary) // Legacy vocabulary moved to More
|
||||||
|
items.add(Dictionary)
|
||||||
|
items.add(Settings)
|
||||||
|
if (showExperimental) {
|
||||||
|
items.add(Exercises)
|
||||||
|
}
|
||||||
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun fromDestination(destination: NavDestination?): Screen {
|
fun fromDestination(destination: NavDestination?): Screen {
|
||||||
val showExperimental = LocalShowExperimentalFeatures.current
|
val showExperimental = LocalShowExperimentalFeatures.current
|
||||||
return getAllScreens(showExperimental).find { screen ->
|
val allScreens = getAllScreens(showExperimental) + getMoreMenuItems(showExperimental) + More
|
||||||
|
return allScreens.find { screen ->
|
||||||
destination?.hierarchy?.any { it.route == screen.route } == true
|
destination?.hierarchy?.any { it.route == screen.route } == true
|
||||||
} ?: Home
|
} ?: Home
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A modernized Material 3 bottom navigation bar with spring animations and haptic feedback.
|
|
||||||
*/
|
|
||||||
@SuppressLint("UnusedBoxWithConstraintsScope")
|
@SuppressLint("UnusedBoxWithConstraintsScope")
|
||||||
@Composable
|
@Composable
|
||||||
fun BottomNavigationBar(
|
fun BottomNavigationBar(
|
||||||
@@ -84,40 +116,87 @@ 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 = 32.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 = 28.dp
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isVisible,
|
visible = isVisible,
|
||||||
enter = slideInVertically(
|
enter = slideInVertically(
|
||||||
animationSpec = androidx.compose.animation.core.tween(durationMillis = 220),
|
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
|
||||||
initialOffsetY = { it }
|
initialOffsetY = { it }
|
||||||
),
|
),
|
||||||
exit = slideOutVertically(
|
exit = slideOutVertically(
|
||||||
animationSpec = androidx.compose.animation.core.tween(durationMillis = 220),
|
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
|
||||||
targetOffsetY = { it }
|
targetOffsetY = { it }
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val baseHeight = if (showLabels) 80.dp else 56.dp
|
val baseHeight = if (showLabels) 80.dp else 56.dp
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val navBarDp = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() }
|
val navBarDp = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() }
|
||||||
val height = baseHeight + navBarDp
|
val height = baseHeight + navBarDp
|
||||||
|
|
||||||
NavigationBar(
|
// Outer Box height is purely determined by the NavigationBar now
|
||||||
modifier = modifier.height(height),
|
Box(
|
||||||
containerColor = MaterialTheme.colorScheme.surface, // Cleaner look than surfaceVariant
|
modifier = modifier.fillMaxWidth(),
|
||||||
tonalElevation = 8.dp, // Slight elevation for depth
|
contentAlignment = Alignment.TopCenter
|
||||||
) {
|
) {
|
||||||
screens.forEach { screen ->
|
|
||||||
val isSelected = screen == selectedItem
|
// The actual Navigation Bar
|
||||||
|
NavigationBar(
|
||||||
|
modifier = Modifier.height(height),
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
tonalElevation = 8.dp,
|
||||||
|
) {
|
||||||
|
// Create a list of 5 items (2 left, 1 empty spacer, 2 right)
|
||||||
|
val allNavItems = buildList {
|
||||||
|
addAll(screens.take(2))
|
||||||
|
add(null) // Empty spacer for Play Button gap
|
||||||
|
if (screens.size > 2) {
|
||||||
|
addAll(screens.drop(2))
|
||||||
|
}
|
||||||
|
add(moreScreen)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
// 1. Spring Animation for the Icon Scale
|
|
||||||
val scale by animateFloatAsState(
|
val scale by animateFloatAsState(
|
||||||
targetValue = if (isSelected) 1.2f else 1.0f, // Subtle pop effect
|
targetValue = if (isSelected) 1.2f else 1.0f,
|
||||||
animationSpec = spring(
|
animationSpec = spring(
|
||||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||||
stiffness = Spring.StiffnessLow
|
stiffness = Spring.StiffnessLow
|
||||||
@@ -129,8 +208,8 @@ fun BottomNavigationBar(
|
|||||||
selected = isSelected,
|
selected = isSelected,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!isSelected) {
|
if (!isSelected) {
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress) // 2. Tactile feedback
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
onItemSelected(screen)
|
if (screen == Screen.More) showMoreMenu = true else onItemSelected(screen)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
label = if (showLabels) {
|
label = if (showLabels) {
|
||||||
@@ -145,12 +224,11 @@ fun BottomNavigationBar(
|
|||||||
}
|
}
|
||||||
} else null,
|
} else null,
|
||||||
icon = {
|
icon = {
|
||||||
// 3. Crossfade between Outlined and Filled icons
|
|
||||||
Crossfade(targetState = isSelected, label = "iconFade") { selected ->
|
Crossfade(targetState = isSelected, label = "iconFade") { selected ->
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (selected) screen.selectedIcon else screen.unselectedIcon,
|
imageVector = if (selected) screen.selectedIcon else screen.unselectedIcon,
|
||||||
contentDescription = title,
|
contentDescription = title,
|
||||||
modifier = Modifier.scale(scale) // Apply the spring scale
|
modifier = Modifier.scale(scale)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -165,6 +243,126 @@ 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(
|
||||||
|
Color(0xFF3B82F6).copy(alpha = 0.5f),
|
||||||
|
Color.Transparent
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shape = CircleShape
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Actual clickable button
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(playButtonSize)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(0xFF3B82F6))
|
||||||
|
.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,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider()
|
||||||
|
|
||||||
|
moreItems.forEach { screen ->
|
||||||
|
MoreMenuItem(
|
||||||
|
screen = screen,
|
||||||
|
onClick = { onItemSelected(screen) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MoreMenuItem(
|
||||||
|
screen: Screen,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = screen.selectedIcon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(screen.title),
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ThemePreviews
|
@ThemePreviews
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ fun AppCard(
|
|||||||
text: String? = null,
|
text: String? = null,
|
||||||
expandable: Boolean = false,
|
expandable: Boolean = false,
|
||||||
initiallyExpanded: Boolean = false,
|
initiallyExpanded: Boolean = false,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
content: @Composable ColumnScope.() -> Unit,
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
var isExpanded by remember { mutableStateOf(initiallyExpanded) }
|
var isExpanded by remember { mutableStateOf(initiallyExpanded) }
|
||||||
@@ -113,6 +114,7 @@ fun AppCard(
|
|||||||
// Check if we need to render the header row
|
// Check if we need to render the header row
|
||||||
// Updated to include icon in the check
|
// Updated to include icon in the check
|
||||||
val hasHeader = title != null || text != null || expandable || icon != null
|
val hasHeader = title != null || text != null || expandable || icon != null
|
||||||
|
val canClickHeader = expandable || onClick != null
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -125,7 +127,7 @@ fun AppCard(
|
|||||||
// Animate height changes when expanding/collapsing
|
// Animate height changes when expanding/collapsing
|
||||||
.animateContentSize(),
|
.animateContentSize(),
|
||||||
shape = ComponentDefaults.CardShape,
|
shape = ComponentDefaults.CardShape,
|
||||||
color = MaterialTheme.colorScheme.surfaceContainer
|
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
// --- Header Row ---
|
// --- Header Row ---
|
||||||
@@ -133,7 +135,12 @@ fun AppCard(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(enabled = expandable) { isExpanded = !isExpanded }
|
.clickable(enabled = canClickHeader) {
|
||||||
|
if (expandable) {
|
||||||
|
isExpanded = !isExpanded
|
||||||
|
}
|
||||||
|
onClick?.invoke()
|
||||||
|
}
|
||||||
.padding(ComponentDefaults.CardPadding),
|
.padding(ComponentDefaults.CardPadding),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
@@ -186,17 +193,27 @@ fun AppCard(
|
|||||||
|
|
||||||
// --- Content Area ---
|
// --- Content Area ---
|
||||||
if (!expandable || isExpanded) {
|
if (!expandable || isExpanded) {
|
||||||
Column(
|
val contentModifier = Modifier
|
||||||
modifier = Modifier.padding(
|
.padding(
|
||||||
start = ComponentDefaults.CardPadding,
|
start = ComponentDefaults.CardPadding,
|
||||||
end = ComponentDefaults.CardPadding,
|
end = ComponentDefaults.CardPadding,
|
||||||
bottom = ComponentDefaults.CardPadding,
|
bottom = ComponentDefaults.CardPadding,
|
||||||
// If we have a header, remove the top padding so content sits closer to the title.
|
// If we have a header, remove the top padding so content sits closer to the title.
|
||||||
// If no header (legacy behavior), keep the top padding.
|
// If no header (legacy behavior), keep the top padding.
|
||||||
top = if (hasHeader) 0.dp else ComponentDefaults.CardPadding
|
top = if (hasHeader) 0.dp else ComponentDefaults.CardPadding
|
||||||
),
|
)
|
||||||
|
|
||||||
|
if (!hasHeader && onClick != null) {
|
||||||
|
Column(
|
||||||
|
modifier = contentModifier.clickable { onClick() },
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
Column(
|
||||||
|
modifier = contentModifier,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ fun VocabularyReviewScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.found_items)) },
|
title = stringResource(R.string.found_items),
|
||||||
hintContent = HintDefinition.REVIEW.hint()
|
hintContent = HintDefinition.REVIEW.hint()
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -28,7 +26,6 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
@@ -346,28 +343,8 @@ fun DictionarySimpleTopBar(
|
|||||||
onNavigateBack: () -> Unit
|
onNavigateBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = {
|
title = "TODO",
|
||||||
Column {
|
onNavigateBack = onNavigateBack
|
||||||
Text(
|
|
||||||
text = word ?: stringResource(R.string.text_loading_3d),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
languageName?.let {
|
|
||||||
Text(
|
|
||||||
text = it,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
fontStyle = FontStyle.Italic
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = onNavigateBack) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
@@ -94,27 +93,8 @@ fun EtymologyResultScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = {
|
title = "TODO",
|
||||||
Column {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
Text(
|
|
||||||
text = word,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
language?.name?.let {
|
|
||||||
Text(
|
|
||||||
text = it,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
actions = {
|
||||||
etymologyData?.let { data ->
|
etymologyData?.let { data ->
|
||||||
if (isTtsAvailable) {
|
if (isTtsAvailable) {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import eu.gaudian.translator.view.LocalConnectionConfigured
|
|||||||
import eu.gaudian.translator.view.NoConnectionScreen
|
import eu.gaudian.translator.view.NoConnectionScreen
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
import eu.gaudian.translator.view.composable.AppIcons
|
||||||
import eu.gaudian.translator.view.composable.AppTabLayout
|
import eu.gaudian.translator.view.composable.AppTabLayout
|
||||||
|
import eu.gaudian.translator.view.composable.Screen
|
||||||
import eu.gaudian.translator.view.composable.TabItem
|
import eu.gaudian.translator.view.composable.TabItem
|
||||||
import eu.gaudian.translator.view.settings.SettingsRoutes
|
import eu.gaudian.translator.view.settings.SettingsRoutes
|
||||||
import eu.gaudian.translator.viewmodel.CorrectionViewModel
|
import eu.gaudian.translator.viewmodel.CorrectionViewModel
|
||||||
@@ -63,7 +64,15 @@ fun MainDictionaryScreen(
|
|||||||
AppTabLayout(
|
AppTabLayout(
|
||||||
tabs = dictionaryTabs,
|
tabs = dictionaryTabs,
|
||||||
selectedTab = selectedTab,
|
selectedTab = selectedTab,
|
||||||
onTabSelected = { selectedTab = it }
|
onTabSelected = { selectedTab = it },
|
||||||
|
onNavigateBack = {
|
||||||
|
if (!navController.popBackStack()) {
|
||||||
|
navController.navigate(Screen.Home.route) {
|
||||||
|
launchSingleTop = true
|
||||||
|
restoreState = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
when (selectedTab) {
|
when (selectedTab) {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ fun ExerciseVocabularyScreen(
|
|||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(title = { Text(stringResource(R.string.text_new_vocabulary_for_this_exercise)) })
|
AppTopAppBar(title =stringResource(R.string.text_new_vocabulary_for_this_exercise))
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
Surface(shadowElevation = 8.dp) {
|
Surface(shadowElevation = 8.dp) {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import eu.gaudian.translator.view.composable.AppIcons
|
|||||||
import eu.gaudian.translator.view.composable.AppOutlinedCard
|
import eu.gaudian.translator.view.composable.AppOutlinedCard
|
||||||
import eu.gaudian.translator.view.composable.AppTabLayout
|
import eu.gaudian.translator.view.composable.AppTabLayout
|
||||||
import eu.gaudian.translator.view.composable.DialogButton
|
import eu.gaudian.translator.view.composable.DialogButton
|
||||||
|
import eu.gaudian.translator.view.composable.Screen
|
||||||
import eu.gaudian.translator.view.composable.TabItem
|
import eu.gaudian.translator.view.composable.TabItem
|
||||||
import eu.gaudian.translator.viewmodel.AiGenerationState
|
import eu.gaudian.translator.viewmodel.AiGenerationState
|
||||||
import eu.gaudian.translator.viewmodel.ExerciseViewModel
|
import eu.gaudian.translator.viewmodel.ExerciseViewModel
|
||||||
@@ -76,7 +77,15 @@ fun MainExerciseScreen(
|
|||||||
AppTabLayout(
|
AppTabLayout(
|
||||||
tabs = ExerciseTab.entries,
|
tabs = ExerciseTab.entries,
|
||||||
selectedTab = selectedTab,
|
selectedTab = selectedTab,
|
||||||
onTabSelected = { selectedTab = it }
|
onTabSelected = { selectedTab = it },
|
||||||
|
onNavigateBack = {
|
||||||
|
if (!navController.popBackStack()) {
|
||||||
|
navController.navigate(Screen.Home.route) {
|
||||||
|
launchSingleTop = true
|
||||||
|
restoreState = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
Box(modifier = Modifier.weight(1f)) {
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
|
|||||||
@@ -0,0 +1,432 @@
|
|||||||
|
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.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.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.Edit
|
||||||
|
import androidx.compose.material.icons.filled.Hearing
|
||||||
|
import androidx.compose.material.icons.filled.List
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material.icons.outlined.Circle
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Slider
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
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.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StartExerciseScreen(
|
||||||
|
navController: NavHostController,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
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() })
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(horizontal = 24.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(32.dp)
|
||||||
|
) {
|
||||||
|
item { Spacer(modifier = Modifier.height(8.dp)) }
|
||||||
|
item { LanguagePairSection() }
|
||||||
|
item { CategoriesSection() }
|
||||||
|
item { DifficultySection() }
|
||||||
|
item { NumberOfCardsSection() }
|
||||||
|
item { QuestionTypesSection() }
|
||||||
|
item { Spacer(modifier = Modifier.height(24.dp)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
BottomButtonSection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TopBarSection(onBackClick: () -> Unit) {
|
||||||
|
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 = "Back",
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Start Exercise",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||||
|
)
|
||||||
|
|
||||||
|
// Spacer to balance the back button for centering
|
||||||
|
Spacer(modifier = Modifier.size(48.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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() {
|
||||||
|
var selectedPair by remember { mutableStateOf(0) }
|
||||||
|
|
||||||
|
Column {
|
||||||
|
SectionHeader(title = "Language Pair", actionText = "Change")
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
LanguageChip(
|
||||||
|
text = "EN → ES",
|
||||||
|
isSelected = selectedPair == 0,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
onClick = { selectedPair = 0 }
|
||||||
|
)
|
||||||
|
LanguageChip(
|
||||||
|
text = "EN → FR",
|
||||||
|
isSelected = selectedPair == 1,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
onClick = { selectedPair = 1 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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() {
|
||||||
|
val categories = listOf("Travel", "Business", "Food", "Technology", "Slang", "Academic", "Relationships")
|
||||||
|
var selectedCategories by remember { mutableStateOf(setOf("Travel", "Food")) }
|
||||||
|
|
||||||
|
Column {
|
||||||
|
SectionHeader(title = "Categories")
|
||||||
|
FlowRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
categories.forEach { category ->
|
||||||
|
val isSelected = selectedCategories.contains(category)
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
onClick = {
|
||||||
|
selectedCategories = if (isSelected) selectedCategories - category else selectedCategories + category
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = category,
|
||||||
|
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 DifficultySection() {
|
||||||
|
val difficulties = listOf("Easy", "Medium", "Hard")
|
||||||
|
var selectedDifficulty by remember { mutableStateOf("Medium") }
|
||||||
|
|
||||||
|
Column {
|
||||||
|
SectionHeader(title = "Difficulty Level")
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(50),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
modifier = Modifier.fillMaxWidth().height(56.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
difficulties.forEach { level ->
|
||||||
|
val isSelected = selectedDifficulty == level
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.clip(RoundedCornerShape(50))
|
||||||
|
.background(if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent)
|
||||||
|
.clickable { selectedDifficulty = level },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = level,
|
||||||
|
color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NumberOfCardsSection() {
|
||||||
|
var sliderPosition by remember { mutableFloatStateOf(25f) }
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "NUMBER OF CARDS",
|
||||||
|
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 = sliderPosition.toInt().toString(),
|
||||||
|
color = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Slider(
|
||||||
|
value = sliderPosition,
|
||||||
|
onValueChange = { sliderPosition = it },
|
||||||
|
valueRange = 5f..50f,
|
||||||
|
steps = 45
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text("5 CARDS", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
Text("50 CARDS", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun QuestionTypesSection() {
|
||||||
|
var selectedTypes by remember { mutableStateOf(setOf("Multiple Choice", "Spelling")) }
|
||||||
|
|
||||||
|
Column {
|
||||||
|
SectionHeader(title = "Question Types")
|
||||||
|
|
||||||
|
QuestionTypeCard(
|
||||||
|
title = "Multiple Choice",
|
||||||
|
subtitle = "Choose the correct meaning",
|
||||||
|
icon = Icons.Default.List,
|
||||||
|
isSelected = selectedTypes.contains("Multiple Choice"),
|
||||||
|
onClick = {
|
||||||
|
selectedTypes = if (selectedTypes.contains("Multiple Choice")) selectedTypes - "Multiple Choice" else selectedTypes + "Multiple Choice"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
QuestionTypeCard(
|
||||||
|
title = "Spelling",
|
||||||
|
subtitle = "Type the translated word",
|
||||||
|
icon = Icons.Default.Edit,
|
||||||
|
isSelected = selectedTypes.contains("Spelling"),
|
||||||
|
onClick = {
|
||||||
|
selectedTypes = if (selectedTypes.contains("Spelling")) selectedTypes - "Spelling" else selectedTypes + "Spelling"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
QuestionTypeCard(
|
||||||
|
title = "Listening",
|
||||||
|
subtitle = "Recognize spoken words",
|
||||||
|
icon = Icons.Default.Hearing,
|
||||||
|
isSelected = selectedTypes.contains("Listening"),
|
||||||
|
onClick = {
|
||||||
|
selectedTypes = if (selectedTypes.contains("Listening")) selectedTypes - "Listening" else selectedTypes + "Listening"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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() {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(24.dp)
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = { /* TODO: Start Session */ },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(56.dp),
|
||||||
|
shape = RoundedCornerShape(28.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Start Session",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.PlayArrow,
|
||||||
|
contentDescription = "Play",
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,8 +16,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -26,12 +24,10 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import eu.gaudian.translator.R
|
|
||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
import eu.gaudian.translator.view.composable.AppIcons
|
||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
@@ -61,12 +57,8 @@ fun YouTubeBrowserScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text("YouTube") },
|
title = "YouTube" ,
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() }
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
|
|||||||
@@ -183,14 +183,8 @@ fun YouTubeExerciseScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(title, maxLines = 1) },
|
title = title,
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(
|
|
||||||
R.string.cd_back
|
|
||||||
))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { onFinishVideo() },
|
onClick = { onFinishVideo() },
|
||||||
|
|||||||
@@ -5,15 +5,9 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import eu.gaudian.translator.R
|
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
|
||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
|
|
||||||
@@ -30,12 +24,8 @@ fun HintScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(title) },
|
title = title,
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() }
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ fun HintsOverviewScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.hint_title_hints_overview), style = MaterialTheme.typography.titleLarge) }
|
title = stringResource(R.string.hint_title_hints_overview)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
368
app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt
Normal file
368
app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
package eu.gaudian.translator.view.home
|
||||||
|
|
||||||
|
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.CheckCircle
|
||||||
|
import androidx.compose.material.icons.filled.ChevronRight
|
||||||
|
import androidx.compose.material.icons.filled.LocalFireDepartment
|
||||||
|
import androidx.compose.material.icons.filled.Person
|
||||||
|
import androidx.compose.material.icons.filled.Psychology
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material.icons.filled.TrendingUp
|
||||||
|
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.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.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import eu.gaudian.translator.utils.findActivity
|
||||||
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
|
import eu.gaudian.translator.view.composable.Screen
|
||||||
|
import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget
|
||||||
|
import eu.gaudian.translator.viewmodel.ProgressViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeScreen(
|
||||||
|
navController: NavHostController,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
// 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) }
|
||||||
|
item { StreakAndGoalSection() }
|
||||||
|
item {
|
||||||
|
ActionCard(
|
||||||
|
title = "Daily Review",
|
||||||
|
subtitle = "42 words need attention",
|
||||||
|
icon = Icons.Default.Psychology,
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
ActionCard(
|
||||||
|
title = "New Words",
|
||||||
|
subtitle = "Expand your vocabulary",
|
||||||
|
icon = Icons.Default.AddCircle,
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
onClick = { navController.navigate("new_word") }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item { WeeklyProgressSection(navController = navController) }
|
||||||
|
item { BottomStatsSection() }
|
||||||
|
|
||||||
|
// Bottom padding for edge-to-edge screens
|
||||||
|
item { Spacer(modifier = Modifier.height(24.dp)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TopProfileSection(navController: NavHostController) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
// Dummy Avatar
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(56.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Person, contentDescription = "Profile", tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = "Welcome back,",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Alex Rivera 👋",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
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 = "Settings",
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StreakAndGoalSection() {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
// Streak Card
|
||||||
|
StatCard(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
icon = Icons.Default.LocalFireDepartment,
|
||||||
|
title = "7 Days",
|
||||||
|
subtitle = "CURRENT STREAK"
|
||||||
|
)
|
||||||
|
// Goal Card
|
||||||
|
GoalCard(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
progress = 0.7f,
|
||||||
|
title = "14 / 20",
|
||||||
|
subtitle = "DAILY GOAL"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StatCard(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
icon: ImageVector,
|
||||||
|
title: String,
|
||||||
|
subtitle: String
|
||||||
|
) {
|
||||||
|
AppCard(
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(20.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
|
||||||
|
) {
|
||||||
|
AppCard(
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(20.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,
|
||||||
|
containerColor: Color,
|
||||||
|
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 = "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 = "Weekly Progress", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||||
|
TextButton(onClick = { navController.navigate("vocabulary_heatmap") }) {
|
||||||
|
Text("See History")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
AppCard(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
if (weeklyActivityStats.isEmpty()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "No activity data available",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
WeeklyActivityChartWidget(weeklyStats = weeklyActivityStats)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BottomStatsSection() {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
// Total Words
|
||||||
|
AppCard(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(20.dp)) {
|
||||||
|
Text(text = "TOTAL WORDS", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(text = "1,284", style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(Icons.Default.TrendingUp, contentDescription = null, tint = Color.Green, modifier = Modifier.size(16.dp))
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(text = "+12 today", style = MaterialTheme.typography.labelSmall, color = Color.Green)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accuracy
|
||||||
|
AppCard(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(20.dp)) {
|
||||||
|
Text(text = "ACCURACY", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(text = "92%", style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(Icons.Default.CheckCircle, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(16.dp))
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(text = "Master level", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,730 @@
|
|||||||
|
@file:Suppress("HardCodedStringLiteral")
|
||||||
|
|
||||||
|
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 = "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 = "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 = "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 = "Search cards or topics...",
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
innerTextField()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
IconButton(onClick = onFilterClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Tune,
|
||||||
|
contentDescription = "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 = "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 = "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 = "Selected",
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
IconButton(onClick = { /* Options menu could go here */ }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.MoreVert,
|
||||||
|
contentDescription = "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 = "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 = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun CategoryCardPreview() {
|
||||||
|
MaterialTheme {
|
||||||
|
CategoryCard(
|
||||||
|
category = eu.gaudian.translator.model.TagCategory(
|
||||||
|
1,
|
||||||
|
"Travel Phrases"
|
||||||
|
),
|
||||||
|
onClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun ExploreMoreCardPreview() {
|
||||||
|
MaterialTheme {
|
||||||
|
ExploreMoreCard(onClick = {})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,588 @@
|
|||||||
|
@file:Suppress("HardCodedStringLiteral")
|
||||||
|
|
||||||
|
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.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.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.model.VocabularyStage
|
||||||
|
import eu.gaudian.translator.utils.findActivity
|
||||||
|
import eu.gaudian.translator.view.composable.AppDropDownMenu
|
||||||
|
import eu.gaudian.translator.view.composable.AppIcons
|
||||||
|
import eu.gaudian.translator.view.composable.AppSwitch
|
||||||
|
import eu.gaudian.translator.view.composable.LargeDropdownMenuItem
|
||||||
|
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.CategoryViewModel
|
||||||
|
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)
|
||||||
|
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
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 { mutableStateOf(0) }
|
||||||
|
var previousScrollOffset by remember { mutableStateOf(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 ->
|
||||||
|
navController.navigate("category_detail/${category.id}")
|
||||||
|
},
|
||||||
|
onExploreMoreClick = {
|
||||||
|
navController.navigate("category_list_screen")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
navController.navigate("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 = "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) {
|
||||||
|
AppDropDownMenu(
|
||||||
|
expanded = showAddMenu,
|
||||||
|
onDismissRequest = { showAddMenu = false }
|
||||||
|
) {
|
||||||
|
LargeDropdownMenuItem(
|
||||||
|
text = stringResource(R.string.label_add_vocabulary),
|
||||||
|
selected = false,
|
||||||
|
enabled = true,
|
||||||
|
onClick = {
|
||||||
|
showAddMenu = false
|
||||||
|
navController.navigate("new_word")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
LargeDropdownMenuItem(
|
||||||
|
text = stringResource(R.string.label_add_category),
|
||||||
|
selected = false,
|
||||||
|
enabled = true,
|
||||||
|
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 = "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("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 = "SORT BY",
|
||||||
|
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 = "Apply Filters",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,6 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -73,12 +72,8 @@ fun AboutScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.label_about)) },
|
title = stringResource(R.string.label_about),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() }
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -134,12 +134,8 @@ fun AddModelScreen(navController: NavController, providerKey: String) {
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(providerName) },
|
title = providerName,
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
hintContent = HintDefinition.ADD_MODEL_SCAN.hint()
|
hintContent = HintDefinition.ADD_MODEL_SCAN.hint()
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ import eu.gaudian.translator.view.composable.AppTabLayout
|
|||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.view.composable.ClickableText
|
import eu.gaudian.translator.view.composable.ClickableText
|
||||||
import eu.gaudian.translator.view.composable.PrimaryButton
|
import eu.gaudian.translator.view.composable.PrimaryButton
|
||||||
|
import eu.gaudian.translator.view.composable.Screen
|
||||||
import eu.gaudian.translator.view.composable.SecondaryButton
|
import eu.gaudian.translator.view.composable.SecondaryButton
|
||||||
import eu.gaudian.translator.view.composable.TabItem
|
import eu.gaudian.translator.view.composable.TabItem
|
||||||
import eu.gaudian.translator.view.hints.HintDefinition
|
import eu.gaudian.translator.view.hints.HintDefinition
|
||||||
@@ -115,12 +116,8 @@ fun ApiKeyScreen(navController: NavController) {
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.label_ai_configuration)) },
|
title = stringResource(R.string.label_ai_configuration),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
hintContent = HintDefinition.API_KEY.hint()
|
hintContent = HintDefinition.API_KEY.hint()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -137,7 +134,15 @@ fun ApiKeyScreen(navController: NavController) {
|
|||||||
AppTabLayout(
|
AppTabLayout(
|
||||||
tabs = apiTabs,
|
tabs = apiTabs,
|
||||||
selectedTab = selectedTab,
|
selectedTab = selectedTab,
|
||||||
onTabSelected = { selectedTab = it }
|
onTabSelected = { selectedTab = it },
|
||||||
|
onNavigateBack = {
|
||||||
|
if (!navController.popBackStack()) {
|
||||||
|
navController.navigate(Screen.Home.route) {
|
||||||
|
launchSingleTop = true
|
||||||
|
restoreState = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Tab Content
|
// Tab Content
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -22,7 +19,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
|
||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.viewmodel.ApiViewModel
|
import eu.gaudian.translator.viewmodel.ApiViewModel
|
||||||
@@ -55,12 +51,8 @@ fun CustomVocabularyPromptScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.text_vocabulary_prompt)) },
|
title = stringResource(R.string.text_vocabulary_prompt),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
hintContent = null //TODO: Add hint
|
hintContent = null //TODO: Add hint
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -31,7 +28,6 @@ import eu.gaudian.translator.R
|
|||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.LocalShowExperimentalFeatures
|
import eu.gaudian.translator.view.LocalShowExperimentalFeatures
|
||||||
import eu.gaudian.translator.view.composable.AppCard
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
|
||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.view.composable.OptionItemSwitch
|
import eu.gaudian.translator.view.composable.OptionItemSwitch
|
||||||
@@ -66,12 +62,8 @@ fun DictionaryOptionsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.label_dictionary_options)) },
|
title = stringResource(R.string.label_dictionary_options),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
hintContent = HintDefinition.DICTIONARY_OPTIONS.hint()
|
hintContent = HintDefinition.DICTIONARY_OPTIONS.hint()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -31,7 +29,6 @@ import eu.gaudian.translator.R
|
|||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.ApiModelDropDown
|
import eu.gaudian.translator.view.composable.ApiModelDropDown
|
||||||
import eu.gaudian.translator.view.composable.AppCard
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
|
||||||
import eu.gaudian.translator.view.composable.AppOutlinedTextField
|
import eu.gaudian.translator.view.composable.AppOutlinedTextField
|
||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
@@ -71,12 +68,8 @@ fun ExerciseSettingsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.exercise_settings)) },
|
title = stringResource(R.string.exercise_settings),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -24,7 +22,6 @@ import androidx.navigation.NavController
|
|||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppCard
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
|
||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppSwitch
|
import eu.gaudian.translator.view.composable.AppSwitch
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
@@ -41,12 +38,8 @@ fun GeneralSettingsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.label_general)) },
|
title = stringResource(R.string.label_general),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -61,12 +61,8 @@ fun LanguageOptionsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.text_language_options)) },
|
title = stringResource(R.string.text_language_options),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import androidx.compose.foundation.shape.CircleShape
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
@@ -101,12 +100,8 @@ fun LayoutOptionsScreen(navController: NavController) {
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.label_appearance)) },
|
title = stringResource(R.string.label_appearance),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = cdBack)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -101,15 +101,8 @@ fun LogsScreen(navController: NavController) {
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.label_logs)) },
|
title = stringResource(R.string.label_logs),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = AppIcons.ArrowBack,
|
|
||||||
contentDescription = stringResource(R.string.cd_back)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
actions = {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
settingsViewModel.clearApiLogs()
|
settingsViewModel.clearApiLogs()
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import eu.gaudian.translator.view.composable.AppIcons
|
|||||||
import eu.gaudian.translator.view.composable.AppOutlinedCard
|
import eu.gaudian.translator.view.composable.AppOutlinedCard
|
||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
|
import eu.gaudian.translator.view.composable.Screen
|
||||||
|
|
||||||
private data class Setting(val titleRes: Int, val icon: ImageVector, val route: String)
|
private data class Setting(val titleRes: Int, val icon: ImageVector, val route: String)
|
||||||
|
|
||||||
@@ -84,7 +85,15 @@ fun MainSettingsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.title_settings), style = MaterialTheme.typography.titleLarge) }
|
title =stringResource(R.string.title_settings),
|
||||||
|
onNavigateBack = {
|
||||||
|
if (!navController.popBackStack()) {
|
||||||
|
navController.navigate(Screen.Home.route) {
|
||||||
|
launchSingleTop = true
|
||||||
|
restoreState = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -86,12 +84,8 @@ fun TextToSpeechSettingsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.settings_title_voice)) },
|
title = stringResource(R.string.settings_title_voice),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -27,7 +24,6 @@ import androidx.navigation.NavController
|
|||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppCard
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
|
||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.view.composable.OptionItemSwitch
|
import eu.gaudian.translator.view.composable.OptionItemSwitch
|
||||||
@@ -64,12 +60,8 @@ fun TranslationSettingsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.label_translation_settings)) },
|
title = stringResource(R.string.label_translation_settings),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
hintContent = null //TODO add hint
|
hintContent = null //TODO add hint
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import androidx.compose.foundation.shape.CircleShape
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
@@ -78,13 +77,8 @@ fun VocabularyProgressOptionsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.vocabulary_settings)) },
|
title = stringResource(R.string.vocabulary_settings),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Here is the new hint content
|
|
||||||
hintContent = HintDefinition.VOCABULARY_PROGRESS.hint()
|
hintContent = HintDefinition.VOCABULARY_PROGRESS.hint()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ import androidx.compose.material3.ButtonDefaults
|
|||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
@@ -44,7 +42,6 @@ import eu.gaudian.translator.utils.StatusMessageService
|
|||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppButton
|
import eu.gaudian.translator.view.composable.AppButton
|
||||||
import eu.gaudian.translator.view.composable.AppCard
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
|
||||||
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
@@ -200,12 +197,8 @@ fun VocabularyRepositoryOptionsScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.vocabulary_repository)) },
|
title = stringResource(R.string.vocabulary_repository),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -0,0 +1,692 @@
|
|||||||
|
@file:Suppress("HardCodedStringLiteral", "AssignedValueIsNeverRead", "UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
|
|
||||||
|
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.Log
|
||||||
|
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.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.ModernStartButtons
|
||||||
|
import eu.gaudian.translator.view.vocabulary.widgets.StatusWidget
|
||||||
|
import eu.gaudian.translator.view.vocabulary.widgets.StreakWidget
|
||||||
|
import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget
|
||||||
|
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(
|
||||||
|
navController: NavHostController,
|
||||||
|
onShowCustomExerciseDialog: () -> Unit = {},
|
||||||
|
startDailyExercise: (Boolean) -> Unit = {},
|
||||||
|
onNavigateToCategoryDetail: ((Int) -> Unit)? = null,
|
||||||
|
onNavigateToCategoryList: (() -> Unit)? = null,
|
||||||
|
onShowWordPairExerciseDialog: () -> Unit = {},
|
||||||
|
onScroll: (Boolean) -> Unit = {},
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
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 ->
|
||||||
|
navController.navigate("stats/category_detail/$categoryId")
|
||||||
|
}
|
||||||
|
val handleNavigateToCategoryList = onNavigateToCategoryList ?: {
|
||||||
|
navController.navigate("stats/category_list_screen")
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
onShowCustomExerciseDialog = onShowCustomExerciseDialog,
|
||||||
|
startDailyExercise = startDailyExercise,
|
||||||
|
onNavigateToCategoryDetail = handleNavigateToCategoryDetail,
|
||||||
|
onNavigateToCategoryList = handleNavigateToCategoryList,
|
||||||
|
onShowWordPairExerciseDialog = onShowWordPairExerciseDialog,
|
||||||
|
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
|
||||||
|
// --------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LazyWidget(
|
||||||
|
widgetType: WidgetType,
|
||||||
|
navController: NavController,
|
||||||
|
vocabularyViewModel: VocabularyViewModel,
|
||||||
|
progressViewModel: ProgressViewModel,
|
||||||
|
onShowCustomExerciseDialog: () -> Unit,
|
||||||
|
startDailyExercise: (Boolean) -> Unit,
|
||||||
|
onNavigateToCategoryDetail: (Int) -> Unit,
|
||||||
|
onNavigateToCategoryList: () -> Unit,
|
||||||
|
onShowWordPairExerciseDialog: () -> Unit,
|
||||||
|
onMissingLanguage: (Int) -> Unit
|
||||||
|
) {
|
||||||
|
when (widgetType) {
|
||||||
|
WidgetType.StartButtons -> ModernStartButtons(
|
||||||
|
onCustomClick = onShowCustomExerciseDialog,
|
||||||
|
onDailyClick = { isSpelling ->
|
||||||
|
if (isSpelling) {
|
||||||
|
onShowWordPairExerciseDialog()
|
||||||
|
} else {
|
||||||
|
startDailyExercise(true)
|
||||||
|
Log.d("DailyExercise", "Starting daily exercise")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
WidgetType.Status -> LazyStatusWidget(
|
||||||
|
vocabularyViewModel = vocabularyViewModel,
|
||||||
|
onNavigateToNew = { navController.navigate("stats/vocabulary_sorting?mode=NEW") },
|
||||||
|
onNavigateToDuplicates = { navController.navigate("stats/vocabulary_sorting?mode=DUPLICATES") },
|
||||||
|
onNavigateToFaulty = { navController.navigate("stats/vocabulary_sorting?mode=FAULTY") },
|
||||||
|
onNavigateToNoGrammar = { navController.navigate("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("stats/vocabulary_heatmap") }
|
||||||
|
)
|
||||||
|
|
||||||
|
WidgetType.WeeklyActivityChart -> WeeklyActivityChartWidget(
|
||||||
|
weeklyStats = progressViewModel.weeklyActivityStats.collectAsState().value
|
||||||
|
)
|
||||||
|
|
||||||
|
WidgetType.AllVocabulary -> AllVocabularyWidget(
|
||||||
|
vocabularyViewModel = vocabularyViewModel,
|
||||||
|
onOpenAllVocabulary = { navController.navigate("stats/vocabulary_list/false/null") },
|
||||||
|
onStageClicked = { stage -> navController.navigate("stats/vocabulary_list/false/$stage") }
|
||||||
|
)
|
||||||
|
|
||||||
|
WidgetType.DueToday -> DueTodayWidget(
|
||||||
|
vocabularyViewModel = vocabularyViewModel,
|
||||||
|
onStageClicked = { stage -> navController.navigate("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("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,
|
||||||
|
onShowCustomExerciseDialog = {},
|
||||||
|
onNavigateToCategoryDetail = {},
|
||||||
|
startDailyExercise = {},
|
||||||
|
onNavigateToCategoryList = {},
|
||||||
|
onShowWordPairExerciseDialog = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
) {
|
||||||
|
Text("Preview Content")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,9 +67,23 @@ fun ActionBar(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TopBarActions(languageViewModel: LanguageViewModel, onSettingsClick: () -> Unit, hintContent: (@Composable () -> Unit)? = null) {
|
fun TopBarActions(
|
||||||
|
languageViewModel: LanguageViewModel,
|
||||||
|
onSettingsClick: () -> Unit,
|
||||||
|
onNavigateBack: (() -> Unit)? = null,
|
||||||
|
hintContent: (@Composable () -> Unit)? = null
|
||||||
|
) {
|
||||||
|
|
||||||
ActionBar(modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp)) {
|
ActionBar(modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp)) {
|
||||||
|
if (onNavigateBack != null) {
|
||||||
|
IconButton(onClick = onNavigateBack) {
|
||||||
|
Icon(
|
||||||
|
imageVector = AppIcons.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.cd_navigate_back)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (hintContent != null) {
|
if (hintContent != null) {
|
||||||
WithHint(hintContent = hintContent) {
|
WithHint(hintContent = hintContent) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,14 @@ fun TranslationScreen(
|
|||||||
settingsViewModel = settingsViewModel,
|
settingsViewModel = settingsViewModel,
|
||||||
onHistoryClick = onHistoryClick,
|
onHistoryClick = onHistoryClick,
|
||||||
onSettingsClick = onSettingsClick,
|
onSettingsClick = onSettingsClick,
|
||||||
|
onNavigateBack = {
|
||||||
|
if (!navController.popBackStack()) {
|
||||||
|
navController.navigate(eu.gaudian.translator.view.composable.Screen.Home.route) {
|
||||||
|
launchSingleTop = true
|
||||||
|
restoreState = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
context = context
|
context = context
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -119,6 +127,7 @@ private fun LoadedTranslationContent(
|
|||||||
settingsViewModel: SettingsViewModel,
|
settingsViewModel: SettingsViewModel,
|
||||||
onHistoryClick: () -> Unit,
|
onHistoryClick: () -> Unit,
|
||||||
onSettingsClick: () -> Unit,
|
onSettingsClick: () -> Unit,
|
||||||
|
onNavigateBack: () -> Unit,
|
||||||
context: Context
|
context: Context
|
||||||
) {
|
) {
|
||||||
val inputText by translationViewModel.inputText.collectAsState()
|
val inputText by translationViewModel.inputText.collectAsState()
|
||||||
@@ -167,6 +176,7 @@ private fun LoadedTranslationContent(
|
|||||||
TopBarActions(
|
TopBarActions(
|
||||||
languageViewModel = languageViewModel,
|
languageViewModel = languageViewModel,
|
||||||
onSettingsClick = onSettingsClick,
|
onSettingsClick = onSettingsClick,
|
||||||
|
onNavigateBack = onNavigateBack,
|
||||||
hintContent = { HintDefinition.TRANSLATION.Render() }
|
hintContent = { HintDefinition.TRANSLATION.Render() }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,16 @@ package eu.gaudian.translator.view.vocabulary
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -30,7 +32,8 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
@@ -43,10 +46,12 @@ import eu.gaudian.translator.view.composable.AppIcons
|
|||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.view.composable.PrimaryButton
|
import eu.gaudian.translator.view.composable.PrimaryButton
|
||||||
|
import eu.gaudian.translator.view.composable.SecondaryButton
|
||||||
import eu.gaudian.translator.view.dialogs.DeleteCategoryDialog
|
import eu.gaudian.translator.view.dialogs.DeleteCategoryDialog
|
||||||
import eu.gaudian.translator.view.dialogs.DeleteItemsDialog
|
import eu.gaudian.translator.view.dialogs.DeleteItemsDialog
|
||||||
import eu.gaudian.translator.view.dialogs.EditCategoryDialog
|
import eu.gaudian.translator.view.dialogs.EditCategoryDialog
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressCircle
|
import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressCircle
|
||||||
|
import eu.gaudian.translator.viewmodel.CategoryProgress
|
||||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||||
import eu.gaudian.translator.viewmodel.ProgressViewModel
|
import eu.gaudian.translator.viewmodel.ProgressViewModel
|
||||||
@@ -89,11 +94,10 @@ fun CategoryDetailScreen(
|
|||||||
if (!hasLangList && !hasPair && !hasStages) {
|
if (!hasLangList && !hasPair && !hasStages) {
|
||||||
append(stringResource(R.string.text_filter_all_items))
|
append(stringResource(R.string.text_filter_all_items))
|
||||||
} else {
|
} else {
|
||||||
//append(stringResource(R.string.filter))
|
|
||||||
append(" ")
|
append(" ")
|
||||||
if (hasPair) {
|
if (hasPair) {
|
||||||
val (a,b) = cat.languagePairs
|
val (a, b) = cat.languagePairs
|
||||||
append("[${languages.value.find{ it.nameResId == a }?.name} - ${languages.value.find{ it.nameResId == b }?.name}]")
|
append("[${languages.value.find { it.nameResId == a }?.name} - ${languages.value.find { it.nameResId == b }?.name}]")
|
||||||
} else if (hasLangList) {
|
} else if (hasLangList) {
|
||||||
append(cat.languages.joinToString(", ") { langId -> languages.value.find { it.nameResId == langId }?.name.toString() })
|
append(cat.languages.joinToString(", ") { langId -> languages.value.find { it.nameResId == langId }?.name.toString() })
|
||||||
} else {
|
} else {
|
||||||
@@ -118,30 +122,8 @@ fun CategoryDetailScreen(
|
|||||||
modifier = Modifier.background(MaterialTheme.colorScheme.surface)
|
modifier = Modifier.background(MaterialTheme.colorScheme.surface)
|
||||||
) {
|
) {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = {
|
title = title,
|
||||||
Column {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = subtitle,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = onBackClick) {
|
|
||||||
Icon(
|
|
||||||
AppIcons.ArrowBack,
|
|
||||||
contentDescription = stringResource(R.string.cd_back)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = { showMenu = !showMenu }) {
|
IconButton(onClick = { showMenu = !showMenu }) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -150,94 +132,58 @@ fun CategoryDetailScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
|
|
||||||
expanded = showMenu,
|
expanded = showMenu,
|
||||||
onDismissRequest = { showMenu = false },
|
onDismissRequest = { showMenu = false },
|
||||||
modifier = Modifier.width(220.dp)
|
modifier = Modifier.width(220.dp)
|
||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(R.string.text_edit_category)) },
|
|
||||||
onClick = {
|
|
||||||
categoryViewModel.setShowEditCategoryDialog(true, categoryId)
|
|
||||||
showMenu = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(stringResource(R.string.text_export_category)) },
|
text = { Text(stringResource(R.string.text_export_category)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
vocabularyViewModel.saveCategory(categoryId)
|
vocabularyViewModel.saveCategory(categoryId)
|
||||||
showMenu = false
|
showMenu = false
|
||||||
}
|
},
|
||||||
|
leadingIcon = { Icon(AppIcons.Share, contentDescription = null) }
|
||||||
)
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(stringResource(R.string.delete_items_category)) },
|
text = { Text(stringResource(R.string.delete_items_category)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
categoryViewModel.setShowDeleteItemsDialog(true, categoryId)
|
categoryViewModel.setShowDeleteItemsDialog(true, categoryId)
|
||||||
showMenu = false
|
showMenu = false
|
||||||
}
|
},
|
||||||
)
|
leadingIcon = { Icon(AppIcons.Delete, contentDescription = null) }
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(R.string.text_delete_category)) },
|
|
||||||
onClick = {
|
|
||||||
categoryViewModel.setShowDeleteCategoryDialog(true, categoryId)
|
|
||||||
showMenu = false
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
// TODO: Review this
|
|
||||||
containerColor = MaterialTheme.colorScheme.surface
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(
|
// Category Header Card with Progress and Action Buttons
|
||||||
modifier = Modifier
|
CategoryHeaderCard(
|
||||||
.fillMaxWidth()
|
subtitle = subtitle,
|
||||||
.padding(vertical = 8.dp, horizontal = 16.dp),
|
categoryProgress = categoryProgress,
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
onStartExerciseClick = {
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
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")
|
||||||
},
|
},
|
||||||
modifier = Modifier.heightIn(max = 80.dp)
|
onEditClick = {
|
||||||
|
categoryViewModel.setShowEditCategoryDialog(true, categoryId)
|
||||||
|
},
|
||||||
|
onDeleteClick = {
|
||||||
|
categoryViewModel.setShowDeleteCategoryDialog(true, categoryId)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(modifier = Modifier.padding(paddingValues)) {
|
Column(modifier = Modifier.padding(paddingValues)) {
|
||||||
VocabularyListScreen(
|
VocabularyListScreen(
|
||||||
categoryId = categoryId,
|
categoryId = categoryId,
|
||||||
showDueTodayOnly = false,
|
showDueTodayOnly = false,
|
||||||
onNavigateToItem = onNavigateToItem,
|
onNavigateToItem = onNavigateToItem,
|
||||||
navController = navController, // Pass the received navController here
|
navController = navController,
|
||||||
isRemoveFromCategoryEnabled = category is TagCategory,
|
isRemoveFromCategoryEnabled = category is TagCategory,
|
||||||
showTopBar = false,
|
showTopBar = false,
|
||||||
enableNavigationButtons = true
|
enableNavigationButtons = true
|
||||||
@@ -266,3 +212,131 @@ fun CategoryDetailScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CategoryHeaderCard(
|
||||||
|
subtitle: String,
|
||||||
|
categoryProgress: CategoryProgress?,
|
||||||
|
onStartExerciseClick: () -> Unit,
|
||||||
|
onEditClick: () -> Unit,
|
||||||
|
onDeleteClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||||
|
),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(20.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
// Subtitle
|
||||||
|
if (subtitle.isNotBlank()) {
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Progress Circle
|
||||||
|
if (categoryProgress != null) {
|
||||||
|
CategoryProgressCircle(
|
||||||
|
totalItems = categoryProgress.totalItems,
|
||||||
|
itemsCompleted = categoryProgress.itemsCompleted,
|
||||||
|
itemsInStages = categoryProgress.itemsInStages,
|
||||||
|
newItems = categoryProgress.newItems,
|
||||||
|
circleSize = 120.dp,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action Buttons
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
// Start Exercise Button (Primary)
|
||||||
|
PrimaryButton(
|
||||||
|
text = stringResource(R.string.label_start_exercise),
|
||||||
|
icon = AppIcons.Play,
|
||||||
|
onClick = onStartExerciseClick,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// Secondary Action Buttons
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
// Edit Button
|
||||||
|
SecondaryButton(
|
||||||
|
text = stringResource(R.string.label_edit),
|
||||||
|
icon = AppIcons.Edit,
|
||||||
|
onClick = onEditClick,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Delete Button
|
||||||
|
SecondaryButton(
|
||||||
|
text = stringResource(R.string.label_delete),
|
||||||
|
icon = AppIcons.Delete,
|
||||||
|
onClick = onDeleteClick,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== PREVIEWS ====================
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun CategoryHeaderCardPreview() {
|
||||||
|
MaterialTheme {
|
||||||
|
CategoryHeaderCard(
|
||||||
|
subtitle = "German - English | All Stages",
|
||||||
|
categoryProgress = null,
|
||||||
|
onStartExerciseClick = {},
|
||||||
|
onEditClick = {},
|
||||||
|
onDeleteClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun CategoryHeaderCardWithProgressPreview() {
|
||||||
|
MaterialTheme {
|
||||||
|
CategoryHeaderCard(
|
||||||
|
subtitle = "Travel Vocabulary",
|
||||||
|
categoryProgress = eu.gaudian.translator.viewmodel.CategoryProgress(
|
||||||
|
vocabularyCategory = eu.gaudian.translator.model.TagCategory(
|
||||||
|
1,
|
||||||
|
"Travel"
|
||||||
|
),
|
||||||
|
totalItems = 50,
|
||||||
|
newItems = 15,
|
||||||
|
itemsInStages = 25,
|
||||||
|
itemsCompleted = 10
|
||||||
|
),
|
||||||
|
onStartExerciseClick = {},
|
||||||
|
onEditClick = {},
|
||||||
|
onDeleteClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -100,13 +100,7 @@ fun CategoryListScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = {
|
title = "TODO",
|
||||||
if (isSelectionMode && selectedCategories.isNotEmpty()) {
|
|
||||||
Text(stringResource(R.string.text_2d_categories_selected, selectedCategories.size))
|
|
||||||
} else {
|
|
||||||
Text(stringResource(R.string.label_categories))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
if (isSelectionMode) {
|
if (isSelectionMode) {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ fun LanguageProgressScreen(navController: NavController) {
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.your_language_journey)) },
|
title = stringResource(R.string.your_language_journey),
|
||||||
onNavigateBack = { navController.popBackStack() }
|
onNavigateBack = { navController.popBackStack() }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import eu.gaudian.translator.view.composable.AppOutlinedCard
|
|||||||
import eu.gaudian.translator.view.composable.AppSlider
|
import eu.gaudian.translator.view.composable.AppSlider
|
||||||
import eu.gaudian.translator.view.composable.AppTabLayout
|
import eu.gaudian.translator.view.composable.AppTabLayout
|
||||||
import eu.gaudian.translator.view.composable.OptionItemSwitch
|
import eu.gaudian.translator.view.composable.OptionItemSwitch
|
||||||
|
import eu.gaudian.translator.view.composable.Screen
|
||||||
import eu.gaudian.translator.view.composable.TabItem
|
import eu.gaudian.translator.view.composable.TabItem
|
||||||
import eu.gaudian.translator.view.dialogs.StartExerciseDialog
|
import eu.gaudian.translator.view.dialogs.StartExerciseDialog
|
||||||
import eu.gaudian.translator.view.dialogs.VocabularyMenu
|
import eu.gaudian.translator.view.dialogs.VocabularyMenu
|
||||||
@@ -329,6 +330,14 @@ fun MainVocabularyScreen(
|
|||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
restoreState = true
|
restoreState = true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onNavigateBack = {
|
||||||
|
if (!navController.popBackStack()) {
|
||||||
|
navController.navigate(Screen.Home.route) {
|
||||||
|
launchSingleTop = true
|
||||||
|
restoreState = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,288 @@
|
|||||||
|
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)
|
||||||
|
navController.popBackStack("new_word", inclusive = false)
|
||||||
|
},
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SummaryHeader(
|
||||||
|
totalCount: Int,
|
||||||
|
selectedCount: Int,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f))
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.found_items),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.text_amount_2d, totalCount),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(horizontalAlignment = Alignment.End) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.label_add_, selectedCount),
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.select_items_to_add),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ReviewList(
|
||||||
|
generatedItems: List<VocabularyItem>,
|
||||||
|
selectedItems: MutableList<VocabularyItem>,
|
||||||
|
duplicates: List<Boolean>,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val duplicateLabel = stringResource(R.string.duplicate)
|
||||||
|
LazyColumn(modifier = modifier) {
|
||||||
|
itemsIndexed(generatedItems) { index, item ->
|
||||||
|
val isDuplicate = duplicates.getOrNull(index) == true
|
||||||
|
val isSelected = selectedItems.contains(item)
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 6.dp),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = if (isDuplicate) {
|
||||||
|
MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.25f)
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.25f)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
AppCheckbox(
|
||||||
|
checked = isSelected,
|
||||||
|
onCheckedChange = { checked ->
|
||||||
|
if (checked) {
|
||||||
|
selectedItems.add(item)
|
||||||
|
} else {
|
||||||
|
selectedItems.remove(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(12.dp))
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(text = item.wordFirst, style = MaterialTheme.typography.titleMedium)
|
||||||
|
Text(text = item.wordSecond, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
}
|
||||||
|
if (isDuplicate) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(MaterialTheme.colorScheme.error.copy(alpha = 0.15f))
|
||||||
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.WarningAmber,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(4.dp))
|
||||||
|
Text(
|
||||||
|
text = duplicateLabel,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ActionRow(
|
||||||
|
selectedCount: Int,
|
||||||
|
onCancel: () -> Unit,
|
||||||
|
onConfirm: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
TextButton(onClick = onCancel) {
|
||||||
|
Text(stringResource(R.string.label_cancel))
|
||||||
|
}
|
||||||
|
AppButton(
|
||||||
|
onClick = onConfirm,
|
||||||
|
enabled = selectedCount > 0
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.label_add_, selectedCount))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,792 @@
|
|||||||
|
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.material.icons.filled.LibraryBooks
|
||||||
|
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.composable.AppButton
|
||||||
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
|
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.SourceLanguageDropdown
|
||||||
|
import eu.gaudian.translator.view.composable.TargetLanguageDropdown
|
||||||
|
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("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) }
|
||||||
|
var parseError by remember { mutableStateOf<String?>(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 errorParsingTable = stringResource(R.string.error_parsing_table)
|
||||||
|
val errorParsingTableWithReason = stringResource(R.string.error_parsing_table_with_reason)
|
||||||
|
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
|
||||||
|
parseError = null
|
||||||
|
} else {
|
||||||
|
parseError = errorParsingTable
|
||||||
|
statusMessageService.showErrorMessage(parseError!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
parseError = e.message
|
||||||
|
statusMessageService.showErrorMessage(
|
||||||
|
(errorParsingTableWithReason + " " + e.message)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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 = "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 = "Recently Added",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
TextButton(onClick = { navController.navigate("library") }) {
|
||||||
|
Text("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 = { navController.navigate("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 = {
|
||||||
|
val errorSelectTwoColumns = stringResource(R.string.error_select_two_columns)
|
||||||
|
val errorSelectLanguages = stringResource(R.string.error_select_languages)
|
||||||
|
val errorNoRowsToImport = stringResource(R.string.error_no_rows_to_import)
|
||||||
|
val infoImportedItemsFrom = stringResource(R.string.info_imported_items_from)
|
||||||
|
TextButton(onClick = {
|
||||||
|
if (selectedColFirst == selectedColSecond) {
|
||||||
|
statusMessageService.showErrorMessage(errorSelectTwoColumns)
|
||||||
|
return@TextButton
|
||||||
|
}
|
||||||
|
val langA = selectedLangFirst
|
||||||
|
val langB = selectedLangSecond
|
||||||
|
if (langA == null || langB == null) {
|
||||||
|
statusMessageService.showErrorMessage(errorSelectLanguages)
|
||||||
|
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.showErrorMessage(errorNoRowsToImport)
|
||||||
|
return@TextButton
|
||||||
|
}
|
||||||
|
vocabularyViewModel.addVocabularyItems(items)
|
||||||
|
statusMessageService.showSuccessMessage(infoImportedItemsFrom + " " + items.size)
|
||||||
|
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 hints = stringArrayResource(R.array.vocabulary_hints)
|
||||||
|
AppCard(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(24.dp)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.AutoAwesome,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "AI Generator",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Text(
|
||||||
|
text = 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 languageLabel = when {
|
||||||
|
selectedSourceLanguage != null && selectedTargetLanguage != null ->
|
||||||
|
"${selectedSourceLanguage?.name} → ${selectedTargetLanguage?.name}"
|
||||||
|
else -> stringResource(R.string.text_select_languages)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
) {
|
||||||
|
// 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 = Icons.Default.LibraryBooks,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Text(
|
||||||
|
text = "Explore Packs",
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
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 = "Import CSV",
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,8 +14,6 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -39,7 +37,6 @@ import androidx.navigation.NavController
|
|||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppButton
|
import eu.gaudian.translator.view.composable.AppButton
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
|
||||||
import eu.gaudian.translator.view.composable.AppSlider
|
import eu.gaudian.translator.view.composable.AppSlider
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||||
@@ -66,12 +63,8 @@ fun NoGrammarItemsScreen(
|
|||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.title_items_without_grammar)) },
|
title = stringResource(R.string.title_items_without_grammar),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ package eu.gaudian.translator.view.vocabulary
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -17,7 +14,6 @@ import androidx.navigation.NavHostController
|
|||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.model.VocabularyStage
|
import eu.gaudian.translator.model.VocabularyStage
|
||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
|
||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.DetailedStageProgressBar
|
import eu.gaudian.translator.view.vocabulary.widgets.DetailedStageProgressBar
|
||||||
@@ -40,15 +36,8 @@ fun StageDetailScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(text = stringResource(R.string.due_today_, stage.toString())) },
|
title = stringResource(R.string.due_today_, stage.toString()),
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(
|
|
||||||
AppIcons.ArrowBack,
|
|
||||||
contentDescription =stringResource(R.string.cd_back)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ private fun StartScreenContent(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.prepare_exercise)) },
|
title = stringResource(R.string.prepare_exercise),
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onClose) {
|
IconButton(onClick = onClose) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|||||||
@@ -75,21 +75,8 @@ fun VocabularyCardHost(
|
|||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
modifier = Modifier.height(56.dp),
|
modifier = Modifier.height(56.dp),
|
||||||
title = {
|
title = stringResource(R.string.item_details),
|
||||||
if (navigationItems.isNotEmpty()) {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
Text(stringResource(R.string.label_card_with_position, navigationPosition + 1, navigationItems.size))
|
|
||||||
} else {
|
|
||||||
Text(stringResource(R.string.item_details))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = { onBackPressed?.invoke() }) {
|
|
||||||
Icon(
|
|
||||||
AppIcons.ArrowBack,
|
|
||||||
contentDescription = stringResource(R.string.cd_back)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
actions = {
|
||||||
// Previous button
|
// Previous button
|
||||||
if (navigationPosition > 0) {
|
if (navigationPosition > 0) {
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ fun VocabularyHeatmapScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.label_vocabulary_activity)) },
|
title = stringResource(R.string.label_vocabulary_activity),
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -446,25 +446,8 @@ private fun DefaultTopAppBar(
|
|||||||
var showSortMenu by remember { mutableStateOf(false) }
|
var showSortMenu by remember { mutableStateOf(false) }
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
modifier = Modifier.height(56.dp),
|
modifier = Modifier.height(56.dp),
|
||||||
title = {
|
title = title,
|
||||||
|
onNavigateBack = onNavigateBack,
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxHeight(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(title)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
onNavigateBack?.let {
|
|
||||||
IconButton(onClick = it) {
|
|
||||||
Icon(
|
|
||||||
AppIcons.ArrowBack,
|
|
||||||
contentDescription = "stringResource(R.string.navigate_back)"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = onSearchClick) {
|
IconButton(onClick = onSearchClick) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -534,7 +517,8 @@ private fun SearchTopAppBar(
|
|||||||
|
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
modifier = Modifier.height(56.dp),
|
modifier = Modifier.height(56.dp),
|
||||||
title = {
|
title = "TODO",
|
||||||
|
additionalContent = {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxHeight(),
|
modifier = Modifier.fillMaxHeight(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
@@ -605,14 +589,7 @@ private fun ContextualTopAppBar(
|
|||||||
|
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
modifier = modifier.height(56.dp),
|
modifier = modifier.height(56.dp),
|
||||||
title = {
|
title = stringResource(R.string.d_selected, selectionCount),
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxHeight(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.d_selected, selectionCount))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onCloseClick) {
|
IconButton(onClick = onCloseClick) {
|
||||||
Icon(imageVector = AppIcons.Close, contentDescription = stringResource(R.string.label_close_selection_mode))
|
Icon(imageVector = AppIcons.Close, contentDescription = stringResource(R.string.label_close_selection_mode))
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ fun VocabularySortingScreen(
|
|||||||
var showFilterMenu by remember { mutableStateOf(false) }
|
var showFilterMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = { Text(stringResource(R.string.sort_new_vocabulary)) },
|
title = stringResource(R.string.sort_new_vocabulary),
|
||||||
actions = {
|
actions = {
|
||||||
Box {
|
Box {
|
||||||
IconButton(onClick = { showFilterMenu = true }) {
|
IconButton(onClick = { showFilterMenu = true }) {
|
||||||
@@ -231,11 +231,7 @@ fun VocabularySortingScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
onNavigateBack = { navController.popBackStack() },
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
hintContent = HintDefinition.SORTING.hint()
|
hintContent = HintDefinition.SORTING.hint()
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
<string-array name="changelog_entries">
|
<string-array name="changelog_entries">
|
||||||
<item>Version 0.3.0 \n• Enabled CSV Import for Vocabulary\n• Option to use a translation server for translations instead of AI models for some supported langugaes\n• UI bug fixes \n• Show word frequency \n• Performance optimizations \n• Improved translations (German and Portuguese)</item>
|
<item>Version 0.3.0 \n• Enabled CSV Import for Vocabulary\n• Option to use a translation server for translations instead of AI models for some supported langugaes\n• UI bug fixes \n• Show word frequency \n• Performance optimizations \n• Improved translations (German and Portuguese)</item>
|
||||||
<item>Version 0.4.0 \n• Added dictionary download (beta) \n• UI enhancements \n• Bugfixes \n• Re-designed vocabulary card with improved UI \n• More pre-configured providers \n• Improved performance</item>
|
<item>Version 0.4.0 \n• Added dictionary download (beta) \n• UI enhancements \n• Bugfixes \n• Re-designed vocabulary card with improved UI \n• More pre-configured providers \n• Improved performance</item>
|
||||||
<item>Version 0.5.0 \n• Reworked hints and help content, added more instcructions and help \n• UI changes in the flashcards with a more intuitive design \n• Lots of bugfixes \n• Improved translations for German and Portuguese</item>
|
<item>Version 0.5.0 \n• Reworked UI with new focus on Flashcards and Exercises \n• Adding vocabulary is easier and more intuitive now </item>
|
||||||
<item> </item>
|
<item> </item>
|
||||||
|
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|||||||
@@ -288,6 +288,7 @@
|
|||||||
<string name="label_hard">Hard</string>
|
<string name="label_hard">Hard</string>
|
||||||
<string name="label_header_row">First Row is a Header</string>
|
<string name="label_header_row">First Row is a Header</string>
|
||||||
<string name="label_hide_examples">Hide examples</string>
|
<string name="label_hide_examples">Hide examples</string>
|
||||||
|
<string name="label_home">Home</string>
|
||||||
<string name="label_import">Import</string>
|
<string name="label_import">Import</string>
|
||||||
<string name="label_import_table_csv_excel">Import Table (CSV)</string>
|
<string name="label_import_table_csv_excel">Import Table (CSV)</string>
|
||||||
<string name="label_in_stages">In Stages</string>
|
<string name="label_in_stages">In Stages</string>
|
||||||
@@ -1114,4 +1115,8 @@
|
|||||||
<string name="message_test_info">This is a generic info message.</string>
|
<string name="message_test_info">This is a generic info message.</string>
|
||||||
<string name="message_test_success">This is a test success message!</string>
|
<string name="message_test_success">This is a test success message!</string>
|
||||||
<string name="message_test_error">Oops, something went wrong :(</string>
|
<string name="message_test_error">Oops, something went wrong :(</string>
|
||||||
|
<string name="label_stats">Stats</string>
|
||||||
|
<string name="label_library">Library</string>
|
||||||
|
<string name="label_legacy_vocabulary">Legacy Vocabulary</string>
|
||||||
|
<string name="label_edit">Edit</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application) apply false
|
alias(libs.plugins.android.application) apply false
|
||||||
alias(libs.plugins.kotlin.android) apply false
|
|
||||||
alias(libs.plugins.kotlin.compose) apply false
|
alias(libs.plugins.kotlin.compose) apply false
|
||||||
id("androidx.navigation.safeargs.kotlin") version "2.9.7" apply false
|
id("androidx.navigation.safeargs.kotlin") version "2.9.7" apply false
|
||||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.3.10"
|
id("org.jetbrains.kotlin.plugin.serialization") version "2.3.10"
|
||||||
|
|||||||
@@ -21,13 +21,4 @@ kotlin.code.style=official
|
|||||||
# resources declared in the library itself and none from the library's dependencies,
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
# thereby reducing the size of the R class for that library
|
# thereby reducing the size of the R class for that library
|
||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
android.defaults.buildfeatures.resvalues=true
|
android.dependency.useConstraints=false
|
||||||
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
|
|
||||||
android.enableAppCompileTimeRClass=false
|
|
||||||
android.usesSdkInManifest.disallowed=false
|
|
||||||
android.uniquePackageNames=false
|
|
||||||
android.dependency.useConstraints=true
|
|
||||||
android.r8.strictFullModeForKeepRules=false
|
|
||||||
android.r8.optimizedResourceShrinking=false
|
|
||||||
android.builtInKotlin=false
|
|
||||||
android.newDsl=false
|
|
||||||
|
|||||||
Reference in New Issue
Block a user