diff --git a/app/src/main/java/eu/gaudian/translator/view/MainActivity.kt b/app/src/main/java/eu/gaudian/translator/view/MainActivity.kt index 2082710..0e3e5c8 100644 --- a/app/src/main/java/eu/gaudian/translator/view/MainActivity.kt +++ b/app/src/main/java/eu/gaudian/translator/view/MainActivity.kt @@ -253,7 +253,7 @@ fun TranslatorApp( val currentDestination = navBackStackEntry?.destination val selectedScreen = Screen.fromDestination(currentDestination) - val isBottomBarHidden = currentDestination?.hierarchy?.any { destination -> + @Suppress("HardCodedStringLiteral") val isBottomBarHidden = currentDestination?.hierarchy?.any { destination -> destination.route in setOf( Screen.Translation.route, Screen.Dictionary.route, @@ -310,6 +310,7 @@ fun TranslatorApp( } }, onPlayClicked = { + @Suppress("HardCodedStringLiteral") navController.navigate("start_exercise") } ) diff --git a/app/src/main/java/eu/gaudian/translator/view/Navigation.kt b/app/src/main/java/eu/gaudian/translator/view/Navigation.kt index 884f66e..43e74cf 100644 --- a/app/src/main/java/eu/gaudian/translator/view/Navigation.kt +++ b/app/src/main/java/eu/gaudian/translator/view/Navigation.kt @@ -37,6 +37,7 @@ import eu.gaudian.translator.view.settings.TranslationSettingsScreen import eu.gaudian.translator.view.settings.settingsGraph import eu.gaudian.translator.view.stats.StatsScreen import eu.gaudian.translator.view.translation.TranslationScreen +import eu.gaudian.translator.view.vocabulary.AllCardsListScreen import eu.gaudian.translator.view.vocabulary.CategoryDetailScreen import eu.gaudian.translator.view.vocabulary.CategoryListScreen import eu.gaudian.translator.view.vocabulary.LanguageProgressScreen @@ -47,11 +48,26 @@ import eu.gaudian.translator.view.vocabulary.StageDetailScreen import eu.gaudian.translator.view.vocabulary.VocabularyCardHost import eu.gaudian.translator.view.vocabulary.VocabularyExerciseHostScreen import eu.gaudian.translator.view.vocabulary.VocabularyHeatmapScreen -import eu.gaudian.translator.view.vocabulary.VocabularyListScreen import eu.gaudian.translator.view.vocabulary.VocabularySortingScreen private const val TRANSITION_DURATION = 300 +object NavigationRoutes { + const val NEW_WORD = "new_word" + const val NEW_WORD_REVIEW = "new_word_review" + const val START_EXERCISE = "start_exercise" + const val CATEGORY_DETAIL = "category_detail" + const val CATEGORY_LIST = "category_list_screen" + const val VOCABULARY_DETAIL = "vocabulary_detail" + const val STATS_VOCABULARY_HEATMAP = "stats/vocabulary_heatmap" + const val STATS_LANGUAGE_PROGRESS = "stats/language_progress" + const val STATS_CATEGORY_DETAIL = "stats/category_detail" + const val STATS_CATEGORY_LIST = "stats/category_list_screen" + const val STATS_VOCABULARY_SORTING = "stats/vocabulary_sorting" + const val STATS_NO_GRAMMAR_ITEMS = "stats/no_grammar_items" + const val STATS_VOCABULARY_LIST = "stats/vocabulary_list" +} + @Composable fun AppNavHost( navController: NavHostController, @@ -130,15 +146,15 @@ fun AppNavHost( HomeScreen(navController = navController) } - composable("new_word") { + composable(NavigationRoutes.NEW_WORD) { NewWordScreen(navController = navController) } - composable("new_word_review") { + composable(NavigationRoutes.NEW_WORD_REVIEW) { NewWordReviewScreen(navController = navController) } - composable("start_exercise") { + composable(NavigationRoutes.START_EXERCISE) { StartExerciseScreen(navController = navController) } @@ -203,7 +219,7 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) { composable("vocabulary_list/{showDueTodayOnly}/{categoryId}") { backStackEntry -> val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull() - VocabularyListScreen( + AllCardsListScreen( navController = navController, showDueTodayOnly = showDueTodayOnly, categoryId = categoryId, @@ -220,7 +236,7 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) { ) } - composable("vocabulary_heatmap") { + composable(NavigationRoutes.STATS_VOCABULARY_HEATMAP) { VocabularyHeatmapScreen( navController = navController, ) @@ -232,7 +248,7 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) { if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it) } - VocabularyListScreen( + AllCardsListScreen( navController = navController, showDueTodayOnly = showDueTodayOnly, stage = stage, @@ -373,7 +389,7 @@ fun NavGraphBuilder.statsGraph( composable("stats/vocabulary_list/{showDueTodayOnly}/{categoryId}") { backStackEntry -> val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull() - VocabularyListScreen( + AllCardsListScreen( navController = navController, showDueTodayOnly = showDueTodayOnly, categoryId = categoryId, @@ -384,12 +400,12 @@ fun NavGraphBuilder.statsGraph( enableNavigationButtons = true ) } - composable("stats/language_progress") { + composable(NavigationRoutes.STATS_LANGUAGE_PROGRESS) { LanguageProgressScreen( navController = navController ) } - composable("stats/vocabulary_heatmap") { + composable(NavigationRoutes.STATS_VOCABULARY_HEATMAP) { VocabularyHeatmapScreen( navController = navController, ) @@ -401,7 +417,7 @@ fun NavGraphBuilder.statsGraph( if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it) } - VocabularyListScreen( + AllCardsListScreen( navController = navController, showDueTodayOnly = showDueTodayOnly, stage = stage, diff --git a/app/src/main/java/eu/gaudian/translator/view/composable/AppTopAppBar.kt b/app/src/main/java/eu/gaudian/translator/view/composable/AppTopAppBar.kt index 412f7a7..6fe3824 100644 --- a/app/src/main/java/eu/gaudian/translator/view/composable/AppTopAppBar.kt +++ b/app/src/main/java/eu/gaudian/translator/view/composable/AppTopAppBar.kt @@ -43,9 +43,8 @@ import eu.gaudian.translator.view.hints.LocalShowHints @Composable fun AppTopAppBar( - title: String, - additionalContent: @Composable () -> Unit = {}, modifier: Modifier = Modifier, + title: String, onNavigateBack: (() -> Unit)? = null, navigationIcon: @Composable (() -> Unit)? = null, actions: @Composable RowScope.() -> Unit = {}, diff --git a/app/src/main/java/eu/gaudian/translator/view/exercises/ExerciseVocabularyScreen.kt b/app/src/main/java/eu/gaudian/translator/view/exercises/ExerciseVocabularyScreen.kt index ad82cb1..d1f9000 100644 --- a/app/src/main/java/eu/gaudian/translator/view/exercises/ExerciseVocabularyScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/exercises/ExerciseVocabularyScreen.kt @@ -15,7 +15,7 @@ import androidx.navigation.NavHostController import eu.gaudian.translator.R import eu.gaudian.translator.view.composable.AppButton import eu.gaudian.translator.view.composable.AppTopAppBar -import eu.gaudian.translator.view.vocabulary.VocabularyListScreen +import eu.gaudian.translator.view.vocabulary.AllCardsListScreen @Composable fun ExerciseVocabularyScreen( @@ -41,7 +41,7 @@ fun ExerciseVocabularyScreen( ) { paddingValues -> Box(modifier = Modifier.padding(paddingValues)) { - VocabularyListScreen( + AllCardsListScreen( navController = navController as NavHostController?, onNavigateToItem = { item -> // Navigate to the detail screen for a specific vocabulary item diff --git a/app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt b/app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt index 4d758f4..4609a3e 100644 --- a/app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt @@ -1,5 +1,6 @@ package eu.gaudian.translator.view.home +import android.content.Context import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -37,12 +38,14 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavHostController import eu.gaudian.translator.R import eu.gaudian.translator.utils.findActivity +import eu.gaudian.translator.view.NavigationRoutes import eu.gaudian.translator.view.composable.AppCard import eu.gaudian.translator.view.composable.Screen import eu.gaudian.translator.view.settings.SettingsRoutes @@ -78,33 +81,36 @@ fun HomeScreen( verticalArrangement = Arrangement.spacedBy(15.dp) ) { item { Spacer(modifier = Modifier.height(16.dp)) } - item { TopProfileSection(navController = navController) } + item { TopProfileSection( + navController = navController, + context = LocalContext.current + ) } item { StreakAndGoalSection( streak = streak, progress = progress, progressTitle = "$todayCompletedCount / $dailyGoal", onGoalClick = { navController.navigate(SettingsRoutes.VOCABULARY_OPTIONS) }, - onStreakClick = { navController.navigate("stats/vocabulary_heatmap") } + onStreakClick = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) } ) } item { + //TODO replace with actual implementation + @Suppress("HardCodedStringLiteral") ActionCard( title = "Daily Review", subtitle = "42 words need attention", icon = Icons.Default.Psychology, - containerColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.onPrimary ) } item { ActionCard( - title = "New Words", - subtitle = "Expand your vocabulary", + title = stringResource(R.string.label_new_words), + subtitle = stringResource(R.string.desc_expand_your_vocabulary), icon = Icons.Default.AddCircle, - containerColor = MaterialTheme.colorScheme.surfaceVariant, contentColor = MaterialTheme.colorScheme.onSurfaceVariant, - onClick = { navController.navigate("new_word") } + onClick = { navController.navigate(NavigationRoutes.NEW_WORD) } ) } item { WeeklyProgressSection(navController = navController) } @@ -117,8 +123,8 @@ fun HomeScreen( } @Composable -fun TopProfileSection(navController: NavHostController) { - val context = LocalContext.current +fun TopProfileSection(navController: NavHostController, context: Context) { + val motivationalPhrases = remember { context.resources.getStringArray(R.array.motivational_phrases) } @@ -126,7 +132,9 @@ fun TopProfileSection(navController: NavHostController) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth().padding(8.dp) + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) ) { Column(modifier = Modifier.weight(1f)) { @@ -146,7 +154,7 @@ fun TopProfileSection(navController: NavHostController) { ) { Icon( imageVector = Icons.Default.Settings, - contentDescription = "Settings", + contentDescription = stringResource(R.string.label_settings), tint = MaterialTheme.colorScheme.onSurfaceVariant ) } @@ -169,8 +177,8 @@ fun StreakAndGoalSection( StatCard( modifier = Modifier.weight(1f), icon = Icons.Default.LocalFireDepartment, - title = "$streak Days", - subtitle = "CURRENT STREAK", + title = stringResource(R.string.label_2d_days, streak), + subtitle = stringResource(R.string.label_current_streak).uppercase(), onClick = onStreakClick ) // Goal Card @@ -178,7 +186,7 @@ fun StreakAndGoalSection( modifier = Modifier.weight(1f), progress = progress, title = progressTitle, - subtitle = "DAILY GOAL", + subtitle = stringResource(R.string.label_daily_goal).uppercase(), onClick = onGoalClick ) } @@ -293,7 +301,6 @@ fun ActionCard( title: String, subtitle: String, icon: ImageVector, - containerColor: Color, contentColor: Color, onClick: (() -> Unit)? = null ) { @@ -314,7 +321,7 @@ fun ActionCard( } Icon( imageVector = Icons.Default.ChevronRight, - contentDescription = "Go", + contentDescription = stringResource(R.string.cd_go), modifier = Modifier.size(24.dp) ) } @@ -351,9 +358,9 @@ fun WeeklyProgressSection( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Text(text = "Weekly Progress", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) - TextButton(onClick = { navController.navigate("stats/vocabulary_heatmap") }) { - Text("See History") + Text(text = stringResource(R.string.label_weekly_progress), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + TextButton(onClick = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) }) { + Text(stringResource(R.string.label_see_history)) } } @@ -361,7 +368,7 @@ fun WeeklyProgressSection( AppCard( modifier = Modifier.fillMaxWidth(), - onClick = { navController.navigate("stats/vocabulary_heatmap") } + onClick = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) } ) { if (weeklyActivityStats.isEmpty()) { Column( @@ -372,7 +379,7 @@ fun WeeklyProgressSection( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = "No activity data available", + text = stringResource(R.string.text_desc_no_activity_data_available), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -403,7 +410,7 @@ fun BottomStatsSection( onClick = { navController.navigate(Screen.Library.route) } ) { Column(modifier = Modifier.padding(20.dp)) { - Text(text = "TOTAL WORDS", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)) + Text(text = stringResource(R.string.label_total_words).uppercase(), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)) Spacer(modifier = Modifier.height(8.dp)) Text(text = totalWords.toString(), style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.height(8.dp)) @@ -413,7 +420,7 @@ fun BottomStatsSection( // Learned AppCard( modifier = Modifier.weight(1f), - onClick = { navController.navigate("stats/language_progress") } + onClick = { navController.navigate(NavigationRoutes.STATS_LANGUAGE_PROGRESS) } ) { Column(modifier = Modifier.padding(20.dp)) { Text(text = "LEARNED", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)) diff --git a/app/src/main/java/eu/gaudian/translator/view/library/LibraryScreen.kt b/app/src/main/java/eu/gaudian/translator/view/library/LibraryScreen.kt index 1dc516f..4467348 100644 --- a/app/src/main/java/eu/gaudian/translator/view/library/LibraryScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/library/LibraryScreen.kt @@ -1,4 +1,4 @@ -@file:Suppress("HardCodedStringLiteral") +@file:Suppress("AssignedValueIsNeverRead") package eu.gaudian.translator.view.library @@ -44,6 +44,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -61,8 +62,10 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import eu.gaudian.translator.R +import eu.gaudian.translator.R.string.text_add_new_word_to_list import eu.gaudian.translator.model.VocabularyStage import eu.gaudian.translator.utils.findActivity +import eu.gaudian.translator.view.NavigationRoutes import eu.gaudian.translator.view.composable.AppIcons import eu.gaudian.translator.view.composable.AppSwitch import eu.gaudian.translator.view.composable.BottomSheetMenuItem @@ -70,7 +73,6 @@ 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 @@ -99,8 +101,6 @@ fun LibraryScreen( 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) } @@ -135,8 +135,8 @@ fun LibraryScreen( 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) } + var previousIndex by remember { mutableIntStateOf(0) } + var previousScrollOffset by remember { mutableIntStateOf(0) } // Set navigation context when vocabulary items are loaded LaunchedEffect(vocabularyItems) { @@ -229,10 +229,11 @@ fun LibraryScreen( CategoriesView( categories = categories, onCategoryClick = { category -> - navController.navigate("category_detail/${category.id}") + @Suppress("HardCodedStringLiteral") + navController.navigate("${NavigationRoutes.CATEGORY_DETAIL}/${category.id}") }, onExploreMoreClick = { - navController.navigate("category_list_screen") + navController.navigate(NavigationRoutes.CATEGORY_LIST) } ) }, @@ -251,7 +252,8 @@ fun LibraryScreen( } } else { vocabularyViewModel.setNavigationContext(vocabularyItems, item.id) - navController.navigate("vocabulary_detail/${item.id}") + @Suppress("HardCodedStringLiteral") + navController.navigate("${NavigationRoutes.VOCABULARY_DETAIL}/${item.id}") } }, onItemLongClick = { item -> @@ -287,7 +289,7 @@ fun LibraryScreen( modifier = Modifier.size(50.dp), containerColor = MaterialTheme.colorScheme.surfaceVariant ) { - Icon(AppIcons.ArrowCircleUp, contentDescription = "Scroll to top") + Icon(AppIcons.ArrowCircleUp, contentDescription = stringResource(R.string.cd_scroll_to_top)) } } } @@ -344,17 +346,17 @@ fun LibraryScreen( BottomSheetMenuItem( icon = Icons.Rounded.Add, // Or any custom vector icon you prefer title = stringResource(R.string.label_add_vocabulary), - subtitle = "Füge ein neues Wort zu deiner Liste hinzu", // Suggest adding this to strings.xml + subtitle = stringResource(text_add_new_word_to_list), // Suggest adding this to strings.xml onClick = { showAddMenu = false - navController.navigate("new_word") + navController.navigate(NavigationRoutes.NEW_WORD) } ) BottomSheetMenuItem( icon = Icons.Rounded.Folder, title = stringResource(R.string.label_add_category), - subtitle = "Organisiere deine Vokabeln in Gruppen", // Suggest adding this to strings.xml + subtitle = stringResource(R.string.text_desc_organize_vocabulary_groups), // Suggest adding this to strings.xml onClick = { showAddMenu = false showAddCategoryDialog = true @@ -418,7 +420,7 @@ fun FilterBottomSheetContent( verticalAlignment = Alignment.CenterVertically ) { Text( - text = "Filter Cards", + text = stringResource(R.string.label_filter_cards), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold ) @@ -430,7 +432,7 @@ fun FilterBottomSheetContent( sortOrder = SortOrder.NEWEST_FIRST onResetClick() }) { - Text("Reset") + Text(stringResource(R.string.label_reset)) } } @@ -446,7 +448,7 @@ fun FilterBottomSheetContent( // Sort Order Column { Text( - text = "SORT BY", + text = stringResource(R.string.label_sort_by).uppercase(), style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold, letterSpacing = 1.sp, @@ -589,7 +591,7 @@ fun FilterBottomSheetContent( shape = RoundedCornerShape(28.dp) ) { Text( - text = "Apply Filters", + text = stringResource(R.string.label_apply_filters), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) diff --git a/app/src/main/java/eu/gaudian/translator/view/stats/StatsScreen.kt b/app/src/main/java/eu/gaudian/translator/view/stats/StatsScreen.kt index 8052251..9c01582 100644 --- a/app/src/main/java/eu/gaudian/translator/view/stats/StatsScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/stats/StatsScreen.kt @@ -1,4 +1,4 @@ -@file:Suppress("HardCodedStringLiteral", "AssignedValueIsNeverRead", "UnusedMaterial3ScaffoldPaddingParameter") +@file:Suppress("AssignedValueIsNeverRead") package eu.gaudian.translator.view.stats @@ -56,8 +56,8 @@ 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.NavigationRoutes import eu.gaudian.translator.view.composable.AppCard import eu.gaudian.translator.view.composable.AppIcons import eu.gaudian.translator.view.composable.AppOutlinedCard @@ -82,14 +82,11 @@ import kotlinx.coroutines.launch @SuppressLint("FrequentlyChangingValue") @Composable fun StatsScreen( + modifier: Modifier = Modifier, 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) @@ -128,10 +125,11 @@ fun StatsScreen( } val handleNavigateToCategoryDetail = onNavigateToCategoryDetail ?: { categoryId -> - navController.navigate("stats/category_detail/$categoryId") + @Suppress("HardCodedStringLiteral") + navController.navigate("${NavigationRoutes.STATS_CATEGORY_DETAIL}/$categoryId") } val handleNavigateToCategoryList = onNavigateToCategoryList ?: { - navController.navigate("stats/category_list_screen") + navController.navigate(NavigationRoutes.STATS_CATEGORY_LIST) } AppOutlinedCard(modifier = modifier) { @@ -270,11 +268,8 @@ fun StatsScreen( navController = navController, vocabularyViewModel = vocabularyViewModel, progressViewModel = progressViewModel, - onShowCustomExerciseDialog = onShowCustomExerciseDialog, - startDailyExercise = startDailyExercise, onNavigateToCategoryDetail = handleNavigateToCategoryDetail, onNavigateToCategoryList = handleNavigateToCategoryList, - onShowWordPairExerciseDialog = onShowWordPairExerciseDialog, onMissingLanguage = { missingId -> selectedMissingLanguageId = missingId showMissingLanguageDialog = true @@ -524,17 +519,15 @@ class DragDropState( // Remainder of your existing components // -------------------------------------------------------------------------------- +@Suppress("HardCodedStringLiteral") @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) { @@ -542,10 +535,10 @@ private fun LazyWidget( 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") }, + onNavigateToNew = { navController.navigate("${NavigationRoutes.STATS_VOCABULARY_SORTING}?mode=NEW") }, + onNavigateToDuplicates = { navController.navigate("${NavigationRoutes.STATS_VOCABULARY_SORTING}?mode=DUPLICATES") }, + onNavigateToFaulty = { navController.navigate("${NavigationRoutes.STATS_VOCABULARY_SORTING}?mode=FAULTY") }, + onNavigateToNoGrammar = { navController.navigate(NavigationRoutes.STATS_NO_GRAMMAR_ITEMS) }, onNavigateToMissingLanguage = onMissingLanguage ) @@ -557,7 +550,7 @@ private fun LazyWidget( lastSevenDays = progressViewModel.lastSevenDays.collectAsState().value, dueTodayCount = progressViewModel.dueTodayCount.collectAsState().value, wordsCompleted = progressViewModel.totalWordsCompleted.collectAsState().value, - onStatisticsClicked = { navController.navigate("stats/vocabulary_heatmap") } + onStatisticsClicked = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) } ) WidgetType.WeeklyActivityChart -> WeeklyActivityChartWidget( @@ -566,13 +559,19 @@ private fun LazyWidget( WidgetType.AllVocabulary -> AllVocabularyWidget( vocabularyViewModel = vocabularyViewModel, - onOpenAllVocabulary = { navController.navigate("stats/vocabulary_list/false/null") }, - onStageClicked = { stage -> navController.navigate("stats/vocabulary_list/false/$stage") } + onOpenAllVocabulary = { navController.navigate("${NavigationRoutes.STATS_VOCABULARY_LIST}/false/null") }, + onStageClicked = { stage -> + @Suppress("HardCodedStringLiteral") + navController.navigate("${NavigationRoutes.STATS_VOCABULARY_LIST}/false/$stage") + } ) WidgetType.DueToday -> DueTodayWidget( vocabularyViewModel = vocabularyViewModel, - onStageClicked = { stage -> navController.navigate("stats/vocabulary_list/false/$stage") } + onStageClicked = { stage -> + @Suppress("HardCodedStringLiteral") + navController.navigate("${NavigationRoutes.STATS_VOCABULARY_LIST}/false/$stage") + } ) WidgetType.CategoryProgress -> CategoryProgressWidget( @@ -586,7 +585,7 @@ private fun LazyWidget( totalWords = vocabularyViewModel.vocabularyItems.collectAsState().value.size, learnedWords = vocabularyViewModel.stageStats.collectAsState().value .firstOrNull { it.stage == VocabularyStage.LEARNED }?.itemCount ?: 0, - onNavigateToProgress = { navController.navigate("stats/language_progress") } + onNavigateToProgress = { navController.navigate(NavigationRoutes.STATS_LANGUAGE_PROGRESS) } ) } @@ -649,11 +648,8 @@ fun StatsScreenPreview() { val navController = rememberNavController() StatsScreen( navController = navController, - onShowCustomExerciseDialog = {}, onNavigateToCategoryDetail = {}, - startDailyExercise = {}, onNavigateToCategoryList = {}, - onShowWordPairExerciseDialog = {}, ) } @@ -675,6 +671,7 @@ fun WidgetContainerPreview() { .padding(16.dp), contentAlignment = Alignment.Center ) { + @Suppress("HardCodedStringLiteral") Text("Preview Content") } } diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/CategoryDetailScreen.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/CategoryDetailScreen.kt index 504dd82..9beac63 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/CategoryDetailScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/CategoryDetailScreen.kt @@ -179,7 +179,7 @@ fun CategoryDetailScreen( } ) { paddingValues -> Column(modifier = Modifier.padding(paddingValues)) { - VocabularyListScreen( + AllCardsListScreen( categoryId = categoryId, showDueTodayOnly = false, onNavigateToItem = onNavigateToItem, @@ -324,8 +324,8 @@ fun CategoryHeaderCardWithProgressPreview() { MaterialTheme { CategoryHeaderCard( subtitle = "Travel Vocabulary", - categoryProgress = eu.gaudian.translator.viewmodel.CategoryProgress( - vocabularyCategory = eu.gaudian.translator.model.TagCategory( + categoryProgress = CategoryProgress( + vocabularyCategory = TagCategory( 1, "Travel" ), diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/NoGrammarItemsScreen.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/NoGrammarItemsScreen.kt index 7c82797..29307bd 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/NoGrammarItemsScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/NoGrammarItemsScreen.kt @@ -80,8 +80,8 @@ fun NoGrammarItemsScreen( } } } else { - // Use the generic VocabularyListScreen to display the items - VocabularyListScreen( + // Use the generic AllCardsListScreen to display the items + AllCardsListScreen( itemsToShow = itemsWithoutGrammar, onNavigateToItem = { item -> @Suppress("HardCodedStringLiteral") diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/StageDetailScreen.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/StageDetailScreen.kt index 5697c54..d90a83e 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/StageDetailScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/StageDetailScreen.kt @@ -49,7 +49,7 @@ fun StageDetailScreen( onStageTapped = {}, ) - VocabularyListScreen( + AllCardsListScreen( categoryId = null, showDueTodayOnly = true, stage = stage, diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyListScreen.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyListScreen.kt index 0b550bd..248619b 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyListScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyListScreen.kt @@ -5,19 +5,13 @@ package eu.gaudian.translator.view.vocabulary import android.os.Parcelable import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade -import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -25,14 +19,10 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.CardDefaults import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.FabPosition @@ -43,7 +33,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults @@ -59,20 +48,15 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController -import androidx.navigation.compose.rememberNavController import eu.gaudian.translator.R import eu.gaudian.translator.model.Language import eu.gaudian.translator.model.VocabularyItem @@ -84,10 +68,10 @@ import eu.gaudian.translator.view.composable.AppScaffold import eu.gaudian.translator.view.composable.AppSwitch import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.MultipleLanguageDropdown -import eu.gaudian.translator.view.composable.insertBreakOpportunities import eu.gaudian.translator.view.dialogs.CategoryDropdown import eu.gaudian.translator.view.dialogs.CategorySelectionDialog import eu.gaudian.translator.view.dialogs.StageSelectionDialog +import eu.gaudian.translator.view.library.AllCardsView import eu.gaudian.translator.viewmodel.CategoryViewModel import eu.gaudian.translator.viewmodel.LanguageConfigViewModel import eu.gaudian.translator.viewmodel.LanguageViewModel @@ -110,7 +94,7 @@ private data class VocabularyFilterState( ) : Parcelable @Composable -fun VocabularyListScreen( +fun AllCardsListScreen( categoryId: Int? = null, showDueTodayOnly: Boolean? = null, stage: VocabularyStage? = null, @@ -245,11 +229,7 @@ fun VocabularyListScreen( ) "search" -> SearchTopAppBar( - searchQuery = filterState.searchQuery, - onQueryChanged = { newQuery -> - filterState = filterState.copy(searchQuery = newQuery) - }, - onCloseSearch = { + onCloseSearch = { isSearchActive = false filterState = filterState.copy(searchQuery = "") } @@ -295,78 +275,40 @@ fun VocabularyListScreen( floatingActionButtonPosition = FabPosition.Center ) { paddingValues -> Column(modifier = Modifier.padding(paddingValues).padding(bottom = 0.dp)) { - if (vocabularyItems.isEmpty()) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Image( - modifier = Modifier.size(200.dp), - painter = painterResource(id = R.drawable.ic_nothing_found), - contentDescription = stringResource(id = R.string.no_vocabulary_items_found_perhaps_try_changing_the_filters) - ) - Spacer(modifier = Modifier.size(16.dp)) - - Box(modifier = Modifier + AllCardsView( + vocabularyItems = vocabularyItems, + allLanguages = allLanguages, + selection = selection, + listState = lazyListState, + modifier = Modifier .fillMaxSize() - .padding(8.dp), contentAlignment = Alignment.Center) { - Text( - text = stringResource(R.string.no_vocabulary_items_found_perhaps_try_changing_the_filters), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = androidx.compose.ui.text.style.TextAlign.Center - ) - } - } - } else { - LazyColumn( - state = lazyListState, - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - contentPadding = PaddingValues(vertical = 0.dp) - ) { - items( - items = vocabularyItems, - key = { it.id } - ) { item -> - val isSelected = selection.contains(item.id.toLong()) - VocabularyListItem( - item = item, - allLanguages = allLanguages, - isSelected = isSelected, - onItemClick = { - if (isInSelectionMode) { - selection = if (isSelected) { - selection - item.id.toLong() - } else { - selection + item.id.toLong() - } - } else { - if (navController != null && enableNavigationButtons) { - vocabularyViewModel.setNavigationContext(vocabularyItems, item.id) - navController.navigate("vocabulary_detail/${item.id}") - } else { - onNavigateToItem(item) - } - } - }, - onItemLongClick = { - if (!isInSelectionMode) { - selection = setOf(item.id.toLong()) - } - }, - onDeleteClick = { - vocabularyViewModel.deleteData(VocabularyViewModel.DeleteType.VOCABULARY_ITEM, item = item) - }, - modifier = Modifier.animateItem() - ) + .padding(horizontal = 8.dp), + onItemClick = { item -> + val isSelected = selection.contains(item.id.toLong()) + if (isInSelectionMode) { + selection = if (isSelected) { + selection - item.id.toLong() + } else { + selection + item.id.toLong() + } + } else { + if (navController != null && enableNavigationButtons) { + vocabularyViewModel.setNavigationContext(vocabularyItems, item.id) + navController.navigate("vocabulary_detail/${item.id}") + } else { + onNavigateToItem(item) + } } + }, + onItemLongClick = { item -> + if (!isInSelectionMode) { + selection = setOf(item.id.toLong()) + } + }, + onDeleteClick = { item -> + vocabularyViewModel.deleteData(VocabularyViewModel.DeleteType.VOCABULARY_ITEM, item = item) } - } + ) } @@ -382,8 +324,7 @@ fun VocabularyListScreen( languageViewModel = languageViewModel, languagesPresent = allLanguages.filter { it.nameResId in languagesPresent }, hideCategory = categoryId != null && categoryId != 0, - hideStage = stage != null, - categoryViewModel = categoryViewModel + hideStage = stage != null ) } @@ -417,21 +358,34 @@ fun VocabularyListScreen( } } -@ThemePreviews +@Deprecated("Use AllCardsListScreen which renders AllCardsView") @Composable -fun VocabularyListScreenPreview() { - val navController = rememberNavController() - VocabularyListScreen( - categoryId = 1, - showDueTodayOnly = false, - stage = VocabularyStage.NEW, - onNavigateToItem = {}, - onNavigateBack = {}, - navController = navController +fun VocabularyListScreen( + categoryId: Int? = null, + showDueTodayOnly: Boolean? = null, + stage: VocabularyStage? = null, + onNavigateToItem: (VocabularyItem) -> Unit?, + onNavigateBack: (() -> Unit)? = null, + navController: NavHostController? = null, + itemsToShow: List = emptyList(), + isRemoveFromCategoryEnabled: Boolean = false, + showTopBar: Boolean = true, + enableNavigationButtons: Boolean = false +) { + AllCardsListScreen( + categoryId = categoryId, + showDueTodayOnly = showDueTodayOnly, + stage = stage, + onNavigateToItem = onNavigateToItem, + onNavigateBack = onNavigateBack, + navController = navController, + itemsToShow = itemsToShow, + isRemoveFromCategoryEnabled = isRemoveFromCategoryEnabled, + showTopBar = showTopBar, + enableNavigationButtons = enableNavigationButtons ) } - @Composable private fun DefaultTopAppBar( title: String, @@ -505,8 +459,6 @@ private fun DefaultTopAppBar( @Composable private fun SearchTopAppBar( - searchQuery: String, - onQueryChanged: (String) -> Unit, onCloseSearch: () -> Unit ) { val focusRequester = remember { FocusRequester() } @@ -518,37 +470,6 @@ private fun SearchTopAppBar( AppTopAppBar( modifier = Modifier.height(56.dp), title = "TODO", - additionalContent = { - Box( - modifier = Modifier.fillMaxHeight(), - contentAlignment = Alignment.Center - ) { - BasicTextField( - value = searchQuery, - onValueChange = onQueryChanged, - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - textStyle = MaterialTheme.typography.bodyLarge.copy( - color = MaterialTheme.colorScheme.onSurface - ), - singleLine = true, - cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), - decorationBox = { innerTextField -> - Box(contentAlignment = Alignment.CenterStart) { - if (searchQuery.isEmpty()) { - Text( - text = stringResource(R.string.search_vocabulary), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - innerTextField() - } - } - ) - } - }, navigationIcon = { IconButton(onClick = onCloseSearch) { Icon( @@ -566,8 +487,6 @@ private fun SearchTopAppBar( @Composable fun SearchTopAppBarPreview() { SearchTopAppBar( - searchQuery = stringResource(R.string.search_query), - onQueryChanged = {}, onCloseSearch = {} ) } @@ -670,112 +589,6 @@ fun ContextualTopAppBarPreview() { ) } -@Composable -private fun VocabularyListItem( - item: VocabularyItem, - allLanguages: List, - isSelected: Boolean, - onItemClick: () -> Unit, - onItemLongClick: () -> Unit, - onDeleteClick: () -> Unit, - modifier: Modifier = Modifier -) { - val languageMap = remember(allLanguages) { allLanguages.associateBy { it.nameResId } } - val langFirst = item.languageFirstId?.let { languageMap[it]?.name } ?: "" - val langSecond = item.languageSecondId?.let { languageMap[it]?.name } ?: "" - - OutlinedCard( - modifier = modifier - .fillMaxWidth() - .clip(CardDefaults.shape) - .combinedClickable( - onClick = onItemClick, - onLongClick = onItemLongClick - ) - .animateContentSize(), - elevation = CardDefaults.cardElevation(defaultElevation = if (isSelected) 4.dp else 0.dp), - colors = CardDefaults.cardColors( - containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer - else MaterialTheme.colorScheme.surface - ), - border = BorderStroke( - width = if (isSelected) 1.5.dp else 1.dp, - color = if (isSelected) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.outline.copy(alpha = 0.5f) - ) - ) { - Row( - modifier = Modifier.padding(horizontal = 4.dp, vertical = 6.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(2.dp) - ) { - LanguageRow(word = item.wordFirst, language = langFirst) - HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)) - LanguageRow(word = item.wordSecond, language = langSecond) - } - - Box( - modifier = Modifier.padding(4.dp), - contentAlignment = Alignment.Center - ) { - Crossfade(targetState = isSelected, label = "action-icon-fade") { selected -> - if (selected) { - Icon( - imageVector = AppIcons.Check, - contentDescription = "Selected", - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(24.dp) - ) - } else { - IconButton(onClick = onDeleteClick) { - Icon( - imageVector = AppIcons.Delete, - contentDescription = stringResource(id = R.string.label_delete), - tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), - ) - } - } - } - } - } - } -} - - - -@Composable -private fun LanguageRow(word: String, language: String) { - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 2.dp)) { - Text( - text = insertBreakOpportunities(word), - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.weight(0.7f) - ) - Spacer(Modifier.width(8.dp)) - Text( - text = language, - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - modifier = Modifier.weight(0.3f) - ) - } -} - -@ThemePreviews -@Composable -fun LanguageRowPreview() { - LanguageRow( - word = "Hello", - language = "English" - ) -} - @Composable private fun FilterSortBottomSheet( @@ -785,8 +598,7 @@ private fun FilterSortBottomSheet( onDismiss: () -> Unit, onApplyFilters: (VocabularyFilterState) -> Unit, hideCategory: Boolean = false, - hideStage: Boolean = false, - categoryViewModel: CategoryViewModel + hideStage: Boolean = false ) { var selectedStage by rememberSaveable { mutableStateOf(currentFilterState.selectedStage) } var dueTodayOnly by rememberSaveable { mutableStateOf(currentFilterState.dueTodayOnly) } @@ -931,20 +743,3 @@ private fun FilterSortBottomSheet( } } -@ThemePreviews -@Composable -fun FilterSortBottomSheetPreview() { - val activity = LocalContext.current.findActivity() - val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity) - val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity) - FilterSortBottomSheet( - currentFilterState = VocabularyFilterState(), - languageViewModel = languageViewModel, - languagesPresent = emptyList(), - onDismiss = {}, - onApplyFilters = {}, - hideCategory = false, - hideStage = false, - categoryViewModel = categoryViewModel - ) -} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3fc82ce..8830c01 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1120,4 +1120,22 @@ Stats Library Edit + Total Words + New Words + Expand your vocabulary + Settings + %1$d Days + Current Streak + Daily Goal + No activity data available + See History + Weekly Progress + Go + Apply Filters + Sort By + Reset + Filter Cards + Organize Your Vocabulary in Groups + Extract a New Word to Your List + Scroll to top