From 5fb01ff1ff07efe27a4b10f7dfea98bc012dc3a2 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Thu, 2 May 2024 18:49:05 +0300 Subject: [PATCH] feat: Tablet UI --- .../org/openedx/core/data/api/CourseApi.kt | 2 +- .../AllEnrolledCoursesFragment.kt | 296 +++++++++--------- .../courses/presentation/UserCoursesScreen.kt | 81 +++-- .../learn/presentation/LearnFragment.kt | 66 ++-- 4 files changed, 235 insertions(+), 210 deletions(-) diff --git a/core/src/main/java/org/openedx/core/data/api/CourseApi.kt b/core/src/main/java/org/openedx/core/data/api/CourseApi.kt index 2d48514dc..6d30a9044 100644 --- a/core/src/main/java/org/openedx/core/data/api/CourseApi.kt +++ b/core/src/main/java/org/openedx/core/data/api/CourseApi.kt @@ -72,7 +72,7 @@ interface CourseApi { suspend fun getUserCourses( @Path("username") username: String, @Query("page") page: Int = 1, - @Query("page_size") pageSize: Int = 9, + @Query("page_size") pageSize: Int = 20, @Query("status") status: String? = null, @Query("requested_fields") fields: List = emptyList() ): CourseEnrollments diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt index 79fd645c5..91d99bbfa 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt @@ -62,10 +62,8 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.platform.testTag @@ -202,17 +200,20 @@ private fun AllEnrolledCoursesScreen( onAction: (AllEnrolledCoursesAction) -> Unit ) { val windowSize = rememberWindowSize() + val layoutDirection = LocalLayoutDirection.current val scaffoldState = rememberScaffoldState() + val scrollState = rememberLazyGridState() + val columns = if (windowSize.isTablet) 3 else 2 val pullRefreshState = rememberPullRefreshState( refreshing = refreshing, onRefresh = { onAction(AllEnrolledCoursesAction.SwipeRefresh) } ) - val tabPagerState = rememberPagerState(pageCount = { CourseStatusFilter.entries.size }) - + val tabPagerState = rememberPagerState(pageCount = { + CourseStatusFilter.entries.size + }) var isInternetConnectionShown by rememberSaveable { mutableStateOf(false) } - val scrollState = rememberLazyGridState() val firstVisibleIndex = remember { mutableIntStateOf(scrollState.firstVisibleItemIndex) } @@ -226,20 +227,28 @@ private fun AllEnrolledCoursesScreen( }, backgroundColor = MaterialTheme.appColors.background ) { paddingValues -> - - val layoutDirection = LocalLayoutDirection.current val contentPaddings by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( expanded = PaddingValues( - top = 32.dp, - bottom = 40.dp + top = 16.dp, + bottom = 40.dp, ), compact = PaddingValues(horizontal = 16.dp, vertical = 16.dp) ) ) } + val roundTapBarPaddings by remember(key1 = windowSize) { + mutableStateOf( + windowSize.windowSizeValue( + expanded = PaddingValues(vertical = 6.dp), + compact = PaddingValues(horizontal = 16.dp, vertical = 6.dp) + ) + ) + } + + val emptyStatePaddings by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( @@ -255,174 +264,167 @@ private fun AllEnrolledCoursesScreen( val contentWidth by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( - expanded = Modifier.widthIn(Dp.Unspecified, 560.dp), + expanded = Modifier.widthIn(Dp.Unspecified, 650.dp), compact = Modifier.fillMaxWidth(), ) ) } HandleUIMessage(uiMessage = uiMessage, scaffoldState = scaffoldState) - - Column( + Box( modifier = Modifier - .padding(paddingValues) - .statusBarsInset() - .displayCutoutForLandscape(), - horizontalAlignment = Alignment.CenterHorizontally + .fillMaxSize() + .padding(paddingValues), + contentAlignment = Alignment.TopCenter ) { - BackBtn( - modifier = Modifier.align(Alignment.Start), - tint = MaterialTheme.appColors.textDark + Column( + modifier = Modifier + .statusBarsInset() + .displayCutoutForLandscape() + .then(contentWidth), + horizontalAlignment = Alignment.CenterHorizontally ) { - onAction(AllEnrolledCoursesAction.Back) - } + BackBtn( + modifier = Modifier.align(Alignment.Start), + tint = MaterialTheme.appColors.textDark + ) { + onAction(AllEnrolledCoursesAction.Back) + } - Surface( - color = MaterialTheme.appColors.background, - shape = MaterialTheme.appShapes.screenBackgroundShape - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .navigationBarsPadding() - .pullRefresh(pullRefreshState), + Surface( + color = MaterialTheme.appColors.background, + shape = MaterialTheme.appShapes.screenBackgroundShape ) { - Column( + Box( modifier = Modifier - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally + .fillMaxWidth() + .navigationBarsPadding() + .pullRefresh(pullRefreshState), ) { - Header( + Column( modifier = Modifier - .padding( - start = contentPaddings.calculateStartPadding(layoutDirection), - end = contentPaddings.calculateEndPadding(layoutDirection) - ), - onSearchClick = { - onAction(AllEnrolledCoursesAction.Search) - } - ) - RoundTabsBar( - items = CourseStatusFilter.entries, - contentPadding = PaddingValues( - start = 16.dp, - end = 16.dp, - top = 8.dp, - bottom = 4.dp - ), - rowState = rememberLazyListState(), - pagerState = tabPagerState, - onTabClicked = { - val newFilter = CourseStatusFilter.entries[it] - onAction(AllEnrolledCoursesAction.FilterChange(newFilter)) - } - ) - when (state) { - is AllEnrolledCoursesUIState.Loading -> { - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator(color = MaterialTheme.appColors.primary) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Header( + modifier = Modifier + .padding( + start = contentPaddings.calculateStartPadding(layoutDirection), + end = contentPaddings.calculateEndPadding(layoutDirection) + ), + onSearchClick = { + onAction(AllEnrolledCoursesAction.Search) } - } - - is AllEnrolledCoursesUIState.Courses -> { - val density = LocalDensity.current - var itemHeight by rememberSaveable { - mutableStateOf(0f) + ) + RoundTabsBar( + modifier = Modifier.align(Alignment.Start), + items = CourseStatusFilter.entries, + contentPadding = roundTapBarPaddings, + rowState = rememberLazyListState(), + pagerState = tabPagerState, + onTabClicked = { + val newFilter = CourseStatusFilter.entries[it] + onAction(AllEnrolledCoursesAction.FilterChange(newFilter)) } - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column( + ) + when (state) { + is AllEnrolledCoursesUIState.Loading -> { + Box( modifier = Modifier - .fillMaxWidth() - .padding(contentPaddings) + .fillMaxSize(), + contentAlignment = Alignment.Center ) { - LazyVerticalGrid( - modifier = Modifier - .fillMaxHeight() - .then(contentWidth), - state = scrollState, - columns = GridCells.Fixed(2), - verticalArrangement = Arrangement.spacedBy(12.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - content = { - items(state.courses) { course -> - CourseItem( - modifier = Modifier - .onSizeChanged { - itemHeight = it.height.toFloat() - }, - course = course, - apiHostUrl = apiHostUrl, - onClick = { - onAction(AllEnrolledCoursesAction.OpenCourse(it)) - } - ) - } - item { - if (canLoadMore) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(with(density) { itemHeight.toDp() }), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator( - color = MaterialTheme.appColors.primary - ) + CircularProgressIndicator(color = MaterialTheme.appColors.primary) + } + } + + is AllEnrolledCoursesUIState.Courses -> { + Box( + modifier = Modifier + .fillMaxSize() + .padding(contentPaddings), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + LazyVerticalGrid( + modifier = Modifier + .fillMaxHeight(), + state = scrollState, + columns = GridCells.Fixed(columns), + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + content = { + items(state.courses) { course -> + CourseItem( + course = course, + apiHostUrl = apiHostUrl, + onClick = { + onAction(AllEnrolledCoursesAction.OpenCourse(it)) + } + ) + } + item { + if (canLoadMore) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(180.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + color = MaterialTheme.appColors.primary + ) + } } } } - } - ) - } - if (scrollState.shouldLoadMore(firstVisibleIndex, 4)) { - onAction(AllEnrolledCoursesAction.EndOfPage) + ) + } + if (scrollState.shouldLoadMore(firstVisibleIndex, 4)) { + onAction(AllEnrolledCoursesAction.EndOfPage) + } } } - } - is AllEnrolledCoursesUIState.Empty -> { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column( - modifier = Modifier - .fillMaxHeight() - .then(contentWidth) - .then(emptyStatePaddings) + is AllEnrolledCoursesUIState.Empty -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center ) { - EmptyState() + Column( + modifier = Modifier + .fillMaxHeight() + .then(emptyStatePaddings) + ) { + EmptyState() + } } } } } - } - PullRefreshIndicator( - refreshing, - pullRefreshState, - Modifier.align(Alignment.TopCenter) - ) - - if (!isInternetConnectionShown && !hasInternetConnection) { - OfflineModeDialog( - Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter), - onDismissCLick = { - isInternetConnectionShown = true - }, - onReloadClick = { - isInternetConnectionShown = true - onAction(AllEnrolledCoursesAction.Reload) - } + PullRefreshIndicator( + refreshing, + pullRefreshState, + Modifier.align(Alignment.TopCenter) ) + + if (!isInternetConnectionShown && !hasInternetConnection) { + OfflineModeDialog( + Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter), + onDismissCLick = { + isInternetConnectionShown = true + }, + onReloadClick = { + isInternetConnectionShown = true + onAction(AllEnrolledCoursesAction.Reload) + } + ) + } } } } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt index 4b750c87f..7685f8b16 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt @@ -1,10 +1,12 @@ package org.openedx.courses.presentation +import android.content.res.Configuration 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.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight @@ -15,8 +17,9 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid +import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll @@ -57,6 +60,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentManager @@ -79,6 +83,7 @@ import org.openedx.core.presentation.course.CourseContainerTab import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.OfflineModeDialog import org.openedx.core.ui.TextIcon +import org.openedx.core.ui.rememberWindowSize import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes @@ -288,37 +293,47 @@ private fun SecondaryCourses( onCourseClick: (EnrolledCourse) -> Unit, onViewAllClick: () -> Unit ) { - val items = courses.take(5) + val windowSize = rememberWindowSize() + val itemsCount = if (windowSize.isTablet) 7 else 5 + val rows = if (windowSize.isTablet) 2 else 1 + val height = if (windowSize.isTablet) 322.dp else 152.dp + val items = courses.take(itemsCount) Column( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 14.dp) + .fillMaxSize() .padding(top = 12.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { TextIcon( + modifier = Modifier.padding(horizontal = 18.dp), text = stringResource(R.string.dashboard_view_all_with_count, courses.size), textStyle = MaterialTheme.appTypography.titleSmall, icon = Icons.Default.ChevronRight, color = MaterialTheme.appColors.textDark, - modifier = Modifier.padding(horizontal = 4.dp), iconModifier = Modifier.size(22.dp), onClick = onViewAllClick ) - LazyRow { - items(items) { - CourseListItem( - course = it, - apiHostUrl = apiHostUrl, - onCourseClick = onCourseClick - ) - } - item { - ViewAllItem( - onViewAllClick = onViewAllClick - ) + LazyHorizontalGrid( + modifier = Modifier + .fillMaxSize() + .height(height), + rows = GridCells.Fixed(rows), + contentPadding = PaddingValues(horizontal = 18.dp), + content = { + items(items) { + CourseListItem( + course = it, + apiHostUrl = apiHostUrl, + onCourseClick = onCourseClick + ) + } + item { + ViewAllItem( + onViewAllClick = onViewAllClick + ) + } } - } + ) } } @@ -715,7 +730,7 @@ private val mockCourse = EnrolledCourse( courseAssignments = mockCourseAssignments, course = EnrolledCourseData( id = "id", - name = "Course name", + name = "Looooooooooooooooooooong Course name", number = "", org = "Org", start = Date(), @@ -753,7 +768,21 @@ private val mockUserCourses = UserCourses( primary = mockCourse ) -@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ViewAllItemPreview() { + OpenEdXTheme { + ViewAllItem( + onViewAllClick = {} + ) + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, device = Devices.NEXUS_9) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, device = Devices.NEXUS_9) @Composable private fun UsersCourseScreenPreview() { OpenEdXTheme { @@ -767,13 +796,3 @@ private fun UsersCourseScreenPreview() { ) } } - -@Preview -@Composable -private fun ViewAllItemPreview() { - OpenEdXTheme { - ViewAllItem( - onViewAllClick = {} - ) - } -} diff --git a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt index 40f8f6d76..5720e6922 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt @@ -95,7 +95,7 @@ private fun LearnScreen( val contentWidth by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( - expanded = Modifier.widthIn(Dp.Unspecified, 560.dp), + expanded = Modifier.widthIn(Dp.Unspecified, 650.dp), compact = Modifier.fillMaxSize(), ) ) @@ -106,43 +106,47 @@ private fun LearnScreen( modifier = Modifier.fillMaxSize(), backgroundColor = MaterialTheme.appColors.background ) { paddingValues -> - - Column( - modifier = Modifier - .padding(paddingValues) - .statusBarsInset() - .displayCutoutForLandscape() - .then(contentWidth), - horizontalAlignment = Alignment.CenterHorizontally + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center ) { - Header( + Column( modifier = Modifier - .padding(horizontal = 16.dp), - label = stringResource(id = R.string.dashboard_learn), - onSearchClick = { - viewModel.onSearchClick(fragmentManager) - } - ) - - if (viewModel.isProgramTypeWebView) { - LearnDropdownMenu( + .padding(paddingValues) + .statusBarsInset() + .displayCutoutForLandscape() + .then(contentWidth), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Header( modifier = Modifier - .align(Alignment.Start) .padding(horizontal = 16.dp), - pagerState = pagerState + label = stringResource(id = R.string.dashboard_learn), + onSearchClick = { + viewModel.onSearchClick(fragmentManager) + } ) - } - HorizontalPager( - modifier = Modifier - .fillMaxSize(), - state = pagerState, - userScrollEnabled = false - ) { page -> - when (page) { - 0 -> UsersCourseScreen(fragmentManager = fragmentManager) + if (viewModel.isProgramTypeWebView) { + LearnDropdownMenu( + modifier = Modifier + .align(Alignment.Start) + .padding(horizontal = 16.dp), + pagerState = pagerState + ) + } - 1 -> InDevelopmentScreen() + HorizontalPager( + modifier = Modifier + .fillMaxSize(), + state = pagerState, + userScrollEnabled = false + ) { page -> + when (page) { + 0 -> UsersCourseScreen(fragmentManager = fragmentManager) + + 1 -> InDevelopmentScreen() + } } } }