From d9d0f9538c2a2f68b23eff61880884c277a99e8f Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Thu, 11 Apr 2024 19:28:25 +0300 Subject: [PATCH 01/23] feat: Created Learn screen. Added course/program navigation. Added endpoint for UserCourses screen. --- .../main/java/org/openedx/app/MainFragment.kt | 22 +- .../java/org/openedx/app/di/ScreenModule.kt | 2 + app/src/main/res/drawable/app_ic_rows.xml | 44 +-- app/src/main/res/menu/bottom_view_menu.xml | 16 +- app/src/main/res/values-uk/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- core/build.gradle | 1 + .../org/openedx/core/data/api/CourseApi.kt | 5 + .../org/openedx/core/ui/ComposeExtensions.kt | 17 ++ .../java/org/openedx/core/ui/theme/Type.kt | 9 +- .../courses/presentation/UserCoursesScreen.kt | 125 ++++++++ .../presentation/UserCoursesUIState.kt | 9 + .../presentation/UserCoursesViewModel.kt | 67 +++++ .../data/repository/DashboardRepository.kt | 13 + .../domain/interactor/DashboardInteractor.kt | 2 + .../main/java/org/openedx/learn/LearnType.kt | 9 + .../learn/presentation/LearnFragment.kt | 266 ++++++++++++++++++ .../programs/presentation/ProgramsScreen.kt | 29 ++ dashboard/src/main/res/values/strings.xml | 2 + 19 files changed, 576 insertions(+), 66 deletions(-) create mode 100644 dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt create mode 100644 dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesUIState.kt create mode 100644 dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt create mode 100644 dashboard/src/main/java/org/openedx/learn/LearnType.kt create mode 100644 dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt create mode 100644 dashboard/src/main/java/org/openedx/programs/presentation/ProgramsScreen.kt diff --git a/app/src/main/java/org/openedx/app/MainFragment.kt b/app/src/main/java/org/openedx/app/MainFragment.kt index a798c4a3f..829cf43a0 100644 --- a/app/src/main/java/org/openedx/app/MainFragment.kt +++ b/app/src/main/java/org/openedx/app/MainFragment.kt @@ -17,6 +17,7 @@ import org.openedx.core.config.Config import org.openedx.core.presentation.global.app_upgrade.UpgradeRequiredFragment import org.openedx.core.presentation.global.viewBinding import org.openedx.dashboard.presentation.DashboardFragment +import org.openedx.learn.presentation.LearnFragment import org.openedx.discovery.presentation.DiscoveryNavigator import org.openedx.discovery.presentation.DiscoveryRouter import org.openedx.discovery.presentation.program.ProgramFragment @@ -47,19 +48,14 @@ class MainFragment : Fragment(R.layout.fragment_main) { binding.bottomNavView.setOnItemSelectedListener { when (it.itemId) { - R.id.fragmentHome -> { - viewModel.logDiscoveryTabClickedEvent() - binding.viewPager.setCurrentItem(0, false) - } - - R.id.fragmentDashboard -> { + R.id.fragmentLearn -> { viewModel.logMyCoursesTabClickedEvent() binding.viewPager.setCurrentItem(1, false) } - R.id.fragmentPrograms -> { - viewModel.logMyProgramsTabClickedEvent() - binding.viewPager.setCurrentItem(2, false) + R.id.fragmentHome -> { + viewModel.logDiscoveryTabClickedEvent() + binding.viewPager.setCurrentItem(0, false) } R.id.fragmentProfile -> { @@ -107,16 +103,10 @@ class MainFragment : Fragment(R.layout.fragment_main) { val discoveryFragment = DiscoveryNavigator(viewModel.isDiscoveryTypeWebView) .getDiscoveryFragment() - val programFragment = if (viewModel.isProgramTypeWebView) { - ProgramFragment(true) - } else { - InDevelopmentFragment() - } adapter = MainNavigationFragmentAdapter(this).apply { addFragment(discoveryFragment) - addFragment(DashboardFragment()) - addFragment(programFragment) + addFragment(LearnFragment()) addFragment(ProfileFragment()) } binding.viewPager.adapter = adapter diff --git a/app/src/main/java/org/openedx/app/di/ScreenModule.kt b/app/src/main/java/org/openedx/app/di/ScreenModule.kt index 51e3a28f1..35248cc14 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -29,6 +29,7 @@ import org.openedx.course.presentation.unit.video.VideoUnitViewModel import org.openedx.course.presentation.unit.video.VideoViewModel import org.openedx.course.presentation.videos.CourseVideoViewModel import org.openedx.course.settings.download.DownloadQueueViewModel +import org.openedx.courses.presentation.UserCoursesViewModel import org.openedx.dashboard.data.repository.DashboardRepository import org.openedx.dashboard.domain.interactor.DashboardInteractor import org.openedx.dashboard.presentation.DashboardViewModel @@ -115,6 +116,7 @@ val screenModule = module { factory { DashboardRepository(get(), get(), get()) } factory { DashboardInteractor(get()) } viewModel { DashboardViewModel(get(), get(), get(), get(), get(), get(), get()) } + viewModel { UserCoursesViewModel(get(), get(), get()) } factory { DiscoveryRepository(get(), get(), get()) } factory { DiscoveryInteractor(get()) } diff --git a/app/src/main/res/drawable/app_ic_rows.xml b/app/src/main/res/drawable/app_ic_rows.xml index 41b74e9b4..e068a37a6 100644 --- a/app/src/main/res/drawable/app_ic_rows.xml +++ b/app/src/main/res/drawable/app_ic_rows.xml @@ -1,38 +1,10 @@ - - - - - - - + android:width="20dp" + android:height="17dp" + android:viewportWidth="20" + android:viewportHeight="17"> + diff --git a/app/src/main/res/menu/bottom_view_menu.xml b/app/src/main/res/menu/bottom_view_menu.xml index 60ba4f78c..6285572db 100644 --- a/app/src/main/res/menu/bottom_view_menu.xml +++ b/app/src/main/res/menu/bottom_view_menu.xml @@ -2,22 +2,16 @@ - - + android:icon="@drawable/app_ic_home"/> Назад Всі курси - Мої курси + Мої курси Програми Профіль \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f24815f30..df82406af 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,7 +4,7 @@ Previous Discover - Dashboard + Learn Programs Profile \ No newline at end of file diff --git a/core/build.gradle b/core/build.gradle index 8c4bdcc6f..0cf01d6e9 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -139,6 +139,7 @@ dependencies { // Koin DI api "io.insert-koin:koin-core:$koin_version" api "io.insert-koin:koin-android:$koin_version" + api("io.insert-koin:koin-androidx-compose:$koin_version") api "io.coil-kt:coil-compose:$coil_version" api "io.coil-kt:coil-gif:$coil_version" 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 4a19c383d..68eadeab4 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 @@ -67,4 +67,9 @@ interface CourseApi { @GET("/api/mobile/v1/course_info/{course_id}/updates") suspend fun getAnnouncements(@Path("course_id") courseId: String): List + + @GET("/api/mobile/v4/users/{username}/course_enrollments/") + suspend fun getUserCourses( + @Path("username") username: String + ): CourseEnrollments } diff --git a/core/src/main/java/org/openedx/core/ui/ComposeExtensions.kt b/core/src/main/java/org/openedx/core/ui/ComposeExtensions.kt index 0cfa9c57c..f408ea60c 100644 --- a/core/src/main/java/org/openedx/core/ui/ComposeExtensions.kt +++ b/core/src/main/java/org/openedx/core/ui/ComposeExtensions.kt @@ -27,12 +27,14 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalView import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.launch @@ -123,6 +125,21 @@ fun Modifier.roundBorderWithoutBottom(borderWidth: Dp, cornerRadius: Dp): Modifi } ) +fun Modifier.crop( + horizontal: Dp = 0.dp, + vertical: Dp = 0.dp, +): Modifier = this.layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + fun Dp.toPxInt(): Int = this.toPx().toInt() + + layout( + placeable.width - (horizontal * 2).toPxInt(), + placeable.height - (vertical * 2).toPxInt() + ) { + placeable.placeRelative(-horizontal.toPx().toInt(), -vertical.toPx().toInt()) + } +} + @Composable fun rememberSaveableMap(init: () -> MutableMap): MutableMap { return rememberSaveable( diff --git a/core/src/main/java/org/openedx/core/ui/theme/Type.kt b/core/src/main/java/org/openedx/core/ui/theme/Type.kt index edd2afcc7..88371c284 100644 --- a/core/src/main/java/org/openedx/core/ui/theme/Type.kt +++ b/core/src/main/java/org/openedx/core/ui/theme/Type.kt @@ -17,6 +17,7 @@ data class AppTypography( val displayLarge: TextStyle, val displayMedium: TextStyle, val displaySmall: TextStyle, + val headlineBolt: TextStyle, val headlineLarge: TextStyle, val headlineMedium: TextStyle, val headlineSmall: TextStyle, @@ -43,7 +44,6 @@ val fontFamily = FontFamily( Font(R.font.thin, FontWeight.Thin, FontStyle.Normal), ) - internal val LocalTypography = staticCompositionLocalOf { AppTypography( displayLarge = TextStyle( @@ -74,6 +74,13 @@ internal val LocalTypography = staticCompositionLocalOf { letterSpacing = 0.sp, fontFamily = fontFamily ), + headlineBolt = TextStyle( + fontSize = 34.sp, + lineHeight = 24.sp, + fontWeight = FontWeight.Bold, + letterSpacing = 0.sp, + fontFamily = fontFamily + ), headlineMedium = TextStyle( fontSize = 28.sp, lineHeight = 36.sp, diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt new file mode 100644 index 000000000..c7a0ab137 --- /dev/null +++ b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt @@ -0,0 +1,125 @@ +package org.openedx.courses.presentation + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import org.openedx.core.domain.model.EnrolledCourse +import org.openedx.core.ui.HandleUIMessage +import org.openedx.core.ui.theme.appColors +import org.openedx.core.ui.theme.appTypography +import org.openedx.dashboard.R + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun UsersCourseScreen( + viewModel: UserCoursesViewModel, + onItemClick: (EnrolledCourse) -> Unit, +) { + val updating by viewModel.updating.observeAsState(false) + val uiMessage by viewModel.uiMessage.collectAsState(null) + val uiState by viewModel.uiState.observeAsState(UserCoursesUIState.Loading) + val scaffoldState = rememberScaffoldState() + val pullRefreshState = rememberPullRefreshState(refreshing = updating, onRefresh = { viewModel.updateCoursed() }) + val scrollState = rememberLazyListState() + + Scaffold( + scaffoldState = scaffoldState, + modifier = Modifier.fillMaxSize(), + backgroundColor = MaterialTheme.appColors.background + ) { paddingValues -> + HandleUIMessage(uiMessage = uiMessage, scaffoldState = scaffoldState) + + Surface( + modifier = Modifier.padding(paddingValues), + color = MaterialTheme.appColors.background + ) { + Box( + Modifier + .fillMaxSize() + .pullRefresh(pullRefreshState), + ) { + when (uiState) { + is UserCoursesUIState.Loading -> { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.appColors.primary + ) + } + + is UserCoursesUIState.Courses -> { + + } + + is UserCoursesUIState.Empty -> { + EmptyState() + } + } + + PullRefreshIndicator( + updating, + pullRefreshState, + Modifier.align(Alignment.TopCenter) + ) + } + } + } +} + +@Composable +private fun EmptyState() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + Modifier.width(185.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + painter = painterResource(id = R.drawable.dashboard_ic_empty), + contentDescription = null, + tint = MaterialTheme.appColors.textFieldBorder + ) + Spacer(Modifier.height(16.dp)) + Text( + modifier = Modifier + .testTag("txt_empty_state_description") + .fillMaxWidth(), + text = stringResource(id = R.string.dashboard_you_are_not_enrolled), + color = MaterialTheme.appColors.textPrimaryVariant, + style = MaterialTheme.appTypography.bodySmall, + textAlign = TextAlign.Center + ) + } + } +} \ No newline at end of file diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesUIState.kt b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesUIState.kt new file mode 100644 index 000000000..df21ce75f --- /dev/null +++ b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesUIState.kt @@ -0,0 +1,9 @@ +package org.openedx.courses.presentation + +import org.openedx.core.domain.model.EnrolledCourse + +sealed class UserCoursesUIState { + data class Courses(val courses: List) : UserCoursesUIState() + object Empty : UserCoursesUIState() + object Loading : UserCoursesUIState() +} diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt new file mode 100644 index 000000000..ce6532cdc --- /dev/null +++ b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt @@ -0,0 +1,67 @@ +package org.openedx.courses.presentation + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import org.openedx.core.BaseViewModel +import org.openedx.core.R +import org.openedx.core.UIMessage +import org.openedx.core.config.Config +import org.openedx.core.extension.isInternetError +import org.openedx.core.system.ResourceManager +import org.openedx.dashboard.domain.interactor.DashboardInteractor + +class UserCoursesViewModel( + private val config: Config, + private val interactor: DashboardInteractor, + private val resourceManager: ResourceManager, +) : BaseViewModel() { + + val apiHostUrl get() = config.getApiHostURL() + + private val _uiState = MutableLiveData(UserCoursesUIState.Loading) + val uiState: LiveData + get() = _uiState + + private val _uiMessage = MutableStateFlow(null) + val uiMessage: SharedFlow + get() = _uiMessage.asStateFlow() + + private val _updating = MutableLiveData() + val updating: LiveData + get() = _updating + + init { + getCourses() + } + + private fun getCourses() { + viewModelScope.launch { + try { + val response = interactor.getUserCourses() + if (response.courses.isEmpty()) { + _uiState.value = UserCoursesUIState.Empty + } else { + _uiState.value = UserCoursesUIState.Courses(response.courses) + } + } catch (e: Exception) { + if (e.isInternetError()) { + _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection))) + } else { + _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error))) + } + } finally { + _updating.value = false + } + } + } + + fun updateCoursed() { + _updating.value = true + getCourses() + } +} diff --git a/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt b/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt index c85390fa1..9642b5633 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt @@ -30,4 +30,17 @@ class DashboardRepository( val list = dao.readAllData() return list.map { it.mapToDomain() } } + + suspend fun getUserCourses(): DashboardCourseList { + val user = preferencesManager.user + val result = api.getUserCourses( + username = user?.username ?: "" + ) + preferencesManager.appConfig = result.configs.mapToDomain() + + dao.clearCachedData() + dao.insertEnrolledCourseEntity(*result.enrollments.results.map { it.mapToRoomEntity() } + .toTypedArray()) + return result.enrollments.mapToDomain() + } } diff --git a/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt b/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt index a29c2cc7e..a491190ab 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt @@ -12,4 +12,6 @@ class DashboardInteractor( } suspend fun getEnrolledCoursesFromCache() = repository.getEnrolledCoursesFromCache() + + suspend fun getUserCourses() = repository.getUserCourses() } \ No newline at end of file diff --git a/dashboard/src/main/java/org/openedx/learn/LearnType.kt b/dashboard/src/main/java/org/openedx/learn/LearnType.kt new file mode 100644 index 000000000..f9cd26b36 --- /dev/null +++ b/dashboard/src/main/java/org/openedx/learn/LearnType.kt @@ -0,0 +1,9 @@ +package org.openedx.learn + +import androidx.annotation.StringRes +import org.openedx.dashboard.R + +enum class LearnType(@StringRes val title: Int) { + COURSES(R.string.dashboard_courses), + PROGRAMS(R.string.dashboard_programs) +} \ No newline at end of file diff --git a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt new file mode 100644 index 000000000..7b6a68d9f --- /dev/null +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt @@ -0,0 +1,266 @@ +package org.openedx.learn.presentation + +import android.content.res.Configuration.UI_MODE_NIGHT_NO +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.graphics.Color +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.fragment.app.Fragment +import androidx.lifecycle.viewmodel.compose.viewModel +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.openedx.core.ui.WindowSize +import org.openedx.core.ui.WindowType +import org.openedx.core.ui.crop +import org.openedx.core.ui.displayCutoutForLandscape +import org.openedx.core.ui.noRippleClickable +import org.openedx.core.ui.rememberWindowSize +import org.openedx.core.ui.statusBarsInset +import org.openedx.core.ui.theme.OpenEdXTheme +import org.openedx.core.ui.theme.appColors +import org.openedx.core.ui.theme.appTypography +import org.openedx.core.ui.windowSizeValue +import org.openedx.courses.presentation.UserCoursesViewModel +import org.openedx.courses.presentation.UsersCourseScreen +import org.openedx.dashboard.R +import org.openedx.learn.LearnType +import org.openedx.programs.presentation.ProgramsScreen + +class LearnFragment : Fragment() { + + private val userCoursesViewModel by viewModel() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ) = ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + OpenEdXTheme { + val windowSize = rememberWindowSize() + LearnScreen( + windowSize = windowSize, + userCoursesViewModel = userCoursesViewModel + ) + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun LearnScreen( + userCoursesViewModel : UserCoursesViewModel = viewModel(), + windowSize: WindowSize +) { + val scaffoldState = rememberScaffoldState() + val pagerState = rememberPagerState { + LearnType.entries.size + } + + Scaffold( + scaffoldState = scaffoldState, + modifier = Modifier + .fillMaxSize(), + backgroundColor = MaterialTheme.appColors.background + ) { paddingValues -> + val contentWidth by remember(key1 = windowSize) { + mutableStateOf( + windowSize.windowSizeValue( + expanded = Modifier.widthIn(Dp.Unspecified, 560.dp), + compact = Modifier.fillMaxWidth(), + ) + ) + } + + Column( + modifier = Modifier + .padding(paddingValues) + .padding(horizontal = 16.dp) + .statusBarsInset() + .displayCutoutForLandscape(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + LearnToolbar( + label = stringResource(id = R.string.dashboard_learn), + onSearchClick = { + //TODO + } + ) + + LearnDropdownMenu( + modifier = Modifier.align(Alignment.Start), + pagerState = pagerState + ) + + HorizontalPager( + modifier = Modifier + .fillMaxHeight() + .then(contentWidth), + state = pagerState, + userScrollEnabled = false + ) { page -> + when (page) { + 0 -> UsersCourseScreen( + viewModel = userCoursesViewModel, + onItemClick = {}, + ) + + 1 -> ProgramsScreen() + } + } + } + } +} + +@Composable +private fun LearnToolbar( + label: String, + onSearchClick: () -> Unit +) { + Box( + modifier = Modifier.fillMaxWidth() + ) { + Text( + modifier = Modifier.align(Alignment.CenterStart), + text = label, + color = MaterialTheme.appColors.textDark, + style = MaterialTheme.appTypography.headlineBolt + ) + IconButton( + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(start = 16.dp), + onClick = { + onSearchClick() + } + ) { + Icon( + imageVector = Icons.Filled.Search, + contentDescription = null, + tint = MaterialTheme.appColors.textDark + ) + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun LearnDropdownMenu( + modifier: Modifier = Modifier, + pagerState: PagerState +) { + var expanded by remember { mutableStateOf(false) } + var currentValue by remember { mutableStateOf(LearnType.COURSES) } + + LaunchedEffect(currentValue) { + pagerState.scrollToPage( + when (currentValue) { + LearnType.COURSES -> 0 + LearnType.PROGRAMS -> 1 + } + ) + } + + Column( + modifier = modifier + ) { + Row( + modifier = Modifier.noRippleClickable { + expanded = true + } + ) { + Text( + text = stringResource(id = currentValue.title), + color = MaterialTheme.appColors.textDark, + ) + Icon( + imageVector = Icons.Default.ExpandMore, + tint = MaterialTheme.appColors.textDark, + contentDescription = null + ) + } + + MaterialTheme( + colors = MaterialTheme.colors.copy(surface = MaterialTheme.appColors.background), + shapes = MaterialTheme.shapes.copy(medium = RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp)) + ) { + DropdownMenu( + modifier = Modifier + .crop(vertical = 8.dp) + .widthIn(min = 182.dp), + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + for (learnType in LearnType.entries) { + DropdownMenuItem( + modifier = Modifier + .background( + if (currentValue == learnType) MaterialTheme.appColors.primary else Color.Transparent + ), + onClick = { + currentValue = learnType + expanded = false + } + ) { + Text(stringResource(id = learnType.title)) + } + } + } + } + } +} + +@Preview(uiMode = UI_MODE_NIGHT_NO) +@Preview(uiMode = UI_MODE_NIGHT_YES) +@Preview(uiMode = UI_MODE_NIGHT_NO, device = Devices.NEXUS_9) +@Preview(uiMode = UI_MODE_NIGHT_YES, device = Devices.NEXUS_9) +@Composable +private fun LearnScreenPreview() { + OpenEdXTheme { + LearnScreen( + windowSize = WindowSize(WindowType.Compact, WindowType.Compact) + ) + } +} diff --git a/dashboard/src/main/java/org/openedx/programs/presentation/ProgramsScreen.kt b/dashboard/src/main/java/org/openedx/programs/presentation/ProgramsScreen.kt new file mode 100644 index 000000000..199a07f8f --- /dev/null +++ b/dashboard/src/main/java/org/openedx/programs/presentation/ProgramsScreen.kt @@ -0,0 +1,29 @@ +package org.openedx.programs.presentation + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import org.openedx.core.ui.theme.appColors +import org.openedx.core.ui.theme.appTypography + +@Composable +fun ProgramsScreen() { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.appColors.secondary), + contentAlignment = Alignment.Center + ) { + Text( + modifier = Modifier.testTag("txt_in_development"), + text = "Will be available soon", + style = MaterialTheme.appTypography.headlineMedium + ) + } +} \ No newline at end of file diff --git a/dashboard/src/main/res/values/strings.xml b/dashboard/src/main/res/values/strings.xml index 583851adc..393f0178b 100644 --- a/dashboard/src/main/res/values/strings.xml +++ b/dashboard/src/main/res/values/strings.xml @@ -4,4 +4,6 @@ Courses Welcome back. Let\'s keep learning. You are not enrolled in any courses yet. + Learn + Programs From 1ecc961f614313ea323c6eccea148420eb5c06ee Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Fri, 12 Apr 2024 15:21:46 +0300 Subject: [PATCH 02/23] feat: Added primary course card --- .../core/data/model/CourseEnrollments.kt | 17 +- .../openedx/core/data/model/EnrolledCourse.kt | 11 +- .../org/openedx/core/data/model/Progress.kt | 23 ++ .../room/discovery/EnrolledCourseEntity.kt | 24 +- .../core/domain/model/EnrolledCourse.kt | 1 + .../org/openedx/core/domain/model/Progress.kt | 14 ++ .../course/presentation/ui/CourseUI.kt | 2 + .../courses/domain/model/UserCourses.kt | 9 + .../courses/presentation/UserCoursesScreen.kt | 225 +++++++++++++++++- .../presentation/UserCoursesUIState.kt | 4 +- .../presentation/UserCoursesViewModel.kt | 4 +- .../data/repository/DashboardRepository.kt | 8 +- .../presentation/DashboardFragment.kt | 2 + .../learn/presentation/LearnFragment.kt | 40 ++-- dashboard/src/main/res/values/strings.xml | 3 +- 15 files changed, 351 insertions(+), 36 deletions(-) create mode 100644 core/src/main/java/org/openedx/core/data/model/Progress.kt create mode 100644 core/src/main/java/org/openedx/core/domain/model/Progress.kt create mode 100644 dashboard/src/main/java/org/openedx/courses/domain/model/UserCourses.kt diff --git a/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt b/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt index 89ecdcab4..4f5f9332f 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt @@ -14,6 +14,9 @@ data class CourseEnrollments( @SerializedName("config") val configs: AppConfig, + + @SerializedName("primary") + val primary: EnrolledCourse?, ) { class Deserializer : JsonDeserializer { override fun deserialize( @@ -23,8 +26,20 @@ data class CourseEnrollments( ): CourseEnrollments { val enrollments = deserializeEnrollments(json) val appConfig = deserializeAppConfig(json) + val primaryCourse = deserializePrimaryCourse(json) + + return CourseEnrollments(enrollments, appConfig, primaryCourse) + } - return CourseEnrollments(enrollments, appConfig) + private fun deserializePrimaryCourse(json: JsonElement?): EnrolledCourse? { + return try { + Gson().fromJson( + (json as JsonObject).get("primary"), + EnrolledCourse::class.java + ) + } catch (ex: Exception) { + null + } } private fun deserializeEnrollments(json: JsonElement?): DashboardCourseList { diff --git a/core/src/main/java/org/openedx/core/data/model/EnrolledCourse.kt b/core/src/main/java/org/openedx/core/data/model/EnrolledCourse.kt index 984794698..dcfc9b92f 100644 --- a/core/src/main/java/org/openedx/core/data/model/EnrolledCourse.kt +++ b/core/src/main/java/org/openedx/core/data/model/EnrolledCourse.kt @@ -2,6 +2,7 @@ package org.openedx.core.data.model import com.google.gson.annotations.SerializedName import org.openedx.core.data.model.room.discovery.EnrolledCourseEntity +import org.openedx.core.data.model.room.discovery.ProgressDb import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.utils.TimeUtils @@ -17,7 +18,9 @@ data class EnrolledCourse( @SerializedName("course") val course: EnrolledCourseData?, @SerializedName("certificate") - val certificate: Certificate? + val certificate: Certificate?, + @SerializedName("progress") + val progress: Progress? ) { fun mapToDomain(): EnrolledCourse { return EnrolledCourse( @@ -26,7 +29,8 @@ data class EnrolledCourse( mode = mode ?: "", isActive = isActive ?: false, course = course?.mapToDomain()!!, - certificate = certificate?.mapToDomain() + certificate = certificate?.mapToDomain(), + progress = progress?.mapToDomain() ?: org.openedx.core.domain.model.Progress.DEFAULT_PROGRESS ) } @@ -38,7 +42,8 @@ data class EnrolledCourse( mode = mode ?: "", isActive = isActive ?: false, course = course?.mapToRoomEntity()!!, - certificate = certificate?.mapToRoomEntity() + certificate = certificate?.mapToRoomEntity(), + progress = progress?.mapToRoomEntity() ?: ProgressDb.DEFAULT_PROGRESS ) } } diff --git a/core/src/main/java/org/openedx/core/data/model/Progress.kt b/core/src/main/java/org/openedx/core/data/model/Progress.kt new file mode 100644 index 000000000..ce0d86960 --- /dev/null +++ b/core/src/main/java/org/openedx/core/data/model/Progress.kt @@ -0,0 +1,23 @@ +package org.openedx.core.data.model + +import com.google.gson.annotations.SerializedName +import org.openedx.core.data.model.room.discovery.ProgressDb + +data class Progress( + @SerializedName("num_points_earned") + val numPointsEarned: Int?, + @SerializedName("num_points_possible") + val numPointsPossible: Int? +) { + fun mapToDomain(): org.openedx.core.domain.model.Progress { + return org.openedx.core.domain.model.Progress( + numPointsEarned = numPointsEarned ?: 0, + numPointsPossible = numPointsPossible ?: 0 + ) + } + + fun mapToRoomEntity() = ProgressDb( + numPointsEarned = numPointsEarned ?: 0, + numPointsPossible = numPointsPossible ?: 0 + ) +} \ No newline at end of file diff --git a/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt b/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt index 05aab3bdd..8c766de8b 100644 --- a/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt +++ b/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt @@ -5,7 +5,12 @@ import androidx.room.Embedded import androidx.room.Entity import androidx.room.PrimaryKey import org.openedx.core.data.model.room.MediaDb -import org.openedx.core.domain.model.* +import org.openedx.core.domain.model.Certificate +import org.openedx.core.domain.model.CourseSharingUtmParameters +import org.openedx.core.domain.model.CoursewareAccess +import org.openedx.core.domain.model.EnrolledCourse +import org.openedx.core.domain.model.EnrolledCourseData +import org.openedx.core.domain.model.Progress import org.openedx.core.utils.TimeUtils @Entity(tableName = "course_enrolled_table") @@ -25,6 +30,8 @@ data class EnrolledCourseEntity( val course: EnrolledCourseDataDb, @Embedded val certificate: CertificateDb?, + @Embedded + val progress: ProgressDb, ) { fun mapToDomain(): EnrolledCourse { @@ -34,7 +41,8 @@ data class EnrolledCourseEntity( mode, isActive, course.mapToDomain(), - certificate?.mapToDomain() + certificate?.mapToDomain(), + progress.mapToDomain() ) } } @@ -151,4 +159,16 @@ data class CourseSharingUtmParametersDb( fun mapToDomain() = CourseSharingUtmParameters( facebook, twitter ) +} + +data class ProgressDb( + @ColumnInfo("numPointsEarned") + val numPointsEarned: Int, + @ColumnInfo("numPointsPossible") + val numPointsPossible: Int, +) { + companion object { + val DEFAULT_PROGRESS = ProgressDb(0,0) + } + fun mapToDomain() = Progress(numPointsEarned,numPointsPossible) } \ No newline at end of file diff --git a/core/src/main/java/org/openedx/core/domain/model/EnrolledCourse.kt b/core/src/main/java/org/openedx/core/domain/model/EnrolledCourse.kt index 8e339b3f6..f8afe86ce 100644 --- a/core/src/main/java/org/openedx/core/domain/model/EnrolledCourse.kt +++ b/core/src/main/java/org/openedx/core/domain/model/EnrolledCourse.kt @@ -12,4 +12,5 @@ data class EnrolledCourse( val isActive: Boolean, val course: EnrolledCourseData, val certificate: Certificate?, + val progress: Progress ) : Parcelable diff --git a/core/src/main/java/org/openedx/core/domain/model/Progress.kt b/core/src/main/java/org/openedx/core/domain/model/Progress.kt new file mode 100644 index 000000000..be43968a9 --- /dev/null +++ b/core/src/main/java/org/openedx/core/domain/model/Progress.kt @@ -0,0 +1,14 @@ +package org.openedx.core.domain.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class Progress( + val numPointsEarned: Int, + val numPointsPossible: Int, +) : Parcelable { + companion object { + val DEFAULT_PROGRESS = Progress(0,0) + } +} diff --git a/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt b/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt index 9523d99e5..b95e5f87d 100644 --- a/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt +++ b/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt @@ -85,6 +85,7 @@ import org.openedx.core.domain.model.CourseSharingUtmParameters import org.openedx.core.domain.model.CoursewareAccess import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.domain.model.EnrolledCourseData +import org.openedx.core.domain.model.Progress import org.openedx.core.extension.isLinkValid import org.openedx.core.extension.nonZero import org.openedx.core.extension.toFileSize @@ -1367,6 +1368,7 @@ private val mockCourse = EnrolledCourse( certificate = Certificate(""), mode = "mode", isActive = true, + progress = Progress.DEFAULT_PROGRESS, course = EnrolledCourseData( id = "id", name = "Course name", diff --git a/dashboard/src/main/java/org/openedx/courses/domain/model/UserCourses.kt b/dashboard/src/main/java/org/openedx/courses/domain/model/UserCourses.kt new file mode 100644 index 000000000..7d8a02112 --- /dev/null +++ b/dashboard/src/main/java/org/openedx/courses/domain/model/UserCourses.kt @@ -0,0 +1,9 @@ +package org.openedx.courses.domain.model + +import org.openedx.core.domain.model.DashboardCourseList +import org.openedx.core.domain.model.EnrolledCourse + +data class UserCourses( + val enrollments: DashboardCourseList, + val primary: EnrolledCourse? +) 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 c7a0ab137..8d13566e7 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt @@ -8,12 +8,14 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Card import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon +import androidx.compose.material.LinearProgressIndicator import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.Surface @@ -28,18 +30,35 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import org.openedx.core.UIMessage +import org.openedx.core.domain.model.Certificate +import org.openedx.core.domain.model.CourseSharingUtmParameters +import org.openedx.core.domain.model.CoursewareAccess +import org.openedx.core.domain.model.DashboardCourseList import org.openedx.core.domain.model.EnrolledCourse +import org.openedx.core.domain.model.EnrolledCourseData +import org.openedx.core.domain.model.Pagination +import org.openedx.core.domain.model.Progress import org.openedx.core.ui.HandleUIMessage +import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors +import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography +import org.openedx.core.utils.TimeUtils +import org.openedx.courses.domain.model.UserCourses import org.openedx.dashboard.R +import java.util.Date -@OptIn(ExperimentalMaterialApi::class) @Composable fun UsersCourseScreen( viewModel: UserCoursesViewModel, @@ -48,8 +67,29 @@ fun UsersCourseScreen( val updating by viewModel.updating.observeAsState(false) val uiMessage by viewModel.uiMessage.collectAsState(null) val uiState by viewModel.uiState.observeAsState(UserCoursesUIState.Loading) + + UsersCourseScreen( + uiMessage = uiMessage, + uiState = uiState, + updating = updating, + apiHostUrl = viewModel.apiHostUrl, + onSwipeRefresh = viewModel::updateCoursed, + onItemClick = onItemClick + ) +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun UsersCourseScreen( + uiMessage: UIMessage?, + uiState: UserCoursesUIState, + updating: Boolean, + apiHostUrl: String, + onSwipeRefresh: () -> Unit, + onItemClick: (EnrolledCourse) -> Unit, +) { val scaffoldState = rememberScaffoldState() - val pullRefreshState = rememberPullRefreshState(refreshing = updating, onRefresh = { viewModel.updateCoursed() }) + val pullRefreshState = rememberPullRefreshState(refreshing = updating, onRefresh = { onSwipeRefresh }) val scrollState = rememberLazyListState() Scaffold( @@ -77,7 +117,12 @@ fun UsersCourseScreen( } is UserCoursesUIState.Courses -> { - + UserCourses( + modifier = Modifier.fillMaxSize(), + userCourses = uiState.userCourses, + apiHostUrl = apiHostUrl, + scrollState = scrollState + ) } is UserCoursesUIState.Empty -> { @@ -95,6 +140,112 @@ fun UsersCourseScreen( } } +@Composable +private fun UserCourses( + modifier: Modifier = Modifier, + userCourses: UserCourses, + scrollState: LazyListState, + apiHostUrl: String +) { + LazyColumn( + modifier = modifier, + state = scrollState + ) { + if (userCourses.primary != null) { + item { + PrimaryCourseCard( + primaryCourse = userCourses.primary, + apiHostUrl = apiHostUrl + ) + } + } + } +} + +@Composable +private fun PrimaryCourseCard( + primaryCourse: EnrolledCourse, + apiHostUrl: String +) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(top = 8.dp), + backgroundColor = MaterialTheme.appColors.background, + shape = MaterialTheme.appShapes.courseImageShape, + elevation = 4.dp + ) { + Column { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(apiHostUrl + primaryCourse.course.courseImage) + .error(org.openedx.core.R.drawable.core_no_image_course) + .placeholder(org.openedx.core.R.drawable.core_no_image_course) + .build(), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .height(140.dp) + ) + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(), + progress = primaryCourse.progress.numPointsEarned.toFloat(), + color = MaterialTheme.appColors.primary, + backgroundColor = MaterialTheme.appColors.divider + ) + PrimaryCourseTitle( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp) + .padding(top = 8.dp, bottom = 16.dp), + primaryCourse = primaryCourse + ) + } + } +} + +@Composable +fun PrimaryCourseTitle( + modifier: Modifier = Modifier, + primaryCourse: EnrolledCourse +) { + Column( + modifier = modifier + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = primaryCourse.course.org, + style = MaterialTheme.appTypography.labelMedium, + color = MaterialTheme.appColors.textFieldHint + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = primaryCourse.course.name, + style = MaterialTheme.appTypography.titleLarge, + color = MaterialTheme.appColors.textDark + ) + Text( + modifier = Modifier.fillMaxWidth(), + style = MaterialTheme.appTypography.labelMedium, + color = MaterialTheme.appColors.textFieldHint, + text = stringResource( + R.string.dashboard_course_date, + TimeUtils.getCourseFormattedDate( + LocalContext.current, + Date(), + primaryCourse.auditAccessExpires, + primaryCourse.course.start, + primaryCourse.course.end, + primaryCourse.course.startType, + primaryCourse.course.startDisplay + ) + ) + ) + } +} + @Composable private fun EmptyState() { Box( @@ -122,4 +273,68 @@ private fun EmptyState() { ) } } +} + +private val mockCourse = EnrolledCourse( + auditAccessExpires = Date(), + created = "created", + certificate = Certificate(""), + mode = "mode", + isActive = true, + progress = Progress.DEFAULT_PROGRESS, + course = EnrolledCourseData( + id = "id", + name = "Course name", + number = "", + org = "Org", + start = Date(), + startDisplay = "", + startType = "", + end = Date(), + dynamicUpgradeDeadline = "", + subscriptionId = "", + coursewareAccess = CoursewareAccess( + true, + "", + "", + "", + "", + "" + ), + media = null, + courseImage = "", + courseAbout = "", + courseSharingUtmParameters = CourseSharingUtmParameters("", ""), + courseUpdates = "", + courseHandouts = "", + discussionUrl = "", + videoOutline = "", + isSelfPaced = false, + ) +) + +private val mockPagination = Pagination(10, "", 4, "1") +private val mockDashboardCourseList = DashboardCourseList( + pagination = mockPagination, + courses = listOf(mockCourse, mockCourse, mockCourse, mockCourse, mockCourse, mockCourse) +) + +private val mockUserCourses = UserCourses( + enrollments = mockDashboardCourseList, + primary = mockCourse +) + +@Preview +@Composable +private fun UsersCourseScreenPreview() { + OpenEdXTheme { + UsersCourseScreen( + uiState = UserCoursesUIState.Courses(mockUserCourses), + apiHostUrl = "", + uiMessage = null, + updating = false, + onSwipeRefresh = { }, + onItemClick = { } + ) + } } \ No newline at end of file diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesUIState.kt b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesUIState.kt index df21ce75f..ee064c315 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesUIState.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesUIState.kt @@ -1,9 +1,9 @@ package org.openedx.courses.presentation -import org.openedx.core.domain.model.EnrolledCourse +import org.openedx.courses.domain.model.UserCourses sealed class UserCoursesUIState { - data class Courses(val courses: List) : UserCoursesUIState() + data class Courses(val userCourses: UserCourses) : UserCoursesUIState() object Empty : UserCoursesUIState() object Loading : UserCoursesUIState() } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt index ce6532cdc..0dbaa7357 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt @@ -43,10 +43,10 @@ class UserCoursesViewModel( viewModelScope.launch { try { val response = interactor.getUserCourses() - if (response.courses.isEmpty()) { + if (response.enrollments.courses.isEmpty()) { _uiState.value = UserCoursesUIState.Empty } else { - _uiState.value = UserCoursesUIState.Courses(response.courses) + _uiState.value = UserCoursesUIState.Courses(response) } } catch (e: Exception) { if (e.isInternetError()) { diff --git a/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt b/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt index 9642b5633..fd226de76 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt @@ -4,6 +4,7 @@ import org.openedx.core.data.api.CourseApi import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.DashboardCourseList import org.openedx.core.domain.model.EnrolledCourse +import org.openedx.courses.domain.model.UserCourses import org.openedx.dashboard.data.DashboardDao class DashboardRepository( @@ -31,7 +32,7 @@ class DashboardRepository( return list.map { it.mapToDomain() } } - suspend fun getUserCourses(): DashboardCourseList { + suspend fun getUserCourses(): UserCourses { val user = preferencesManager.user val result = api.getUserCourses( username = user?.username ?: "" @@ -41,6 +42,9 @@ class DashboardRepository( dao.clearCachedData() dao.insertEnrolledCourseEntity(*result.enrollments.results.map { it.mapToRoomEntity() } .toTypedArray()) - return result.enrollments.mapToDomain() + return UserCourses( + enrollments = result.enrollments.mapToDomain(), + primary = result.primary?.mapToDomain() + ) } } diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt index 1c314c445..0d863e617 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt @@ -77,6 +77,7 @@ import org.openedx.core.domain.model.CourseSharingUtmParameters import org.openedx.core.domain.model.CoursewareAccess import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.domain.model.EnrolledCourseData +import org.openedx.core.domain.model.Progress import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRecommendedBox import org.openedx.core.system.notifier.AppUpgradeEvent import org.openedx.core.ui.HandleUIMessage @@ -607,6 +608,7 @@ private val mockCourseEnrolled = EnrolledCourse( certificate = Certificate(""), mode = "mode", isActive = true, + progress = Progress.DEFAULT_PROGRESS, course = EnrolledCourseData( id = "id", name = "name", 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 7b6a68d9f..136992f00 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -91,38 +90,40 @@ class LearnFragment : Fragment() { @OptIn(ExperimentalFoundationApi::class) @Composable private fun LearnScreen( - userCoursesViewModel : UserCoursesViewModel = viewModel(), + userCoursesViewModel : UserCoursesViewModel, windowSize: WindowSize ) { val scaffoldState = rememberScaffoldState() val pagerState = rememberPagerState { LearnType.entries.size } + val contentWidth by remember(key1 = windowSize) { + mutableStateOf( + windowSize.windowSizeValue( + expanded = Modifier.widthIn(Dp.Unspecified, 560.dp), + compact = Modifier.fillMaxSize(), + ) + ) + } Scaffold( scaffoldState = scaffoldState, - modifier = Modifier - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), backgroundColor = MaterialTheme.appColors.background ) { paddingValues -> - val contentWidth by remember(key1 = windowSize) { - mutableStateOf( - windowSize.windowSizeValue( - expanded = Modifier.widthIn(Dp.Unspecified, 560.dp), - compact = Modifier.fillMaxWidth(), - ) - ) - } + Column( modifier = Modifier .padding(paddingValues) - .padding(horizontal = 16.dp) .statusBarsInset() - .displayCutoutForLandscape(), + .displayCutoutForLandscape() + .then(contentWidth), horizontalAlignment = Alignment.CenterHorizontally ) { LearnToolbar( + modifier = Modifier + .padding(horizontal = 16.dp), label = stringResource(id = R.string.dashboard_learn), onSearchClick = { //TODO @@ -130,14 +131,15 @@ private fun LearnScreen( ) LearnDropdownMenu( - modifier = Modifier.align(Alignment.Start), + modifier = Modifier + .align(Alignment.Start) + .padding(horizontal = 16.dp), pagerState = pagerState ) HorizontalPager( modifier = Modifier - .fillMaxHeight() - .then(contentWidth), + .fillMaxSize(), state = pagerState, userScrollEnabled = false ) { page -> @@ -156,11 +158,12 @@ private fun LearnScreen( @Composable private fun LearnToolbar( + modifier: Modifier = Modifier, label: String, onSearchClick: () -> Unit ) { Box( - modifier = Modifier.fillMaxWidth() + modifier = modifier.fillMaxWidth() ) { Text( modifier = Modifier.align(Alignment.CenterStart), @@ -260,6 +263,7 @@ private fun LearnDropdownMenu( private fun LearnScreenPreview() { OpenEdXTheme { LearnScreen( + userCoursesViewModel = viewModel(), windowSize = WindowSize(WindowType.Compact, WindowType.Compact) ) } diff --git a/dashboard/src/main/res/values/strings.xml b/dashboard/src/main/res/values/strings.xml index 393f0178b..7cd907e3c 100644 --- a/dashboard/src/main/res/values/strings.xml +++ b/dashboard/src/main/res/values/strings.xml @@ -1,9 +1,10 @@ - + Dashboard Courses Welcome back. Let\'s keep learning. You are not enrolled in any courses yet. Learn Programs + Course %1$s From fadede8442ca0009fa4355a443798174b84f1cfc Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Fri, 12 Apr 2024 18:01:54 +0300 Subject: [PATCH 03/23] feat: Added start/resume course button --- .../openedx/core/data/model/CourseStatus.kt | 30 ++++++++ .../openedx/core/data/model/EnrolledCourse.kt | 10 ++- .../room/discovery/EnrolledCourseEntity.kt | 26 ++++++- .../openedx/core/domain/model/CourseStatus.kt | 12 ++++ .../core/domain/model/EnrolledCourse.kt | 3 +- .../java/org/openedx/core/utils/TimeUtils.kt | 2 +- core/src/main/res/values-uk/strings.xml | 2 +- core/src/main/res/values/strings.xml | 2 +- .../course/presentation/ui/CourseUI.kt | 42 ----------- .../courses/presentation/UserCoursesScreen.kt | 72 +++++++++++++++++-- .../presentation/DashboardFragment.kt | 2 + dashboard/src/main/res/values/strings.xml | 2 + 12 files changed, 148 insertions(+), 57 deletions(-) create mode 100644 core/src/main/java/org/openedx/core/data/model/CourseStatus.kt create mode 100644 core/src/main/java/org/openedx/core/domain/model/CourseStatus.kt diff --git a/core/src/main/java/org/openedx/core/data/model/CourseStatus.kt b/core/src/main/java/org/openedx/core/data/model/CourseStatus.kt new file mode 100644 index 000000000..609459055 --- /dev/null +++ b/core/src/main/java/org/openedx/core/data/model/CourseStatus.kt @@ -0,0 +1,30 @@ +package org.openedx.core.data.model + +import com.google.gson.annotations.SerializedName +import org.openedx.core.data.model.room.discovery.CourseStatusDb + +data class CourseStatus( + @SerializedName("last_visited_module_id") + val lastVisitedModuleId: String?, + @SerializedName("last_visited_module_path") + val lastVisitedModulePath: List?, + @SerializedName("last_visited_block_id") + val lastVisitedBlockId: String?, + @SerializedName("last_visited_unit_display_name") + val lastVisitedUnitDisplayName: String? +) { + fun mapToDomain(): org.openedx.core.domain.model.CourseStatus = + org.openedx.core.domain.model.CourseStatus( + lastVisitedModuleId = lastVisitedModuleId ?: "", + lastVisitedModulePath = lastVisitedModulePath ?: emptyList(), + lastVisitedBlockId = lastVisitedBlockId ?: "", + lastVisitedUnitDisplayName = lastVisitedUnitDisplayName ?: "" + ) + + fun mapToRoomEntity() = CourseStatusDb( + lastVisitedModuleId = lastVisitedModuleId ?: "", + lastVisitedModulePath = lastVisitedModulePath ?: emptyList(), + lastVisitedBlockId = lastVisitedBlockId ?: "", + lastVisitedUnitDisplayName = lastVisitedUnitDisplayName ?: "" + ) +} diff --git a/core/src/main/java/org/openedx/core/data/model/EnrolledCourse.kt b/core/src/main/java/org/openedx/core/data/model/EnrolledCourse.kt index dcfc9b92f..64ec226a1 100644 --- a/core/src/main/java/org/openedx/core/data/model/EnrolledCourse.kt +++ b/core/src/main/java/org/openedx/core/data/model/EnrolledCourse.kt @@ -20,7 +20,9 @@ data class EnrolledCourse( @SerializedName("certificate") val certificate: Certificate?, @SerializedName("progress") - val progress: Progress? + val progress: Progress?, + @SerializedName("course_status") + val courseStatus: CourseStatus? ) { fun mapToDomain(): EnrolledCourse { return EnrolledCourse( @@ -30,7 +32,8 @@ data class EnrolledCourse( isActive = isActive ?: false, course = course?.mapToDomain()!!, certificate = certificate?.mapToDomain(), - progress = progress?.mapToDomain() ?: org.openedx.core.domain.model.Progress.DEFAULT_PROGRESS + progress = progress?.mapToDomain() ?: org.openedx.core.domain.model.Progress.DEFAULT_PROGRESS, + courseStatus = courseStatus?.mapToDomain() ) } @@ -43,7 +46,8 @@ data class EnrolledCourse( isActive = isActive ?: false, course = course?.mapToRoomEntity()!!, certificate = certificate?.mapToRoomEntity(), - progress = progress?.mapToRoomEntity() ?: ProgressDb.DEFAULT_PROGRESS + progress = progress?.mapToRoomEntity() ?: ProgressDb.DEFAULT_PROGRESS, + courseStatus = courseStatus?.mapToRoomEntity() ) } } diff --git a/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt b/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt index 8c766de8b..a6999ca71 100644 --- a/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt +++ b/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt @@ -7,6 +7,7 @@ import androidx.room.PrimaryKey import org.openedx.core.data.model.room.MediaDb import org.openedx.core.domain.model.Certificate import org.openedx.core.domain.model.CourseSharingUtmParameters +import org.openedx.core.domain.model.CourseStatus import org.openedx.core.domain.model.CoursewareAccess import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.domain.model.EnrolledCourseData @@ -32,6 +33,8 @@ data class EnrolledCourseEntity( val certificate: CertificateDb?, @Embedded val progress: ProgressDb, + @Embedded + val courseStatus: CourseStatusDb?, ) { fun mapToDomain(): EnrolledCourse { @@ -42,7 +45,8 @@ data class EnrolledCourseEntity( isActive, course.mapToDomain(), certificate?.mapToDomain(), - progress.mapToDomain() + progress.mapToDomain(), + courseStatus?.mapToDomain() ) } } @@ -168,7 +172,23 @@ data class ProgressDb( val numPointsPossible: Int, ) { companion object { - val DEFAULT_PROGRESS = ProgressDb(0,0) + val DEFAULT_PROGRESS = ProgressDb(0, 0) } - fun mapToDomain() = Progress(numPointsEarned,numPointsPossible) + + fun mapToDomain() = Progress(numPointsEarned, numPointsPossible) +} + +data class CourseStatusDb( + @ColumnInfo("lastVisitedModuleId") + val lastVisitedModuleId: String, + @ColumnInfo("lastVisitedModulePath") + val lastVisitedModulePath: List, + @ColumnInfo("lastVisitedBlockId") + val lastVisitedBlockId: String, + @ColumnInfo("lastVisitedUnitDisplayName") + val lastVisitedUnitDisplayName: String, +) { + fun mapToDomain() = CourseStatus( + lastVisitedModuleId, lastVisitedModulePath, lastVisitedBlockId, lastVisitedUnitDisplayName + ) } \ No newline at end of file diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseStatus.kt b/core/src/main/java/org/openedx/core/domain/model/CourseStatus.kt new file mode 100644 index 000000000..b94721f40 --- /dev/null +++ b/core/src/main/java/org/openedx/core/domain/model/CourseStatus.kt @@ -0,0 +1,12 @@ +package org.openedx.core.domain.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class CourseStatus( + val lastVisitedModuleId: String, + val lastVisitedModulePath: List, + val lastVisitedBlockId: String, + val lastVisitedUnitDisplayName: String +): Parcelable \ No newline at end of file diff --git a/core/src/main/java/org/openedx/core/domain/model/EnrolledCourse.kt b/core/src/main/java/org/openedx/core/domain/model/EnrolledCourse.kt index f8afe86ce..7b71ca945 100644 --- a/core/src/main/java/org/openedx/core/domain/model/EnrolledCourse.kt +++ b/core/src/main/java/org/openedx/core/domain/model/EnrolledCourse.kt @@ -12,5 +12,6 @@ data class EnrolledCourse( val isActive: Boolean, val course: EnrolledCourseData, val certificate: Certificate?, - val progress: Progress + val progress: Progress, + val courseStatus: CourseStatus? ) : Parcelable diff --git a/core/src/main/java/org/openedx/core/utils/TimeUtils.kt b/core/src/main/java/org/openedx/core/utils/TimeUtils.kt index d77a1ab5e..8e3b6fa1f 100644 --- a/core/src/main/java/org/openedx/core/utils/TimeUtils.kt +++ b/core/src/main/java/org/openedx/core/utils/TimeUtils.kt @@ -59,7 +59,7 @@ object TimeUtils { private fun dateToCourseDate(resourceManager: ResourceManager, date: Date?): String { return formatDate( - format = resourceManager.getString(R.string.core_date_format_MMMM_dd), date = date + format = resourceManager.getString(R.string.core_date_format_MMMM_dd_yyyy), date = date ) } diff --git a/core/src/main/res/values-uk/strings.xml b/core/src/main/res/values-uk/strings.xml index dc6e2ffa2..17362feb3 100644 --- a/core/src/main/res/values-uk/strings.xml +++ b/core/src/main/res/values-uk/strings.xml @@ -31,7 +31,7 @@ Обліковий запис користувача не активовано. Будь ласка, спочатку активуйте свій обліковий запис. Надіслати електронний лист за допомогою ... Не встановлено жодного поштового клієнта - dd MMMM + dd MMMM, yyyy dd MMM yyyy HH:mm Оновлення додатку Ми рекомендуємо вам оновитись до останньої версії. Оновіться зараз, щоб отримати останні функції та виправлення. diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 40c288675..47eed77f9 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -46,7 +46,7 @@ OS version: Device model: Feedback - MMMM dd + MMMM dd, yyyy dd MMM yyyy hh:mm aaa App Update We recommend that you update to the latest version. Upgrade now to receive the latest features and fixes. diff --git a/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt b/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt index b95e5f87d..c8090899a 100644 --- a/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt +++ b/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt @@ -81,11 +81,6 @@ import org.openedx.core.domain.model.Block import org.openedx.core.domain.model.BlockCounts import org.openedx.core.domain.model.Certificate import org.openedx.core.domain.model.CourseDatesBannerInfo -import org.openedx.core.domain.model.CourseSharingUtmParameters -import org.openedx.core.domain.model.CoursewareAccess -import org.openedx.core.domain.model.EnrolledCourse -import org.openedx.core.domain.model.EnrolledCourseData -import org.openedx.core.domain.model.Progress import org.openedx.core.extension.isLinkValid import org.openedx.core.extension.nonZero import org.openedx.core.extension.toFileSize @@ -1362,43 +1357,6 @@ private fun OfflineQueueCardPreview() { } } -private val mockCourse = EnrolledCourse( - auditAccessExpires = Date(), - created = "created", - certificate = Certificate(""), - mode = "mode", - isActive = true, - progress = Progress.DEFAULT_PROGRESS, - course = EnrolledCourseData( - id = "id", - name = "Course name", - number = "", - org = "Org", - start = Date(), - startDisplay = "", - startType = "", - end = Date(), - dynamicUpgradeDeadline = "", - subscriptionId = "", - coursewareAccess = CoursewareAccess( - true, - "", - "", - "", - "", - "" - ), - media = null, - courseImage = "", - courseAbout = "", - courseSharingUtmParameters = CourseSharingUtmParameters("", ""), - courseUpdates = "", - courseHandouts = "", - discussionUrl = "", - videoOutline = "", - isSelfPaced = false - ) -) private val mockChapterBlock = Block( id = "id", blockId = "blockId", 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 8d13566e7..f1cb0a9e6 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt @@ -1,11 +1,16 @@ package org.openedx.courses.presentation +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.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.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn @@ -20,6 +25,8 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.Surface import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.School import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState @@ -43,6 +50,7 @@ import coil.request.ImageRequest import org.openedx.core.UIMessage import org.openedx.core.domain.model.Certificate import org.openedx.core.domain.model.CourseSharingUtmParameters +import org.openedx.core.domain.model.CourseStatus import org.openedx.core.domain.model.CoursewareAccess import org.openedx.core.domain.model.DashboardCourseList import org.openedx.core.domain.model.EnrolledCourse @@ -190,7 +198,9 @@ private fun PrimaryCourseCard( .height(140.dp) ) LinearProgressIndicator( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .height(8.dp), progress = primaryCourse.progress.numPointsEarned.toFloat(), color = MaterialTheme.appColors.primary, backgroundColor = MaterialTheme.appColors.divider @@ -202,12 +212,65 @@ private fun PrimaryCourseCard( .padding(top = 8.dp, bottom = 16.dp), primaryCourse = primaryCourse ) + ResumeButton( + modifier = Modifier.fillMaxWidth(), + primaryCourse = primaryCourse + ) } } } @Composable -fun PrimaryCourseTitle( +fun ResumeButton( + modifier: Modifier = Modifier, + primaryCourse: EnrolledCourse +) { + Row( + modifier = modifier + .heightIn(min = 60.dp) + .background(MaterialTheme.appColors.primary) + .clickable { + //TODO + } + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + if (primaryCourse.courseStatus == null) { + Text( + modifier = modifier + .fillMaxWidth(), + text = stringResource(R.string.dashboard_start_course), + textAlign = TextAlign.Center, + color = MaterialTheme.appColors.buttonText, + style = MaterialTheme.appTypography.titleSmall + ) + } else { + Icon( + imageVector = Icons.Default.School, + tint = MaterialTheme.appColors.buttonText, + contentDescription = null + ) + Column( + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Text( + text = stringResource(R.string.dashboard_resume_course), + color = MaterialTheme.appColors.buttonText, + style = MaterialTheme.appTypography.labelSmall + ) + Text( + text = primaryCourse.courseStatus?.lastVisitedUnitDisplayName ?: "", + color = MaterialTheme.appColors.buttonText, + style = MaterialTheme.appTypography.titleSmall + ) + } + } + } +} + +@Composable +private fun PrimaryCourseTitle( modifier: Modifier = Modifier, primaryCourse: EnrolledCourse ) { @@ -282,6 +345,7 @@ private val mockCourse = EnrolledCourse( mode = "mode", isActive = true, progress = Progress.DEFAULT_PROGRESS, + courseStatus = CourseStatus("", emptyList(), "", ""), course = EnrolledCourseData( id = "id", name = "Course name", @@ -312,13 +376,11 @@ private val mockCourse = EnrolledCourse( isSelfPaced = false, ) ) - private val mockPagination = Pagination(10, "", 4, "1") private val mockDashboardCourseList = DashboardCourseList( pagination = mockPagination, courses = listOf(mockCourse, mockCourse, mockCourse, mockCourse, mockCourse, mockCourse) ) - private val mockUserCourses = UserCourses( enrollments = mockDashboardCourseList, primary = mockCourse @@ -337,4 +399,4 @@ private fun UsersCourseScreenPreview() { onItemClick = { } ) } -} \ No newline at end of file +} diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt index 0d863e617..029eef59b 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt @@ -74,6 +74,7 @@ import org.openedx.core.AppUpdateState import org.openedx.core.UIMessage import org.openedx.core.domain.model.Certificate import org.openedx.core.domain.model.CourseSharingUtmParameters +import org.openedx.core.domain.model.CourseStatus import org.openedx.core.domain.model.CoursewareAccess import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.domain.model.EnrolledCourseData @@ -609,6 +610,7 @@ private val mockCourseEnrolled = EnrolledCourse( mode = "mode", isActive = true, progress = Progress.DEFAULT_PROGRESS, + courseStatus = CourseStatus("", emptyList(), "", ""), course = EnrolledCourseData( id = "id", name = "name", diff --git a/dashboard/src/main/res/values/strings.xml b/dashboard/src/main/res/values/strings.xml index 7cd907e3c..c192b383e 100644 --- a/dashboard/src/main/res/values/strings.xml +++ b/dashboard/src/main/res/values/strings.xml @@ -7,4 +7,6 @@ Learn Programs Course %1$s + Start course + Resume Course From 34ab0d5545a8d5b66c5426e733551e4452d3f71b Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Mon, 15 Apr 2024 16:35:01 +0300 Subject: [PATCH 04/23] feat: Added alignment items --- .../org/openedx/app/InDevelopmentFragment.kt | 56 -------- .../main/java/org/openedx/app/MainFragment.kt | 4 +- .../java/org/openedx/app/MainViewModel.kt | 2 - .../java/org/openedx/app/di/ScreenModule.kt | 2 +- .../core/data/model/CourseAssignments.kt | 26 ++++ .../core/data/model/CourseDateBlock.kt | 32 ++++- .../openedx/core/data/model/EnrolledCourse.kt | 10 +- .../room/discovery/EnrolledCourseEntity.kt | 54 +++++++- .../core/domain/model/CourseAssignments.kt | 10 ++ .../core/domain/model/CourseDateBlock.kt | 5 +- .../core/domain/model/EnrolledCourse.kt | 3 +- .../global/InDevelopmentScreen.kt | 8 +- .../res/drawable/ic_core_chapter_icon.xml | 0 .../course/data/storage/CourseConverter.kt | 13 ++ .../section/CourseSectionFragment.kt | 44 ++++++- .../course/presentation/ui/CourseUI.kt | 4 +- .../courses/presentation/UserCoursesScreen.kt | 124 ++++++++++++++---- .../presentation/UserCoursesViewModel.kt | 21 ++- .../presentation/DashboardFragment.kt | 3 + .../learn/presentation/LearnFragment.kt | 18 +-- dashboard/src/main/res/values/strings.xml | 1 + 21 files changed, 326 insertions(+), 114 deletions(-) delete mode 100644 app/src/main/java/org/openedx/app/InDevelopmentFragment.kt create mode 100644 core/src/main/java/org/openedx/core/data/model/CourseAssignments.kt create mode 100644 core/src/main/java/org/openedx/core/domain/model/CourseAssignments.kt rename dashboard/src/main/java/org/openedx/programs/presentation/ProgramsScreen.kt => core/src/main/java/org/openedx/core/presentation/global/InDevelopmentScreen.kt (86%) rename course/src/main/res/drawable/ic_course_chapter_icon.xml => core/src/main/res/drawable/ic_core_chapter_icon.xml (100%) diff --git a/app/src/main/java/org/openedx/app/InDevelopmentFragment.kt b/app/src/main/java/org/openedx/app/InDevelopmentFragment.kt deleted file mode 100644 index d8ca717d4..000000000 --- a/app/src/main/java/org/openedx/app/InDevelopmentFragment.kt +++ /dev/null @@ -1,56 +0,0 @@ -package org.openedx.app - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Scaffold -import androidx.compose.material.Text -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.testTagsAsResourceId -import androidx.fragment.app.Fragment -import org.openedx.core.ui.theme.appColors -import org.openedx.core.ui.theme.appTypography - -class InDevelopmentFragment : Fragment() { - - @OptIn(ExperimentalComposeUiApi::class) - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = ComposeView(requireContext()).apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setContent { - Scaffold( - modifier = Modifier.semantics { - testTagsAsResourceId = true - }, - ) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(it) - .background(MaterialTheme.appColors.secondary), - contentAlignment = Alignment.Center - ) { - Text( - modifier = Modifier.testTag("txt_in_development"), - text = "Will be available soon", - style = MaterialTheme.appTypography.headlineMedium - ) - } - } - } - } -} diff --git a/app/src/main/java/org/openedx/app/MainFragment.kt b/app/src/main/java/org/openedx/app/MainFragment.kt index 829cf43a0..7e20f474d 100644 --- a/app/src/main/java/org/openedx/app/MainFragment.kt +++ b/app/src/main/java/org/openedx/app/MainFragment.kt @@ -16,11 +16,9 @@ import org.openedx.app.databinding.FragmentMainBinding import org.openedx.core.config.Config import org.openedx.core.presentation.global.app_upgrade.UpgradeRequiredFragment import org.openedx.core.presentation.global.viewBinding -import org.openedx.dashboard.presentation.DashboardFragment -import org.openedx.learn.presentation.LearnFragment import org.openedx.discovery.presentation.DiscoveryNavigator import org.openedx.discovery.presentation.DiscoveryRouter -import org.openedx.discovery.presentation.program.ProgramFragment +import org.openedx.learn.presentation.LearnFragment import org.openedx.profile.presentation.profile.ProfileFragment class MainFragment : Fragment(R.layout.fragment_main) { diff --git a/app/src/main/java/org/openedx/app/MainViewModel.kt b/app/src/main/java/org/openedx/app/MainViewModel.kt index 6a30533ea..3f90e1aa1 100644 --- a/app/src/main/java/org/openedx/app/MainViewModel.kt +++ b/app/src/main/java/org/openedx/app/MainViewModel.kt @@ -31,8 +31,6 @@ class MainViewModel( val isDiscoveryTypeWebView get() = config.getDiscoveryConfig().isViewTypeWebView() - val isProgramTypeWebView get() = config.getProgramConfig().isViewTypeWebView() - override fun onCreate(owner: LifecycleOwner) { super.onCreate(owner) notifier.notifier.onEach { diff --git a/app/src/main/java/org/openedx/app/di/ScreenModule.kt b/app/src/main/java/org/openedx/app/di/ScreenModule.kt index 35248cc14..92a8d7e1e 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -116,7 +116,7 @@ val screenModule = module { factory { DashboardRepository(get(), get(), get()) } factory { DashboardInteractor(get()) } viewModel { DashboardViewModel(get(), get(), get(), get(), get(), get(), get()) } - viewModel { UserCoursesViewModel(get(), get(), get()) } + viewModel { UserCoursesViewModel(get(), get(), get(), get()) } factory { DiscoveryRepository(get(), get(), get()) } factory { DiscoveryInteractor(get()) } diff --git a/core/src/main/java/org/openedx/core/data/model/CourseAssignments.kt b/core/src/main/java/org/openedx/core/data/model/CourseAssignments.kt new file mode 100644 index 000000000..27d97fee2 --- /dev/null +++ b/core/src/main/java/org/openedx/core/data/model/CourseAssignments.kt @@ -0,0 +1,26 @@ +package org.openedx.core.data.model + +import com.google.gson.annotations.SerializedName +import org.openedx.core.data.model.room.discovery.CourseAssignmentsDb + +data class CourseAssignments( + @SerializedName("future_assignment") + val futureAssignment: CourseDateBlock?, + @SerializedName("past_assignments") + val pastAssignments: List? +) { + fun mapToDomain(): org.openedx.core.domain.model.CourseAssignments = + org.openedx.core.domain.model.CourseAssignments( + futureAssignment = futureAssignment?.mapToDomain(), + pastAssignments = pastAssignments?.map { + it.mapToDomain() + } + ) + + fun mapToRoomEntity() = CourseAssignmentsDb( + futureAssignment = futureAssignment?.mapToRoomEntity(), + pastAssignments = pastAssignments?.map { + it.mapToRoomEntity() + } + ) +} diff --git a/core/src/main/java/org/openedx/core/data/model/CourseDateBlock.kt b/core/src/main/java/org/openedx/core/data/model/CourseDateBlock.kt index 887112845..355595b75 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseDateBlock.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseDateBlock.kt @@ -1,8 +1,13 @@ package org.openedx.core.data.model +import android.os.Parcelable import com.google.gson.annotations.SerializedName -import java.util.* +import kotlinx.parcelize.Parcelize +import org.openedx.core.data.model.room.discovery.CourseDateBlockDb +import org.openedx.core.utils.TimeUtils +import java.util.Date +@Parcelize data class CourseDateBlock( @SerializedName("complete") val complete: Boolean = false, @@ -25,4 +30,27 @@ data class CourseDateBlock( // component blockId in-case of navigating inside the app for component available in mobile @SerializedName("first_component_block_id") val blockId: String = "", -) +): Parcelable { + fun mapToDomain() = org.openedx.core.domain.model.CourseDateBlock( + complete = complete, + date = TimeUtils.iso8601ToDate(date) ?: Date(), + assignmentType = assignmentType, + dateType = dateType, + description = description, + learnerHasAccess = learnerHasAccess, + link = link, + title = title, + blockId = blockId + ) + fun mapToRoomEntity() = CourseDateBlockDb( + complete = complete, + date = date, + assignmentType = assignmentType, + dateType = dateType, + description = description, + learnerHasAccess = learnerHasAccess, + link = link, + title = title, + blockId = blockId + ) +} diff --git a/core/src/main/java/org/openedx/core/data/model/EnrolledCourse.kt b/core/src/main/java/org/openedx/core/data/model/EnrolledCourse.kt index 64ec226a1..3cff7c74b 100644 --- a/core/src/main/java/org/openedx/core/data/model/EnrolledCourse.kt +++ b/core/src/main/java/org/openedx/core/data/model/EnrolledCourse.kt @@ -22,7 +22,9 @@ data class EnrolledCourse( @SerializedName("progress") val progress: Progress?, @SerializedName("course_status") - val courseStatus: CourseStatus? + val courseStatus: CourseStatus?, + @SerializedName("course_assignments") + val courseAssignments: CourseAssignments? ) { fun mapToDomain(): EnrolledCourse { return EnrolledCourse( @@ -33,7 +35,8 @@ data class EnrolledCourse( course = course?.mapToDomain()!!, certificate = certificate?.mapToDomain(), progress = progress?.mapToDomain() ?: org.openedx.core.domain.model.Progress.DEFAULT_PROGRESS, - courseStatus = courseStatus?.mapToDomain() + courseStatus = courseStatus?.mapToDomain(), + courseAssignments = courseAssignments?.mapToDomain() ) } @@ -47,7 +50,8 @@ data class EnrolledCourse( course = course?.mapToRoomEntity()!!, certificate = certificate?.mapToRoomEntity(), progress = progress?.mapToRoomEntity() ?: ProgressDb.DEFAULT_PROGRESS, - courseStatus = courseStatus?.mapToRoomEntity() + courseStatus = courseStatus?.mapToRoomEntity(), + courseAssignments = courseAssignments?.mapToRoomEntity() ) } } diff --git a/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt b/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt index a6999ca71..e4fc125e3 100644 --- a/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt +++ b/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt @@ -4,8 +4,11 @@ import androidx.room.ColumnInfo import androidx.room.Embedded import androidx.room.Entity import androidx.room.PrimaryKey +import org.openedx.core.data.model.DateType import org.openedx.core.data.model.room.MediaDb import org.openedx.core.domain.model.Certificate +import org.openedx.core.domain.model.CourseAssignments +import org.openedx.core.domain.model.CourseDateBlock import org.openedx.core.domain.model.CourseSharingUtmParameters import org.openedx.core.domain.model.CourseStatus import org.openedx.core.domain.model.CoursewareAccess @@ -13,6 +16,7 @@ import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.domain.model.EnrolledCourseData import org.openedx.core.domain.model.Progress import org.openedx.core.utils.TimeUtils +import java.util.Date @Entity(tableName = "course_enrolled_table") data class EnrolledCourseEntity( @@ -35,6 +39,8 @@ data class EnrolledCourseEntity( val progress: ProgressDb, @Embedded val courseStatus: CourseStatusDb?, + @Embedded + val courseAssignments: CourseAssignmentsDb? ) { fun mapToDomain(): EnrolledCourse { @@ -46,7 +52,8 @@ data class EnrolledCourseEntity( course.mapToDomain(), certificate?.mapToDomain(), progress.mapToDomain(), - courseStatus?.mapToDomain() + courseStatus?.mapToDomain(), + courseAssignments?.mapToDomain() ) } } @@ -191,4 +198,49 @@ data class CourseStatusDb( fun mapToDomain() = CourseStatus( lastVisitedModuleId, lastVisitedModulePath, lastVisitedBlockId, lastVisitedUnitDisplayName ) +} + +data class CourseAssignmentsDb( + @Embedded + val futureAssignment: CourseDateBlockDb?, + @ColumnInfo("pastAssignments") + val pastAssignments: List? +) { + fun mapToDomain() = CourseAssignments( + futureAssignment = futureAssignment?.mapToDomain(), + pastAssignments = pastAssignments?.map { it.mapToDomain() } + ) +} + +data class CourseDateBlockDb( + @ColumnInfo("title") + val title: String = "", + @ColumnInfo("description") + val description: String = "", + @ColumnInfo("link") + val link: String = "", + @ColumnInfo("blockId") + val blockId: String = "", + @ColumnInfo("learnerHasAccess") + val learnerHasAccess: Boolean = false, + @ColumnInfo("complete") + val complete: Boolean = false, + @ColumnInfo("date") + val date: String, + @ColumnInfo("dateType") + val dateType: DateType = DateType.NONE, + @ColumnInfo("assignmentType") + val assignmentType: String? = "", +) { + fun mapToDomain() = CourseDateBlock( + title = title, + description = description, + link = link, + blockId = blockId, + learnerHasAccess = learnerHasAccess, + complete = complete, + date = TimeUtils.iso8601ToDate(date) ?: Date(), + dateType = dateType, + assignmentType = assignmentType + ) } \ No newline at end of file diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseAssignments.kt b/core/src/main/java/org/openedx/core/domain/model/CourseAssignments.kt new file mode 100644 index 000000000..6fcaadade --- /dev/null +++ b/core/src/main/java/org/openedx/core/domain/model/CourseAssignments.kt @@ -0,0 +1,10 @@ +package org.openedx.core.domain.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class CourseAssignments( + val futureAssignment: CourseDateBlock?, + val pastAssignments: List? +): Parcelable diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseDateBlock.kt b/core/src/main/java/org/openedx/core/domain/model/CourseDateBlock.kt index 7e91c59fa..394ebdd56 100644 --- a/core/src/main/java/org/openedx/core/domain/model/CourseDateBlock.kt +++ b/core/src/main/java/org/openedx/core/domain/model/CourseDateBlock.kt @@ -1,10 +1,13 @@ package org.openedx.core.domain.model +import android.os.Parcelable +import kotlinx.parcelize.Parcelize import org.openedx.core.data.model.DateType import org.openedx.core.utils.isTimeLessThan24Hours import org.openedx.core.utils.isToday import java.util.Date +@Parcelize data class CourseDateBlock( val title: String = "", val description: String = "", @@ -15,7 +18,7 @@ data class CourseDateBlock( val date: Date, val dateType: DateType = DateType.NONE, val assignmentType: String? = "", -) { +) : Parcelable { fun isCompleted(): Boolean { return complete || (dateType in setOf( DateType.COURSE_START_DATE, diff --git a/core/src/main/java/org/openedx/core/domain/model/EnrolledCourse.kt b/core/src/main/java/org/openedx/core/domain/model/EnrolledCourse.kt index 7b71ca945..184fc3aa4 100644 --- a/core/src/main/java/org/openedx/core/domain/model/EnrolledCourse.kt +++ b/core/src/main/java/org/openedx/core/domain/model/EnrolledCourse.kt @@ -13,5 +13,6 @@ data class EnrolledCourse( val course: EnrolledCourseData, val certificate: Certificate?, val progress: Progress, - val courseStatus: CourseStatus? + val courseStatus: CourseStatus?, + val courseAssignments: CourseAssignments? ) : Parcelable diff --git a/dashboard/src/main/java/org/openedx/programs/presentation/ProgramsScreen.kt b/core/src/main/java/org/openedx/core/presentation/global/InDevelopmentScreen.kt similarity index 86% rename from dashboard/src/main/java/org/openedx/programs/presentation/ProgramsScreen.kt rename to core/src/main/java/org/openedx/core/presentation/global/InDevelopmentScreen.kt index 199a07f8f..9cf2472f9 100644 --- a/dashboard/src/main/java/org/openedx/programs/presentation/ProgramsScreen.kt +++ b/core/src/main/java/org/openedx/core/presentation/global/InDevelopmentScreen.kt @@ -1,4 +1,4 @@ -package org.openedx.programs.presentation +package org.openedx.core.presentation.global import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -13,9 +13,11 @@ import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appTypography @Composable -fun ProgramsScreen() { +fun InDevelopmentScreen( + modifier: Modifier = Modifier +) { Box( - modifier = Modifier + modifier = modifier .fillMaxSize() .background(MaterialTheme.appColors.secondary), contentAlignment = Alignment.Center diff --git a/course/src/main/res/drawable/ic_course_chapter_icon.xml b/core/src/main/res/drawable/ic_core_chapter_icon.xml similarity index 100% rename from course/src/main/res/drawable/ic_course_chapter_icon.xml rename to core/src/main/res/drawable/ic_core_chapter_icon.xml diff --git a/course/src/main/java/org/openedx/course/data/storage/CourseConverter.kt b/course/src/main/java/org/openedx/course/data/storage/CourseConverter.kt index 91ac5a610..1865a3c34 100644 --- a/course/src/main/java/org/openedx/course/data/storage/CourseConverter.kt +++ b/course/src/main/java/org/openedx/course/data/storage/CourseConverter.kt @@ -4,6 +4,7 @@ import androidx.room.TypeConverter import com.google.gson.Gson import org.openedx.core.data.model.room.BlockDb import org.openedx.core.data.model.room.VideoInfoDb +import org.openedx.core.data.model.room.discovery.CourseDateBlockDb import org.openedx.core.extension.genericType class CourseConverter { @@ -57,4 +58,16 @@ class CourseConverter { return gson.toJson(map) } + @TypeConverter + fun fromListOfCourseDateBlockDb(value: List): String { + val json = Gson().toJson(value) + return json.toString() + } + + @TypeConverter + fun toListOfCourseDateBlockDb(value: String): List { + val type = genericType>() + return Gson().fromJson(value, type) + } + } diff --git a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt index 297545117..7896e2dca 100644 --- a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt @@ -5,14 +5,40 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +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.navigationBarsPadding +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.lazy.items -import androidx.compose.material.* +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Surface +import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close -import androidx.compose.runtime.* +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -40,15 +66,23 @@ import org.openedx.core.domain.model.BlockCounts import org.openedx.core.extension.serializable import org.openedx.core.module.db.DownloadedState import org.openedx.core.presentation.course.CourseViewMode -import org.openedx.core.ui.* +import org.openedx.core.ui.BackBtn +import org.openedx.core.ui.HandleUIMessage +import org.openedx.core.ui.WindowSize +import org.openedx.core.ui.WindowType +import org.openedx.core.ui.displayCutoutForLandscape +import org.openedx.core.ui.rememberWindowSize +import org.openedx.core.ui.statusBarsInset import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography +import org.openedx.core.ui.windowSizeValue import org.openedx.course.R import org.openedx.course.presentation.CourseRouter import org.openedx.course.presentation.ui.CardArrow import java.io.File +import org.openedx.core.R as CoreR class CourseSectionFragment : Fragment() { @@ -276,7 +310,7 @@ private fun CourseSubsectionItem( ) { val completedIconPainter = if (block.isCompleted()) painterResource(R.drawable.course_ic_task_alt) else painterResource( - R.drawable.ic_course_chapter_icon + CoreR.drawable.ic_core_chapter_icon ) val completedIconColor = if (block.isCompleted()) MaterialTheme.appColors.primary else MaterialTheme.appColors.onSurface diff --git a/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt b/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt index c8090899a..a2fe93804 100644 --- a/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt +++ b/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt @@ -212,7 +212,7 @@ fun CourseSectionCard( ) { val completedIconPainter = if (block.isCompleted()) painterResource(R.drawable.course_ic_task_alt) else painterResource( - R.drawable.ic_course_chapter_icon + coreR.drawable.ic_core_chapter_icon ) val completedIconColor = if (block.isCompleted()) MaterialTheme.appColors.primary else MaterialTheme.appColors.onSurface @@ -760,7 +760,7 @@ fun CourseSubSectionItem( ) { val icon = if (block.isCompleted()) painterResource(R.drawable.course_ic_task_alt) else painterResource( - R.drawable.ic_course_chapter_icon + coreR.drawable.ic_core_chapter_icon ) val iconColor = if (block.isCompleted()) MaterialTheme.appColors.primary else MaterialTheme.appColors.onSurface 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 f1cb0a9e6..26ef425c0 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt @@ -13,11 +13,11 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Card import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Divider import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.LinearProgressIndicator @@ -27,6 +27,7 @@ import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.School +import androidx.compose.material.icons.filled.Warning import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState @@ -37,6 +38,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag @@ -49,6 +52,7 @@ import coil.compose.AsyncImage import coil.request.ImageRequest import org.openedx.core.UIMessage import org.openedx.core.domain.model.Certificate +import org.openedx.core.domain.model.CourseAssignments import org.openedx.core.domain.model.CourseSharingUtmParameters import org.openedx.core.domain.model.CourseStatus import org.openedx.core.domain.model.CoursewareAccess @@ -66,6 +70,7 @@ import org.openedx.core.utils.TimeUtils import org.openedx.courses.domain.model.UserCourses import org.openedx.dashboard.R import java.util.Date +import org.openedx.core.R as CoreR @Composable fun UsersCourseScreen( @@ -81,7 +86,7 @@ fun UsersCourseScreen( uiState = uiState, updating = updating, apiHostUrl = viewModel.apiHostUrl, - onSwipeRefresh = viewModel::updateCoursed, + onSwipeRefresh = viewModel::updateCourses, onItemClick = onItemClick ) } @@ -97,8 +102,7 @@ private fun UsersCourseScreen( onItemClick: (EnrolledCourse) -> Unit, ) { val scaffoldState = rememberScaffoldState() - val pullRefreshState = rememberPullRefreshState(refreshing = updating, onRefresh = { onSwipeRefresh }) - val scrollState = rememberLazyListState() + val pullRefreshState = rememberPullRefreshState(refreshing = updating, onRefresh = { onSwipeRefresh() }) Scaffold( scaffoldState = scaffoldState, @@ -108,13 +112,16 @@ private fun UsersCourseScreen( HandleUIMessage(uiMessage = uiMessage, scaffoldState = scaffoldState) Surface( - modifier = Modifier.padding(paddingValues), + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), color = MaterialTheme.appColors.background ) { Box( Modifier .fillMaxSize() - .pullRefresh(pullRefreshState), + .pullRefresh(pullRefreshState) + .verticalScroll(rememberScrollState()), ) { when (uiState) { is UserCoursesUIState.Loading -> { @@ -128,13 +135,14 @@ private fun UsersCourseScreen( UserCourses( modifier = Modifier.fillMaxSize(), userCourses = uiState.userCourses, - apiHostUrl = apiHostUrl, - scrollState = scrollState + apiHostUrl = apiHostUrl ) } is UserCoursesUIState.Empty -> { - EmptyState() + EmptyState( + modifier = Modifier.align(Alignment.Center) + ) } } @@ -152,18 +160,61 @@ private fun UsersCourseScreen( private fun UserCourses( modifier: Modifier = Modifier, userCourses: UserCourses, - scrollState: LazyListState, apiHostUrl: String ) { - LazyColumn( - modifier = modifier, - state = scrollState + Column( + modifier = modifier ) { if (userCourses.primary != null) { - item { - PrimaryCourseCard( - primaryCourse = userCourses.primary, - apiHostUrl = apiHostUrl + PrimaryCourseCard( + primaryCourse = userCourses.primary, + apiHostUrl = apiHostUrl + ) + } + SecondaryCourses( + courses = userCourses.enrollments.courses + ) + } +} + +@Composable +private fun SecondaryCourses( + courses: List +) { + +} + +@Composable +fun AssignmentItem( + painter: Painter, + title: String?, + info: String +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp, horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painter, + tint = MaterialTheme.appColors.textDark, + contentDescription = null + ) + Column( + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = info, + color = MaterialTheme.appColors.textDark, + style = MaterialTheme.appTypography.labelSmall + ) + if (!title.isNullOrEmpty()) { + Text( + text = title, + color = MaterialTheme.appColors.textDark, + style = MaterialTheme.appTypography.titleSmall ) } } @@ -212,6 +263,25 @@ private fun PrimaryCourseCard( .padding(top = 8.dp, bottom = 16.dp), primaryCourse = primaryCourse ) + val pastAssignments = primaryCourse.courseAssignments?.pastAssignments + if (!pastAssignments.isNullOrEmpty()) { + val title = if (pastAssignments.size == 1) pastAssignments.first().title else null + Divider() + AssignmentItem( + painter = rememberVectorPainter(Icons.Default.Warning), + title = title, + info = stringResource(R.string.dashboard_past_due_assignment, pastAssignments.size) + ) + } + val futureAssignment = primaryCourse.courseAssignments?.futureAssignment + if (futureAssignment != null) { + Divider() + AssignmentItem( + painter = painterResource(id = CoreR.drawable.ic_core_chapter_icon), + title = futureAssignment.title, + info = "${futureAssignment.assignmentType} Due in ${futureAssignment.date} days" + ) + } ResumeButton( modifier = Modifier.fillMaxWidth(), primaryCourse = primaryCourse @@ -275,7 +345,8 @@ private fun PrimaryCourseTitle( primaryCourse: EnrolledCourse ) { Column( - modifier = modifier + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(4.dp) ) { Text( modifier = Modifier.fillMaxWidth(), @@ -310,9 +381,11 @@ private fun PrimaryCourseTitle( } @Composable -private fun EmptyState() { +private fun EmptyState( + modifier: Modifier = Modifier +) { Box( - modifier = Modifier.fillMaxSize(), + modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Column( @@ -338,6 +411,8 @@ private fun EmptyState() { } } + +private val mockCourseAssignments = CourseAssignments(null, emptyList()) private val mockCourse = EnrolledCourse( auditAccessExpires = Date(), created = "created", @@ -346,6 +421,7 @@ private val mockCourse = EnrolledCourse( isActive = true, progress = Progress.DEFAULT_PROGRESS, courseStatus = CourseStatus("", emptyList(), "", ""), + courseAssignments = mockCourseAssignments, course = EnrolledCourseData( id = "id", name = "Course name", @@ -363,7 +439,7 @@ private val mockCourse = EnrolledCourse( "", "", "", - "" + "", ), media = null, courseImage = "", @@ -373,7 +449,7 @@ private val mockCourse = EnrolledCourse( courseHandouts = "", discussionUrl = "", videoOutline = "", - isSelfPaced = false, + isSelfPaced = false ) ) private val mockPagination = Pagination(10, "", 4, "1") diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt index 0dbaa7357..c58a6b2bf 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt @@ -1,5 +1,6 @@ package org.openedx.courses.presentation +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope @@ -13,15 +14,19 @@ import org.openedx.core.UIMessage import org.openedx.core.config.Config import org.openedx.core.extension.isInternetError import org.openedx.core.system.ResourceManager +import org.openedx.core.system.notifier.CourseDashboardUpdate +import org.openedx.core.system.notifier.DiscoveryNotifier import org.openedx.dashboard.domain.interactor.DashboardInteractor class UserCoursesViewModel( private val config: Config, private val interactor: DashboardInteractor, private val resourceManager: ResourceManager, + private val discoveryNotifier: DiscoveryNotifier, ) : BaseViewModel() { val apiHostUrl get() = config.getApiHostURL() + val isProgramTypeWebView get() = config.getProgramConfig().isViewTypeWebView() private val _uiState = MutableLiveData(UserCoursesUIState.Loading) val uiState: LiveData @@ -35,6 +40,18 @@ class UserCoursesViewModel( val updating: LiveData get() = _updating + override fun onCreate(owner: LifecycleOwner) { + super.onCreate(owner) + viewModelScope.launch { + discoveryNotifier.notifier.collect { + // TODO Notifier doesn't collect data + if (it is CourseDashboardUpdate) { + updateCourses() + } + } + } + } + init { getCourses() } @@ -43,7 +60,7 @@ class UserCoursesViewModel( viewModelScope.launch { try { val response = interactor.getUserCourses() - if (response.enrollments.courses.isEmpty()) { + if (response.primary == null) { _uiState.value = UserCoursesUIState.Empty } else { _uiState.value = UserCoursesUIState.Courses(response) @@ -60,7 +77,7 @@ class UserCoursesViewModel( } } - fun updateCoursed() { + fun updateCourses() { _updating.value = true getCourses() } diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt index 029eef59b..2f19fefa9 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt @@ -73,6 +73,7 @@ import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.core.AppUpdateState import org.openedx.core.UIMessage import org.openedx.core.domain.model.Certificate +import org.openedx.core.domain.model.CourseAssignments import org.openedx.core.domain.model.CourseSharingUtmParameters import org.openedx.core.domain.model.CourseStatus import org.openedx.core.domain.model.CoursewareAccess @@ -603,6 +604,7 @@ private fun MyCoursesScreenTabletPreview() { } } +private val mockCourseAssignments = CourseAssignments(null, emptyList()) private val mockCourseEnrolled = EnrolledCourse( auditAccessExpires = Date(), created = "created", @@ -611,6 +613,7 @@ private val mockCourseEnrolled = EnrolledCourse( isActive = true, progress = Progress.DEFAULT_PROGRESS, courseStatus = CourseStatus("", emptyList(), "", ""), + courseAssignments = mockCourseAssignments, course = EnrolledCourseData( id = "id", name = "name", 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 136992f00..6e67650c4 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt @@ -48,6 +48,7 @@ import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment import androidx.lifecycle.viewmodel.compose.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel +import org.openedx.core.presentation.global.InDevelopmentScreen import org.openedx.core.ui.WindowSize import org.openedx.core.ui.WindowType import org.openedx.core.ui.crop @@ -63,7 +64,6 @@ import org.openedx.courses.presentation.UserCoursesViewModel import org.openedx.courses.presentation.UsersCourseScreen import org.openedx.dashboard.R import org.openedx.learn.LearnType -import org.openedx.programs.presentation.ProgramsScreen class LearnFragment : Fragment() { @@ -130,12 +130,14 @@ private fun LearnScreen( } ) - LearnDropdownMenu( - modifier = Modifier - .align(Alignment.Start) - .padding(horizontal = 16.dp), - pagerState = pagerState - ) + if (userCoursesViewModel.isProgramTypeWebView) { + LearnDropdownMenu( + modifier = Modifier + .align(Alignment.Start) + .padding(horizontal = 16.dp), + pagerState = pagerState + ) + } HorizontalPager( modifier = Modifier @@ -149,7 +151,7 @@ private fun LearnScreen( onItemClick = {}, ) - 1 -> ProgramsScreen() + 1 -> InDevelopmentScreen() } } } diff --git a/dashboard/src/main/res/values/strings.xml b/dashboard/src/main/res/values/strings.xml index c192b383e..864c950ad 100644 --- a/dashboard/src/main/res/values/strings.xml +++ b/dashboard/src/main/res/values/strings.xml @@ -9,4 +9,5 @@ Course %1$s Start course Resume Course + %1$d Past Due Assignment From 69c77190ea4d356d433ea2cb4bee1736bebd4c08 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Tue, 16 Apr 2024 15:12:31 +0300 Subject: [PATCH 05/23] feat: Fix future assignment date, add courses list, add onSearch and onCourse clicks --- .../java/org/openedx/core/ui/ComposeCommon.kt | 12 +- .../container/CourseContainerAdapter.kt | 2 +- .../container/CourseContainerFragment.kt | 2 +- .../container/CourseContainerViewModel.kt | 2 +- .../courses/presentation/UserCoursesScreen.kt | 126 +++++++++++++++--- .../dashboard/presentation/DashboardRouter.kt | 2 + .../learn/presentation/LearnFragment.kt | 69 +++++++--- dashboard/src/main/res/values/strings.xml | 2 + 8 files changed, 179 insertions(+), 38 deletions(-) diff --git a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt index 8e71eebcd..afb31bcea 100644 --- a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt +++ b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt @@ -902,21 +902,23 @@ fun TextIcon( icon: ImageVector, color: Color, textStyle: TextStyle = MaterialTheme.appTypography.bodySmall, + modifier: Modifier = Modifier, + iconModifier: Modifier? = null, onClick: (() -> Unit)? = null, ) { - val modifier = if (onClick == null) { - Modifier + val rowModifier = if (onClick == null) { + modifier } else { - Modifier.noRippleClickable { onClick.invoke() } + modifier.noRippleClickable { onClick.invoke() } } Row( - modifier = modifier, + modifier = rowModifier, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Text(text = text, color = color, style = textStyle) Icon( - modifier = Modifier.size((textStyle.fontSize.value + 4).dp), + modifier = iconModifier ?: Modifier.size((textStyle.fontSize.value + 4).dp), imageVector = icon, contentDescription = null, tint = color diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerAdapter.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerAdapter.kt index defa6b8a7..c9d1ffc1a 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerAdapter.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerAdapter.kt @@ -27,5 +27,5 @@ enum class CourseContainerTab(val itemId: Int, val titleResId: Int) { VIDEOS(itemId = R.id.videos, titleResId = R.string.course_navigation_videos), DISCUSSION(itemId = R.id.discussions, titleResId = R.string.course_navigation_discussions), DATES(itemId = R.id.dates, titleResId = R.string.course_navigation_dates), - HANDOUTS(itemId = R.id.resources, titleResId = R.string.course_navigation_more), + MORE(itemId = R.id.resources, titleResId = R.string.course_navigation_more), } diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt index ee4cea674..9eadc1e85 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt @@ -140,7 +140,7 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { ) ) addFragment( - Tabs.HANDOUTS, + Tabs.MORE, HandoutsFragment.newInstance(viewModel.courseId) ) } diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt index cf78bb207..b12a37fdf 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt @@ -168,7 +168,7 @@ class CourseContainerViewModel( CourseContainerTab.VIDEOS -> videoTabClickedEvent() CourseContainerTab.DISCUSSION -> discussionTabClickedEvent() CourseContainerTab.DATES -> datesTabClickedEvent() - CourseContainerTab.HANDOUTS -> handoutsTabClickedEvent() + CourseContainerTab.MORE -> handoutsTabClickedEvent() } } 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 26ef425c0..f3a6f8fe0 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt @@ -7,12 +7,16 @@ 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.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.Card @@ -26,6 +30,7 @@ import androidx.compose.material.Scaffold import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.School import androidx.compose.material.icons.filled.Warning import androidx.compose.material.pullrefresh.PullRefreshIndicator @@ -46,6 +51,7 @@ import androidx.compose.ui.platform.testTag 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.Preview import androidx.compose.ui.unit.dp import coil.compose.AsyncImage @@ -62,6 +68,7 @@ import org.openedx.core.domain.model.EnrolledCourseData import org.openedx.core.domain.model.Pagination import org.openedx.core.domain.model.Progress import org.openedx.core.ui.HandleUIMessage +import org.openedx.core.ui.TextIcon import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes @@ -75,7 +82,8 @@ import org.openedx.core.R as CoreR @Composable fun UsersCourseScreen( viewModel: UserCoursesViewModel, - onItemClick: (EnrolledCourse) -> Unit, + onCourseClick: (EnrolledCourse) -> Unit, + onViewAllClick: () -> Unit, ) { val updating by viewModel.updating.observeAsState(false) val uiMessage by viewModel.uiMessage.collectAsState(null) @@ -87,7 +95,8 @@ fun UsersCourseScreen( updating = updating, apiHostUrl = viewModel.apiHostUrl, onSwipeRefresh = viewModel::updateCourses, - onItemClick = onItemClick + onCourseClick = onCourseClick, + onViewAllClick = onViewAllClick ) } @@ -99,7 +108,8 @@ private fun UsersCourseScreen( updating: Boolean, apiHostUrl: String, onSwipeRefresh: () -> Unit, - onItemClick: (EnrolledCourse) -> Unit, + onCourseClick: (EnrolledCourse) -> Unit, + onViewAllClick: () -> Unit ) { val scaffoldState = rememberScaffoldState() val pullRefreshState = rememberPullRefreshState(refreshing = updating, onRefresh = { onSwipeRefresh() }) @@ -135,7 +145,9 @@ private fun UsersCourseScreen( UserCourses( modifier = Modifier.fillMaxSize(), userCourses = uiState.userCourses, - apiHostUrl = apiHostUrl + apiHostUrl = apiHostUrl, + onCourseClick = onCourseClick, + onViewAllClick = onViewAllClick ) } @@ -160,10 +172,13 @@ private fun UsersCourseScreen( private fun UserCourses( modifier: Modifier = Modifier, userCourses: UserCourses, - apiHostUrl: String + apiHostUrl: String, + onCourseClick: (EnrolledCourse) -> Unit, + onViewAllClick: () -> Unit ) { Column( modifier = modifier + .padding(vertical = 12.dp) ) { if (userCourses.primary != null) { PrimaryCourseCard( @@ -172,20 +187,94 @@ private fun UserCourses( ) } SecondaryCourses( - courses = userCourses.enrollments.courses + courses = userCourses.enrollments.courses, + apiHostUrl = apiHostUrl, + onCourseClick = onCourseClick, + onViewAllClick = onViewAllClick ) } } @Composable private fun SecondaryCourses( - courses: List + courses: List, + apiHostUrl: String, + onCourseClick: (EnrolledCourse) -> Unit, + onViewAllClick: () -> Unit ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp) + .padding(top = 8.dp) + ) { + TextIcon( + text = stringResource(R.string.dashboard_view_all, 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(courses) { + CourseListItem( + course = it, + apiHostUrl = apiHostUrl, + onCourseClick = onCourseClick + ) + } + } + } +} +@Composable +private fun CourseListItem( + course: EnrolledCourse, + apiHostUrl: String, + onCourseClick: (EnrolledCourse) -> Unit, +) { + Card( + modifier = Modifier + .width(140.dp) + .padding(4.dp) + .clickable { + onCourseClick(course) + }, + backgroundColor = MaterialTheme.appColors.background, + shape = MaterialTheme.appShapes.courseImageShape, + elevation = 2.dp + ) { + Column { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(apiHostUrl + course.course.courseImage) + .error(org.openedx.core.R.drawable.core_no_image_course) + .placeholder(org.openedx.core.R.drawable.core_no_image_course) + .build(), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .height(90.dp) + ) + Text( + modifier = Modifier + .fillMaxHeight() + .padding(horizontal = 4.dp, vertical = 8.dp), + text = course.course.name, + style = MaterialTheme.appTypography.titleSmall, + color = MaterialTheme.appColors.textDark, + overflow = TextOverflow.Ellipsis, + maxLines = 2 + ) + } + } } @Composable -fun AssignmentItem( +private fun AssignmentItem( painter: Painter, title: String?, info: String @@ -193,7 +282,8 @@ fun AssignmentItem( Row( modifier = Modifier .fillMaxWidth() - .padding(vertical = 12.dp, horizontal = 16.dp), + .heightIn(min = 62.dp) + .padding(vertical = 12.dp, horizontal = 14.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically ) { @@ -226,18 +316,19 @@ private fun PrimaryCourseCard( primaryCourse: EnrolledCourse, apiHostUrl: String ) { + val context = LocalContext.current Card( modifier = Modifier - .fillMaxWidth() .padding(horizontal = 16.dp) - .padding(top = 8.dp), + .fillMaxWidth() + .padding(2.dp), backgroundColor = MaterialTheme.appColors.background, shape = MaterialTheme.appShapes.courseImageShape, elevation = 4.dp ) { Column { AsyncImage( - model = ImageRequest.Builder(LocalContext.current) + model = ImageRequest.Builder(context) .data(apiHostUrl + primaryCourse.course.courseImage) .error(org.openedx.core.R.drawable.core_no_image_course) .placeholder(org.openedx.core.R.drawable.core_no_image_course) @@ -279,7 +370,11 @@ private fun PrimaryCourseCard( AssignmentItem( painter = painterResource(id = CoreR.drawable.ic_core_chapter_icon), title = futureAssignment.title, - info = "${futureAssignment.assignmentType} Due in ${futureAssignment.date} days" + info = stringResource( + R.string.dashboard_assignment_due_in_days, + futureAssignment.assignmentType ?: "", + TimeUtils.getCourseFormattedDate(context, futureAssignment.date) + ) ) } ResumeButton( @@ -291,7 +386,7 @@ private fun PrimaryCourseCard( } @Composable -fun ResumeButton( +private fun ResumeButton( modifier: Modifier = Modifier, primaryCourse: EnrolledCourse ) { @@ -472,7 +567,8 @@ private fun UsersCourseScreenPreview() { uiMessage = null, updating = false, onSwipeRefresh = { }, - onItemClick = { } + onCourseClick = { }, + onViewAllClick = { } ) } } diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt index 6cd185fa9..a40a3e7ba 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt @@ -10,4 +10,6 @@ interface DashboardRouter { courseTitle: String, enrollmentMode: String, ) + + fun navigateToCourseSearch(fm: FragmentManager, querySearch: String) } 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 6e67650c4..d9f90d019 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt @@ -47,7 +47,9 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment import androidx.lifecycle.viewmodel.compose.viewModel +import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel +import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.presentation.global.InDevelopmentScreen import org.openedx.core.ui.WindowSize import org.openedx.core.ui.WindowType @@ -63,11 +65,13 @@ import org.openedx.core.ui.windowSizeValue import org.openedx.courses.presentation.UserCoursesViewModel import org.openedx.courses.presentation.UsersCourseScreen import org.openedx.dashboard.R +import org.openedx.dashboard.presentation.DashboardRouter import org.openedx.learn.LearnType class LearnFragment : Fragment() { private val userCoursesViewModel by viewModel() + private val router by inject() override fun onCreateView( inflater: LayoutInflater, @@ -80,7 +84,23 @@ class LearnFragment : Fragment() { val windowSize = rememberWindowSize() LearnScreen( windowSize = windowSize, - userCoursesViewModel = userCoursesViewModel + userCoursesViewModel = userCoursesViewModel, + onCourseClick = { + router.navigateToCourseOutline( + requireParentFragment().parentFragmentManager, + it.course.id, + it.course.name, + it.mode + ) + }, + onViewAllClick = { + //TODO + }, + onSearchClick = { + router.navigateToCourseSearch( + requireParentFragment().parentFragmentManager, "" + ) + } ) } } @@ -90,8 +110,11 @@ class LearnFragment : Fragment() { @OptIn(ExperimentalFoundationApi::class) @Composable private fun LearnScreen( - userCoursesViewModel : UserCoursesViewModel, - windowSize: WindowSize + windowSize: WindowSize, + userCoursesViewModel: UserCoursesViewModel, + onCourseClick: (course: EnrolledCourse) -> Unit, + onViewAllClick: () -> Unit, + onSearchClick: () -> Unit, ) { val scaffoldState = rememberScaffoldState() val pagerState = rememberPagerState { @@ -125,9 +148,7 @@ private fun LearnScreen( modifier = Modifier .padding(horizontal = 16.dp), label = stringResource(id = R.string.dashboard_learn), - onSearchClick = { - //TODO - } + onSearchClick = onSearchClick ) if (userCoursesViewModel.isProgramTypeWebView) { @@ -148,7 +169,8 @@ private fun LearnScreen( when (page) { 0 -> UsersCourseScreen( viewModel = userCoursesViewModel, - onItemClick = {}, + onCourseClick = onCourseClick, + onViewAllClick = onViewAllClick ) 1 -> InDevelopmentScreen() @@ -212,13 +234,16 @@ private fun LearnDropdownMenu( modifier = modifier ) { Row( - modifier = Modifier.noRippleClickable { - expanded = true - } + modifier = Modifier + .noRippleClickable { + expanded = true + }, + verticalAlignment = Alignment.CenterVertically ) { Text( text = stringResource(id = currentValue.title), color = MaterialTheme.appColors.textDark, + style = MaterialTheme.appTypography.titleSmall ) Icon( imageVector = Icons.Default.ExpandMore, @@ -239,17 +264,28 @@ private fun LearnDropdownMenu( onDismissRequest = { expanded = false } ) { for (learnType in LearnType.entries) { + val background: Color + val textColor: Color + if (currentValue == learnType) { + background = MaterialTheme.appColors.primary + textColor = MaterialTheme.appColors.buttonText + } else { + background = Color.Transparent + textColor = MaterialTheme.appColors.textDark + } DropdownMenuItem( modifier = Modifier - .background( - if (currentValue == learnType) MaterialTheme.appColors.primary else Color.Transparent - ), + .background(background), onClick = { currentValue = learnType expanded = false } ) { - Text(stringResource(id = learnType.title)) + Text( + text = stringResource(id = learnType.title), + style = MaterialTheme.appTypography.titleSmall, + color = textColor + ) } } } @@ -266,7 +302,10 @@ private fun LearnScreenPreview() { OpenEdXTheme { LearnScreen( userCoursesViewModel = viewModel(), - windowSize = WindowSize(WindowType.Compact, WindowType.Compact) + windowSize = WindowSize(WindowType.Compact, WindowType.Compact), + onCourseClick = {}, + onViewAllClick = {}, + onSearchClick = {} ) } } diff --git a/dashboard/src/main/res/values/strings.xml b/dashboard/src/main/res/values/strings.xml index 864c950ad..a370880ce 100644 --- a/dashboard/src/main/res/values/strings.xml +++ b/dashboard/src/main/res/values/strings.xml @@ -10,4 +10,6 @@ Start course Resume Course %1$d Past Due Assignment + View All (%1$d) + %1$s Due in %2$s From 7854c1c05787b27c8feb9db5799894e3e75b6a5c Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Tue, 16 Apr 2024 17:58:43 +0300 Subject: [PATCH 06/23] feat: Add feature flag for enabling new/old dashboard screen, add UserCoursesScreen onClick methods --- .../main/java/org/openedx/app/AppRouter.kt | 4 +- .../main/java/org/openedx/app/MainFragment.kt | 17 ++- .../openedx/core/CourseContainerTabEntity.kt | 9 ++ .../java/org/openedx/core/config/Config.kt | 5 + .../container/CourseContainerAdapter.kt | 22 ++- .../container/CourseContainerFragment.kt | 9 +- .../courses/presentation/UserCoursesScreen.kt | 132 +++++++++++++----- .../presentation/DashboardFragment.kt | 4 +- .../dashboard/presentation/DashboardRouter.kt | 11 ++ .../learn/presentation/LearnFragment.kt | 41 +++++- default_config/dev/config.yaml | 2 + default_config/prod/config.yaml | 2 + default_config/stage/config.yaml | 2 + .../discovery/presentation/DiscoveryRouter.kt | 7 +- 14 files changed, 211 insertions(+), 56 deletions(-) create mode 100644 core/src/main/java/org/openedx/core/CourseContainerTabEntity.kt diff --git a/app/src/main/java/org/openedx/app/AppRouter.kt b/app/src/main/java/org/openedx/app/AppRouter.kt index 474f4a8e9..c9a09a100 100644 --- a/app/src/main/java/org/openedx/app/AppRouter.kt +++ b/app/src/main/java/org/openedx/app/AppRouter.kt @@ -8,6 +8,7 @@ import org.openedx.auth.presentation.logistration.LogistrationFragment import org.openedx.auth.presentation.restore.RestorePasswordFragment import org.openedx.auth.presentation.signin.SignInFragment import org.openedx.auth.presentation.signup.SignUpFragment +import org.openedx.core.CourseContainerTabEntity import org.openedx.core.FragmentViewType import org.openedx.core.presentation.course.CourseViewMode import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRouter @@ -136,10 +137,11 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di courseId: String, courseTitle: String, enrollmentMode: String, + openTab: CourseContainerTabEntity ) { replaceFragmentWithBackStack( fm, - CourseContainerFragment.newInstance(courseId, courseTitle, enrollmentMode) + CourseContainerFragment.newInstance(courseId, courseTitle, enrollmentMode, openTab) ) } diff --git a/app/src/main/java/org/openedx/app/MainFragment.kt b/app/src/main/java/org/openedx/app/MainFragment.kt index 7e20f474d..911addb26 100644 --- a/app/src/main/java/org/openedx/app/MainFragment.kt +++ b/app/src/main/java/org/openedx/app/MainFragment.kt @@ -16,6 +16,7 @@ import org.openedx.app.databinding.FragmentMainBinding import org.openedx.core.config.Config import org.openedx.core.presentation.global.app_upgrade.UpgradeRequiredFragment import org.openedx.core.presentation.global.viewBinding +import org.openedx.dashboard.presentation.DashboardFragment import org.openedx.discovery.presentation.DiscoveryNavigator import org.openedx.discovery.presentation.DiscoveryRouter import org.openedx.learn.presentation.LearnFragment @@ -48,17 +49,17 @@ class MainFragment : Fragment(R.layout.fragment_main) { when (it.itemId) { R.id.fragmentLearn -> { viewModel.logMyCoursesTabClickedEvent() - binding.viewPager.setCurrentItem(1, false) + binding.viewPager.setCurrentItem(0, false) } R.id.fragmentHome -> { viewModel.logDiscoveryTabClickedEvent() - binding.viewPager.setCurrentItem(0, false) + binding.viewPager.setCurrentItem(1, false) } R.id.fragmentProfile -> { viewModel.logProfileTabClickedEvent() - binding.viewPager.setCurrentItem(3, false) + binding.viewPager.setCurrentItem(2, false) } } true @@ -99,12 +100,16 @@ class MainFragment : Fragment(R.layout.fragment_main) { binding.viewPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL binding.viewPager.offscreenPageLimit = 4 - val discoveryFragment = DiscoveryNavigator(viewModel.isDiscoveryTypeWebView) - .getDiscoveryFragment() + val discoveryFragment = DiscoveryNavigator(viewModel.isDiscoveryTypeWebView).getDiscoveryFragment() + val dashboardFragment = if (config.isDashboardNewScreenEnabled()) { + LearnFragment() + } else { + DashboardFragment() + } adapter = MainNavigationFragmentAdapter(this).apply { + addFragment(dashboardFragment) addFragment(discoveryFragment) - addFragment(LearnFragment()) addFragment(ProfileFragment()) } binding.viewPager.adapter = adapter diff --git a/core/src/main/java/org/openedx/core/CourseContainerTabEntity.kt b/core/src/main/java/org/openedx/core/CourseContainerTabEntity.kt new file mode 100644 index 000000000..bcee74aaa --- /dev/null +++ b/core/src/main/java/org/openedx/core/CourseContainerTabEntity.kt @@ -0,0 +1,9 @@ +package org.openedx.core + +enum class CourseContainerTabEntity { + COURSE, + VIDEOS, + DISCUSSION, + DATES, + MORE; +} \ No newline at end of file diff --git a/core/src/main/java/org/openedx/core/config/Config.kt b/core/src/main/java/org/openedx/core/config/Config.kt index 9f626cc2e..dffd8d167 100644 --- a/core/src/main/java/org/openedx/core/config/Config.kt +++ b/core/src/main/java/org/openedx/core/config/Config.kt @@ -119,6 +119,10 @@ class Config(context: Context) { return getBoolean(COURSE_UNIT_PROGRESS_ENABLED, false) } + fun isDashboardNewScreenEnabled(): Boolean { + return getBoolean(DASHBOARD_NEW_SCREEN_ENABLED, false) + } + private fun getString(key: String, defaultValue: String): String { val element = getObject(key) return if (element != null) { @@ -178,6 +182,7 @@ class Config(context: Context) { private const val COURSE_TOP_TAB_BAR_ENABLED = "COURSE_TOP_TAB_BAR_ENABLED" private const val COURSE_UNIT_PROGRESS_ENABLED = "COURSE_UNIT_PROGRESS_ENABLED" private const val PLATFORM_NAME = "PLATFORM_NAME" + private const val DASHBOARD_NEW_SCREEN_ENABLED = "DASHBOARD_NEW_SCREEN_ENABLED" } enum class ViewType { diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerAdapter.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerAdapter.kt index c9d1ffc1a..e24a5dfef 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerAdapter.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerAdapter.kt @@ -1,7 +1,10 @@ package org.openedx.course.presentation.container +import android.os.Parcelable import androidx.fragment.app.Fragment import androidx.viewpager2.adapter.FragmentStateAdapter +import kotlinx.parcelize.Parcelize +import org.openedx.core.CourseContainerTabEntity import org.openedx.course.R class CourseContainerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) { @@ -11,7 +14,7 @@ class CourseContainerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment override fun getItemCount(): Int = fragments.size override fun createFragment(position: Int): Fragment { - val tab = CourseContainerTab.values().find { it.ordinal == position } + val tab = CourseContainerTab.entries.find { it.ordinal == position } return fragments[tab] ?: throw IllegalStateException("Fragment not found for tab $tab") } @@ -22,10 +25,23 @@ class CourseContainerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment fun getFragment(tab: CourseContainerTab): Fragment? = fragments[tab] } -enum class CourseContainerTab(val itemId: Int, val titleResId: Int) { +@Parcelize +enum class CourseContainerTab(val itemId: Int, val titleResId: Int): Parcelable { COURSE(itemId = R.id.course, titleResId = R.string.course_navigation_course), VIDEOS(itemId = R.id.videos, titleResId = R.string.course_navigation_videos), DISCUSSION(itemId = R.id.discussions, titleResId = R.string.course_navigation_discussions), DATES(itemId = R.id.dates, titleResId = R.string.course_navigation_dates), - MORE(itemId = R.id.resources, titleResId = R.string.course_navigation_more), + MORE(itemId = R.id.resources, titleResId = R.string.course_navigation_more); + + companion object { + fun fromEntity(courseContainerTabEntity: CourseContainerTabEntity): CourseContainerTab { + return when (courseContainerTabEntity) { + CourseContainerTabEntity.COURSE -> COURSE + CourseContainerTabEntity.VIDEOS -> VIDEOS + CourseContainerTabEntity.DISCUSSION -> DISCUSSION + CourseContainerTabEntity.DATES -> DATES + CourseContainerTabEntity.MORE -> MORE + } + } + } } diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt index 9eadc1e85..0830bb8e9 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt @@ -17,6 +17,7 @@ import com.google.android.material.tabs.TabLayoutMediator import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf +import org.openedx.core.extension.parcelable import org.openedx.core.extension.takeIfNotEmpty import org.openedx.core.presentation.global.viewBinding import org.openedx.core.ui.theme.OpenEdXTheme @@ -177,6 +178,9 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { } binding.bottomNavView.isVisible = true } + + val openTab = requireArguments().parcelable(ARG_OPEN_TAB) ?: CourseContainerTab.COURSE + binding.viewPager.setCurrentItem(openTab.ordinal, false) viewModel.courseContainerTabClickedEvent(Tabs.entries[binding.viewPager.currentItem]) } @@ -290,16 +294,19 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { private const val ARG_COURSE_ID = "courseId" private const val ARG_TITLE = "title" private const val ARG_ENROLLMENT_MODE = "enrollmentMode" + private const val ARG_OPEN_TAB = "openTab" fun newInstance( courseId: String, courseTitle: String, enrollmentMode: String, + openTab: org.openedx.core.CourseContainerTabEntity ): CourseContainerFragment { val fragment = CourseContainerFragment() fragment.arguments = bundleOf( ARG_COURSE_ID to courseId, ARG_TITLE to courseTitle, - ARG_ENROLLMENT_MODE to enrollmentMode + ARG_ENROLLMENT_MODE to enrollmentMode, + ARG_OPEN_TAB to CourseContainerTab.fromEntity(openTab) ) return fragment } 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 f3a6f8fe0..dca02acc0 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.Card import androidx.compose.material.CircularProgressIndicator @@ -31,6 +32,7 @@ import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ChevronRight +import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.School import androidx.compose.material.icons.filled.Warning import androidx.compose.material.pullrefresh.PullRefreshIndicator @@ -43,6 +45,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.layout.ContentScale @@ -83,7 +86,9 @@ import org.openedx.core.R as CoreR fun UsersCourseScreen( viewModel: UserCoursesViewModel, onCourseClick: (EnrolledCourse) -> Unit, + openDates: (EnrolledCourse) -> Unit, onViewAllClick: () -> Unit, + onResumeClick: (componentId: String) -> Unit, ) { val updating by viewModel.updating.observeAsState(false) val uiMessage by viewModel.uiMessage.collectAsState(null) @@ -96,7 +101,9 @@ fun UsersCourseScreen( apiHostUrl = viewModel.apiHostUrl, onSwipeRefresh = viewModel::updateCourses, onCourseClick = onCourseClick, - onViewAllClick = onViewAllClick + onViewAllClick = onViewAllClick, + openDates = openDates, + onResumeClick = onResumeClick ) } @@ -109,7 +116,9 @@ private fun UsersCourseScreen( apiHostUrl: String, onSwipeRefresh: () -> Unit, onCourseClick: (EnrolledCourse) -> Unit, - onViewAllClick: () -> Unit + openDates: (EnrolledCourse) -> Unit, + onViewAllClick: () -> Unit, + onResumeClick: (componentId: String) -> Unit, ) { val scaffoldState = rememberScaffoldState() val pullRefreshState = rememberPullRefreshState(refreshing = updating, onRefresh = { onSwipeRefresh() }) @@ -147,7 +156,9 @@ private fun UsersCourseScreen( userCourses = uiState.userCourses, apiHostUrl = apiHostUrl, onCourseClick = onCourseClick, - onViewAllClick = onViewAllClick + onViewAllClick = onViewAllClick, + openDates = openDates, + onResumeClick = onResumeClick ) } @@ -174,7 +185,9 @@ private fun UserCourses( userCourses: UserCourses, apiHostUrl: String, onCourseClick: (EnrolledCourse) -> Unit, - onViewAllClick: () -> Unit + openDates: (EnrolledCourse) -> Unit, + onViewAllClick: () -> Unit, + onResumeClick: (componentId: String) -> Unit, ) { Column( modifier = modifier @@ -183,7 +196,10 @@ private fun UserCourses( if (userCourses.primary != null) { PrimaryCourseCard( primaryCourse = userCourses.primary, - apiHostUrl = apiHostUrl + apiHostUrl = apiHostUrl, + openDates = openDates, + onResumeClick = onResumeClick, + onCourseClick = onCourseClick ) } SecondaryCourses( @@ -246,41 +262,60 @@ private fun CourseListItem( shape = MaterialTheme.appShapes.courseImageShape, elevation = 2.dp ) { - Column { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(apiHostUrl + course.course.courseImage) - .error(org.openedx.core.R.drawable.core_no_image_course) - .placeholder(org.openedx.core.R.drawable.core_no_image_course) - .build(), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxWidth() - .height(90.dp) - ) - Text( - modifier = Modifier - .fillMaxHeight() - .padding(horizontal = 4.dp, vertical = 8.dp), - text = course.course.name, - style = MaterialTheme.appTypography.titleSmall, - color = MaterialTheme.appColors.textDark, - overflow = TextOverflow.Ellipsis, - maxLines = 2 - ) + Box { + Column { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(apiHostUrl + course.course.courseImage) + .error(org.openedx.core.R.drawable.core_no_image_course) + .placeholder(org.openedx.core.R.drawable.core_no_image_course) + .build(), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .height(90.dp) + ) + Text( + modifier = Modifier + .fillMaxHeight() + .padding(horizontal = 4.dp, vertical = 8.dp), + text = course.course.name, + style = MaterialTheme.appTypography.titleSmall, + color = MaterialTheme.appColors.textDark, + overflow = TextOverflow.Ellipsis, + maxLines = 2 + ) + } + if (!course.course.coursewareAccess?.errorCode.isNullOrEmpty()) { + Icon( + modifier = Modifier + .size(32.dp) + .padding(top = 8.dp, end = 8.dp) + .background( + color = Color.White, + shape = CircleShape + ) + .padding(4.dp) + .align(Alignment.TopEnd), + imageVector = Icons.Default.Lock, + contentDescription = null, + tint = MaterialTheme.appColors.textWarning + ) + } } } } @Composable private fun AssignmentItem( + modifier: Modifier = Modifier, painter: Painter, title: String?, info: String ) { Row( - modifier = Modifier + modifier = modifier .fillMaxWidth() .heightIn(min = 62.dp) .padding(vertical = 12.dp, horizontal = 14.dp), @@ -314,7 +349,10 @@ private fun AssignmentItem( @Composable private fun PrimaryCourseCard( primaryCourse: EnrolledCourse, - apiHostUrl: String + apiHostUrl: String, + openDates: (EnrolledCourse) -> Unit, + onResumeClick: (componentId: String) -> Unit, + onCourseClick: (EnrolledCourse) -> Unit, ) { val context = LocalContext.current Card( @@ -359,6 +397,13 @@ private fun PrimaryCourseCard( val title = if (pastAssignments.size == 1) pastAssignments.first().title else null Divider() AssignmentItem( + modifier = Modifier.clickable { + if (pastAssignments.size == 1) { + onResumeClick(pastAssignments.first().blockId) + } else { + openDates(primaryCourse) + } + }, painter = rememberVectorPainter(Icons.Default.Warning), title = title, info = stringResource(R.string.dashboard_past_due_assignment, pastAssignments.size) @@ -368,6 +413,9 @@ private fun PrimaryCourseCard( if (futureAssignment != null) { Divider() AssignmentItem( + modifier = Modifier.clickable { + onResumeClick(futureAssignment.blockId) + }, painter = painterResource(id = CoreR.drawable.ic_core_chapter_icon), title = futureAssignment.title, info = stringResource( @@ -378,8 +426,14 @@ private fun PrimaryCourseCard( ) } ResumeButton( - modifier = Modifier.fillMaxWidth(), - primaryCourse = primaryCourse + primaryCourse = primaryCourse, + onClick = { + if (primaryCourse.courseStatus == null) { + onCourseClick(primaryCourse) + } else { + onResumeClick(primaryCourse.courseStatus?.lastVisitedBlockId ?: "") + } + } ) } } @@ -388,15 +442,15 @@ private fun PrimaryCourseCard( @Composable private fun ResumeButton( modifier: Modifier = Modifier, - primaryCourse: EnrolledCourse + primaryCourse: EnrolledCourse, + onClick: () -> Unit ) { Row( modifier = modifier + .fillMaxWidth() + .clickable { onClick() } .heightIn(min = 60.dp) .background(MaterialTheme.appColors.primary) - .clickable { - //TODO - } .padding(12.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp) @@ -515,7 +569,7 @@ private val mockCourse = EnrolledCourse( mode = "mode", isActive = true, progress = Progress.DEFAULT_PROGRESS, - courseStatus = CourseStatus("", emptyList(), "", ""), + courseStatus = CourseStatus("", emptyList(), "", "Unit name"), courseAssignments = mockCourseAssignments, course = EnrolledCourseData( id = "id", @@ -568,7 +622,9 @@ private fun UsersCourseScreenPreview() { updating = false, onSwipeRefresh = { }, onCourseClick = { }, - onViewAllClick = { } + onViewAllClick = { }, + openDates = { }, + onResumeClick = { } ) } } diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt index 2f19fefa9..ed764348b 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt @@ -71,6 +71,7 @@ import coil.request.ImageRequest import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.core.AppUpdateState +import org.openedx.core.CourseContainerTabEntity import org.openedx.core.UIMessage import org.openedx.core.domain.model.Certificate import org.openedx.core.domain.model.CourseAssignments @@ -142,7 +143,8 @@ class DashboardFragment : Fragment() { requireParentFragment().parentFragmentManager, it.course.id, it.course.name, - it.mode + it.mode, + CourseContainerTabEntity.COURSE ) }, onSwipeRefresh = { diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt index a40a3e7ba..f24953d82 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt @@ -1,6 +1,8 @@ package org.openedx.dashboard.presentation import androidx.fragment.app.FragmentManager +import org.openedx.core.CourseContainerTabEntity +import org.openedx.core.presentation.course.CourseViewMode interface DashboardRouter { @@ -9,6 +11,15 @@ interface DashboardRouter { courseId: String, courseTitle: String, enrollmentMode: String, + openTab: CourseContainerTabEntity + ) + + fun navigateToCourseContainer( + fm: FragmentManager, + courseId: String, + unitId: String, + componentId: String, + mode: CourseViewMode ) fun navigateToCourseSearch(fm: FragmentManager, querySearch: String) 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 d9f90d019..088e6ff97 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt @@ -7,6 +7,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -49,13 +50,13 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.viewmodel.compose.viewModel import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel +import org.openedx.core.CourseContainerTabEntity import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.presentation.global.InDevelopmentScreen import org.openedx.core.ui.WindowSize import org.openedx.core.ui.WindowType import org.openedx.core.ui.crop import org.openedx.core.ui.displayCutoutForLandscape -import org.openedx.core.ui.noRippleClickable import org.openedx.core.ui.rememberWindowSize import org.openedx.core.ui.statusBarsInset import org.openedx.core.ui.theme.OpenEdXTheme @@ -90,9 +91,33 @@ class LearnFragment : Fragment() { requireParentFragment().parentFragmentManager, it.course.id, it.course.name, - it.mode + it.mode, + CourseContainerTabEntity.COURSE ) }, + openDates = { + router.navigateToCourseOutline( + requireParentFragment().parentFragmentManager, + it.course.id, + it.course.name, + it.mode, + CourseContainerTabEntity.DATES + ) + }, + onResumeClick = { componentId -> +// viewModel.resumeSectionBlock?.let { subSection -> +// viewModel.resumeCourseTappedEvent(subSection.id) +// viewModel.resumeVerticalBlock?.let { unit -> +// router.navigateToCourseContainer( +// fm = requireActivity().supportFragmentManager, +// courseId = viewModel.courseId, +// unitId = unit.id, +// componentId = componentId, +// mode = CourseViewMode.FULL +// ) +// } +// } + }, onViewAllClick = { //TODO }, @@ -113,6 +138,8 @@ private fun LearnScreen( windowSize: WindowSize, userCoursesViewModel: UserCoursesViewModel, onCourseClick: (course: EnrolledCourse) -> Unit, + openDates: (course: EnrolledCourse) -> Unit, + onResumeClick: (componentId: String) -> Unit, onViewAllClick: () -> Unit, onSearchClick: () -> Unit, ) { @@ -170,7 +197,9 @@ private fun LearnScreen( 0 -> UsersCourseScreen( viewModel = userCoursesViewModel, onCourseClick = onCourseClick, - onViewAllClick = onViewAllClick + onViewAllClick = onViewAllClick, + openDates = openDates, + onResumeClick = onResumeClick ) 1 -> InDevelopmentScreen() @@ -235,7 +264,7 @@ private fun LearnDropdownMenu( ) { Row( modifier = Modifier - .noRippleClickable { + .clickable { expanded = true }, verticalAlignment = Alignment.CenterVertically @@ -305,7 +334,9 @@ private fun LearnScreenPreview() { windowSize = WindowSize(WindowType.Compact, WindowType.Compact), onCourseClick = {}, onViewAllClick = {}, - onSearchClick = {} + onSearchClick = {}, + openDates = {}, + onResumeClick = {} ) } } diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml index 59372a8ef..bc016de0c 100644 --- a/default_config/dev/config.yaml +++ b/default_config/dev/config.yaml @@ -80,4 +80,6 @@ COURSE_NESTED_LIST_ENABLED: false COURSE_BANNER_ENABLED: true COURSE_TOP_TAB_BAR_ENABLED: false COURSE_UNIT_PROGRESS_ENABLED: false +#Dashboard feature flags +DASHBOARD_NEW_SCREEN_ENABLED: true diff --git a/default_config/prod/config.yaml b/default_config/prod/config.yaml index 423ca0b3c..5d9004490 100644 --- a/default_config/prod/config.yaml +++ b/default_config/prod/config.yaml @@ -80,3 +80,5 @@ COURSE_NESTED_LIST_ENABLED: false COURSE_BANNER_ENABLED: true COURSE_TOP_TAB_BAR_ENABLED: false COURSE_UNIT_PROGRESS_ENABLED: false +#Dashboard feature flags +DASHBOARD_NEW_SCREEN_ENABLED: true diff --git a/default_config/stage/config.yaml b/default_config/stage/config.yaml index 423ca0b3c..5d9004490 100644 --- a/default_config/stage/config.yaml +++ b/default_config/stage/config.yaml @@ -80,3 +80,5 @@ COURSE_NESTED_LIST_ENABLED: false COURSE_BANNER_ENABLED: true COURSE_TOP_TAB_BAR_ENABLED: false COURSE_UNIT_PROGRESS_ENABLED: false +#Dashboard feature flags +DASHBOARD_NEW_SCREEN_ENABLED: true diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryRouter.kt b/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryRouter.kt index c1b1c423d..236802455 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryRouter.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryRouter.kt @@ -1,11 +1,16 @@ package org.openedx.discovery.presentation import androidx.fragment.app.FragmentManager +import org.openedx.core.CourseContainerTabEntity interface DiscoveryRouter { fun navigateToCourseOutline( - fm: FragmentManager, courseId: String, courseTitle: String, enrollmentMode: String + fm: FragmentManager, + courseId: String, + courseTitle: String, + enrollmentMode: String, + openTab: CourseContainerTabEntity = CourseContainerTabEntity.COURSE ) fun navigateToLogistration(fm: FragmentManager, courseId: String?) From f86fb519e9db9128e898c4a0c1cf3f3f90c6057c Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Wed, 17 Apr 2024 13:44:09 +0300 Subject: [PATCH 07/23] feat: Create AllEnrolledCoursesFragment. Add endpoint parameters --- .../main/java/org/openedx/app/AppRouter.kt | 5 + .../java/org/openedx/app/di/ScreenModule.kt | 2 + .../org/openedx/core/data/api/CourseApi.kt | 5 +- .../java/org/openedx/core/ui/ComposeCommon.kt | 2 +- .../AllEnrolledCoursesFragment.kt | 649 ++++++++++++++++++ .../presentation/AllEnrolledCoursesUIState.kt | 9 + .../AllEnrolledCoursesViewModel.kt | 182 +++++ .../dashboard/presentation/DashboardRouter.kt | 11 +- .../learn/presentation/LearnFragment.kt | 4 +- 9 files changed, 857 insertions(+), 12 deletions(-) create mode 100644 dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt create mode 100644 dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesUIState.kt create mode 100644 dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt diff --git a/app/src/main/java/org/openedx/app/AppRouter.kt b/app/src/main/java/org/openedx/app/AppRouter.kt index c9a09a100..65a78a99d 100644 --- a/app/src/main/java/org/openedx/app/AppRouter.kt +++ b/app/src/main/java/org/openedx/app/AppRouter.kt @@ -26,6 +26,7 @@ import org.openedx.course.presentation.unit.container.CourseUnitContainerFragmen import org.openedx.course.presentation.unit.video.VideoFullScreenFragment import org.openedx.course.presentation.unit.video.YoutubeVideoFullScreenFragment import org.openedx.course.settings.download.DownloadQueueFragment +import org.openedx.courses.presentation.AllEnrolledCoursesFragment import org.openedx.dashboard.presentation.DashboardRouter import org.openedx.discovery.presentation.DiscoveryRouter import org.openedx.discovery.presentation.NativeDiscoveryFragment @@ -121,6 +122,10 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di replaceFragmentWithBackStack(fm, UpgradeRequiredFragment()) } + override fun navigateToAllEnrolledCourses(fm: FragmentManager) { + replaceFragmentWithBackStack(fm, AllEnrolledCoursesFragment()) + } + override fun navigateToCourseInfo( fm: FragmentManager, courseId: String, diff --git a/app/src/main/java/org/openedx/app/di/ScreenModule.kt b/app/src/main/java/org/openedx/app/di/ScreenModule.kt index 92a8d7e1e..497e7f89f 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -29,6 +29,7 @@ import org.openedx.course.presentation.unit.video.VideoUnitViewModel import org.openedx.course.presentation.unit.video.VideoViewModel import org.openedx.course.presentation.videos.CourseVideoViewModel import org.openedx.course.settings.download.DownloadQueueViewModel +import org.openedx.courses.presentation.AllEnrolledCoursesViewModel import org.openedx.courses.presentation.UserCoursesViewModel import org.openedx.dashboard.data.repository.DashboardRepository import org.openedx.dashboard.domain.interactor.DashboardInteractor @@ -117,6 +118,7 @@ val screenModule = module { factory { DashboardInteractor(get()) } viewModel { DashboardViewModel(get(), get(), get(), get(), get(), get(), get()) } viewModel { UserCoursesViewModel(get(), get(), get(), get()) } + viewModel { AllEnrolledCoursesViewModel(get(), get(), get(), get(), get(), get(), get()) } factory { DiscoveryRepository(get(), get(), get()) } factory { DiscoveryInteractor(get()) } 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 68eadeab4..00a4b520d 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 @@ -70,6 +70,9 @@ interface CourseApi { @GET("/api/mobile/v4/users/{username}/course_enrollments/") suspend fun getUserCourses( - @Path("username") username: String + @Path("username") username: String, + @Query("page") page: Int = 1, + @Query("status") status: String? = null, + @Query("requested_fields") fields: List = listOf("progress") ): CourseEnrollments } diff --git a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt index afb31bcea..53f039d7d 100644 --- a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt +++ b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt @@ -909,7 +909,7 @@ fun TextIcon( val rowModifier = if (onClick == null) { modifier } else { - modifier.noRippleClickable { onClick.invoke() } + modifier.clickable { onClick.invoke() } } Row( modifier = rowModifier, diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt new file mode 100644 index 000000000..04a81c4c1 --- /dev/null +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt @@ -0,0 +1,649 @@ +package org.openedx.courses.presentation + +import android.content.res.Configuration.UI_MODE_NIGHT_NO +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +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 +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.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Divider +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForward +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId +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.compose.ui.unit.dp +import androidx.fragment.app.Fragment +import coil.compose.AsyncImage +import coil.request.ImageRequest +import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.openedx.core.AppUpdateState +import org.openedx.core.CourseContainerTabEntity +import org.openedx.core.UIMessage +import org.openedx.core.domain.model.Certificate +import org.openedx.core.domain.model.CourseAssignments +import org.openedx.core.domain.model.CourseSharingUtmParameters +import org.openedx.core.domain.model.CourseStatus +import org.openedx.core.domain.model.CoursewareAccess +import org.openedx.core.domain.model.EnrolledCourse +import org.openedx.core.domain.model.EnrolledCourseData +import org.openedx.core.domain.model.Progress +import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRecommendedBox +import org.openedx.core.system.notifier.AppUpgradeEvent +import org.openedx.core.ui.HandleUIMessage +import org.openedx.core.ui.OfflineModeDialog +import org.openedx.core.ui.Toolbar +import org.openedx.core.ui.WindowSize +import org.openedx.core.ui.WindowType +import org.openedx.core.ui.displayCutoutForLandscape +import org.openedx.core.ui.rememberWindowSize +import org.openedx.core.ui.shouldLoadMore +import org.openedx.core.ui.statusBarsInset +import org.openedx.core.ui.theme.OpenEdXTheme +import org.openedx.core.ui.theme.appColors +import org.openedx.core.ui.theme.appShapes +import org.openedx.core.ui.theme.appTypography +import org.openedx.core.ui.windowSizeValue +import org.openedx.core.utils.TimeUtils +import org.openedx.dashboard.R +import org.openedx.dashboard.presentation.DashboardRouter +import java.util.Date + +class AllEnrolledCoursesFragment : Fragment() { + + private val viewModel by viewModel() + private val router by inject() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + lifecycle.addObserver(viewModel) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ) = ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + OpenEdXTheme { + val windowSize = rememberWindowSize() + val uiState by viewModel.uiState.observeAsState() + val uiMessage by viewModel.uiMessage.observeAsState() + val refreshing by viewModel.updating.observeAsState(false) + val canLoadMore by viewModel.canLoadMore.observeAsState(false) + val appUpgradeEvent by viewModel.appUpgradeEvent.observeAsState() + + AllEnrolledCoursesScreen( + windowSize = windowSize, + viewModel.apiHostUrl, + uiState!!, + uiMessage, + canLoadMore = canLoadMore, + refreshing = refreshing, + hasInternetConnection = viewModel.hasInternetConnection, + onReloadClick = { + viewModel.getCourses() + }, + onItemClick = { + viewModel.dashboardCourseClickedEvent(it.course.id, it.course.name) + router.navigateToCourseOutline( + requireParentFragment().parentFragmentManager, + it.course.id, + it.course.name, + it.mode, + CourseContainerTabEntity.COURSE + ) + }, + onSwipeRefresh = { + viewModel.updateCourses() + }, + paginationCallback = { + viewModel.fetchMore() + }, + appUpgradeParameters = AppUpdateState.AppUpgradeParameters( + appUpgradeEvent = appUpgradeEvent, + onAppUpgradeRecommendedBoxClick = { + AppUpdateState.openPlayMarket(requireContext()) + }, + ), + ) + } + } + } +} + +@OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class) +@Composable +internal fun AllEnrolledCoursesScreen( + windowSize: WindowSize, + apiHostUrl: String, + state: AllEnrolledCoursesUIState, + uiMessage: UIMessage?, + canLoadMore: Boolean, + refreshing: Boolean, + hasInternetConnection: Boolean, + onReloadClick: () -> Unit, + onSwipeRefresh: () -> Unit, + paginationCallback: () -> Unit, + onItemClick: (EnrolledCourse) -> Unit, + appUpgradeParameters: AppUpdateState.AppUpgradeParameters, +) { + val scaffoldState = rememberScaffoldState() + val pullRefreshState = + rememberPullRefreshState(refreshing = refreshing, onRefresh = { onSwipeRefresh() }) + + var isInternetConnectionShown by rememberSaveable { + mutableStateOf(false) + } + val scrollState = rememberLazyListState() + val firstVisibleIndex = remember { + mutableStateOf(scrollState.firstVisibleItemIndex) + } + + Scaffold( + scaffoldState = scaffoldState, + modifier = Modifier + .fillMaxSize() + .semantics { + testTagsAsResourceId = true + }, + backgroundColor = MaterialTheme.appColors.background + ) { paddingValues -> + + val contentPaddings by remember(key1 = windowSize) { + mutableStateOf( + windowSize.windowSizeValue( + expanded = PaddingValues( + top = 32.dp, + bottom = 40.dp + ), + compact = PaddingValues(horizontal = 24.dp, vertical = 24.dp) + ) + ) + } + + val emptyStatePaddings by remember(key1 = windowSize) { + mutableStateOf( + windowSize.windowSizeValue( + expanded = Modifier.padding( + top = 32.dp, + bottom = 40.dp + ), + compact = Modifier.padding(horizontal = 24.dp, vertical = 24.dp) + ) + ) + } + + val contentWidth by remember(key1 = windowSize) { + mutableStateOf( + windowSize.windowSizeValue( + expanded = Modifier.widthIn(Dp.Unspecified, 560.dp), + compact = Modifier.fillMaxWidth(), + ) + ) + } + + HandleUIMessage(uiMessage = uiMessage, scaffoldState = scaffoldState) + + Column( + modifier = Modifier + .padding(paddingValues) + .statusBarsInset() + .displayCutoutForLandscape(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Toolbar(label = stringResource(id = R.string.dashboard_title)) + + Surface( + color = MaterialTheme.appColors.background, + shape = MaterialTheme.appShapes.screenBackgroundShape + ) { + Box( + Modifier + .fillMaxWidth() + .pullRefresh(pullRefreshState), + ) { + when (state) { + is AllEnrolledCoursesUIState.Loading -> { + Box( + Modifier + .fillMaxSize(), contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = MaterialTheme.appColors.primary) + } + } + + is AllEnrolledCoursesUIState.Courses -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + LazyColumn( + modifier = Modifier + .fillMaxHeight() + .then(contentWidth), + state = scrollState, + contentPadding = contentPaddings, + content = { + item() { + Column { + Header() + Spacer(modifier = Modifier.height(16.dp)) + } + } + items(state.courses) { course -> + CourseItem( + apiHostUrl, + course, + windowSize, + onClick = { onItemClick(it) }) + Divider() + } + item { + if (canLoadMore) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = MaterialTheme.appColors.primary) + } + } + } + }) + if (scrollState.shouldLoadMore(firstVisibleIndex, 4)) { + paginationCallback() + } + } + } + + is AllEnrolledCoursesUIState.Empty -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + Modifier + .fillMaxHeight() + .then(contentWidth) + .then(emptyStatePaddings) + ) { + Header() + EmptyState() + } + } + } + } + PullRefreshIndicator( + refreshing, + pullRefreshState, + Modifier.align(Alignment.TopCenter) + ) + Column( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + ) { + when (appUpgradeParameters.appUpgradeEvent) { + is AppUpgradeEvent.UpgradeRecommendedEvent -> { + AppUpgradeRecommendedBox( + modifier = Modifier.fillMaxWidth(), + onClick = appUpgradeParameters.onAppUpgradeRecommendedBoxClick + ) + } + + else -> {} + } + + if (!isInternetConnectionShown && !hasInternetConnection) { + OfflineModeDialog( + Modifier + .fillMaxWidth(), + onDismissCLick = { + isInternetConnectionShown = true + }, + onReloadClick = { + isInternetConnectionShown = true + onReloadClick() + } + ) + } + } + } + } + } + } +} + +@Composable +private fun CourseItem( + apiHostUrl: String, + enrolledCourse: EnrolledCourse, + windowSize: WindowSize, + onClick: (EnrolledCourse) -> Unit +) { + val imageWidth by remember(key1 = windowSize) { + mutableStateOf( + windowSize.windowSizeValue( + expanded = 170.dp, + compact = 105.dp + ) + ) + } + val imageUrl = apiHostUrl.dropLast(1) + enrolledCourse.course.courseImage + val context = LocalContext.current + Surface( + modifier = Modifier + .testTag("btn_course_item") + .height(142.dp) + .fillMaxWidth() + .clickable { onClick(enrolledCourse) } + .background(MaterialTheme.appColors.background), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.appColors.background), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(imageUrl) + .error(org.openedx.core.R.drawable.core_no_image_course) + .placeholder(org.openedx.core.R.drawable.core_no_image_course) + .build(), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .height(105.dp) + .width(imageWidth) + .clip(MaterialTheme.appShapes.courseImageShape) + ) + Column( + Modifier + .fillMaxWidth() + .height(105.dp) + .background(MaterialTheme.appColors.background) + ) { + Text( + modifier = Modifier.testTag("txt_course_org"), + text = enrolledCourse.course.org, + color = MaterialTheme.appColors.textFieldHint, + style = MaterialTheme.appTypography.labelMedium + ) + Spacer(modifier = Modifier.height(4.dp)) + Column( + Modifier + .fillMaxSize() + .background(MaterialTheme.appColors.background), + verticalArrangement = Arrangement.SpaceBetween + ) { + Text( + modifier = Modifier.testTag("txt_course_name"), + text = enrolledCourse.course.name, + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.titleSmall, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + Row( + Modifier + .fillMaxWidth() + .background(MaterialTheme.appColors.background), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + modifier = Modifier.testTag("txt_course_date"), + text = TimeUtils.getCourseFormattedDate( + context, + Date(), + enrolledCourse.auditAccessExpires, + enrolledCourse.course.start, + enrolledCourse.course.end, + enrolledCourse.course.startType, + enrolledCourse.course.startDisplay + ), + color = MaterialTheme.appColors.textFieldHint, + style = MaterialTheme.appTypography.labelMedium + ) + Box( + Modifier + .size(32.dp) + .clip(CircleShape), + contentAlignment = Alignment.Center + ) { + Icon( + modifier = Modifier + .testTag("ic_course_item") + .size(15.dp), + imageVector = Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = null, + tint = MaterialTheme.appColors.primary + ) + } + } + } + } + } + } +} + +@Composable +private fun Header() { + Text( + modifier = Modifier.testTag("txt_courses_title"), + text = stringResource(id = R.string.dashboard_courses), + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.displaySmall + ) + Text( + modifier = Modifier + .testTag("txt_courses_description") + .padding(top = 4.dp), + text = stringResource(id = R.string.dashboard_welcome_back), + color = MaterialTheme.appColors.textPrimaryVariant, + style = MaterialTheme.appTypography.titleSmall + ) +} + +@Composable +private fun EmptyState() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + Modifier.width(185.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + painter = painterResource(id = R.drawable.dashboard_ic_empty), + contentDescription = null, + tint = MaterialTheme.appColors.textFieldBorder + ) + Spacer(Modifier.height(16.dp)) + Text( + modifier = Modifier + .testTag("txt_empty_state_description") + .fillMaxWidth(), + text = stringResource(id = R.string.dashboard_you_are_not_enrolled), + color = MaterialTheme.appColors.textPrimaryVariant, + style = MaterialTheme.appTypography.bodySmall, + textAlign = TextAlign.Center + ) + } + } +} + +@Preview(uiMode = UI_MODE_NIGHT_NO) +@Preview(uiMode = UI_MODE_NIGHT_YES) +@Composable +private fun CourseItemPreview() { + OpenEdXTheme() { + CourseItem( + "http://localhost:8000", + mockCourseEnrolled, + WindowSize(WindowType.Compact, WindowType.Compact), + onClick = {}) + } +} + +@Preview(uiMode = UI_MODE_NIGHT_NO) +@Preview(uiMode = UI_MODE_NIGHT_YES) +@Composable +private fun AllEnrolledCoursesPreview() { + OpenEdXTheme { + AllEnrolledCoursesScreen( + windowSize = WindowSize(WindowType.Compact, WindowType.Compact), + apiHostUrl = "http://localhost:8000", + state = AllEnrolledCoursesUIState.Courses( + listOf( + mockCourseEnrolled, + mockCourseEnrolled, + mockCourseEnrolled, + mockCourseEnrolled, + mockCourseEnrolled, + mockCourseEnrolled + ) + ), + uiMessage = null, + onSwipeRefresh = {}, + onItemClick = {}, + onReloadClick = {}, + hasInternetConnection = true, + refreshing = false, + canLoadMore = false, + paginationCallback = {}, + appUpgradeParameters = AppUpdateState.AppUpgradeParameters() + ) + } +} + +@Preview(uiMode = UI_MODE_NIGHT_NO, device = Devices.NEXUS_9) +@Preview(uiMode = UI_MODE_NIGHT_YES, device = Devices.NEXUS_9) +@Composable +private fun AllEnrolledCoursesTabletPreview() { + OpenEdXTheme { + AllEnrolledCoursesScreen( + windowSize = WindowSize(WindowType.Medium, WindowType.Medium), + apiHostUrl = "http://localhost:8000", + state = AllEnrolledCoursesUIState.Courses( + listOf( + mockCourseEnrolled, + mockCourseEnrolled, + mockCourseEnrolled, + mockCourseEnrolled, + mockCourseEnrolled, + mockCourseEnrolled + ) + ), + uiMessage = null, + onSwipeRefresh = {}, + onItemClick = {}, + onReloadClick = {}, + hasInternetConnection = true, + refreshing = false, + canLoadMore = false, + paginationCallback = {}, + appUpgradeParameters = AppUpdateState.AppUpgradeParameters() + ) + } +} + +private val mockCourseAssignments = CourseAssignments(null, emptyList()) +private val mockCourseEnrolled = EnrolledCourse( + auditAccessExpires = Date(), + created = "created", + certificate = Certificate(""), + mode = "mode", + isActive = true, + progress = Progress.DEFAULT_PROGRESS, + courseStatus = CourseStatus("", emptyList(), "", ""), + courseAssignments = mockCourseAssignments, + course = EnrolledCourseData( + id = "id", + name = "name", + number = "", + org = "Org", + start = Date(), + startDisplay = "", + startType = "", + end = Date(), + dynamicUpgradeDeadline = "", + subscriptionId = "", + coursewareAccess = CoursewareAccess( + true, + "", + "", + "", + "", + "" + ), + media = null, + courseImage = "", + courseAbout = "", + courseSharingUtmParameters = CourseSharingUtmParameters("", ""), + courseUpdates = "", + courseHandouts = "", + discussionUrl = "", + videoOutline = "", + isSelfPaced = false + ) +) diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesUIState.kt b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesUIState.kt new file mode 100644 index 000000000..3ec4551f4 --- /dev/null +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesUIState.kt @@ -0,0 +1,9 @@ +package org.openedx.courses.presentation + +import org.openedx.core.domain.model.EnrolledCourse + +sealed class AllEnrolledCoursesUIState { + data class Courses(val courses: List) : AllEnrolledCoursesUIState() + object Empty : AllEnrolledCoursesUIState() + object Loading : AllEnrolledCoursesUIState() +} diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt new file mode 100644 index 000000000..aca3e6118 --- /dev/null +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt @@ -0,0 +1,182 @@ +package org.openedx.courses.presentation + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import org.openedx.core.BaseViewModel +import org.openedx.core.R +import org.openedx.core.SingleEventLiveData +import org.openedx.core.UIMessage +import org.openedx.core.config.Config +import org.openedx.core.domain.model.EnrolledCourse +import org.openedx.core.extension.isInternetError +import org.openedx.core.system.ResourceManager +import org.openedx.core.system.connection.NetworkConnection +import org.openedx.core.system.notifier.AppUpgradeEvent +import org.openedx.core.system.notifier.AppUpgradeNotifier +import org.openedx.core.system.notifier.CourseDashboardUpdate +import org.openedx.core.system.notifier.DiscoveryNotifier +import org.openedx.dashboard.domain.interactor.DashboardInteractor +import org.openedx.dashboard.presentation.DashboardAnalytics + + +class AllEnrolledCoursesViewModel( + private val config: Config, + private val networkConnection: NetworkConnection, + private val interactor: DashboardInteractor, + private val resourceManager: ResourceManager, + private val discoveryNotifier: DiscoveryNotifier, + private val analytics: DashboardAnalytics, + private val appUpgradeNotifier: AppUpgradeNotifier +) : BaseViewModel() { + + private val coursesList = mutableListOf() + private var page = 1 + private var isLoading = false + + val apiHostUrl get() = config.getApiHostURL() + + private val _uiState = MutableLiveData(AllEnrolledCoursesUIState.Loading) + val uiState: LiveData + get() = _uiState + + private val _uiMessage = SingleEventLiveData() + val uiMessage: LiveData + get() = _uiMessage + + private val _updating = MutableLiveData() + val updating: LiveData + get() = _updating + + val hasInternetConnection: Boolean + get() = networkConnection.isOnline() + + private val _canLoadMore = MutableLiveData() + val canLoadMore: LiveData + get() = _canLoadMore + + private val _appUpgradeEvent = MutableLiveData() + val appUpgradeEvent: LiveData + get() = _appUpgradeEvent + + override fun onCreate(owner: LifecycleOwner) { + super.onCreate(owner) + viewModelScope.launch { + discoveryNotifier.notifier.collect { + if (it is CourseDashboardUpdate) { + updateCourses() + } + } + } + } + + init { + getCourses() + collectAppUpgradeEvent() + } + + fun getCourses() { + _uiState.value = AllEnrolledCoursesUIState.Loading + coursesList.clear() + internalLoadingCourses() + } + + fun updateCourses() { + viewModelScope.launch { + try { + _updating.value = true + isLoading = true + page = 1 + val response = interactor.getEnrolledCourses(page) + if (response.pagination.next.isNotEmpty() && page != response.pagination.numPages) { + _canLoadMore.value = true + page++ + } else { + _canLoadMore.value = false + page = -1 + } + coursesList.clear() + coursesList.addAll(response.courses) + if (coursesList.isEmpty()) { + _uiState.value = AllEnrolledCoursesUIState.Empty + } else { + _uiState.value = AllEnrolledCoursesUIState.Courses(ArrayList(coursesList)) + } + } catch (e: Exception) { + if (e.isInternetError()) { + _uiMessage.value = + UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) + } else { + _uiMessage.value = + UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) + } + } + _updating.value = false + isLoading = false + } + } + + private fun internalLoadingCourses() { + viewModelScope.launch { + try { + isLoading = true + val response = if (networkConnection.isOnline() || page > 1) { + interactor.getEnrolledCourses(page) + } else { + null + } + if (response != null) { + if (response.pagination.next.isNotEmpty() && page != response.pagination.numPages) { + _canLoadMore.value = true + page++ + } else { + _canLoadMore.value = false + page = -1 + } + coursesList.addAll(response.courses) + } else { + val cachedList = interactor.getEnrolledCoursesFromCache() + _canLoadMore.value = false + page = -1 + coursesList.addAll(cachedList) + } + if (coursesList.isEmpty()) { + _uiState.value = AllEnrolledCoursesUIState.Empty + } else { + _uiState.value = AllEnrolledCoursesUIState.Courses(ArrayList(coursesList)) + } + } catch (e: Exception) { + if (e.isInternetError()) { + _uiMessage.value = + UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) + } else { + _uiMessage.value = + UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) + } + } + _updating.value = false + isLoading = false + } + } + + fun fetchMore() { + if (!isLoading && page != -1) { + internalLoadingCourses() + } + } + + private fun collectAppUpgradeEvent() { + viewModelScope.launch { + appUpgradeNotifier.notifier.collect { event -> + _appUpgradeEvent.value = event + } + } + } + + fun dashboardCourseClickedEvent(courseId: String, courseName: String) { + analytics.dashboardCourseClickedEvent(courseId, courseName) + } + +} diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt index f24953d82..61cf211b1 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt @@ -2,7 +2,6 @@ package org.openedx.dashboard.presentation import androidx.fragment.app.FragmentManager import org.openedx.core.CourseContainerTabEntity -import org.openedx.core.presentation.course.CourseViewMode interface DashboardRouter { @@ -14,13 +13,7 @@ interface DashboardRouter { openTab: CourseContainerTabEntity ) - fun navigateToCourseContainer( - fm: FragmentManager, - courseId: String, - unitId: String, - componentId: String, - mode: CourseViewMode - ) - fun navigateToCourseSearch(fm: FragmentManager, querySearch: String) + + fun navigateToAllEnrolledCourses(fm: FragmentManager) } 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 088e6ff97..02905c6bd 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt @@ -119,7 +119,9 @@ class LearnFragment : Fragment() { // } }, onViewAllClick = { - //TODO + router.navigateToAllEnrolledCourses( + requireParentFragment().parentFragmentManager + ) }, onSearchClick = { router.navigateToCourseSearch( From 77d32492605bc03b4602d7a625ad1ffdc44debb0 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Wed, 17 Apr 2024 18:02:47 +0300 Subject: [PATCH 08/23] feat: AllEnrolledCoursesFragment UI --- .../java/org/openedx/app/di/ScreenModule.kt | 2 +- .../org/openedx/core/ui/ComposeExtensions.kt | 11 + .../AllEnrolledCoursesFragment.kt | 357 +++++++++--------- .../AllEnrolledCoursesViewModel.kt | 25 +- .../courses/presentation/UserCoursesScreen.kt | 6 +- .../data/repository/DashboardRepository.kt | 7 +- .../dashboard/domain/CourseStatusFilter.kt | 11 + .../domain/interactor/DashboardInteractor.kt | 9 +- dashboard/src/main/res/values/strings.xml | 5 + 9 files changed, 224 insertions(+), 209 deletions(-) create mode 100644 dashboard/src/main/java/org/openedx/dashboard/domain/CourseStatusFilter.kt diff --git a/app/src/main/java/org/openedx/app/di/ScreenModule.kt b/app/src/main/java/org/openedx/app/di/ScreenModule.kt index 497e7f89f..5988cab6a 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -118,7 +118,7 @@ val screenModule = module { factory { DashboardInteractor(get()) } viewModel { DashboardViewModel(get(), get(), get(), get(), get(), get(), get()) } viewModel { UserCoursesViewModel(get(), get(), get(), get()) } - viewModel { AllEnrolledCoursesViewModel(get(), get(), get(), get(), get(), get(), get()) } + viewModel { AllEnrolledCoursesViewModel(get(), get(), get(), get(), get(), get()) } factory { DiscoveryRepository(get(), get(), get()) } factory { DiscoveryInteractor(get()) } diff --git a/core/src/main/java/org/openedx/core/ui/ComposeExtensions.kt b/core/src/main/java/org/openedx/core/ui/ComposeExtensions.kt index f408ea60c..ddfa3a472 100644 --- a/core/src/main/java/org/openedx/core/ui/ComposeExtensions.kt +++ b/core/src/main/java/org/openedx/core/ui/ComposeExtensions.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.pager.PagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -72,6 +73,16 @@ fun LazyListState.shouldLoadMore(rememberedIndex: MutableState, threshold: return false } +fun LazyGridState.shouldLoadMore(rememberedIndex: MutableState, threshold: Int): Boolean { + val firstVisibleIndex = this.firstVisibleItemIndex + if (rememberedIndex.value != firstVisibleIndex) { + rememberedIndex.value = firstVisibleIndex + val lastVisibleIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 + return lastVisibleIndex >= layoutInfo.totalItemsCount - 1 - threshold + } + return false +} + fun Modifier.statusBarsInset(): Modifier = composed { val topInset = (LocalContext.current as? InsetHolder)?.topInset ?: 0 return@composed this 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 04a81c4c1..568d7319c 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt @@ -11,30 +11,34 @@ 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.offset 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.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Card import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.Divider import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.LinearProgressIndicator import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowForward +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.Search import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState @@ -49,7 +53,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalContext @@ -70,7 +74,6 @@ import coil.compose.AsyncImage import coil.request.ImageRequest import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel -import org.openedx.core.AppUpdateState import org.openedx.core.CourseContainerTabEntity import org.openedx.core.UIMessage import org.openedx.core.domain.model.Certificate @@ -81,11 +84,9 @@ import org.openedx.core.domain.model.CoursewareAccess import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.domain.model.EnrolledCourseData import org.openedx.core.domain.model.Progress -import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRecommendedBox -import org.openedx.core.system.notifier.AppUpgradeEvent +import org.openedx.core.ui.BackBtn import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.OfflineModeDialog -import org.openedx.core.ui.Toolbar import org.openedx.core.ui.WindowSize import org.openedx.core.ui.WindowType import org.openedx.core.ui.displayCutoutForLandscape @@ -125,7 +126,6 @@ class AllEnrolledCoursesFragment : Fragment() { val uiMessage by viewModel.uiMessage.observeAsState() val refreshing by viewModel.updating.observeAsState(false) val canLoadMore by viewModel.canLoadMore.observeAsState(false) - val appUpgradeEvent by viewModel.appUpgradeEvent.observeAsState() AllEnrolledCoursesScreen( windowSize = windowSize, @@ -141,7 +141,7 @@ class AllEnrolledCoursesFragment : Fragment() { onItemClick = { viewModel.dashboardCourseClickedEvent(it.course.id, it.course.name) router.navigateToCourseOutline( - requireParentFragment().parentFragmentManager, + requireActivity().supportFragmentManager, it.course.id, it.course.name, it.mode, @@ -154,12 +154,14 @@ class AllEnrolledCoursesFragment : Fragment() { paginationCallback = { viewModel.fetchMore() }, - appUpgradeParameters = AppUpdateState.AppUpgradeParameters( - appUpgradeEvent = appUpgradeEvent, - onAppUpgradeRecommendedBoxClick = { - AppUpdateState.openPlayMarket(requireContext()) - }, - ), + onBackClick = { + requireActivity().supportFragmentManager.popBackStack() + }, + onSearchClick = { + router.navigateToCourseSearch( + requireActivity().supportFragmentManager, "" + ) + } ) } } @@ -179,8 +181,9 @@ internal fun AllEnrolledCoursesScreen( onReloadClick: () -> Unit, onSwipeRefresh: () -> Unit, paginationCallback: () -> Unit, + onBackClick: () -> Unit, + onSearchClick: () -> Unit, onItemClick: (EnrolledCourse) -> Unit, - appUpgradeParameters: AppUpdateState.AppUpgradeParameters, ) { val scaffoldState = rememberScaffoldState() val pullRefreshState = @@ -189,7 +192,7 @@ internal fun AllEnrolledCoursesScreen( var isInternetConnectionShown by rememberSaveable { mutableStateOf(false) } - val scrollState = rememberLazyListState() + val scrollState = rememberLazyGridState() val firstVisibleIndex = remember { mutableStateOf(scrollState.firstVisibleItemIndex) } @@ -211,7 +214,7 @@ internal fun AllEnrolledCoursesScreen( top = 32.dp, bottom = 40.dp ), - compact = PaddingValues(horizontal = 24.dp, vertical = 24.dp) + compact = PaddingValues(horizontal = 16.dp, vertical = 16.dp) ) ) } @@ -246,7 +249,12 @@ internal fun AllEnrolledCoursesScreen( .displayCutoutForLandscape(), horizontalAlignment = Alignment.CenterHorizontally ) { - Toolbar(label = stringResource(id = R.string.dashboard_title)) + BackBtn( + modifier = Modifier.align(Alignment.Start), + tint = MaterialTheme.appColors.textDark + ) { + onBackClick() + } Surface( color = MaterialTheme.appColors.background, @@ -272,40 +280,44 @@ internal fun AllEnrolledCoursesScreen( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - LazyColumn( - modifier = Modifier - .fillMaxHeight() - .then(contentWidth), - state = scrollState, - contentPadding = contentPaddings, - content = { - item() { - Column { - Header() - Spacer(modifier = Modifier.height(16.dp)) + Column( + modifier = Modifier.padding(contentPaddings) + ) { + Header( + onSearchClick = onSearchClick + ) + Spacer(modifier = Modifier.height(8.dp)) + 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( + course, + apiHostUrl, + onClick = { onItemClick(it) } + ) } - } - items(state.courses) { course -> - CourseItem( - apiHostUrl, - course, - windowSize, - onClick = { onItemClick(it) }) - Divider() - } - item { - if (canLoadMore) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator(color = MaterialTheme.appColors.primary) + item { + if (canLoadMore) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = MaterialTheme.appColors.primary) + } } } } - }) + ) + } if (scrollState.shouldLoadMore(firstVisibleIndex, 4)) { paginationCallback() } @@ -323,7 +335,7 @@ internal fun AllEnrolledCoursesScreen( .then(contentWidth) .then(emptyStatePaddings) ) { - Header() + Header(onSearchClick = onSearchClick) EmptyState() } } @@ -339,17 +351,6 @@ internal fun AllEnrolledCoursesScreen( .fillMaxWidth() .align(Alignment.BottomCenter) ) { - when (appUpgradeParameters.appUpgradeEvent) { - is AppUpgradeEvent.UpgradeRecommendedEvent -> { - AppUpgradeRecommendedBox( - modifier = Modifier.fillMaxWidth(), - onClick = appUpgradeParameters.onAppUpgradeRecommendedBoxClick - ) - } - - else -> {} - } - if (!isInternetConnectionShown && !hasInternetConnection) { OfflineModeDialog( Modifier @@ -372,135 +373,122 @@ internal fun AllEnrolledCoursesScreen( @Composable private fun CourseItem( + course: EnrolledCourse, apiHostUrl: String, - enrolledCourse: EnrolledCourse, - windowSize: WindowSize, - onClick: (EnrolledCourse) -> Unit + onClick: (EnrolledCourse) -> Unit, ) { - val imageWidth by remember(key1 = windowSize) { - mutableStateOf( - windowSize.windowSizeValue( - expanded = 170.dp, - compact = 105.dp - ) - ) - } - val imageUrl = apiHostUrl.dropLast(1) + enrolledCourse.course.courseImage - val context = LocalContext.current - Surface( + Card( modifier = Modifier - .testTag("btn_course_item") - .height(142.dp) - .fillMaxWidth() - .clickable { onClick(enrolledCourse) } - .background(MaterialTheme.appColors.background), + .width(170.dp) + .clickable { + onClick(course) + }, + backgroundColor = MaterialTheme.appColors.background, + shape = MaterialTheme.appShapes.courseImageShape, + elevation = 2.dp ) { - Row( - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.appColors.background), - horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalAlignment = Alignment.CenterVertically - ) { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(imageUrl) - .error(org.openedx.core.R.drawable.core_no_image_course) - .placeholder(org.openedx.core.R.drawable.core_no_image_course) - .build(), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .height(105.dp) - .width(imageWidth) - .clip(MaterialTheme.appShapes.courseImageShape) - ) - Column( - Modifier - .fillMaxWidth() - .height(105.dp) - .background(MaterialTheme.appColors.background) - ) { + Box { + Column { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(apiHostUrl + course.course.courseImage) + .error(org.openedx.core.R.drawable.core_no_image_course) + .placeholder(org.openedx.core.R.drawable.core_no_image_course) + .build(), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .height(90.dp) + ) + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .height(8.dp), + progress = course.progress.numPointsEarned.toFloat(), + color = MaterialTheme.appColors.primary, + backgroundColor = MaterialTheme.appColors.divider + ) Text( - modifier = Modifier.testTag("txt_course_org"), - text = enrolledCourse.course.org, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + .padding(top = 4.dp), + style = MaterialTheme.appTypography.labelMedium, color = MaterialTheme.appColors.textFieldHint, - style = MaterialTheme.appTypography.labelMedium - ) - Spacer(modifier = Modifier.height(4.dp)) - Column( - Modifier - .fillMaxSize() - .background(MaterialTheme.appColors.background), - verticalArrangement = Arrangement.SpaceBetween - ) { - Text( - modifier = Modifier.testTag("txt_course_name"), - text = enrolledCourse.course.name, - color = MaterialTheme.appColors.textPrimary, - style = MaterialTheme.appTypography.titleSmall, - maxLines = 2, - overflow = TextOverflow.Ellipsis + text = stringResource( + R.string.dashboard_course_date, + TimeUtils.getCourseFormattedDate( + LocalContext.current, + Date(), + course.auditAccessExpires, + course.course.start, + course.course.end, + course.course.startType, + course.course.startDisplay + ) ) - Row( - Modifier - .fillMaxWidth() - .background(MaterialTheme.appColors.background), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - modifier = Modifier.testTag("txt_course_date"), - text = TimeUtils.getCourseFormattedDate( - context, - Date(), - enrolledCourse.auditAccessExpires, - enrolledCourse.course.start, - enrolledCourse.course.end, - enrolledCourse.course.startType, - enrolledCourse.course.startDisplay - ), - color = MaterialTheme.appColors.textFieldHint, - style = MaterialTheme.appTypography.labelMedium + ) + Text( + modifier = Modifier + .padding(horizontal = 8.dp, vertical = 4.dp), + text = course.course.name, + style = MaterialTheme.appTypography.titleSmall, + color = MaterialTheme.appColors.textDark, + overflow = TextOverflow.Ellipsis, + minLines = 2, + maxLines = 2 + ) + } + if (!course.course.coursewareAccess?.errorCode.isNullOrEmpty()) { + Icon( + modifier = Modifier + .size(32.dp) + .padding(top = 8.dp, end = 8.dp) + .background( + color = Color.White, + shape = CircleShape ) - Box( - Modifier - .size(32.dp) - .clip(CircleShape), - contentAlignment = Alignment.Center - ) { - Icon( - modifier = Modifier - .testTag("ic_course_item") - .size(15.dp), - imageVector = Icons.AutoMirrored.Filled.ArrowForward, - contentDescription = null, - tint = MaterialTheme.appColors.primary - ) - } - } - } + .padding(4.dp) + .align(Alignment.TopEnd), + imageVector = Icons.Default.Lock, + contentDescription = null, + tint = MaterialTheme.appColors.textWarning + ) } } } } @Composable -private fun Header() { - Text( - modifier = Modifier.testTag("txt_courses_title"), - text = stringResource(id = R.string.dashboard_courses), - color = MaterialTheme.appColors.textPrimary, - style = MaterialTheme.appTypography.displaySmall - ) - Text( - modifier = Modifier - .testTag("txt_courses_description") - .padding(top = 4.dp), - text = stringResource(id = R.string.dashboard_welcome_back), - color = MaterialTheme.appColors.textPrimaryVariant, - style = MaterialTheme.appTypography.titleSmall - ) +private fun Header( + modifier: Modifier = Modifier, + onSearchClick: () -> Unit +) { + Box( + modifier = modifier.fillMaxWidth() + ) { + Text( + modifier = Modifier.align(Alignment.CenterStart), + text = stringResource(id = R.string.dashboard_all_courses), + color = MaterialTheme.appColors.textDark, + style = MaterialTheme.appTypography.headlineBolt + ) + IconButton( + modifier = Modifier + .align(Alignment.CenterEnd) + .offset(x = 12.dp), + onClick = { + onSearchClick() + } + ) { + Icon( + imageVector = Icons.Filled.Search, + contentDescription = null, + tint = MaterialTheme.appColors.textDark + ) + } + } } @Composable @@ -538,9 +526,8 @@ private fun EmptyState() { private fun CourseItemPreview() { OpenEdXTheme() { CourseItem( - "http://localhost:8000", mockCourseEnrolled, - WindowSize(WindowType.Compact, WindowType.Compact), + "http://localhost:8000", onClick = {}) } } @@ -571,7 +558,8 @@ private fun AllEnrolledCoursesPreview() { refreshing = false, canLoadMore = false, paginationCallback = {}, - appUpgradeParameters = AppUpdateState.AppUpgradeParameters() + onBackClick = {}, + onSearchClick = {} ) } } @@ -602,7 +590,8 @@ private fun AllEnrolledCoursesTabletPreview() { refreshing = false, canLoadMore = false, paginationCallback = {}, - appUpgradeParameters = AppUpdateState.AppUpgradeParameters() + onBackClick = {}, + onSearchClick = {} ) } } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt index aca3e6118..0ccf91ed8 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel import org.openedx.core.R @@ -14,22 +15,19 @@ import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.extension.isInternetError import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection -import org.openedx.core.system.notifier.AppUpgradeEvent -import org.openedx.core.system.notifier.AppUpgradeNotifier import org.openedx.core.system.notifier.CourseDashboardUpdate import org.openedx.core.system.notifier.DiscoveryNotifier +import org.openedx.dashboard.domain.CourseStatusFilter import org.openedx.dashboard.domain.interactor.DashboardInteractor import org.openedx.dashboard.presentation.DashboardAnalytics - class AllEnrolledCoursesViewModel( private val config: Config, private val networkConnection: NetworkConnection, private val interactor: DashboardInteractor, private val resourceManager: ResourceManager, private val discoveryNotifier: DiscoveryNotifier, - private val analytics: DashboardAnalytics, - private val appUpgradeNotifier: AppUpgradeNotifier + private val analytics: DashboardAnalytics ) : BaseViewModel() { private val coursesList = mutableListOf() @@ -57,9 +55,7 @@ class AllEnrolledCoursesViewModel( val canLoadMore: LiveData get() = _canLoadMore - private val _appUpgradeEvent = MutableLiveData() - val appUpgradeEvent: LiveData - get() = _appUpgradeEvent + val courseStatusFilter: MutableStateFlow = MutableStateFlow(CourseStatusFilter.ALL) override fun onCreate(owner: LifecycleOwner) { super.onCreate(owner) @@ -74,7 +70,6 @@ class AllEnrolledCoursesViewModel( init { getCourses() - collectAppUpgradeEvent() } fun getCourses() { @@ -89,7 +84,7 @@ class AllEnrolledCoursesViewModel( _updating.value = true isLoading = true page = 1 - val response = interactor.getEnrolledCourses(page) + val response = interactor.getUserCourses(page, courseStatusFilter.value).enrollments if (response.pagination.next.isNotEmpty() && page != response.pagination.numPages) { _canLoadMore.value = true page++ @@ -123,7 +118,7 @@ class AllEnrolledCoursesViewModel( try { isLoading = true val response = if (networkConnection.isOnline() || page > 1) { - interactor.getEnrolledCourses(page) + interactor.getUserCourses(page, courseStatusFilter.value).enrollments } else { null } @@ -167,14 +162,6 @@ class AllEnrolledCoursesViewModel( } } - private fun collectAppUpgradeEvent() { - viewModelScope.launch { - appUpgradeNotifier.notifier.collect { event -> - _appUpgradeEvent.value = event - } - } - } - fun dashboardCourseClickedEvent(courseId: String, courseName: String) { analytics.dashboardCourseClickedEvent(courseId, courseName) } 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 dca02acc0..075d3068d 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt @@ -222,7 +222,8 @@ private fun SecondaryCourses( modifier = Modifier .fillMaxWidth() .padding(horizontal = 14.dp) - .padding(top = 8.dp) + .padding(top = 12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { TextIcon( text = stringResource(R.string.dashboard_view_all, courses.size), @@ -284,7 +285,8 @@ private fun CourseListItem( style = MaterialTheme.appTypography.titleSmall, color = MaterialTheme.appColors.textDark, overflow = TextOverflow.Ellipsis, - maxLines = 2 + maxLines = 2, + minLines = 2 ) } if (!course.course.coursewareAccess?.errorCode.isNullOrEmpty()) { diff --git a/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt b/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt index fd226de76..5ac162441 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt @@ -6,6 +6,7 @@ import org.openedx.core.domain.model.DashboardCourseList import org.openedx.core.domain.model.EnrolledCourse import org.openedx.courses.domain.model.UserCourses import org.openedx.dashboard.data.DashboardDao +import org.openedx.dashboard.domain.CourseStatusFilter class DashboardRepository( private val api: CourseApi, @@ -32,10 +33,12 @@ class DashboardRepository( return list.map { it.mapToDomain() } } - suspend fun getUserCourses(): UserCourses { + suspend fun getUserCourses(page: Int, status: CourseStatusFilter?): UserCourses { val user = preferencesManager.user val result = api.getUserCourses( - username = user?.username ?: "" + username = user?.username ?: "", + page = page, + status = status?.key ) preferencesManager.appConfig = result.configs.mapToDomain() diff --git a/dashboard/src/main/java/org/openedx/dashboard/domain/CourseStatusFilter.kt b/dashboard/src/main/java/org/openedx/dashboard/domain/CourseStatusFilter.kt new file mode 100644 index 000000000..9e05a5844 --- /dev/null +++ b/dashboard/src/main/java/org/openedx/dashboard/domain/CourseStatusFilter.kt @@ -0,0 +1,11 @@ +package org.openedx.dashboard.domain + +import androidx.annotation.StringRes +import org.openedx.dashboard.R + +enum class CourseStatusFilter(val key: String, @StringRes val text: Int) { + ALL("all", R.string.dashboard_course_filter_all), + IN_PROGRESS("in_progress", R.string.dashboard_course_filter_in_progress), + COMPLETE("completed", R.string.dashboard_course_filter_completed), + EXPIRED("expired", R.string.dashboard_course_filter_expired) +} \ No newline at end of file diff --git a/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt b/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt index a491190ab..a3bff4d82 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt @@ -1,7 +1,9 @@ package org.openedx.dashboard.domain.interactor import org.openedx.core.domain.model.DashboardCourseList +import org.openedx.courses.domain.model.UserCourses import org.openedx.dashboard.data.repository.DashboardRepository +import org.openedx.dashboard.domain.CourseStatusFilter class DashboardInteractor( private val repository: DashboardRepository @@ -13,5 +15,10 @@ class DashboardInteractor( suspend fun getEnrolledCoursesFromCache() = repository.getEnrolledCoursesFromCache() - suspend fun getUserCourses() = repository.getUserCourses() + suspend fun getUserCourses(page: Int = 1, status: CourseStatusFilter? = null): UserCourses { + return repository.getUserCourses( + page, + status + ) + } } \ No newline at end of file diff --git a/dashboard/src/main/res/values/strings.xml b/dashboard/src/main/res/values/strings.xml index a370880ce..f2a29e18d 100644 --- a/dashboard/src/main/res/values/strings.xml +++ b/dashboard/src/main/res/values/strings.xml @@ -12,4 +12,9 @@ %1$d Past Due Assignment View All (%1$d) %1$s Due in %2$s + All + In Progress + Completed + Expired + All Courses From 7e219d2de3530ffe1bda80c106db214817ba6f95 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Fri, 19 Apr 2024 18:24:04 +0300 Subject: [PATCH 09/23] feat: Minor code refactoring, show cached data if no internet connection --- .../java/org/openedx/app/di/ScreenModule.kt | 2 +- .../AllEnrolledCoursesFragment.kt | 68 +++++++++---------- .../AllEnrolledCoursesViewModel.kt | 44 ++++++------ .../courses/presentation/UserCoursesScreen.kt | 54 +++++++++++---- .../presentation/UserCoursesUIState.kt | 4 +- .../presentation/UserCoursesViewModel.kt | 48 +++++++++---- .../presentation/DashboardViewModel.kt | 1 - .../learn/presentation/LearnFragment.kt | 17 +---- 8 files changed, 136 insertions(+), 102 deletions(-) diff --git a/app/src/main/java/org/openedx/app/di/ScreenModule.kt b/app/src/main/java/org/openedx/app/di/ScreenModule.kt index 5988cab6a..c9c773824 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -117,7 +117,7 @@ val screenModule = module { factory { DashboardRepository(get(), get(), get()) } factory { DashboardInteractor(get()) } viewModel { DashboardViewModel(get(), get(), get(), get(), get(), get(), get()) } - viewModel { UserCoursesViewModel(get(), get(), get(), get()) } + viewModel { UserCoursesViewModel(get(), get(), get(), get(), get()) } viewModel { AllEnrolledCoursesViewModel(get(), get(), get(), get(), get(), get()) } factory { DiscoveryRepository(get(), get(), get()) } 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 568d7319c..f76ccdc94 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt @@ -16,6 +16,7 @@ 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.navigationBarsPadding import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -44,8 +45,9 @@ import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable @@ -122,15 +124,15 @@ class AllEnrolledCoursesFragment : Fragment() { setContent { OpenEdXTheme { val windowSize = rememberWindowSize() - val uiState by viewModel.uiState.observeAsState() - val uiMessage by viewModel.uiMessage.observeAsState() - val refreshing by viewModel.updating.observeAsState(false) - val canLoadMore by viewModel.canLoadMore.observeAsState(false) + val uiState by viewModel.uiState.collectAsState() + val uiMessage by viewModel.uiMessage.collectAsState(null) + val refreshing by viewModel.updating.collectAsState(false) + val canLoadMore by viewModel.canLoadMore.collectAsState(false) AllEnrolledCoursesScreen( windowSize = windowSize, viewModel.apiHostUrl, - uiState!!, + uiState, uiMessage, canLoadMore = canLoadMore, refreshing = refreshing, @@ -186,15 +188,17 @@ internal fun AllEnrolledCoursesScreen( onItemClick: (EnrolledCourse) -> Unit, ) { val scaffoldState = rememberScaffoldState() - val pullRefreshState = - rememberPullRefreshState(refreshing = refreshing, onRefresh = { onSwipeRefresh() }) + val pullRefreshState = rememberPullRefreshState( + refreshing = refreshing, + onRefresh = { onSwipeRefresh() } + ) var isInternetConnectionShown by rememberSaveable { mutableStateOf(false) } val scrollState = rememberLazyGridState() val firstVisibleIndex = remember { - mutableStateOf(scrollState.firstVisibleItemIndex) + mutableIntStateOf(scrollState.firstVisibleItemIndex) } Scaffold( @@ -261,15 +265,17 @@ internal fun AllEnrolledCoursesScreen( shape = MaterialTheme.appShapes.screenBackgroundShape ) { Box( - Modifier + modifier = Modifier .fillMaxWidth() + .navigationBarsPadding() .pullRefresh(pullRefreshState), ) { when (state) { is AllEnrolledCoursesUIState.Loading -> { Box( - Modifier - .fillMaxSize(), contentAlignment = Alignment.Center + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center ) { CircularProgressIndicator(color = MaterialTheme.appColors.primary) } @@ -283,9 +289,7 @@ internal fun AllEnrolledCoursesScreen( Column( modifier = Modifier.padding(contentPaddings) ) { - Header( - onSearchClick = onSearchClick - ) + Header(onSearchClick = onSearchClick) Spacer(modifier = Modifier.height(8.dp)) LazyVerticalGrid( modifier = Modifier @@ -330,7 +334,7 @@ internal fun AllEnrolledCoursesScreen( contentAlignment = Alignment.Center ) { Column( - Modifier + modifier = Modifier .fillMaxHeight() .then(contentWidth) .then(emptyStatePaddings) @@ -346,24 +350,20 @@ internal fun AllEnrolledCoursesScreen( pullRefreshState, Modifier.align(Alignment.TopCenter) ) - Column( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter) - ) { - if (!isInternetConnectionShown && !hasInternetConnection) { - OfflineModeDialog( - Modifier - .fillMaxWidth(), - onDismissCLick = { - isInternetConnectionShown = true - }, - onReloadClick = { - isInternetConnectionShown = true - onReloadClick() - } - ) - } + + if (!isInternetConnectionShown && !hasInternetConnection) { + OfflineModeDialog( + Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter), + onDismissCLick = { + isInternetConnectionShown = true + }, + onReloadClick = { + isInternetConnectionShown = true + onReloadClick() + } + ) } } } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt index 0ccf91ed8..970cd3f85 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt @@ -1,14 +1,16 @@ package org.openedx.courses.presentation import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel import org.openedx.core.R -import org.openedx.core.SingleEventLiveData import org.openedx.core.UIMessage import org.openedx.core.config.Config import org.openedx.core.domain.model.EnrolledCourse @@ -36,24 +38,24 @@ class AllEnrolledCoursesViewModel( val apiHostUrl get() = config.getApiHostURL() - private val _uiState = MutableLiveData(AllEnrolledCoursesUIState.Loading) - val uiState: LiveData - get() = _uiState + private val _uiState = MutableStateFlow(AllEnrolledCoursesUIState.Loading) + val uiState: StateFlow + get() = _uiState.asStateFlow() - private val _uiMessage = SingleEventLiveData() - val uiMessage: LiveData - get() = _uiMessage + private val _uiMessage = MutableSharedFlow() + val uiMessage: SharedFlow + get() = _uiMessage.asSharedFlow() - private val _updating = MutableLiveData() - val updating: LiveData - get() = _updating + private val _updating = MutableStateFlow(false) + val updating: StateFlow + get() = _updating.asStateFlow() val hasInternetConnection: Boolean get() = networkConnection.isOnline() - private val _canLoadMore = MutableLiveData() - val canLoadMore: LiveData - get() = _canLoadMore + private val _canLoadMore = MutableStateFlow(false) + val canLoadMore: StateFlow + get() = _canLoadMore.asStateFlow() val courseStatusFilter: MutableStateFlow = MutableStateFlow(CourseStatusFilter.ALL) @@ -101,11 +103,9 @@ class AllEnrolledCoursesViewModel( } } catch (e: Exception) { if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) + _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection))) } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) + _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error))) } } _updating.value = false @@ -144,11 +144,9 @@ class AllEnrolledCoursesViewModel( } } catch (e: Exception) { if (e.isInternetError()) { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) + _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection))) } else { - _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) + _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error))) } } _updating.value = false 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 075d3068d..e0062405d 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt @@ -42,7 +42,9 @@ import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +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.graphics.Color @@ -71,6 +73,7 @@ import org.openedx.core.domain.model.EnrolledCourseData import org.openedx.core.domain.model.Pagination import org.openedx.core.domain.model.Progress import org.openedx.core.ui.HandleUIMessage +import org.openedx.core.ui.OfflineModeDialog import org.openedx.core.ui.TextIcon import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors @@ -90,9 +93,9 @@ fun UsersCourseScreen( onViewAllClick: () -> Unit, onResumeClick: (componentId: String) -> Unit, ) { - val updating by viewModel.updating.observeAsState(false) + val updating by viewModel.updating.collectAsState(false) val uiMessage by viewModel.uiMessage.collectAsState(null) - val uiState by viewModel.uiState.observeAsState(UserCoursesUIState.Loading) + val uiState by viewModel.uiState.collectAsState(UserCoursesUIState.Loading) UsersCourseScreen( uiMessage = uiMessage, @@ -100,10 +103,12 @@ fun UsersCourseScreen( updating = updating, apiHostUrl = viewModel.apiHostUrl, onSwipeRefresh = viewModel::updateCourses, + hasInternetConnection = viewModel.hasInternetConnection, onCourseClick = onCourseClick, onViewAllClick = onViewAllClick, openDates = openDates, - onResumeClick = onResumeClick + onResumeClick = onResumeClick, + onReloadClick = viewModel::getCourses ) } @@ -118,10 +123,18 @@ private fun UsersCourseScreen( onCourseClick: (EnrolledCourse) -> Unit, openDates: (EnrolledCourse) -> Unit, onViewAllClick: () -> Unit, + onReloadClick: () -> Unit, onResumeClick: (componentId: String) -> Unit, + hasInternetConnection: Boolean ) { val scaffoldState = rememberScaffoldState() - val pullRefreshState = rememberPullRefreshState(refreshing = updating, onRefresh = { onSwipeRefresh() }) + val pullRefreshState = rememberPullRefreshState( + refreshing = updating, + onRefresh = { onSwipeRefresh() } + ) + var isInternetConnectionShown by rememberSaveable { + mutableStateOf(false) + } Scaffold( scaffoldState = scaffoldState, @@ -153,7 +166,7 @@ private fun UsersCourseScreen( is UserCoursesUIState.Courses -> { UserCourses( modifier = Modifier.fillMaxSize(), - userCourses = uiState.userCourses, + userCourses = uiState, apiHostUrl = apiHostUrl, onCourseClick = onCourseClick, onViewAllClick = onViewAllClick, @@ -174,6 +187,21 @@ private fun UsersCourseScreen( pullRefreshState, Modifier.align(Alignment.TopCenter) ) + + if (!isInternetConnectionShown && !hasInternetConnection) { + OfflineModeDialog( + Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter), + onDismissCLick = { + isInternetConnectionShown = true + }, + onReloadClick = { + isInternetConnectionShown = true + onReloadClick() + } + ) + } } } } @@ -182,7 +210,7 @@ private fun UsersCourseScreen( @Composable private fun UserCourses( modifier: Modifier = Modifier, - userCourses: UserCourses, + userCourses: UserCoursesUIState.Courses, apiHostUrl: String, onCourseClick: (EnrolledCourse) -> Unit, openDates: (EnrolledCourse) -> Unit, @@ -193,9 +221,9 @@ private fun UserCourses( modifier = modifier .padding(vertical = 12.dp) ) { - if (userCourses.primary != null) { + if (userCourses.primaryCourse != null) { PrimaryCourseCard( - primaryCourse = userCourses.primary, + primaryCourse = userCourses.primaryCourse, apiHostUrl = apiHostUrl, openDates = openDates, onResumeClick = onResumeClick, @@ -203,7 +231,7 @@ private fun UserCourses( ) } SecondaryCourses( - courses = userCourses.enrollments.courses, + courses = userCourses.enrolledCourses, apiHostUrl = apiHostUrl, onCourseClick = onCourseClick, onViewAllClick = onViewAllClick @@ -618,15 +646,17 @@ private val mockUserCourses = UserCourses( private fun UsersCourseScreenPreview() { OpenEdXTheme { UsersCourseScreen( - uiState = UserCoursesUIState.Courses(mockUserCourses), + uiState = UserCoursesUIState.Courses(mockUserCourses.enrollments.courses, mockUserCourses.primary), apiHostUrl = "", uiMessage = null, updating = false, + hasInternetConnection = false, onSwipeRefresh = { }, onCourseClick = { }, onViewAllClick = { }, openDates = { }, - onResumeClick = { } + onResumeClick = { }, + onReloadClick = { } ) } } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesUIState.kt b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesUIState.kt index ee064c315..1b7656d17 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesUIState.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesUIState.kt @@ -1,9 +1,9 @@ package org.openedx.courses.presentation -import org.openedx.courses.domain.model.UserCourses +import org.openedx.core.domain.model.EnrolledCourse sealed class UserCoursesUIState { - data class Courses(val userCourses: UserCourses) : UserCoursesUIState() + data class Courses(val enrolledCourses: List, val primaryCourse: EnrolledCourse?) : UserCoursesUIState() object Empty : UserCoursesUIState() object Loading : UserCoursesUIState() } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt index c58a6b2bf..71646bb33 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt @@ -1,11 +1,12 @@ package org.openedx.courses.presentation import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel @@ -14,6 +15,7 @@ import org.openedx.core.UIMessage import org.openedx.core.config.Config import org.openedx.core.extension.isInternetError import org.openedx.core.system.ResourceManager +import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CourseDashboardUpdate import org.openedx.core.system.notifier.DiscoveryNotifier import org.openedx.dashboard.domain.interactor.DashboardInteractor @@ -23,22 +25,26 @@ class UserCoursesViewModel( private val interactor: DashboardInteractor, private val resourceManager: ResourceManager, private val discoveryNotifier: DiscoveryNotifier, + private val networkConnection: NetworkConnection ) : BaseViewModel() { val apiHostUrl get() = config.getApiHostURL() val isProgramTypeWebView get() = config.getProgramConfig().isViewTypeWebView() - private val _uiState = MutableLiveData(UserCoursesUIState.Loading) - val uiState: LiveData - get() = _uiState + private val _uiState = MutableStateFlow(UserCoursesUIState.Loading) + val uiState: StateFlow + get() = _uiState.asStateFlow() - private val _uiMessage = MutableStateFlow(null) + private val _uiMessage = MutableSharedFlow() val uiMessage: SharedFlow - get() = _uiMessage.asStateFlow() + get() = _uiMessage.asSharedFlow() - private val _updating = MutableLiveData() - val updating: LiveData - get() = _updating + private val _updating = MutableStateFlow(false) + val updating: StateFlow + get() = _updating.asStateFlow() + + val hasInternetConnection: Boolean + get() = networkConnection.isOnline() override fun onCreate(owner: LifecycleOwner) { super.onCreate(owner) @@ -56,14 +62,26 @@ class UserCoursesViewModel( getCourses() } - private fun getCourses() { + fun getCourses() { viewModelScope.launch { try { - val response = interactor.getUserCourses() - if (response.primary == null) { - _uiState.value = UserCoursesUIState.Empty + if (networkConnection.isOnline()) { + val response = interactor.getUserCourses() + if (response.primary == null && response.enrollments.courses.isNotEmpty()) { + _uiState.value = UserCoursesUIState.Empty + } else { + _uiState.value = UserCoursesUIState.Courses(response.enrollments.courses, response.primary) + } } else { - _uiState.value = UserCoursesUIState.Courses(response) + val cachedList = interactor.getEnrolledCoursesFromCache() + if (cachedList.isEmpty()) { + _uiState.value = UserCoursesUIState.Empty + } else { + _uiState.value = UserCoursesUIState.Courses( + cachedList, + null + ) + } } } catch (e: Exception) { if (e.isInternetError()) { diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardViewModel.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardViewModel.kt index 0ec06a2c3..685e449d0 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardViewModel.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardViewModel.kt @@ -20,7 +20,6 @@ import org.openedx.core.system.notifier.CourseDashboardUpdate import org.openedx.core.system.notifier.DiscoveryNotifier import org.openedx.dashboard.domain.interactor.DashboardInteractor - class DashboardViewModel( private val config: Config, private val networkConnection: NetworkConnection, 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 02905c6bd..9e92baa51 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt @@ -105,18 +105,7 @@ class LearnFragment : Fragment() { ) }, onResumeClick = { componentId -> -// viewModel.resumeSectionBlock?.let { subSection -> -// viewModel.resumeCourseTappedEvent(subSection.id) -// viewModel.resumeVerticalBlock?.let { unit -> -// router.navigateToCourseContainer( -// fm = requireActivity().supportFragmentManager, -// courseId = viewModel.courseId, -// unitId = unit.id, -// componentId = componentId, -// mode = CourseViewMode.FULL -// ) -// } -// } + //TODO }, onViewAllClick = { router.navigateToAllEnrolledCourses( @@ -173,7 +162,7 @@ private fun LearnScreen( .then(contentWidth), horizontalAlignment = Alignment.CenterHorizontally ) { - LearnToolbar( + Header( modifier = Modifier .padding(horizontal = 16.dp), label = stringResource(id = R.string.dashboard_learn), @@ -212,7 +201,7 @@ private fun LearnScreen( } @Composable -private fun LearnToolbar( +private fun Header( modifier: Modifier = Modifier, label: String, onSearchClick: () -> Unit From baac26cf3dd175660b1d6adb9f0948eaaa9cb836 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Mon, 22 Apr 2024 13:13:30 +0300 Subject: [PATCH 10/23] feat: UserCourses screen data caching --- .../main/java/org/openedx/app/di/AppModule.kt | 3 ++ .../java/org/openedx/app/di/ScreenModule.kt | 4 +-- .../org/openedx/core/data/api/CourseApi.kt | 2 +- .../openedx/core/module/TranscriptManager.kt | 9 ++++-- .../java/org/openedx/core/utils/FileUtil.kt | 23 +++++++++++++-- .../courses/domain/model/UserCourses.kt | 3 +- .../AllEnrolledCoursesViewModel.kt | 4 +-- .../courses/presentation/UserCoursesScreen.kt | 14 ++++----- .../presentation/UserCoursesUIState.kt | 8 ++--- .../presentation/UserCoursesViewModel.kt | 20 ++++++------- .../data/repository/DashboardRepository.kt | 29 ++++++++++++++----- .../domain/interactor/DashboardInteractor.kt | 8 +++-- 12 files changed, 85 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/org/openedx/app/di/AppModule.kt b/app/src/main/java/org/openedx/app/di/AppModule.kt index dc0a70335..7c9377f9d 100644 --- a/app/src/main/java/org/openedx/app/di/AppModule.kt +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt @@ -46,6 +46,7 @@ import org.openedx.core.system.notifier.CourseNotifier import org.openedx.core.system.notifier.DiscoveryNotifier import org.openedx.core.system.notifier.DownloadNotifier import org.openedx.core.system.notifier.VideoNotifier +import org.openedx.core.utils.FileUtil import org.openedx.course.data.storage.CoursePreferences import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.CourseRouter @@ -178,4 +179,6 @@ val appModule = module { factory { GoogleAuthHelper(get()) } factory { MicrosoftAuthHelper() } factory { OAuthHelper(get(), get(), get()) } + + factory { FileUtil(get()) } } diff --git a/app/src/main/java/org/openedx/app/di/ScreenModule.kt b/app/src/main/java/org/openedx/app/di/ScreenModule.kt index c9c773824..20a8a0e2b 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -114,10 +114,10 @@ val screenModule = module { } viewModel { RestorePasswordViewModel(get(), get(), get(), get()) } - factory { DashboardRepository(get(), get(), get()) } + factory { DashboardRepository(get(), get(), get(), get()) } factory { DashboardInteractor(get()) } viewModel { DashboardViewModel(get(), get(), get(), get(), get(), get(), get()) } - viewModel { UserCoursesViewModel(get(), get(), get(), get(), get()) } + viewModel { UserCoursesViewModel(get(), get(), get(), get(), get(), get()) } viewModel { AllEnrolledCoursesViewModel(get(), get(), get(), get(), get(), get()) } factory { DiscoveryRepository(get(), get(), get()) } 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 00a4b520d..edbff1481 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 @@ -73,6 +73,6 @@ interface CourseApi { @Path("username") username: String, @Query("page") page: Int = 1, @Query("status") status: String? = null, - @Query("requested_fields") fields: List = listOf("progress") + @Query("requested_fields") fields: List = emptyList() ): CourseEnrollments } diff --git a/core/src/main/java/org/openedx/core/module/TranscriptManager.kt b/core/src/main/java/org/openedx/core/module/TranscriptManager.kt index 863586900..ba67d1a54 100644 --- a/core/src/main/java/org/openedx/core/module/TranscriptManager.kt +++ b/core/src/main/java/org/openedx/core/module/TranscriptManager.kt @@ -1,9 +1,12 @@ package org.openedx.core.module import android.content.Context -import org.openedx.core.module.download.AbstractDownloader -import org.openedx.core.utils.* import okhttp3.OkHttpClient +import org.openedx.core.module.download.AbstractDownloader +import org.openedx.core.utils.Directories +import org.openedx.core.utils.FileUtil +import org.openedx.core.utils.IOUtils +import org.openedx.core.utils.Sha1Util import subtitleFile.FormatSRT import subtitleFile.TimedTextObject import java.io.File @@ -113,7 +116,7 @@ class TranscriptManager( } private fun getTranscriptDir(): File? { - val externalAppDir: File = FileUtil.getExternalAppDir(context) + val externalAppDir: File = FileUtil(context).getExternalAppDir() if (externalAppDir.exists()) { val videosDir = File(externalAppDir, Directories.VIDEOS.name) val transcriptDir = File(videosDir, Directories.SUBTITLES.name) diff --git a/core/src/main/java/org/openedx/core/utils/FileUtil.kt b/core/src/main/java/org/openedx/core/utils/FileUtil.kt index 001d03f4f..d1ae17c04 100644 --- a/core/src/main/java/org/openedx/core/utils/FileUtil.kt +++ b/core/src/main/java/org/openedx/core/utils/FileUtil.kt @@ -1,11 +1,13 @@ package org.openedx.core.utils import android.content.Context +import com.google.gson.Gson +import com.google.gson.GsonBuilder import java.io.File -object FileUtil { +class FileUtil(val context: Context) { - fun getExternalAppDir(context: Context): File { + fun getExternalAppDir(): File { val dir = context.externalCacheDir.toString() + File.separator + context.getString(org.openedx.core.R.string.app_name).replace(Regex("\\s"), "_") val file = File(dir) @@ -13,6 +15,23 @@ object FileUtil { return file } + inline fun < reified T> saveObjectToFile(obj: T, fileName: String = "${T::class.java.simpleName}.json") { + val gson: Gson = GsonBuilder().setPrettyPrinting().create() + val jsonString = gson.toJson(obj) + File(getExternalAppDir().path + fileName).writeText(jsonString) + } + + inline fun getObjectFromFile(fileName: String = "${T::class.java.simpleName}.json"): T? { + val file = File(getExternalAppDir().path + fileName) + return if (file.exists()) { + val gson: Gson = GsonBuilder().setPrettyPrinting().create() + val jsonString = file.readText() + gson.fromJson(jsonString, T::class.java) + } else { + null + } + } + } diff --git a/dashboard/src/main/java/org/openedx/courses/domain/model/UserCourses.kt b/dashboard/src/main/java/org/openedx/courses/domain/model/UserCourses.kt index 7d8a02112..5371b1cc3 100644 --- a/dashboard/src/main/java/org/openedx/courses/domain/model/UserCourses.kt +++ b/dashboard/src/main/java/org/openedx/courses/domain/model/UserCourses.kt @@ -1,9 +1,8 @@ package org.openedx.courses.domain.model -import org.openedx.core.domain.model.DashboardCourseList import org.openedx.core.domain.model.EnrolledCourse data class UserCourses( - val enrollments: DashboardCourseList, + val enrollments: List, val primary: EnrolledCourse? ) diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt index 970cd3f85..1f3e7df95 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt @@ -86,7 +86,7 @@ class AllEnrolledCoursesViewModel( _updating.value = true isLoading = true page = 1 - val response = interactor.getUserCourses(page, courseStatusFilter.value).enrollments + val response = interactor.getAllUserCourses(page, courseStatusFilter.value) if (response.pagination.next.isNotEmpty() && page != response.pagination.numPages) { _canLoadMore.value = true page++ @@ -118,7 +118,7 @@ class AllEnrolledCoursesViewModel( try { isLoading = true val response = if (networkConnection.isOnline() || page > 1) { - interactor.getUserCourses(page, courseStatusFilter.value).enrollments + interactor.getAllUserCourses(page, courseStatusFilter.value) } else { null } 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 e0062405d..b01e66a6b 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt @@ -166,7 +166,7 @@ private fun UsersCourseScreen( is UserCoursesUIState.Courses -> { UserCourses( modifier = Modifier.fillMaxSize(), - userCourses = uiState, + userCourses = uiState.userCourses, apiHostUrl = apiHostUrl, onCourseClick = onCourseClick, onViewAllClick = onViewAllClick, @@ -210,7 +210,7 @@ private fun UsersCourseScreen( @Composable private fun UserCourses( modifier: Modifier = Modifier, - userCourses: UserCoursesUIState.Courses, + userCourses: UserCourses, apiHostUrl: String, onCourseClick: (EnrolledCourse) -> Unit, openDates: (EnrolledCourse) -> Unit, @@ -221,9 +221,9 @@ private fun UserCourses( modifier = modifier .padding(vertical = 12.dp) ) { - if (userCourses.primaryCourse != null) { + if (userCourses.primary != null) { PrimaryCourseCard( - primaryCourse = userCourses.primaryCourse, + primaryCourse = userCourses.primary, apiHostUrl = apiHostUrl, openDates = openDates, onResumeClick = onResumeClick, @@ -231,7 +231,7 @@ private fun UserCourses( ) } SecondaryCourses( - courses = userCourses.enrolledCourses, + courses = userCourses.enrollments, apiHostUrl = apiHostUrl, onCourseClick = onCourseClick, onViewAllClick = onViewAllClick @@ -637,7 +637,7 @@ private val mockDashboardCourseList = DashboardCourseList( courses = listOf(mockCourse, mockCourse, mockCourse, mockCourse, mockCourse, mockCourse) ) private val mockUserCourses = UserCourses( - enrollments = mockDashboardCourseList, + enrollments = mockDashboardCourseList.courses, primary = mockCourse ) @@ -646,7 +646,7 @@ private val mockUserCourses = UserCourses( private fun UsersCourseScreenPreview() { OpenEdXTheme { UsersCourseScreen( - uiState = UserCoursesUIState.Courses(mockUserCourses.enrollments.courses, mockUserCourses.primary), + uiState = UserCoursesUIState.Courses(mockUserCourses), apiHostUrl = "", uiMessage = null, updating = false, diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesUIState.kt b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesUIState.kt index 1b7656d17..2e0382d8d 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesUIState.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesUIState.kt @@ -1,9 +1,9 @@ package org.openedx.courses.presentation -import org.openedx.core.domain.model.EnrolledCourse +import org.openedx.courses.domain.model.UserCourses sealed class UserCoursesUIState { - data class Courses(val enrolledCourses: List, val primaryCourse: EnrolledCourse?) : UserCoursesUIState() - object Empty : UserCoursesUIState() - object Loading : UserCoursesUIState() + data class Courses(val userCourses: UserCourses) : UserCoursesUIState() + data object Empty : UserCoursesUIState() + data object Loading : UserCoursesUIState() } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt index 71646bb33..db1c42057 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt @@ -18,6 +18,8 @@ import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CourseDashboardUpdate import org.openedx.core.system.notifier.DiscoveryNotifier +import org.openedx.core.utils.FileUtil +import org.openedx.courses.domain.model.UserCourses import org.openedx.dashboard.domain.interactor.DashboardInteractor class UserCoursesViewModel( @@ -25,7 +27,8 @@ class UserCoursesViewModel( private val interactor: DashboardInteractor, private val resourceManager: ResourceManager, private val discoveryNotifier: DiscoveryNotifier, - private val networkConnection: NetworkConnection + private val networkConnection: NetworkConnection, + private val fileUtil: FileUtil ) : BaseViewModel() { val apiHostUrl get() = config.getApiHostURL() @@ -66,21 +69,18 @@ class UserCoursesViewModel( viewModelScope.launch { try { if (networkConnection.isOnline()) { - val response = interactor.getUserCourses() - if (response.primary == null && response.enrollments.courses.isNotEmpty()) { + val response = interactor.getMainUserCourses() + if (response.primary == null && response.enrollments.isNotEmpty()) { _uiState.value = UserCoursesUIState.Empty } else { - _uiState.value = UserCoursesUIState.Courses(response.enrollments.courses, response.primary) + _uiState.value = UserCoursesUIState.Courses(response) } } else { - val cachedList = interactor.getEnrolledCoursesFromCache() - if (cachedList.isEmpty()) { + val cachedUserCourses = fileUtil.getObjectFromFile() + if (cachedUserCourses == null) { _uiState.value = UserCoursesUIState.Empty } else { - _uiState.value = UserCoursesUIState.Courses( - cachedList, - null - ) + _uiState.value = UserCoursesUIState.Courses(cachedUserCourses) } } } catch (e: Exception) { diff --git a/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt b/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt index 5ac162441..cd3ca27da 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt @@ -4,6 +4,7 @@ import org.openedx.core.data.api.CourseApi import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.DashboardCourseList import org.openedx.core.domain.model.EnrolledCourse +import org.openedx.core.utils.FileUtil import org.openedx.courses.domain.model.UserCourses import org.openedx.dashboard.data.DashboardDao import org.openedx.dashboard.domain.CourseStatusFilter @@ -11,7 +12,8 @@ import org.openedx.dashboard.domain.CourseStatusFilter class DashboardRepository( private val api: CourseApi, private val dao: DashboardDao, - private val preferencesManager: CorePreferences + private val preferencesManager: CorePreferences, + private val fileUtil: FileUtil, ) { suspend fun getEnrolledCourses(page: Int): DashboardCourseList { @@ -33,21 +35,34 @@ class DashboardRepository( return list.map { it.mapToDomain() } } - suspend fun getUserCourses(page: Int, status: CourseStatusFilter?): UserCourses { + suspend fun getMainUserCourses(): UserCourses { + val user = preferencesManager.user + val result = api.getUserCourses( + username = user?.username ?: "" + ) + preferencesManager.appConfig = result.configs.mapToDomain() + + val userCourses = UserCourses( + enrollments = result.enrollments.mapToDomain().courses, + primary = result.primary?.mapToDomain() + ) + fileUtil.saveObjectToFile(userCourses) + return userCourses + } + + suspend fun getAllUserCourses(page: Int, status: CourseStatusFilter?): DashboardCourseList { val user = preferencesManager.user val result = api.getUserCourses( username = user?.username ?: "", page = page, - status = status?.key + status = status?.key, + fields = listOf("progress") ) preferencesManager.appConfig = result.configs.mapToDomain() dao.clearCachedData() dao.insertEnrolledCourseEntity(*result.enrollments.results.map { it.mapToRoomEntity() } .toTypedArray()) - return UserCourses( - enrollments = result.enrollments.mapToDomain(), - primary = result.primary?.mapToDomain() - ) + return result.enrollments.mapToDomain() } } diff --git a/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt b/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt index a3bff4d82..f8ec45ed8 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt @@ -15,8 +15,12 @@ class DashboardInteractor( suspend fun getEnrolledCoursesFromCache() = repository.getEnrolledCoursesFromCache() - suspend fun getUserCourses(page: Int = 1, status: CourseStatusFilter? = null): UserCourses { - return repository.getUserCourses( + suspend fun getMainUserCourses(): UserCourses { + return repository.getMainUserCourses( ) + } + + suspend fun getAllUserCourses(page: Int = 1, status: CourseStatusFilter? = null): DashboardCourseList { + return repository.getAllUserCourses( page, status ) From 5e04ae61b9b12b46a2cfe266033628ff44085eca Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Wed, 1 May 2024 18:41:57 +0300 Subject: [PATCH 11/23] feat: Dashboard --- .../main/java/org/openedx/app/AppRouter.kt | 19 +- .../java/org/openedx/app/di/ScreenModule.kt | 12 +- .../openedx/core/CourseContainerTabEntity.kt | 9 - .../org/openedx/core/data/api/CourseApi.kt | 1 + .../core/data/model/CourseAssignments.kt | 12 +- .../room/discovery/EnrolledCourseEntity.kt | 6 +- .../core/domain/model/CourseAssignments.kt | 2 +- .../core/system/notifier/CourseNotifier.kt | 1 + .../core/system/notifier/CourseOpenBlock.kt | 3 + .../java/org/openedx/core/ui/ComposeCommon.kt | 35 +- .../openedx/core/ui/PreviewFragmentManager.kt | 5 + .../main/java/org/openedx/core/ui/TabItem.kt | 2 +- .../container/CollapsingLayout.kt | 3 +- .../container/CourseContainerAdapter.kt | 47 -- .../container/CourseContainerFragment.kt | 35 +- .../container/CourseContainerViewModel.kt | 5 + .../presentation/dates/CourseDatesScreen.kt | 40 +- .../dates/CourseDatesViewModel.kt | 2 + .../outline/CourseOutlineScreen.kt | 79 ++- .../outline/CourseOutlineViewModel.kt | 59 ++- .../course/presentation/ui/CourseVideosUI.kt | 58 ++- .../videos/CourseVideoViewModel.kt | 2 + .../AllEnrolledCoursesFragment.kt | 465 +++++++++--------- .../AllEnrolledCoursesViewModel.kt | 32 +- .../courses/presentation/UserCoursesScreen.kt | 274 ++++++++--- .../presentation/UserCoursesViewModel.kt | 39 +- .../dashboard/domain/CourseStatusFilter.kt | 9 +- .../presentation/DashboardFragment.kt | 4 +- .../dashboard/presentation/DashboardRouter.kt | 3 + .../learn/presentation/LearnFragment.kt | 137 ++---- .../learn/presentation/LearnViewModel.kt | 38 ++ .../main/res/drawable/dashboard_ic_book.xml | 44 ++ dashboard/src/main/res/values/strings.xml | 4 +- .../discovery/presentation/DiscoveryRouter.kt | 1 + 34 files changed, 870 insertions(+), 617 deletions(-) delete mode 100644 core/src/main/java/org/openedx/core/CourseContainerTabEntity.kt create mode 100644 core/src/main/java/org/openedx/core/system/notifier/CourseOpenBlock.kt create mode 100644 core/src/main/java/org/openedx/core/ui/PreviewFragmentManager.kt delete mode 100644 course/src/main/java/org/openedx/course/presentation/container/CourseContainerAdapter.kt create mode 100644 dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt create mode 100644 dashboard/src/main/res/drawable/dashboard_ic_book.xml diff --git a/app/src/main/java/org/openedx/app/AppRouter.kt b/app/src/main/java/org/openedx/app/AppRouter.kt index 1cba9697f..4b908987d 100644 --- a/app/src/main/java/org/openedx/app/AppRouter.kt +++ b/app/src/main/java/org/openedx/app/AppRouter.kt @@ -8,8 +8,8 @@ import org.openedx.auth.presentation.logistration.LogistrationFragment import org.openedx.auth.presentation.restore.RestorePasswordFragment import org.openedx.auth.presentation.signin.SignInFragment import org.openedx.auth.presentation.signup.SignUpFragment -import org.openedx.core.CourseContainerTabEntity import org.openedx.core.FragmentViewType +import org.openedx.core.presentation.course.CourseContainerTab import org.openedx.core.presentation.course.CourseViewMode import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRouter import org.openedx.core.presentation.global.app_upgrade.UpgradeRequiredFragment @@ -135,6 +135,18 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di ) { replaceFragmentWithBackStack(fm, CourseInfoFragment.newInstance(courseId, infoType)) } + + override fun navigateToCourseOutline( + fm: FragmentManager, + courseId: String, + courseTitle: String, + enrollmentMode: String + ) { + replaceFragmentWithBackStack( + fm, + CourseContainerFragment.newInstance(courseId, courseTitle, enrollmentMode) + ) + } //endregion //region DashboardRouter @@ -144,11 +156,12 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di courseId: String, courseTitle: String, enrollmentMode: String, - openTab: CourseContainerTabEntity + requiredTab: CourseContainerTab, + openBlock: String ) { replaceFragmentWithBackStack( fm, - CourseContainerFragment.newInstance(courseId, courseTitle, enrollmentMode, openTab) + CourseContainerFragment.newInstance(courseId, courseTitle, enrollmentMode, requiredTab, openBlock) ) } diff --git a/app/src/main/java/org/openedx/app/di/ScreenModule.kt b/app/src/main/java/org/openedx/app/di/ScreenModule.kt index 086d6b43c..8522ddaab 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -51,6 +51,7 @@ import org.openedx.discussion.presentation.search.DiscussionSearchThreadViewMode import org.openedx.discussion.presentation.threads.DiscussionAddThreadViewModel import org.openedx.discussion.presentation.threads.DiscussionThreadsViewModel import org.openedx.discussion.presentation.topics.DiscussionTopicsViewModel +import org.openedx.learn.presentation.LearnViewModel import org.openedx.profile.data.repository.ProfileRepository import org.openedx.profile.domain.interactor.ProfileInteractor import org.openedx.profile.domain.model.Account @@ -119,8 +120,9 @@ val screenModule = module { factory { DashboardRepository(get(), get(), get(), get()) } factory { DashboardInteractor(get()) } viewModel { DashboardViewModel(get(), get(), get(), get(), get(), get(), get()) } - viewModel { UserCoursesViewModel(get(), get(), get(), get(), get(), get()) } - viewModel { AllEnrolledCoursesViewModel(get(), get(), get(), get(), get(), get()) } + viewModel { UserCoursesViewModel(get(), get(), get(), get(), get(), get(), get()) } + viewModel { AllEnrolledCoursesViewModel(get(), get(), get(), get(), get(), get(), get()) } + viewModel { LearnViewModel(get(), get()) } factory { DiscoveryRepository(get(), get(), get()) } factory { DiscoveryInteractor(get()) } @@ -183,10 +185,11 @@ val screenModule = module { get() ) } - viewModel { (courseId: String, courseTitle: String, enrollmentMode: String) -> + viewModel { (courseId: String, courseTitle: String, enrollmentMode: String, openBlock: String) -> CourseContainerViewModel( courseId, courseTitle, + openBlock, enrollmentMode, get(), get(), @@ -215,6 +218,7 @@ val screenModule = module { get(), get(), get(), + get() ) } viewModel { (courseId: String) -> @@ -256,6 +260,7 @@ val screenModule = module { get(), get(), get(), + get() ) } viewModel { (courseId: String) -> BaseVideoViewModel(courseId, get()) } @@ -293,6 +298,7 @@ val screenModule = module { get(), get(), get(), + get() ) } viewModel { (courseId: String, handoutsType: String) -> diff --git a/core/src/main/java/org/openedx/core/CourseContainerTabEntity.kt b/core/src/main/java/org/openedx/core/CourseContainerTabEntity.kt deleted file mode 100644 index bcee74aaa..000000000 --- a/core/src/main/java/org/openedx/core/CourseContainerTabEntity.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.openedx.core - -enum class CourseContainerTabEntity { - COURSE, - VIDEOS, - DISCUSSION, - DATES, - MORE; -} \ No newline at end of file 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 edbff1481..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,6 +72,7 @@ interface CourseApi { suspend fun getUserCourses( @Path("username") username: String, @Query("page") page: Int = 1, + @Query("page_size") pageSize: Int = 20, @Query("status") status: String? = null, @Query("requested_fields") fields: List = emptyList() ): CourseEnrollments diff --git a/core/src/main/java/org/openedx/core/data/model/CourseAssignments.kt b/core/src/main/java/org/openedx/core/data/model/CourseAssignments.kt index 27d97fee2..66f468609 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseAssignments.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseAssignments.kt @@ -4,21 +4,25 @@ import com.google.gson.annotations.SerializedName import org.openedx.core.data.model.room.discovery.CourseAssignmentsDb data class CourseAssignments( - @SerializedName("future_assignment") - val futureAssignment: CourseDateBlock?, + @SerializedName("future_assignments") + val futureAssignments: List?, @SerializedName("past_assignments") val pastAssignments: List? ) { fun mapToDomain(): org.openedx.core.domain.model.CourseAssignments = org.openedx.core.domain.model.CourseAssignments( - futureAssignment = futureAssignment?.mapToDomain(), + futureAssignments = futureAssignments?.map { + it.mapToDomain() + }, pastAssignments = pastAssignments?.map { it.mapToDomain() } ) fun mapToRoomEntity() = CourseAssignmentsDb( - futureAssignment = futureAssignment?.mapToRoomEntity(), + futureAssignments = futureAssignments?.map { + it.mapToRoomEntity() + }, pastAssignments = pastAssignments?.map { it.mapToRoomEntity() } diff --git a/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt b/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt index e4fc125e3..23f2e9717 100644 --- a/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt +++ b/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt @@ -201,13 +201,13 @@ data class CourseStatusDb( } data class CourseAssignmentsDb( - @Embedded - val futureAssignment: CourseDateBlockDb?, + @ColumnInfo("futureAssignments") + val futureAssignments: List?, @ColumnInfo("pastAssignments") val pastAssignments: List? ) { fun mapToDomain() = CourseAssignments( - futureAssignment = futureAssignment?.mapToDomain(), + futureAssignments = futureAssignments?.map { it.mapToDomain() }, pastAssignments = pastAssignments?.map { it.mapToDomain() } ) } diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseAssignments.kt b/core/src/main/java/org/openedx/core/domain/model/CourseAssignments.kt index 6fcaadade..feb039fc7 100644 --- a/core/src/main/java/org/openedx/core/domain/model/CourseAssignments.kt +++ b/core/src/main/java/org/openedx/core/domain/model/CourseAssignments.kt @@ -5,6 +5,6 @@ import kotlinx.parcelize.Parcelize @Parcelize data class CourseAssignments( - val futureAssignment: CourseDateBlock?, + val futureAssignments: List?, val pastAssignments: List? ): Parcelable diff --git a/core/src/main/java/org/openedx/core/system/notifier/CourseNotifier.kt b/core/src/main/java/org/openedx/core/system/notifier/CourseNotifier.kt index 63660b4de..b4582f9c1 100644 --- a/core/src/main/java/org/openedx/core/system/notifier/CourseNotifier.kt +++ b/core/src/main/java/org/openedx/core/system/notifier/CourseNotifier.kt @@ -20,4 +20,5 @@ class CourseNotifier { suspend fun send(event: CourseLoading) = channel.emit(event) suspend fun send(event: CourseDataReady) = channel.emit(event) suspend fun send(event: CourseRefresh) = channel.emit(event) + suspend fun send(event: CourseOpenBlock) = channel.emit(event) } diff --git a/core/src/main/java/org/openedx/core/system/notifier/CourseOpenBlock.kt b/core/src/main/java/org/openedx/core/system/notifier/CourseOpenBlock.kt new file mode 100644 index 000000000..6704f1256 --- /dev/null +++ b/core/src/main/java/org/openedx/core/system/notifier/CourseOpenBlock.kt @@ -0,0 +1,3 @@ +package org.openedx.core.system.notifier + +data class CourseOpenBlock(val blockId: String) : CourseEvent diff --git a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt index 7edfbb2bf..aed1ba642 100644 --- a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt +++ b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt @@ -28,6 +28,7 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape @@ -1204,17 +1205,22 @@ fun RoundTabsBar( modifier: Modifier = Modifier, items: List, pagerState: PagerState, + contentPadding: PaddingValues = PaddingValues(), + withPager: Boolean = false, rowState: LazyListState = rememberLazyListState(), - onPageChange: (Int) -> Unit + onTabClicked: (Int) -> Unit = { } ) { + // The pager state does not work without the pager and the tabs do not change. + if (!withPager) { + HorizontalPager(state = pagerState) { } + } + val scope = rememberCoroutineScope() - val windowSize = rememberWindowSize() - val horizontalPadding = if (!windowSize.isTablet) 12.dp else 98.dp LazyRow( modifier = modifier, state = rowState, horizontalArrangement = Arrangement.spacedBy(8.dp), - contentPadding = PaddingValues(vertical = 16.dp, horizontal = horizontalPadding), + contentPadding = contentPadding, ) { itemsIndexed(items) { index, item -> val isSelected = pagerState.currentPage == index @@ -1237,10 +1243,10 @@ fun RoundTabsBar( .clickable { scope.launch { pagerState.scrollToPage(index) - onPageChange(index) + onTabClicked(index) } } - .padding(horizontal = 12.dp), + .padding(horizontal = 16.dp), item = item, contentColor = contentColor ) @@ -1259,12 +1265,15 @@ private fun RoundTab( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { - Icon( - painter = rememberVectorPainter(item.icon), - tint = contentColor, - contentDescription = null - ) - Spacer(modifier = Modifier.width(4.dp)) + val icon = item.icon + if (icon != null) { + Icon( + painter = rememberVectorPainter(icon), + tint = contentColor, + contentDescription = null + ) + Spacer(modifier = Modifier.width(4.dp)) + } Text( text = stringResource(item.labelResId), color = contentColor @@ -1365,7 +1374,7 @@ private fun RoundTabsBarPreview() { items = listOf(mockTab, mockTab, mockTab), rowState = rememberLazyListState(), pagerState = rememberPagerState(pageCount = { 3 }), - onPageChange = { } + onTabClicked = { } ) } } diff --git a/core/src/main/java/org/openedx/core/ui/PreviewFragmentManager.kt b/core/src/main/java/org/openedx/core/ui/PreviewFragmentManager.kt new file mode 100644 index 000000000..ecf8a2700 --- /dev/null +++ b/core/src/main/java/org/openedx/core/ui/PreviewFragmentManager.kt @@ -0,0 +1,5 @@ +package org.openedx.core.ui + +import androidx.fragment.app.FragmentManager + +object PreviewFragmentManager : FragmentManager() \ No newline at end of file diff --git a/core/src/main/java/org/openedx/core/ui/TabItem.kt b/core/src/main/java/org/openedx/core/ui/TabItem.kt index 65a88861e..d6952c010 100644 --- a/core/src/main/java/org/openedx/core/ui/TabItem.kt +++ b/core/src/main/java/org/openedx/core/ui/TabItem.kt @@ -6,5 +6,5 @@ import androidx.compose.ui.graphics.vector.ImageVector interface TabItem { @get:StringRes val labelResId: Int - val icon: ImageVector + val icon: ImageVector? } diff --git a/course/src/main/java/org/openedx/course/presentation/container/CollapsingLayout.kt b/course/src/main/java/org/openedx/course/presentation/container/CollapsingLayout.kt index b5d73adaf..74c85873f 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CollapsingLayout.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CollapsingLayout.kt @@ -748,8 +748,7 @@ private fun CollapsingLayoutPreview() { RoundTabsBar( items = CourseContainerTab.entries, rowState = rememberLazyListState(), - pagerState = rememberPagerState(pageCount = { 5 }), - onPageChange = { } + pagerState = rememberPagerState(pageCount = { CourseContainerTab.entries.size }) ) }, onBackClick = {}, diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerAdapter.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerAdapter.kt deleted file mode 100644 index e24a5dfef..000000000 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerAdapter.kt +++ /dev/null @@ -1,47 +0,0 @@ -package org.openedx.course.presentation.container - -import android.os.Parcelable -import androidx.fragment.app.Fragment -import androidx.viewpager2.adapter.FragmentStateAdapter -import kotlinx.parcelize.Parcelize -import org.openedx.core.CourseContainerTabEntity -import org.openedx.course.R - -class CourseContainerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) { - - private val fragments = HashMap() - - override fun getItemCount(): Int = fragments.size - - override fun createFragment(position: Int): Fragment { - val tab = CourseContainerTab.entries.find { it.ordinal == position } - return fragments[tab] ?: throw IllegalStateException("Fragment not found for tab $tab") - } - - fun addFragment(tab: CourseContainerTab, fragment: Fragment) { - fragments[tab] = fragment - } - - fun getFragment(tab: CourseContainerTab): Fragment? = fragments[tab] -} - -@Parcelize -enum class CourseContainerTab(val itemId: Int, val titleResId: Int): Parcelable { - COURSE(itemId = R.id.course, titleResId = R.string.course_navigation_course), - VIDEOS(itemId = R.id.videos, titleResId = R.string.course_navigation_videos), - DISCUSSION(itemId = R.id.discussions, titleResId = R.string.course_navigation_discussions), - DATES(itemId = R.id.dates, titleResId = R.string.course_navigation_dates), - MORE(itemId = R.id.resources, titleResId = R.string.course_navigation_more); - - companion object { - fun fromEntity(courseContainerTabEntity: CourseContainerTabEntity): CourseContainerTab { - return when (courseContainerTabEntity) { - CourseContainerTabEntity.COURSE -> COURSE - CourseContainerTabEntity.VIDEOS -> VIDEOS - CourseContainerTabEntity.DISCUSSION -> DISCUSSION - CourseContainerTabEntity.DATES -> DATES - CourseContainerTabEntity.MORE -> MORE - } - } - } -} diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt index 3b16eb57c..868c7d1e7 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt @@ -5,6 +5,7 @@ import android.view.View import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -51,7 +52,7 @@ import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf -import org.openedx.core.extension.parcelable +import org.openedx.core.extension.serializable import org.openedx.core.extension.takeIfNotEmpty import org.openedx.core.presentation.course.CourseContainerTab import org.openedx.core.presentation.global.viewBinding @@ -83,7 +84,8 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { parametersOf( requireArguments().getString(ARG_COURSE_ID, ""), requireArguments().getString(ARG_TITLE, ""), - requireArguments().getString(ARG_ENROLLMENT_MODE, "") + requireArguments().getString(ARG_ENROLLMENT_MODE, ""), + requireArguments().getString(ARG_OPEN_BLOCK, "") ) } @@ -255,16 +257,22 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { const val ARG_COURSE_ID = "courseId" const val ARG_TITLE = "title" const val ARG_ENROLLMENT_MODE = "enrollmentMode" + const val ARG_REQUIRED_TAB = "requiredTab" + const val ARG_OPEN_BLOCK = "resume_block" fun newInstance( courseId: String, courseTitle: String, enrollmentMode: String, + requiredTab: CourseContainerTab = CourseContainerTab.HOME, + openBlock: String = "" ): CourseContainerFragment { val fragment = CourseContainerFragment() fragment.arguments = bundleOf( ARG_COURSE_ID to courseId, ARG_TITLE to courseTitle, - ARG_ENROLLMENT_MODE to enrollmentMode + ARG_ENROLLMENT_MODE to enrollmentMode, + ARG_REQUIRED_TAB to requiredTab, + ARG_OPEN_BLOCK to openBlock ) return fragment } @@ -295,7 +303,11 @@ fun CourseDashboard( val refreshing by viewModel.refreshing.collectAsState(true) val courseImage by viewModel.courseImage.collectAsState() val uiMessage by viewModel.uiMessage.collectAsState(null) - val pagerState = rememberPagerState(pageCount = { CourseContainerTab.entries.size }) + val requiredTab = bundle.serializable(CourseContainerFragment.ARG_REQUIRED_TAB) + val pagerState = rememberPagerState( + initialPage = CourseContainerTab.entries.indexOf(requiredTab), + pageCount = { CourseContainerTab.entries.size } + ) val tabState = rememberLazyListState() val snackState = remember { SnackbarHostState() } val pullRefreshState = rememberPullRefreshState( @@ -340,9 +352,11 @@ fun CourseDashboard( if (isNavigationEnabled) { RoundTabsBar( items = CourseContainerTab.entries, + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 16.dp), rowState = tabState, pagerState = pagerState, - onPageChange = viewModel::courseContainerTabClickedEvent + withPager = true, + onTabClicked = viewModel::courseContainerTabClickedEvent ) } else { Spacer(modifier = Modifier.height(52.dp)) @@ -426,7 +440,7 @@ fun DashboardPager( CourseContainerTab.HOME -> { CourseOutlineScreen( windowSize = windowSize, - courseOutlineViewModel = koinViewModel( + viewModel = koinViewModel( parameters = { parametersOf( bundle.getString(CourseContainerFragment.ARG_COURSE_ID, ""), @@ -434,7 +448,6 @@ fun DashboardPager( ) } ), - courseRouter = viewModel.courseRouter, fragmentManager = fragmentManager, onResetDatesClick = { viewModel.onRefresh(CourseContainerTab.DATES) @@ -445,7 +458,7 @@ fun DashboardPager( CourseContainerTab.VIDEOS -> { CourseVideosScreen( windowSize = windowSize, - courseVideoViewModel = koinViewModel( + viewModel = koinViewModel( parameters = { parametersOf( bundle.getString(CourseContainerFragment.ARG_COURSE_ID, ""), @@ -453,14 +466,13 @@ fun DashboardPager( ) } ), - fragmentManager = fragmentManager, - courseRouter = viewModel.courseRouter, + fragmentManager = fragmentManager ) } CourseContainerTab.DATES -> { CourseDatesScreen( - courseDatesViewModel = koinViewModel( + viewModel = koinViewModel( parameters = { parametersOf( bundle.getString(CourseContainerFragment.ARG_ENROLLMENT_MODE, "") @@ -468,7 +480,6 @@ fun DashboardPager( } ), windowSize = windowSize, - courseRouter = viewModel.courseRouter, fragmentManager = fragmentManager, isFragmentResumed = isResumed, updateCourseStructure = { diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt index 8c61a00d3..73472da3f 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt @@ -35,6 +35,7 @@ import org.openedx.core.system.notifier.CourseDataReady import org.openedx.core.system.notifier.CourseDatesShifted import org.openedx.core.system.notifier.CourseLoading import org.openedx.core.system.notifier.CourseNotifier +import org.openedx.core.system.notifier.CourseOpenBlock import org.openedx.core.system.notifier.CourseRefresh import org.openedx.core.system.notifier.CourseStructureUpdated import org.openedx.core.utils.TimeUtils @@ -58,6 +59,7 @@ import org.openedx.core.R as CoreR class CourseContainerViewModel( val courseId: String, var courseName: String, + private var openBlock: String, private val enrollmentMode: String, private val config: Config, private val interactor: CourseInteractor, @@ -184,6 +186,9 @@ class CourseContainerViewModel( if (isReady) { _isNavigationEnabled.value = true courseNotifier.send(CourseDataReady(courseStructure)) + if (openBlock.isNotEmpty()) { + courseNotifier.send(CourseOpenBlock(openBlock)) + } } isReady } diff --git a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesScreen.kt b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesScreen.kt index 715584497..66583f95f 100644 --- a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesScreen.kt +++ b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesScreen.kt @@ -85,7 +85,6 @@ import org.openedx.core.ui.windowSizeValue import org.openedx.core.utils.TimeUtils import org.openedx.core.utils.clearTime import org.openedx.course.R -import org.openedx.course.presentation.CourseRouter import org.openedx.course.presentation.calendarsync.CalendarSyncUIState import org.openedx.course.presentation.ui.CourseDatesBanner import org.openedx.course.presentation.ui.CourseDatesBannerTablet @@ -95,50 +94,49 @@ import org.openedx.core.R as CoreR @Composable fun CourseDatesScreen( windowSize: WindowSize, - courseDatesViewModel: CourseDatesViewModel, - courseRouter: CourseRouter, + viewModel: CourseDatesViewModel, fragmentManager: FragmentManager, isFragmentResumed: Boolean, updateCourseStructure: () -> Unit ) { - val uiState by courseDatesViewModel.uiState.observeAsState(DatesUIState.Loading) - val uiMessage by courseDatesViewModel.uiMessage.collectAsState(null) - val calendarSyncUIState by courseDatesViewModel.calendarSyncUIState.collectAsState() + val uiState by viewModel.uiState.observeAsState(DatesUIState.Loading) + val uiMessage by viewModel.uiMessage.collectAsState(null) + val calendarSyncUIState by viewModel.calendarSyncUIState.collectAsState() val context = LocalContext.current CourseDatesUI( windowSize = windowSize, uiState = uiState, uiMessage = uiMessage, - isSelfPaced = courseDatesViewModel.isSelfPaced, + isSelfPaced = viewModel.isSelfPaced, calendarSyncUIState = calendarSyncUIState, onItemClick = { block -> if (block.blockId.isNotEmpty()) { - courseDatesViewModel.getVerticalBlock(block.blockId) + viewModel.getVerticalBlock(block.blockId) ?.let { verticalBlock -> - courseDatesViewModel.logCourseComponentTapped(true, block) - if (courseDatesViewModel.isCourseExpandableSectionsEnabled) { - courseRouter.navigateToCourseContainer( + viewModel.logCourseComponentTapped(true, block) + if (viewModel.isCourseExpandableSectionsEnabled) { + viewModel.courseRouter.navigateToCourseContainer( fm = fragmentManager, - courseId = courseDatesViewModel.courseId, + courseId = viewModel.courseId, unitId = verticalBlock.id, componentId = "", mode = CourseViewMode.FULL ) } else { - courseDatesViewModel.getSequentialBlock(verticalBlock.id) + viewModel.getSequentialBlock(verticalBlock.id) ?.let { sequentialBlock -> - courseRouter.navigateToCourseSubsections( + viewModel.courseRouter.navigateToCourseSubsections( fm = fragmentManager, subSectionId = sequentialBlock.id, - courseId = courseDatesViewModel.courseId, + courseId = viewModel.courseId, unitId = verticalBlock.id, mode = CourseViewMode.FULL ) } } } ?: { - courseDatesViewModel.logCourseComponentTapped(false, block) + viewModel.logCourseComponentTapped(false, block) ActionDialogFragment.newInstance( title = context.getString(CoreR.string.core_leaving_the_app), message = context.getString( @@ -157,20 +155,20 @@ fun CourseDatesScreen( }, onPLSBannerViewed = { if (isFragmentResumed) { - courseDatesViewModel.logPlsBannerViewed() + viewModel.logPlsBannerViewed() } }, onSyncDates = { - courseDatesViewModel.logPlsShiftButtonClicked() - courseDatesViewModel.resetCourseDatesBanner { - courseDatesViewModel.logPlsShiftDates(it) + viewModel.logPlsShiftButtonClicked() + viewModel.resetCourseDatesBanner { + viewModel.logPlsShiftDates(it) if (it) { updateCourseStructure() } } }, onCalendarSyncSwitch = { isChecked -> - courseDatesViewModel.handleCalendarSyncState(isChecked) + viewModel.handleCalendarSyncState(isChecked) }, ) } diff --git a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt index 79f866ba7..4593fe254 100644 --- a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt @@ -35,6 +35,7 @@ import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.CourseAnalyticsEvent import org.openedx.course.presentation.CourseAnalyticsKey +import org.openedx.course.presentation.CourseRouter import org.openedx.course.presentation.calendarsync.CalendarManager import org.openedx.course.presentation.calendarsync.CalendarSyncDialogType import org.openedx.course.presentation.calendarsync.CalendarSyncUIState @@ -49,6 +50,7 @@ class CourseDatesViewModel( private val corePreferences: CorePreferences, private val courseAnalytics: CourseAnalytics, private val config: Config, + val courseRouter: CourseRouter ) : BaseViewModel() { var courseId = "" diff --git a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineScreen.kt b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineScreen.kt index 7e950cba8..b767f1918 100644 --- a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineScreen.kt +++ b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -62,7 +63,6 @@ import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appTypography import org.openedx.core.ui.windowSizeValue import org.openedx.course.R -import org.openedx.course.presentation.CourseRouter import org.openedx.course.presentation.ui.CourseDatesBanner import org.openedx.course.presentation.ui.CourseDatesBannerTablet import org.openedx.course.presentation.ui.CourseExpandableChapterCard @@ -75,90 +75,77 @@ import org.openedx.core.R as CoreR @Composable fun CourseOutlineScreen( windowSize: WindowSize, - courseOutlineViewModel: CourseOutlineViewModel, - courseRouter: CourseRouter, + viewModel: CourseOutlineViewModel, fragmentManager: FragmentManager, onResetDatesClick: () -> Unit ) { - val uiState by courseOutlineViewModel.uiState.collectAsState() - val uiMessage by courseOutlineViewModel.uiMessage.collectAsState(null) + val uiState by viewModel.uiState.collectAsState() + val uiMessage by viewModel.uiMessage.collectAsState(null) + val openBlock by viewModel.openBlock.collectAsState("") val context = LocalContext.current + LaunchedEffect(openBlock) { + if (openBlock.isNotEmpty()) { + viewModel.openBlock(fragmentManager, openBlock) + } + } + CourseOutlineUI( windowSize = windowSize, uiState = uiState, - isCourseNestedListEnabled = courseOutlineViewModel.isCourseNestedListEnabled, + isCourseNestedListEnabled = viewModel.isCourseNestedListEnabled, uiMessage = uiMessage, onItemClick = { block -> - courseOutlineViewModel.sequentialClickedEvent( + viewModel.sequentialClickedEvent( block.blockId, block.displayName ) - courseRouter.navigateToCourseSubsections( + viewModel.courseRouter.navigateToCourseSubsections( fm = fragmentManager, - courseId = courseOutlineViewModel.courseId, + courseId = viewModel.courseId, subSectionId = block.id, mode = CourseViewMode.FULL ) }, onExpandClick = { block -> - if (courseOutlineViewModel.switchCourseSections(block.id)) { - courseOutlineViewModel.sequentialClickedEvent( + if (viewModel.switchCourseSections(block.id)) { + viewModel.sequentialClickedEvent( block.blockId, block.displayName ) } }, onSubSectionClick = { subSectionBlock -> - courseOutlineViewModel.courseSubSectionUnit[subSectionBlock.id]?.let { unit -> - courseOutlineViewModel.logUnitDetailViewedEvent( + viewModel.courseSubSectionUnit[subSectionBlock.id]?.let { unit -> + viewModel.logUnitDetailViewedEvent( unit.blockId, unit.displayName ) - courseRouter.navigateToCourseContainer( + viewModel.courseRouter.navigateToCourseContainer( fragmentManager, - courseId = courseOutlineViewModel.courseId, + courseId = viewModel.courseId, unitId = unit.id, mode = CourseViewMode.FULL ) } }, onResumeClick = { componentId -> - courseOutlineViewModel.resumeSectionBlock?.let { subSection -> - courseOutlineViewModel.resumeCourseTappedEvent(subSection.id) - courseOutlineViewModel.resumeVerticalBlock?.let { unit -> - if (courseOutlineViewModel.isCourseExpandableSectionsEnabled) { - courseRouter.navigateToCourseContainer( - fm = fragmentManager, - courseId = courseOutlineViewModel.courseId, - unitId = unit.id, - componentId = componentId, - mode = CourseViewMode.FULL - ) - } else { - courseRouter.navigateToCourseSubsections( - fragmentManager, - courseId = courseOutlineViewModel.courseId, - subSectionId = subSection.id, - mode = CourseViewMode.FULL, - unitId = unit.id, - componentId = componentId - ) - } - } - } + viewModel.openBlock( + fragmentManager, + componentId + ) }, onDownloadClick = { - if (courseOutlineViewModel.isBlockDownloading(it.id)) { - courseRouter.navigateToDownloadQueue( + if (viewModel.isBlockDownloading(it.id)) { + viewModel.courseRouter.navigateToDownloadQueue( fm = fragmentManager, - courseOutlineViewModel.getDownloadableChildren(it.id) + viewModel.getDownloadableChildren(it.id) ?: arrayListOf() ) - } else if (courseOutlineViewModel.isBlockDownloaded(it.id)) { - courseOutlineViewModel.removeDownloadModels(it.id) + } else if (viewModel.isBlockDownloaded(it.id)) { + viewModel.removeDownloadModels(it.id) } else { - courseOutlineViewModel.saveDownloadModels( + viewModel.saveDownloadModels( context.externalCacheDir.toString() + File.separator + context @@ -168,14 +155,14 @@ fun CourseOutlineScreen( } }, onResetDatesClick = { - courseOutlineViewModel.resetCourseDatesBanner( + viewModel.resetCourseDatesBanner( onResetDates = { onResetDatesClick() } ) }, onCertificateClick = { - courseOutlineViewModel.viewCertificateTappedEvent() + viewModel.viewCertificateTappedEvent() it.takeIfNotEmpty() ?.let { url -> AndroidUriHandler(context).openUri(url) } } diff --git a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt index 569498ab6..5561fbeab 100644 --- a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt @@ -1,5 +1,6 @@ package org.openedx.course.presentation.outline +import androidx.fragment.app.FragmentManager import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -25,6 +26,7 @@ import org.openedx.core.module.DownloadWorkerController import org.openedx.core.module.db.DownloadDao import org.openedx.core.module.download.BaseDownloadViewModel import org.openedx.core.presentation.CoreAnalytics +import org.openedx.core.presentation.course.CourseViewMode import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CalendarSyncEvent.CreateCalendarSyncEvent @@ -32,11 +34,13 @@ import org.openedx.core.system.notifier.CourseDataReady import org.openedx.core.system.notifier.CourseDatesShifted import org.openedx.core.system.notifier.CourseLoading import org.openedx.core.system.notifier.CourseNotifier +import org.openedx.core.system.notifier.CourseOpenBlock import org.openedx.core.system.notifier.CourseStructureUpdated import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.CourseAnalyticsEvent import org.openedx.course.presentation.CourseAnalyticsKey +import org.openedx.course.presentation.CourseRouter import org.openedx.course.presentation.calendarsync.CalendarSyncDialogType import org.openedx.course.R as courseR @@ -50,6 +54,7 @@ class CourseOutlineViewModel( private val networkConnection: NetworkConnection, private val preferencesManager: CorePreferences, private val analytics: CourseAnalytics, + val courseRouter: CourseRouter, coreAnalytics: CoreAnalytics, downloadDao: DownloadDao, workerController: DownloadWorkerController, @@ -70,12 +75,14 @@ class CourseOutlineViewModel( val uiMessage: SharedFlow get() = _uiMessage.asSharedFlow() - var resumeSectionBlock: Block? = null - private set - var resumeVerticalBlock: Block? = null - private set + private val _openBlock = MutableSharedFlow() + val openBlock: SharedFlow + get() = _openBlock.asSharedFlow() - val isCourseExpandableSectionsEnabled get() = config.isCourseNestedListEnabled() + private var resumeSectionBlock: Block? = null + private var resumeVerticalBlock: Block? = null + + private val isCourseExpandableSectionsEnabled get() = config.isCourseNestedListEnabled() private val courseSubSections = mutableMapOf>() private val subSectionsDownloadsCount = mutableMapOf() @@ -84,15 +91,20 @@ class CourseOutlineViewModel( init { viewModelScope.launch { courseNotifier.notifier.collect { event -> - when(event) { + when (event) { is CourseStructureUpdated -> { if (event.courseId == courseId) { updateCourseData() } } + is CourseDataReady -> { getCourseData() } + + is CourseOpenBlock -> { + _openBlock.emit(event.blockId) + } } } } @@ -279,6 +291,39 @@ class CourseOutlineViewModel( } } + fun openBlock(fragmentManager: FragmentManager, blockId: String) { + val courseStructure = interactor.getCourseStructureFromCache() + val blocks = courseStructure.blockData + getResumeBlock(blocks, blockId) + resumeBlock(fragmentManager, blockId) + } + + private fun resumeBlock(fragmentManager: FragmentManager, blockId: String) { + resumeSectionBlock?.let { subSection -> + resumeCourseTappedEvent(subSection.id) + resumeVerticalBlock?.let { unit -> + if (isCourseExpandableSectionsEnabled) { + courseRouter.navigateToCourseContainer( + fm = fragmentManager, + courseId = courseId, + unitId = unit.id, + componentId = blockId, + mode = CourseViewMode.FULL + ) + } else { + courseRouter.navigateToCourseSubsections( + fragmentManager, + courseId = courseId, + subSectionId = subSection.id, + mode = CourseViewMode.FULL, + unitId = unit.id, + componentId = blockId + ) + } + } + } + } + fun viewCertificateTappedEvent() { analytics.logEvent( CourseAnalyticsEvent.VIEW_CERTIFICATE.eventName, @@ -289,7 +334,7 @@ class CourseOutlineViewModel( ) } - fun resumeCourseTappedEvent(blockId: String) { + private fun resumeCourseTappedEvent(blockId: String) { val currentState = uiState.value if (currentState is CourseOutlineUIState.CourseData) { analytics.logEvent( diff --git a/course/src/main/java/org/openedx/course/presentation/ui/CourseVideosUI.kt b/course/src/main/java/org/openedx/course/presentation/ui/CourseVideosUI.kt index c69e26c0d..0c085fa53 100644 --- a/course/src/main/java/org/openedx/course/presentation/ui/CourseVideosUI.kt +++ b/course/src/main/java/org/openedx/course/presentation/ui/CourseVideosUI.kt @@ -77,7 +77,6 @@ import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appTypography import org.openedx.core.ui.windowSizeValue import org.openedx.course.R -import org.openedx.course.presentation.CourseRouter import org.openedx.course.presentation.videos.CourseVideoViewModel import org.openedx.course.presentation.videos.CourseVideosUIState import java.io.File @@ -86,58 +85,57 @@ import java.util.Date @Composable fun CourseVideosScreen( windowSize: WindowSize, - courseVideoViewModel: CourseVideoViewModel, - fragmentManager: FragmentManager, - courseRouter: CourseRouter + viewModel: CourseVideoViewModel, + fragmentManager: FragmentManager ) { - val uiState by courseVideoViewModel.uiState.collectAsState(CourseVideosUIState.Loading) - val uiMessage by courseVideoViewModel.uiMessage.collectAsState(null) - val videoSettings by courseVideoViewModel.videoSettings.collectAsState() + val uiState by viewModel.uiState.collectAsState(CourseVideosUIState.Loading) + val uiMessage by viewModel.uiMessage.collectAsState(null) + val videoSettings by viewModel.videoSettings.collectAsState() val context = LocalContext.current CourseVideosUI( windowSize = windowSize, uiState = uiState, uiMessage = uiMessage, - courseTitle = courseVideoViewModel.courseTitle, - isCourseNestedListEnabled = courseVideoViewModel.isCourseNestedListEnabled, + courseTitle = viewModel.courseTitle, + isCourseNestedListEnabled = viewModel.isCourseNestedListEnabled, videoSettings = videoSettings, onItemClick = { block -> - courseRouter.navigateToCourseSubsections( + viewModel.courseRouter.navigateToCourseSubsections( fm = fragmentManager, - courseId = courseVideoViewModel.courseId, + courseId = viewModel.courseId, subSectionId = block.id, mode = CourseViewMode.VIDEOS ) }, onExpandClick = { block -> - courseVideoViewModel.switchCourseSections(block.id) + viewModel.switchCourseSections(block.id) }, onSubSectionClick = { subSectionBlock -> - courseVideoViewModel.courseSubSectionUnit[subSectionBlock.id]?.let { unit -> - courseVideoViewModel.sequentialClickedEvent( + viewModel.courseSubSectionUnit[subSectionBlock.id]?.let { unit -> + viewModel.sequentialClickedEvent( unit.blockId, unit.displayName ) - courseRouter.navigateToCourseContainer( + viewModel.courseRouter.navigateToCourseContainer( fm = fragmentManager, - courseId = courseVideoViewModel.courseId, + courseId = viewModel.courseId, unitId = unit.id, mode = CourseViewMode.VIDEOS ) } }, onDownloadClick = { - if (courseVideoViewModel.isBlockDownloading(it.id)) { - courseRouter.navigateToDownloadQueue( + if (viewModel.isBlockDownloading(it.id)) { + viewModel.courseRouter.navigateToDownloadQueue( fm = fragmentManager, - courseVideoViewModel.getDownloadableChildren(it.id) + viewModel.getDownloadableChildren(it.id) ?: arrayListOf() ) - } else if (courseVideoViewModel.isBlockDownloaded(it.id)) { - courseVideoViewModel.removeDownloadModels(it.id) + } else if (viewModel.isBlockDownloaded(it.id)) { + viewModel.removeDownloadModels(it.id) } else { - courseVideoViewModel.saveDownloadModels( + viewModel.saveDownloadModels( context.externalCacheDir.toString() + File.separator + context @@ -147,11 +145,11 @@ fun CourseVideosScreen( } }, onDownloadAllClick = { isAllBlocksDownloadedOrDownloading -> - courseVideoViewModel.logBulkDownloadToggleEvent(!isAllBlocksDownloadedOrDownloading) + viewModel.logBulkDownloadToggleEvent(!isAllBlocksDownloadedOrDownloading) if (isAllBlocksDownloadedOrDownloading) { - courseVideoViewModel.removeAllDownloadModels() + viewModel.removeAllDownloadModels() } else { - courseVideoViewModel.saveAllDownloadModels( + viewModel.saveAllDownloadModels( context.externalCacheDir.toString() + File.separator + context @@ -161,15 +159,15 @@ fun CourseVideosScreen( } }, onDownloadQueueClick = { - if (courseVideoViewModel.hasDownloadModelsInQueue()) { - courseRouter.navigateToDownloadQueue(fm = fragmentManager) + if (viewModel.hasDownloadModelsInQueue()) { + viewModel.courseRouter.navigateToDownloadQueue(fm = fragmentManager) } }, onVideoDownloadQualityClick = { - if (courseVideoViewModel.hasDownloadModelsInQueue()) { - courseVideoViewModel.onChangingVideoQualityWhileDownloading() + if (viewModel.hasDownloadModelsInQueue()) { + viewModel.onChangingVideoQualityWhileDownloading() } else { - courseRouter.navigateToVideoQuality( + viewModel.courseRouter.navigateToVideoQuality( fragmentManager, VideoQualityType.Download ) diff --git a/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt b/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt index dc88105a8..a4c2be553 100644 --- a/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt @@ -29,6 +29,7 @@ import org.openedx.core.system.notifier.VideoQualityChanged import org.openedx.course.R import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.CourseAnalytics +import org.openedx.course.presentation.CourseRouter class CourseVideoViewModel( val courseId: String, @@ -41,6 +42,7 @@ class CourseVideoViewModel( private val courseNotifier: CourseNotifier, private val videoNotifier: VideoNotifier, private val analytics: CourseAnalytics, + val courseRouter: CourseRouter, coreAnalytics: CoreAnalytics, downloadDao: DownloadDao, workerController: DownloadWorkerController 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 f76ccdc94..91d99bbfa 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt @@ -5,6 +5,7 @@ import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -12,6 +13,8 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -26,6 +29,8 @@ import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Card import androidx.compose.material.CircularProgressIndicator @@ -59,6 +64,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource @@ -72,11 +78,10 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager import coil.compose.AsyncImage import coil.request.ImageRequest -import org.koin.android.ext.android.inject -import org.koin.androidx.viewmodel.ext.android.viewModel -import org.openedx.core.CourseContainerTabEntity +import org.koin.androidx.compose.koinViewModel import org.openedx.core.UIMessage import org.openedx.core.domain.model.Certificate import org.openedx.core.domain.model.CourseAssignments @@ -89,8 +94,7 @@ import org.openedx.core.domain.model.Progress import org.openedx.core.ui.BackBtn import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.OfflineModeDialog -import org.openedx.core.ui.WindowSize -import org.openedx.core.ui.WindowType +import org.openedx.core.ui.RoundTabsBar import org.openedx.core.ui.displayCutoutForLandscape import org.openedx.core.ui.rememberWindowSize import org.openedx.core.ui.shouldLoadMore @@ -102,19 +106,11 @@ import org.openedx.core.ui.theme.appTypography import org.openedx.core.ui.windowSizeValue import org.openedx.core.utils.TimeUtils import org.openedx.dashboard.R -import org.openedx.dashboard.presentation.DashboardRouter +import org.openedx.dashboard.domain.CourseStatusFilter import java.util.Date class AllEnrolledCoursesFragment : Fragment() { - private val viewModel by viewModel() - private val router by inject() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - lifecycle.addObserver(viewModel) - } - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -123,80 +119,101 @@ class AllEnrolledCoursesFragment : Fragment() { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { OpenEdXTheme { - val windowSize = rememberWindowSize() - val uiState by viewModel.uiState.collectAsState() - val uiMessage by viewModel.uiMessage.collectAsState(null) - val refreshing by viewModel.updating.collectAsState(false) - val canLoadMore by viewModel.canLoadMore.collectAsState(false) - AllEnrolledCoursesScreen( - windowSize = windowSize, - viewModel.apiHostUrl, - uiState, - uiMessage, - canLoadMore = canLoadMore, - refreshing = refreshing, - hasInternetConnection = viewModel.hasInternetConnection, - onReloadClick = { - viewModel.getCourses() - }, - onItemClick = { - viewModel.dashboardCourseClickedEvent(it.course.id, it.course.name) - router.navigateToCourseOutline( - requireActivity().supportFragmentManager, - it.course.id, - it.course.name, - it.mode, - CourseContainerTabEntity.COURSE - ) - }, - onSwipeRefresh = { - viewModel.updateCourses() - }, - paginationCallback = { - viewModel.fetchMore() - }, - onBackClick = { - requireActivity().supportFragmentManager.popBackStack() - }, - onSearchClick = { - router.navigateToCourseSearch( - requireActivity().supportFragmentManager, "" - ) - } + fragmentManager = requireActivity().supportFragmentManager ) } } } } -@OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class) @Composable -internal fun AllEnrolledCoursesScreen( - windowSize: WindowSize, +private fun AllEnrolledCoursesScreen( + viewModel: AllEnrolledCoursesViewModel = koinViewModel(), + fragmentManager: FragmentManager +) { + val uiState by viewModel.uiState.collectAsState() + val uiMessage by viewModel.uiMessage.collectAsState(null) + val refreshing by viewModel.updating.collectAsState(false) + val canLoadMore by viewModel.canLoadMore.collectAsState(false) + + AllEnrolledCoursesScreen( + apiHostUrl = viewModel.apiHostUrl, + state = uiState, + uiMessage = uiMessage, + canLoadMore = canLoadMore, + refreshing = refreshing, + hasInternetConnection = viewModel.hasInternetConnection, + onAction = { action -> + when (action) { + AllEnrolledCoursesAction.Reload -> { + viewModel.getCourses() + } + + AllEnrolledCoursesAction.SwipeRefresh -> { + viewModel.updateCourses() + } + + AllEnrolledCoursesAction.EndOfPage -> { + viewModel.fetchMore() + } + + AllEnrolledCoursesAction.Back -> { + fragmentManager.popBackStack() + } + + AllEnrolledCoursesAction.Search -> { + viewModel.dashboardRouter.navigateToCourseSearch( + fragmentManager, "" + ) + } + + is AllEnrolledCoursesAction.OpenCourse -> { + with(action.enrolledCourse) { + viewModel.dashboardCourseClickedEvent(course.id, course.name) + viewModel.dashboardRouter.navigateToCourseOutline( + fragmentManager, + course.id, + course.name, + mode + ) + } + } + + is AllEnrolledCoursesAction.FilterChange -> { + viewModel.getCourses(action.courseStatusFilter) + } + } + } + ) +} + +@OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) +@Composable +private fun AllEnrolledCoursesScreen( apiHostUrl: String, state: AllEnrolledCoursesUIState, uiMessage: UIMessage?, canLoadMore: Boolean, refreshing: Boolean, hasInternetConnection: Boolean, - onReloadClick: () -> Unit, - onSwipeRefresh: () -> Unit, - paginationCallback: () -> Unit, - onBackClick: () -> Unit, - onSearchClick: () -> Unit, - onItemClick: (EnrolledCourse) -> Unit, + 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 = { onSwipeRefresh() } + onRefresh = { onAction(AllEnrolledCoursesAction.SwipeRefresh) } ) - + val tabPagerState = rememberPagerState(pageCount = { + CourseStatusFilter.entries.size + }) var isInternetConnectionShown by rememberSaveable { mutableStateOf(false) } - val scrollState = rememberLazyGridState() val firstVisibleIndex = remember { mutableIntStateOf(scrollState.firstVisibleItemIndex) } @@ -210,19 +227,28 @@ internal fun AllEnrolledCoursesScreen( }, backgroundColor = MaterialTheme.appColors.background ) { paddingValues -> - 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( @@ -238,133 +264,168 @@ internal 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 ) { - onBackClick() - } + 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 ) { - when (state) { - is AllEnrolledCoursesUIState.Loading -> { - Box( + Box( + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + .pullRefresh(pullRefreshState), + ) { + Column( + modifier = Modifier + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Header( modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator(color = MaterialTheme.appColors.primary) - } - } + .padding( + start = contentPaddings.calculateStartPadding(layoutDirection), + end = contentPaddings.calculateEndPadding(layoutDirection) + ), + onSearchClick = { + onAction(AllEnrolledCoursesAction.Search) + } + ) + 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)) + } + ) + when (state) { + is AllEnrolledCoursesUIState.Loading -> { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = MaterialTheme.appColors.primary) + } + } - is AllEnrolledCoursesUIState.Courses -> { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column( - modifier = Modifier.padding(contentPaddings) - ) { - Header(onSearchClick = onSearchClick) - Spacer(modifier = Modifier.height(8.dp)) - LazyVerticalGrid( + is AllEnrolledCoursesUIState.Courses -> { + Box( 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( - course, - apiHostUrl, - onClick = { onItemClick(it) } - ) - } - item { - if (canLoadMore) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator(color = MaterialTheme.appColors.primary) + .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)) { - paginationCallback() + + is AllEnrolledCoursesUIState.Empty -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier + .fillMaxHeight() + .then(emptyStatePaddings) + ) { + EmptyState() + } + } } } } + PullRefreshIndicator( + refreshing, + pullRefreshState, + Modifier.align(Alignment.TopCenter) + ) - is AllEnrolledCoursesUIState.Empty -> { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column( - modifier = Modifier - .fillMaxHeight() - .then(contentWidth) - .then(emptyStatePaddings) - ) { - Header(onSearchClick = onSearchClick) - EmptyState() + 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 - onReloadClick() - } - ) - } } } } @@ -373,13 +434,15 @@ internal fun AllEnrolledCoursesScreen( @Composable private fun CourseItem( + modifier: Modifier = Modifier, course: EnrolledCourse, apiHostUrl: String, onClick: (EnrolledCourse) -> Unit, ) { Card( - modifier = Modifier + modifier = modifier .width(170.dp) + .height(180.dp) .clickable { onClick(course) }, @@ -409,6 +472,7 @@ private fun CourseItem( color = MaterialTheme.appColors.primary, backgroundColor = MaterialTheme.appColors.divider ) + Text( modifier = Modifier .fillMaxWidth() @@ -416,6 +480,9 @@ private fun CourseItem( .padding(top = 4.dp), style = MaterialTheme.appTypography.labelMedium, color = MaterialTheme.appColors.textFieldHint, + overflow = TextOverflow.Ellipsis, + minLines = 1, + maxLines = 2, text = stringResource( R.string.dashboard_course_date, TimeUtils.getCourseFormattedDate( @@ -436,7 +503,7 @@ private fun CourseItem( style = MaterialTheme.appTypography.titleSmall, color = MaterialTheme.appColors.textDark, overflow = TextOverflow.Ellipsis, - minLines = 2, + minLines = 1, maxLines = 2 ) } @@ -522,55 +589,12 @@ private fun EmptyState() { @Preview(uiMode = UI_MODE_NIGHT_NO) @Preview(uiMode = UI_MODE_NIGHT_YES) -@Composable -private fun CourseItemPreview() { - OpenEdXTheme() { - CourseItem( - mockCourseEnrolled, - "http://localhost:8000", - onClick = {}) - } -} - -@Preview(uiMode = UI_MODE_NIGHT_NO) -@Preview(uiMode = UI_MODE_NIGHT_YES) -@Composable -private fun AllEnrolledCoursesPreview() { - OpenEdXTheme { - AllEnrolledCoursesScreen( - windowSize = WindowSize(WindowType.Compact, WindowType.Compact), - apiHostUrl = "http://localhost:8000", - state = AllEnrolledCoursesUIState.Courses( - listOf( - mockCourseEnrolled, - mockCourseEnrolled, - mockCourseEnrolled, - mockCourseEnrolled, - mockCourseEnrolled, - mockCourseEnrolled - ) - ), - uiMessage = null, - onSwipeRefresh = {}, - onItemClick = {}, - onReloadClick = {}, - hasInternetConnection = true, - refreshing = false, - canLoadMore = false, - paginationCallback = {}, - onBackClick = {}, - onSearchClick = {} - ) - } -} - @Preview(uiMode = UI_MODE_NIGHT_NO, device = Devices.NEXUS_9) @Preview(uiMode = UI_MODE_NIGHT_YES, device = Devices.NEXUS_9) @Composable -private fun AllEnrolledCoursesTabletPreview() { +private fun AllEnrolledCoursesPreview() { OpenEdXTheme { AllEnrolledCoursesScreen( - windowSize = WindowSize(WindowType.Medium, WindowType.Medium), apiHostUrl = "http://localhost:8000", state = AllEnrolledCoursesUIState.Courses( listOf( @@ -583,15 +607,10 @@ private fun AllEnrolledCoursesTabletPreview() { ) ), uiMessage = null, - onSwipeRefresh = {}, - onItemClick = {}, - onReloadClick = {}, hasInternetConnection = true, refreshing = false, canLoadMore = false, - paginationCallback = {}, - onBackClick = {}, - onSearchClick = {} + onAction = {} ) } } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt index 1f3e7df95..5c70677e8 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt @@ -22,6 +22,7 @@ import org.openedx.core.system.notifier.DiscoveryNotifier import org.openedx.dashboard.domain.CourseStatusFilter import org.openedx.dashboard.domain.interactor.DashboardInteractor import org.openedx.dashboard.presentation.DashboardAnalytics +import org.openedx.dashboard.presentation.DashboardRouter class AllEnrolledCoursesViewModel( private val config: Config, @@ -29,7 +30,8 @@ class AllEnrolledCoursesViewModel( private val interactor: DashboardInteractor, private val resourceManager: ResourceManager, private val discoveryNotifier: DiscoveryNotifier, - private val analytics: DashboardAnalytics + private val analytics: DashboardAnalytics, + val dashboardRouter: DashboardRouter ) : BaseViewModel() { private val coursesList = mutableListOf() @@ -57,7 +59,7 @@ class AllEnrolledCoursesViewModel( val canLoadMore: StateFlow get() = _canLoadMore.asStateFlow() - val courseStatusFilter: MutableStateFlow = MutableStateFlow(CourseStatusFilter.ALL) + private val currentFilter: MutableStateFlow = MutableStateFlow(CourseStatusFilter.ALL) override fun onCreate(owner: LifecycleOwner) { super.onCreate(owner) @@ -71,13 +73,13 @@ class AllEnrolledCoursesViewModel( } init { - getCourses() + getCourses(currentFilter.value) } - fun getCourses() { + fun getCourses(courseStatusFilter: CourseStatusFilter? = null) { _uiState.value = AllEnrolledCoursesUIState.Loading coursesList.clear() - internalLoadingCourses() + internalLoadingCourses(courseStatusFilter ?: currentFilter.value) } fun updateCourses() { @@ -86,7 +88,7 @@ class AllEnrolledCoursesViewModel( _updating.value = true isLoading = true page = 1 - val response = interactor.getAllUserCourses(page, courseStatusFilter.value) + val response = interactor.getAllUserCourses(page, currentFilter.value) if (response.pagination.next.isNotEmpty() && page != response.pagination.numPages) { _canLoadMore.value = true page++ @@ -113,12 +115,16 @@ class AllEnrolledCoursesViewModel( } } - private fun internalLoadingCourses() { + private fun internalLoadingCourses(courseStatusFilter: CourseStatusFilter? = null) { + if (courseStatusFilter != null) { + page = 1 + currentFilter.value = courseStatusFilter + } viewModelScope.launch { try { isLoading = true val response = if (networkConnection.isOnline() || page > 1) { - interactor.getAllUserCourses(page, courseStatusFilter.value) + interactor.getAllUserCourses(page, currentFilter.value) } else { null } @@ -165,3 +171,13 @@ class AllEnrolledCoursesViewModel( } } + +interface AllEnrolledCoursesAction { + object Reload : AllEnrolledCoursesAction + object SwipeRefresh : AllEnrolledCoursesAction + object EndOfPage : AllEnrolledCoursesAction + object Back : AllEnrolledCoursesAction + object Search : AllEnrolledCoursesAction + data class OpenCourse(val enrolledCourse: EnrolledCourse) : AllEnrolledCoursesAction + data class FilterChange(val courseStatusFilter: CourseStatusFilter?) : AllEnrolledCoursesAction +} 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 b01e66a6b..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,13 +60,17 @@ 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 import coil.compose.AsyncImage import coil.request.ImageRequest +import org.koin.androidx.compose.koinViewModel import org.openedx.core.UIMessage import org.openedx.core.domain.model.Certificate import org.openedx.core.domain.model.CourseAssignments +import org.openedx.core.domain.model.CourseDateBlock import org.openedx.core.domain.model.CourseSharingUtmParameters import org.openedx.core.domain.model.CourseStatus import org.openedx.core.domain.model.CoursewareAccess @@ -72,9 +79,11 @@ import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.domain.model.EnrolledCourseData import org.openedx.core.domain.model.Pagination import org.openedx.core.domain.model.Progress +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 @@ -87,11 +96,8 @@ import org.openedx.core.R as CoreR @Composable fun UsersCourseScreen( - viewModel: UserCoursesViewModel, - onCourseClick: (EnrolledCourse) -> Unit, - openDates: (EnrolledCourse) -> Unit, - onViewAllClick: () -> Unit, - onResumeClick: (componentId: String) -> Unit, + viewModel: UserCoursesViewModel = koinViewModel(), + fragmentManager: FragmentManager, ) { val updating by viewModel.updating.collectAsState(false) val uiMessage by viewModel.uiMessage.collectAsState(null) @@ -102,13 +108,51 @@ fun UsersCourseScreen( uiState = uiState, updating = updating, apiHostUrl = viewModel.apiHostUrl, - onSwipeRefresh = viewModel::updateCourses, hasInternetConnection = viewModel.hasInternetConnection, - onCourseClick = onCourseClick, - onViewAllClick = onViewAllClick, - openDates = openDates, - onResumeClick = onResumeClick, - onReloadClick = viewModel::getCourses + onAction = { action -> + when (action) { + UserCoursesScreenAction.SwipeRefresh -> { + viewModel.updateCourses() + } + + UserCoursesScreenAction.ViewAll -> { + viewModel.dashboardRouter.navigateToAllEnrolledCourses(fragmentManager) + } + + UserCoursesScreenAction.Reload -> { + viewModel.getCourses() + } + + is UserCoursesScreenAction.OpenCourse -> { + viewModel.dashboardRouter.navigateToCourseOutline( + fm = fragmentManager, + courseId = action.enrolledCourse.course.id, + courseTitle = action.enrolledCourse.course.name, + enrollmentMode = action.enrolledCourse.mode + ) + } + + is UserCoursesScreenAction.NavigateToDates -> { + viewModel.dashboardRouter.navigateToCourseOutline( + fm = fragmentManager, + courseId = action.enrolledCourse.course.id, + courseTitle = action.enrolledCourse.course.name, + enrollmentMode = action.enrolledCourse.mode, + requiredTab = CourseContainerTab.DATES + ) + } + + is UserCoursesScreenAction.OpenBlock -> { + viewModel.dashboardRouter.navigateToCourseOutline( + fm = fragmentManager, + courseId = action.enrolledCourse.course.id, + courseTitle = action.enrolledCourse.course.name, + enrollmentMode = action.enrolledCourse.mode, + openBlock = action.blockId + ) + } + } + } ) } @@ -119,18 +163,13 @@ private fun UsersCourseScreen( uiState: UserCoursesUIState, updating: Boolean, apiHostUrl: String, - onSwipeRefresh: () -> Unit, - onCourseClick: (EnrolledCourse) -> Unit, - openDates: (EnrolledCourse) -> Unit, - onViewAllClick: () -> Unit, - onReloadClick: () -> Unit, - onResumeClick: (componentId: String) -> Unit, + onAction: (UserCoursesScreenAction) -> Unit, hasInternetConnection: Boolean ) { val scaffoldState = rememberScaffoldState() val pullRefreshState = rememberPullRefreshState( refreshing = updating, - onRefresh = { onSwipeRefresh() } + onRefresh = { onAction(UserCoursesScreenAction.SwipeRefresh) } ) var isInternetConnectionShown by rememberSaveable { mutableStateOf(false) @@ -168,10 +207,18 @@ private fun UsersCourseScreen( modifier = Modifier.fillMaxSize(), userCourses = uiState.userCourses, apiHostUrl = apiHostUrl, - onCourseClick = onCourseClick, - onViewAllClick = onViewAllClick, - openDates = openDates, - onResumeClick = onResumeClick + openCourse = { + onAction(UserCoursesScreenAction.OpenCourse(it)) + }, + onViewAllClick = { + onAction(UserCoursesScreenAction.ViewAll) + }, + navigateToDates = { + onAction(UserCoursesScreenAction.NavigateToDates(it)) + }, + openBlock = { course, blockId -> + onAction(UserCoursesScreenAction.OpenBlock(course, blockId)) + } ) } @@ -198,7 +245,7 @@ private fun UsersCourseScreen( }, onReloadClick = { isInternetConnectionShown = true - onReloadClick() + onAction(UserCoursesScreenAction.Reload) } ) } @@ -212,10 +259,10 @@ private fun UserCourses( modifier: Modifier = Modifier, userCourses: UserCourses, apiHostUrl: String, - onCourseClick: (EnrolledCourse) -> Unit, - openDates: (EnrolledCourse) -> Unit, + openCourse: (EnrolledCourse) -> Unit, + navigateToDates: (EnrolledCourse) -> Unit, onViewAllClick: () -> Unit, - onResumeClick: (componentId: String) -> Unit, + openBlock: (enrolledCourse: EnrolledCourse, blockId: String) -> Unit, ) { Column( modifier = modifier @@ -225,15 +272,15 @@ private fun UserCourses( PrimaryCourseCard( primaryCourse = userCourses.primary, apiHostUrl = apiHostUrl, - openDates = openDates, - onResumeClick = onResumeClick, - onCourseClick = onCourseClick + navigateToDates = navigateToDates, + openBlock = openBlock, + openCourse = openCourse ) } SecondaryCourses( courses = userCourses.enrollments, apiHostUrl = apiHostUrl, - onCourseClick = onCourseClick, + onCourseClick = openCourse, onViewAllClick = onViewAllClick ) } @@ -246,30 +293,86 @@ private fun SecondaryCourses( onCourseClick: (EnrolledCourse) -> Unit, onViewAllClick: () -> Unit ) { + 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( - text = stringResource(R.string.dashboard_view_all, courses.size), + 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(courses) { - CourseListItem( - course = it, - apiHostUrl = apiHostUrl, - onCourseClick = onCourseClick - ) + 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 + ) + } } + ) + } +} + +@Composable +private fun ViewAllItem( + onViewAllClick: () -> Unit +) { + Card( + modifier = Modifier + .width(140.dp) + .height(152.dp) + .padding(4.dp) + .clickable( + onClickLabel = stringResource(id = R.string.dashboard_view_all), + onClick = { + onViewAllClick() + } + ), + backgroundColor = MaterialTheme.appColors.cardViewBackground, + shape = MaterialTheme.appShapes.courseImageShape, + elevation = 2.dp, + ) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon( + modifier = Modifier.size(48.dp), + painter = painterResource(id = R.drawable.dashboard_ic_book), + tint = MaterialTheme.appColors.textFieldBorder, + contentDescription = null + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = stringResource(id = R.string.dashboard_view_all), + style = MaterialTheme.appTypography.titleSmall, + color = MaterialTheme.appColors.textDark + ) } } } @@ -283,6 +386,7 @@ private fun CourseListItem( Card( modifier = Modifier .width(140.dp) + .height(152.dp) .padding(4.dp) .clickable { onCourseClick(course) @@ -360,10 +464,15 @@ private fun AssignmentItem( Column( verticalArrangement = Arrangement.spacedBy(4.dp) ) { + val infoTextStyle = if (title.isNullOrEmpty()) { + MaterialTheme.appTypography.titleSmall + } else { + MaterialTheme.appTypography.labelSmall + } Text( text = info, color = MaterialTheme.appColors.textDark, - style = MaterialTheme.appTypography.labelSmall + style = infoTextStyle ) if (!title.isNullOrEmpty()) { Text( @@ -380,9 +489,9 @@ private fun AssignmentItem( private fun PrimaryCourseCard( primaryCourse: EnrolledCourse, apiHostUrl: String, - openDates: (EnrolledCourse) -> Unit, - onResumeClick: (componentId: String) -> Unit, - onCourseClick: (EnrolledCourse) -> Unit, + navigateToDates: (EnrolledCourse) -> Unit, + openBlock: (enrolledCourse: EnrolledCourse, blockId: String) -> Unit, + openCourse: (EnrolledCourse) -> Unit, ) { val context = LocalContext.current Card( @@ -407,11 +516,16 @@ private fun PrimaryCourseCard( .fillMaxWidth() .height(140.dp) ) + val progress: Float = try { + (primaryCourse.progress.numPointsEarned / primaryCourse.progress.numPointsPossible).toFloat() + } catch (_: ArithmeticException) { + 0f + } LinearProgressIndicator( modifier = Modifier .fillMaxWidth() .height(8.dp), - progress = primaryCourse.progress.numPointsEarned.toFloat(), + progress = progress, color = MaterialTheme.appColors.primary, backgroundColor = MaterialTheme.appColors.divider ) @@ -424,14 +538,15 @@ private fun PrimaryCourseCard( ) val pastAssignments = primaryCourse.courseAssignments?.pastAssignments if (!pastAssignments.isNullOrEmpty()) { - val title = if (pastAssignments.size == 1) pastAssignments.first().title else null + val nearestAssignment = pastAssignments.maxBy { it.date } + val title = if (pastAssignments.size == 1) nearestAssignment.title else null Divider() AssignmentItem( modifier = Modifier.clickable { if (pastAssignments.size == 1) { - onResumeClick(pastAssignments.first().blockId) + openBlock(primaryCourse, nearestAssignment.blockId) } else { - openDates(primaryCourse) + navigateToDates(primaryCourse) } }, painter = rememberVectorPainter(Icons.Default.Warning), @@ -439,19 +554,25 @@ private fun PrimaryCourseCard( info = stringResource(R.string.dashboard_past_due_assignment, pastAssignments.size) ) } - val futureAssignment = primaryCourse.courseAssignments?.futureAssignment - if (futureAssignment != null) { + val futureAssignments = primaryCourse.courseAssignments?.futureAssignments + if (!futureAssignments.isNullOrEmpty()) { + val nearestAssignment = futureAssignments.minBy { it.date } + val title = if (futureAssignments.size == 1) nearestAssignment.title else null Divider() AssignmentItem( modifier = Modifier.clickable { - onResumeClick(futureAssignment.blockId) + if (futureAssignments.size == 1) { + openBlock(primaryCourse, nearestAssignment.blockId) + } else { + navigateToDates(primaryCourse) + } }, painter = painterResource(id = CoreR.drawable.ic_core_chapter_icon), - title = futureAssignment.title, + title = title, info = stringResource( R.string.dashboard_assignment_due_in_days, - futureAssignment.assignmentType ?: "", - TimeUtils.getCourseFormattedDate(context, futureAssignment.date) + nearestAssignment.assignmentType ?: "", + TimeUtils.getCourseFormattedDate(context, nearestAssignment.date) ) ) } @@ -459,9 +580,9 @@ private fun PrimaryCourseCard( primaryCourse = primaryCourse, onClick = { if (primaryCourse.courseStatus == null) { - onCourseClick(primaryCourse) + openCourse(primaryCourse) } else { - onResumeClick(primaryCourse.courseStatus?.lastVisitedBlockId ?: "") + openBlock(primaryCourse, primaryCourse.courseStatus?.lastVisitedBlockId ?: "") } } ) @@ -590,8 +711,14 @@ private fun EmptyState( } } - -private val mockCourseAssignments = CourseAssignments(null, emptyList()) +private val mockCourseDateBlock = CourseDateBlock( + title = "Homework 1: ABCD", + description = "After this date, course content will be archived", + date = TimeUtils.iso8601ToDate("2023-10-20T15:08:07Z")!!, + assignmentType = "Homework" +) +private val mockCourseAssignments = + CourseAssignments(listOf(mockCourseDateBlock), listOf(mockCourseDateBlock, mockCourseDateBlock)) private val mockCourse = EnrolledCourse( auditAccessExpires = Date(), created = "created", @@ -603,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(), @@ -641,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 { @@ -651,12 +792,7 @@ private fun UsersCourseScreenPreview() { uiMessage = null, updating = false, hasInternetConnection = false, - onSwipeRefresh = { }, - onCourseClick = { }, - onViewAllClick = { }, - openDates = { }, - onResumeClick = { }, - onReloadClick = { } + onAction = {} ) } } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt index db1c42057..094112e5d 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt @@ -1,6 +1,5 @@ package org.openedx.courses.presentation -import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -13,6 +12,7 @@ import org.openedx.core.BaseViewModel import org.openedx.core.R import org.openedx.core.UIMessage import org.openedx.core.config.Config +import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.extension.isInternetError import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection @@ -21,6 +21,7 @@ import org.openedx.core.system.notifier.DiscoveryNotifier import org.openedx.core.utils.FileUtil import org.openedx.courses.domain.model.UserCourses import org.openedx.dashboard.domain.interactor.DashboardInteractor +import org.openedx.dashboard.presentation.DashboardRouter class UserCoursesViewModel( private val config: Config, @@ -28,11 +29,11 @@ class UserCoursesViewModel( private val resourceManager: ResourceManager, private val discoveryNotifier: DiscoveryNotifier, private val networkConnection: NetworkConnection, - private val fileUtil: FileUtil + private val fileUtil: FileUtil, + val dashboardRouter: DashboardRouter ) : BaseViewModel() { val apiHostUrl get() = config.getApiHostURL() - val isProgramTypeWebView get() = config.getProgramConfig().isViewTypeWebView() private val _uiState = MutableStateFlow(UserCoursesUIState.Loading) val uiState: StateFlow @@ -49,19 +50,8 @@ class UserCoursesViewModel( val hasInternetConnection: Boolean get() = networkConnection.isOnline() - override fun onCreate(owner: LifecycleOwner) { - super.onCreate(owner) - viewModelScope.launch { - discoveryNotifier.notifier.collect { - // TODO Notifier doesn't collect data - if (it is CourseDashboardUpdate) { - updateCourses() - } - } - } - } - init { + collectDiscoveryNotifier() getCourses() } @@ -99,4 +89,23 @@ class UserCoursesViewModel( _updating.value = true getCourses() } + + private fun collectDiscoveryNotifier() { + viewModelScope.launch { + discoveryNotifier.notifier.collect { + if (it is CourseDashboardUpdate) { + updateCourses() + } + } + } + } +} + +interface UserCoursesScreenAction { + object SwipeRefresh : UserCoursesScreenAction + object ViewAll : UserCoursesScreenAction + object Reload : UserCoursesScreenAction + data class OpenBlock(val enrolledCourse: EnrolledCourse, val blockId: String) : UserCoursesScreenAction + data class OpenCourse(val enrolledCourse: EnrolledCourse) : UserCoursesScreenAction + data class NavigateToDates(val enrolledCourse: EnrolledCourse) : UserCoursesScreenAction } diff --git a/dashboard/src/main/java/org/openedx/dashboard/domain/CourseStatusFilter.kt b/dashboard/src/main/java/org/openedx/dashboard/domain/CourseStatusFilter.kt index 9e05a5844..e53d7fb88 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/domain/CourseStatusFilter.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/domain/CourseStatusFilter.kt @@ -1,9 +1,16 @@ package org.openedx.dashboard.domain import androidx.annotation.StringRes +import androidx.compose.ui.graphics.vector.ImageVector +import org.openedx.core.ui.TabItem import org.openedx.dashboard.R -enum class CourseStatusFilter(val key: String, @StringRes val text: Int) { +enum class CourseStatusFilter( + val key: String, + @StringRes + override val labelResId: Int, + override val icon: ImageVector? = null +) : TabItem { ALL("all", R.string.dashboard_course_filter_all), IN_PROGRESS("in_progress", R.string.dashboard_course_filter_in_progress), COMPLETE("completed", R.string.dashboard_course_filter_completed), diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt index 52f55c9cc..ae01484f3 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt @@ -71,7 +71,6 @@ import coil.request.ImageRequest import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.core.AppUpdateState -import org.openedx.core.CourseContainerTabEntity import org.openedx.core.UIMessage import org.openedx.core.domain.model.Certificate import org.openedx.core.domain.model.CourseAssignments @@ -144,8 +143,7 @@ class DashboardFragment : Fragment() { requireParentFragment().parentFragmentManager, it.course.id, it.course.name, - it.mode, - CourseContainerTabEntity.COURSE + it.mode ) }, onSwipeRefresh = { diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt index f3df5ead7..937091044 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt @@ -1,6 +1,7 @@ package org.openedx.dashboard.presentation import androidx.fragment.app.FragmentManager +import org.openedx.core.presentation.course.CourseContainerTab interface DashboardRouter { @@ -9,6 +10,8 @@ interface DashboardRouter { courseId: String, courseTitle: String, enrollmentMode: String, + requiredTab: CourseContainerTab = CourseContainerTab.HOME, + openBlock: String = "" ) fun navigateToSettings(fm: FragmentManager) 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 9e92baa51..5720e6922 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt @@ -47,14 +47,10 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment -import androidx.lifecycle.viewmodel.compose.viewModel -import org.koin.android.ext.android.inject -import org.koin.androidx.viewmodel.ext.android.viewModel -import org.openedx.core.CourseContainerTabEntity -import org.openedx.core.domain.model.EnrolledCourse +import androidx.fragment.app.FragmentManager +import org.koin.androidx.compose.koinViewModel import org.openedx.core.presentation.global.InDevelopmentScreen -import org.openedx.core.ui.WindowSize -import org.openedx.core.ui.WindowType +import org.openedx.core.ui.PreviewFragmentManager import org.openedx.core.ui.crop import org.openedx.core.ui.displayCutoutForLandscape import org.openedx.core.ui.rememberWindowSize @@ -63,17 +59,12 @@ import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appTypography import org.openedx.core.ui.windowSizeValue -import org.openedx.courses.presentation.UserCoursesViewModel import org.openedx.courses.presentation.UsersCourseScreen import org.openedx.dashboard.R -import org.openedx.dashboard.presentation.DashboardRouter import org.openedx.learn.LearnType class LearnFragment : Fragment() { - private val userCoursesViewModel by viewModel() - private val router by inject() - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -82,41 +73,8 @@ class LearnFragment : Fragment() { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { OpenEdXTheme { - val windowSize = rememberWindowSize() LearnScreen( - windowSize = windowSize, - userCoursesViewModel = userCoursesViewModel, - onCourseClick = { - router.navigateToCourseOutline( - requireParentFragment().parentFragmentManager, - it.course.id, - it.course.name, - it.mode, - CourseContainerTabEntity.COURSE - ) - }, - openDates = { - router.navigateToCourseOutline( - requireParentFragment().parentFragmentManager, - it.course.id, - it.course.name, - it.mode, - CourseContainerTabEntity.DATES - ) - }, - onResumeClick = { componentId -> - //TODO - }, - onViewAllClick = { - router.navigateToAllEnrolledCourses( - requireParentFragment().parentFragmentManager - ) - }, - onSearchClick = { - router.navigateToCourseSearch( - requireParentFragment().parentFragmentManager, "" - ) - } + fragmentManager = requireParentFragment().parentFragmentManager ) } } @@ -126,14 +84,10 @@ class LearnFragment : Fragment() { @OptIn(ExperimentalFoundationApi::class) @Composable private fun LearnScreen( - windowSize: WindowSize, - userCoursesViewModel: UserCoursesViewModel, - onCourseClick: (course: EnrolledCourse) -> Unit, - openDates: (course: EnrolledCourse) -> Unit, - onResumeClick: (componentId: String) -> Unit, - onViewAllClick: () -> Unit, - onSearchClick: () -> Unit, + viewModel: LearnViewModel = koinViewModel(), + fragmentManager: FragmentManager, ) { + val windowSize = rememberWindowSize() val scaffoldState = rememberScaffoldState() val pagerState = rememberPagerState { LearnType.entries.size @@ -141,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(), ) ) @@ -152,48 +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 = onSearchClick - ) - - if (userCoursesViewModel.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( - viewModel = userCoursesViewModel, - onCourseClick = onCourseClick, - onViewAllClick = onViewAllClick, - openDates = openDates, - onResumeClick = onResumeClick + 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() + } } } } @@ -321,13 +274,7 @@ private fun LearnDropdownMenu( private fun LearnScreenPreview() { OpenEdXTheme { LearnScreen( - userCoursesViewModel = viewModel(), - windowSize = WindowSize(WindowType.Compact, WindowType.Compact), - onCourseClick = {}, - onViewAllClick = {}, - onSearchClick = {}, - openDates = {}, - onResumeClick = {} + fragmentManager = PreviewFragmentManager ) } } diff --git a/dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt b/dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt new file mode 100644 index 000000000..8497cd290 --- /dev/null +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt @@ -0,0 +1,38 @@ +package org.openedx.learn.presentation + +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import org.openedx.core.BaseViewModel +import org.openedx.core.R +import org.openedx.core.UIMessage +import org.openedx.core.config.Config +import org.openedx.core.domain.model.EnrolledCourse +import org.openedx.core.extension.isInternetError +import org.openedx.core.system.ResourceManager +import org.openedx.core.system.connection.NetworkConnection +import org.openedx.core.system.notifier.CourseDashboardUpdate +import org.openedx.core.system.notifier.DiscoveryNotifier +import org.openedx.core.utils.FileUtil +import org.openedx.courses.domain.model.UserCourses +import org.openedx.dashboard.domain.interactor.DashboardInteractor +import org.openedx.dashboard.presentation.DashboardRouter + +class LearnViewModel( + private val config: Config, + private val dashboardRouter: DashboardRouter +) : BaseViewModel() { + + val isProgramTypeWebView get() = config.getProgramConfig().isViewTypeWebView() + + fun onSearchClick(fragmentManager: FragmentManager) { + dashboardRouter.navigateToCourseSearch(fragmentManager, "") + } +} diff --git a/dashboard/src/main/res/drawable/dashboard_ic_book.xml b/dashboard/src/main/res/drawable/dashboard_ic_book.xml new file mode 100644 index 000000000..a26c83ec7 --- /dev/null +++ b/dashboard/src/main/res/drawable/dashboard_ic_book.xml @@ -0,0 +1,44 @@ + + + + + + + + + + diff --git a/dashboard/src/main/res/values/strings.xml b/dashboard/src/main/res/values/strings.xml index f2a29e18d..fdcd630d9 100644 --- a/dashboard/src/main/res/values/strings.xml +++ b/dashboard/src/main/res/values/strings.xml @@ -10,11 +10,13 @@ Start course Resume Course %1$d Past Due Assignment - View All (%1$d) + View All (%1$d) + View All %1$s Due in %2$s All In Progress Completed Expired All Courses + All diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryRouter.kt b/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryRouter.kt index e1c4baa74..4d07b90ed 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryRouter.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryRouter.kt @@ -1,6 +1,7 @@ package org.openedx.discovery.presentation import androidx.fragment.app.FragmentManager +import org.openedx.core.presentation.course.CourseContainerTab interface DiscoveryRouter { From 83506fc0f254bffe80d66acced6fc7c181cd462e Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Thu, 9 May 2024 16:34:52 +0300 Subject: [PATCH 12/23] refactor: Dashboard type flag change, start course button change --- .../main/java/org/openedx/app/AppRouter.kt | 2 +- .../main/java/org/openedx/app/MainFragment.kt | 12 +-- .../java/org/openedx/app/di/ScreenModule.kt | 8 +- .../java/org/openedx/core/config/Config.kt | 10 +-- .../openedx/core/config/DashboardConfig.kt | 16 ++++ .../org/openedx/core/data/model/Progress.kt | 18 ++--- .../room/discovery/EnrolledCourseEntity.kt | 12 +-- .../openedx/core/domain/model/CourseStatus.kt | 2 +- .../org/openedx/core/domain/model/Progress.kt | 4 +- .../openedx/core/ui/PreviewFragmentManager.kt | 5 -- .../presentation/MyCoursesScreenTest.kt | 6 +- .../AllEnrolledCoursesFragment.kt | 7 +- .../AllEnrolledCoursesViewModel.kt | 8 +- ...oursesScreen.kt => PrimaryCourseScreen.kt} | 77 +++++++++++-------- .../presentation/PrimaryCourseUIState.kt | 9 +++ ...ViewModel.kt => PrimaryCourseViewModel.kt} | 28 +++---- .../presentation/UserCoursesUIState.kt | 9 --- ...rdFragment.kt => ListDashboardFragment.kt} | 16 ++-- ...ViewModel.kt => ListDashboardViewModel.kt} | 2 +- .../learn/presentation/LearnViewModel.kt | 20 ----- ...t.kt => PrimaryCourseDashboardFragment.kt} | 55 +++++++------ .../presentation/DashboardViewModelTest.kt | 20 ++--- default_config/dev/config.yaml | 8 +- default_config/prod/config.yaml | 5 +- default_config/stage/config.yaml | 5 +- 25 files changed, 189 insertions(+), 175 deletions(-) create mode 100644 core/src/main/java/org/openedx/core/config/DashboardConfig.kt delete mode 100644 core/src/main/java/org/openedx/core/ui/PreviewFragmentManager.kt rename dashboard/src/main/java/org/openedx/courses/presentation/{UserCoursesScreen.kt => PrimaryCourseScreen.kt} (91%) create mode 100644 dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseUIState.kt rename dashboard/src/main/java/org/openedx/courses/presentation/{UserCoursesViewModel.kt => PrimaryCourseViewModel.kt} (82%) delete mode 100644 dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesUIState.kt rename dashboard/src/main/java/org/openedx/dashboard/presentation/{DashboardFragment.kt => ListDashboardFragment.kt} (98%) rename dashboard/src/main/java/org/openedx/dashboard/presentation/{DashboardViewModel.kt => ListDashboardViewModel.kt} (99%) rename dashboard/src/main/java/org/openedx/learn/presentation/{LearnFragment.kt => PrimaryCourseDashboardFragment.kt} (88%) diff --git a/app/src/main/java/org/openedx/app/AppRouter.kt b/app/src/main/java/org/openedx/app/AppRouter.kt index 4b908987d..ecabfed23 100644 --- a/app/src/main/java/org/openedx/app/AppRouter.kt +++ b/app/src/main/java/org/openedx/app/AppRouter.kt @@ -166,7 +166,7 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di } override fun navigateToEnrolledProgramInfo(fm: FragmentManager, pathId: String) { - replaceFragmentWithBackStack(fm, ProgramFragment.newInstance(pathId)) + replaceFragmentWithBackStack(fm, ProgramFragment(true)) } override fun navigateToNoAccess( diff --git a/app/src/main/java/org/openedx/app/MainFragment.kt b/app/src/main/java/org/openedx/app/MainFragment.kt index 911addb26..b73982630 100644 --- a/app/src/main/java/org/openedx/app/MainFragment.kt +++ b/app/src/main/java/org/openedx/app/MainFragment.kt @@ -14,12 +14,13 @@ import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.app.adapter.MainNavigationFragmentAdapter import org.openedx.app.databinding.FragmentMainBinding import org.openedx.core.config.Config +import org.openedx.core.config.DashboardConfig import org.openedx.core.presentation.global.app_upgrade.UpgradeRequiredFragment import org.openedx.core.presentation.global.viewBinding -import org.openedx.dashboard.presentation.DashboardFragment +import org.openedx.dashboard.presentation.ListDashboardFragment import org.openedx.discovery.presentation.DiscoveryNavigator import org.openedx.discovery.presentation.DiscoveryRouter -import org.openedx.learn.presentation.LearnFragment +import org.openedx.learn.presentation.PrimaryCourseDashboardFragment import org.openedx.profile.presentation.profile.ProfileFragment class MainFragment : Fragment(R.layout.fragment_main) { @@ -101,10 +102,9 @@ class MainFragment : Fragment(R.layout.fragment_main) { binding.viewPager.offscreenPageLimit = 4 val discoveryFragment = DiscoveryNavigator(viewModel.isDiscoveryTypeWebView).getDiscoveryFragment() - val dashboardFragment = if (config.isDashboardNewScreenEnabled()) { - LearnFragment() - } else { - DashboardFragment() + val dashboardFragment = when (config.getDashboardConfig().getType()) { + DashboardConfig.DashboardType.LIST -> ListDashboardFragment() + DashboardConfig.DashboardType.PRIMARY_COURSE -> PrimaryCourseDashboardFragment() } adapter = MainNavigationFragmentAdapter(this).apply { diff --git a/app/src/main/java/org/openedx/app/di/ScreenModule.kt b/app/src/main/java/org/openedx/app/di/ScreenModule.kt index 8522ddaab..11d0116fc 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -30,10 +30,10 @@ import org.openedx.course.presentation.unit.video.VideoViewModel import org.openedx.course.presentation.videos.CourseVideoViewModel import org.openedx.course.settings.download.DownloadQueueViewModel import org.openedx.courses.presentation.AllEnrolledCoursesViewModel -import org.openedx.courses.presentation.UserCoursesViewModel +import org.openedx.courses.presentation.PrimaryCourseViewModel import org.openedx.dashboard.data.repository.DashboardRepository import org.openedx.dashboard.domain.interactor.DashboardInteractor -import org.openedx.dashboard.presentation.DashboardViewModel +import org.openedx.dashboard.presentation.ListDashboardViewModel import org.openedx.discovery.data.repository.DiscoveryRepository import org.openedx.discovery.domain.interactor.DiscoveryInteractor import org.openedx.discovery.presentation.NativeDiscoveryViewModel @@ -119,8 +119,8 @@ val screenModule = module { factory { DashboardRepository(get(), get(), get(), get()) } factory { DashboardInteractor(get()) } - viewModel { DashboardViewModel(get(), get(), get(), get(), get(), get(), get()) } - viewModel { UserCoursesViewModel(get(), get(), get(), get(), get(), get(), get()) } + viewModel { ListDashboardViewModel(get(), get(), get(), get(), get(), get(), get()) } + viewModel { PrimaryCourseViewModel(get(), get(), get(), get(), get(), get(), get()) } viewModel { AllEnrolledCoursesViewModel(get(), get(), get(), get(), get(), get(), get()) } viewModel { LearnViewModel(get(), get()) } diff --git a/core/src/main/java/org/openedx/core/config/Config.kt b/core/src/main/java/org/openedx/core/config/Config.kt index cd7387ce1..2efea3135 100644 --- a/core/src/main/java/org/openedx/core/config/Config.kt +++ b/core/src/main/java/org/openedx/core/config/Config.kt @@ -91,6 +91,10 @@ class Config(context: Context) { return getObjectOrNewInstance(PROGRAM, ProgramConfig::class.java) } + fun getDashboardConfig(): DashboardConfig { + return getObjectOrNewInstance(DASHBOARD, DashboardConfig::class.java) + } + fun getBranchConfig(): BranchConfig { return getObjectOrNewInstance(BRANCH, BranchConfig::class.java) } @@ -111,10 +115,6 @@ class Config(context: Context) { return getBoolean(COURSE_UNIT_PROGRESS_ENABLED, false) } - fun isDashboardNewScreenEnabled(): Boolean { - return getBoolean(DASHBOARD_NEW_SCREEN_ENABLED, false) - } - private fun getString(key: String, defaultValue: String): String { val element = getObject(key) return if (element != null) { @@ -168,11 +168,11 @@ class Config(context: Context) { private const val PRE_LOGIN_EXPERIENCE_ENABLED = "PRE_LOGIN_EXPERIENCE_ENABLED" private const val DISCOVERY = "DISCOVERY" private const val PROGRAM = "PROGRAM" + private const val DASHBOARD = "DASHBOARD" private const val BRANCH = "BRANCH" private const val COURSE_NESTED_LIST_ENABLED = "COURSE_NESTED_LIST_ENABLED" private const val COURSE_UNIT_PROGRESS_ENABLED = "COURSE_UNIT_PROGRESS_ENABLED" private const val PLATFORM_NAME = "PLATFORM_NAME" - private const val DASHBOARD_NEW_SCREEN_ENABLED = "DASHBOARD_NEW_SCREEN_ENABLED" } enum class ViewType { diff --git a/core/src/main/java/org/openedx/core/config/DashboardConfig.kt b/core/src/main/java/org/openedx/core/config/DashboardConfig.kt new file mode 100644 index 000000000..1a1b33105 --- /dev/null +++ b/core/src/main/java/org/openedx/core/config/DashboardConfig.kt @@ -0,0 +1,16 @@ +package org.openedx.core.config + +import com.google.gson.annotations.SerializedName + +data class DashboardConfig( + @SerializedName("TYPE") + private val viewType: String = DashboardType.PRIMARY_COURSE.name, +) { + fun getType(): DashboardType { + return DashboardType.valueOf(viewType.uppercase()) + } + + enum class DashboardType { + LIST, PRIMARY_COURSE + } +} diff --git a/core/src/main/java/org/openedx/core/data/model/Progress.kt b/core/src/main/java/org/openedx/core/data/model/Progress.kt index ce0d86960..eab863b2e 100644 --- a/core/src/main/java/org/openedx/core/data/model/Progress.kt +++ b/core/src/main/java/org/openedx/core/data/model/Progress.kt @@ -4,20 +4,20 @@ import com.google.gson.annotations.SerializedName import org.openedx.core.data.model.room.discovery.ProgressDb data class Progress( - @SerializedName("num_points_earned") - val numPointsEarned: Int?, - @SerializedName("num_points_possible") - val numPointsPossible: Int? + @SerializedName("assignments_completed") + val assignmentsCompleted: Int?, + @SerializedName("total_assignments_count") + val totalAssignmentsCount: Int? ) { fun mapToDomain(): org.openedx.core.domain.model.Progress { return org.openedx.core.domain.model.Progress( - numPointsEarned = numPointsEarned ?: 0, - numPointsPossible = numPointsPossible ?: 0 + assignmentsCompleted = assignmentsCompleted ?: 0, + totalAssignmentsCount = totalAssignmentsCount ?: 0 ) } fun mapToRoomEntity() = ProgressDb( - numPointsEarned = numPointsEarned ?: 0, - numPointsPossible = numPointsPossible ?: 0 + assignmentsCompleted = assignmentsCompleted ?: 0, + totalAssignmentsCount = totalAssignmentsCount ?: 0 ) -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt b/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt index 23f2e9717..4c1553dcd 100644 --- a/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt +++ b/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt @@ -173,16 +173,16 @@ data class CourseSharingUtmParametersDb( } data class ProgressDb( - @ColumnInfo("numPointsEarned") - val numPointsEarned: Int, - @ColumnInfo("numPointsPossible") - val numPointsPossible: Int, + @ColumnInfo("assignments_completed") + val assignmentsCompleted: Int, + @ColumnInfo("total_assignments_count") + val totalAssignmentsCount: Int, ) { companion object { val DEFAULT_PROGRESS = ProgressDb(0, 0) } - fun mapToDomain() = Progress(numPointsEarned, numPointsPossible) + fun mapToDomain() = Progress(assignmentsCompleted, totalAssignmentsCount) } data class CourseStatusDb( @@ -243,4 +243,4 @@ data class CourseDateBlockDb( dateType = dateType, assignmentType = assignmentType ) -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseStatus.kt b/core/src/main/java/org/openedx/core/domain/model/CourseStatus.kt index b94721f40..e25490430 100644 --- a/core/src/main/java/org/openedx/core/domain/model/CourseStatus.kt +++ b/core/src/main/java/org/openedx/core/domain/model/CourseStatus.kt @@ -9,4 +9,4 @@ data class CourseStatus( val lastVisitedModulePath: List, val lastVisitedBlockId: String, val lastVisitedUnitDisplayName: String -): Parcelable \ No newline at end of file +): Parcelable diff --git a/core/src/main/java/org/openedx/core/domain/model/Progress.kt b/core/src/main/java/org/openedx/core/domain/model/Progress.kt index be43968a9..9f021aa52 100644 --- a/core/src/main/java/org/openedx/core/domain/model/Progress.kt +++ b/core/src/main/java/org/openedx/core/domain/model/Progress.kt @@ -5,8 +5,8 @@ import kotlinx.parcelize.Parcelize @Parcelize data class Progress( - val numPointsEarned: Int, - val numPointsPossible: Int, + val assignmentsCompleted: Int, + val totalAssignmentsCount: Int, ) : Parcelable { companion object { val DEFAULT_PROGRESS = Progress(0,0) diff --git a/core/src/main/java/org/openedx/core/ui/PreviewFragmentManager.kt b/core/src/main/java/org/openedx/core/ui/PreviewFragmentManager.kt deleted file mode 100644 index ecf8a2700..000000000 --- a/core/src/main/java/org/openedx/core/ui/PreviewFragmentManager.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.openedx.core.ui - -import androidx.fragment.app.FragmentManager - -object PreviewFragmentManager : FragmentManager() \ No newline at end of file diff --git a/dashboard/src/androidTest/java/org/openedx/dashboard/presentation/MyCoursesScreenTest.kt b/dashboard/src/androidTest/java/org/openedx/dashboard/presentation/MyCoursesScreenTest.kt index dbf15acd4..cc14b481a 100644 --- a/dashboard/src/androidTest/java/org/openedx/dashboard/presentation/MyCoursesScreenTest.kt +++ b/dashboard/src/androidTest/java/org/openedx/dashboard/presentation/MyCoursesScreenTest.kt @@ -68,7 +68,7 @@ class MyCoursesScreenTest { @Test fun dashboardScreenLoading() { composeTestRule.setContent { - MyCoursesScreen( + ListDashboardScreen( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), apiHostUrl = "http://localhost:8000", state = DashboardUIState.Courses(listOf(mockCourseEnrolled, mockCourseEnrolled)), @@ -101,7 +101,7 @@ class MyCoursesScreenTest { @Test fun dashboardScreenLoaded() { composeTestRule.setContent { - MyCoursesScreen( + ListDashboardScreen( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), apiHostUrl = "http://localhost:8000", state = DashboardUIState.Courses(listOf(mockCourseEnrolled, mockCourseEnrolled)), @@ -127,7 +127,7 @@ class MyCoursesScreenTest { @Test fun dashboardScreenRefreshing() { composeTestRule.setContent { - MyCoursesScreen( + ListDashboardScreen( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), apiHostUrl = "http://localhost:8000", state = DashboardUIState.Courses(listOf(mockCourseEnrolled, mockCourseEnrolled)), 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 91d99bbfa..635029919 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt @@ -108,6 +108,7 @@ import org.openedx.core.utils.TimeUtils import org.openedx.dashboard.R import org.openedx.dashboard.domain.CourseStatusFilter import java.util.Date +import org.openedx.core.R as CoreR class AllEnrolledCoursesFragment : Fragment() { @@ -455,8 +456,8 @@ private fun CourseItem( AsyncImage( model = ImageRequest.Builder(LocalContext.current) .data(apiHostUrl + course.course.courseImage) - .error(org.openedx.core.R.drawable.core_no_image_course) - .placeholder(org.openedx.core.R.drawable.core_no_image_course) + .error(CoreR.drawable.core_no_image_course) + .placeholder(CoreR.drawable.core_no_image_course) .build(), contentDescription = null, contentScale = ContentScale.Crop, @@ -468,7 +469,7 @@ private fun CourseItem( modifier = Modifier .fillMaxWidth() .height(8.dp), - progress = course.progress.numPointsEarned.toFloat(), + progress = course.progress.assignmentsCompleted.toFloat(), color = MaterialTheme.appColors.primary, backgroundColor = MaterialTheme.appColors.divider ) diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt index 5c70677e8..59368b04f 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt @@ -34,12 +34,14 @@ class AllEnrolledCoursesViewModel( val dashboardRouter: DashboardRouter ) : BaseViewModel() { + val apiHostUrl get() = config.getApiHostURL() + val hasInternetConnection: Boolean + get() = networkConnection.isOnline() + private val coursesList = mutableListOf() private var page = 1 private var isLoading = false - val apiHostUrl get() = config.getApiHostURL() - private val _uiState = MutableStateFlow(AllEnrolledCoursesUIState.Loading) val uiState: StateFlow get() = _uiState.asStateFlow() @@ -52,8 +54,6 @@ class AllEnrolledCoursesViewModel( val updating: StateFlow get() = _updating.asStateFlow() - val hasInternetConnection: Boolean - get() = networkConnection.isOnline() private val _canLoadMore = MutableStateFlow(false) val canLoadMore: StateFlow diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt similarity index 91% rename from dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt rename to dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt index 7685f8b16..1c4e102c9 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesScreen.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt @@ -34,6 +34,7 @@ import androidx.compose.material.Scaffold import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.School @@ -95,15 +96,15 @@ import java.util.Date import org.openedx.core.R as CoreR @Composable -fun UsersCourseScreen( - viewModel: UserCoursesViewModel = koinViewModel(), +fun PrimaryCourseScreen( + viewModel: PrimaryCourseViewModel = koinViewModel(), fragmentManager: FragmentManager, ) { val updating by viewModel.updating.collectAsState(false) val uiMessage by viewModel.uiMessage.collectAsState(null) - val uiState by viewModel.uiState.collectAsState(UserCoursesUIState.Loading) + val uiState by viewModel.uiState.collectAsState(PrimaryCourseUIState.Loading) - UsersCourseScreen( + PrimaryCourseScreen( uiMessage = uiMessage, uiState = uiState, updating = updating, @@ -111,19 +112,19 @@ fun UsersCourseScreen( hasInternetConnection = viewModel.hasInternetConnection, onAction = { action -> when (action) { - UserCoursesScreenAction.SwipeRefresh -> { + PrimaryCourseScreenAction.SwipeRefresh -> { viewModel.updateCourses() } - UserCoursesScreenAction.ViewAll -> { + PrimaryCourseScreenAction.ViewAll -> { viewModel.dashboardRouter.navigateToAllEnrolledCourses(fragmentManager) } - UserCoursesScreenAction.Reload -> { + PrimaryCourseScreenAction.Reload -> { viewModel.getCourses() } - is UserCoursesScreenAction.OpenCourse -> { + is PrimaryCourseScreenAction.OpenCourse -> { viewModel.dashboardRouter.navigateToCourseOutline( fm = fragmentManager, courseId = action.enrolledCourse.course.id, @@ -132,7 +133,7 @@ fun UsersCourseScreen( ) } - is UserCoursesScreenAction.NavigateToDates -> { + is PrimaryCourseScreenAction.NavigateToDates -> { viewModel.dashboardRouter.navigateToCourseOutline( fm = fragmentManager, courseId = action.enrolledCourse.course.id, @@ -142,7 +143,7 @@ fun UsersCourseScreen( ) } - is UserCoursesScreenAction.OpenBlock -> { + is PrimaryCourseScreenAction.OpenBlock -> { viewModel.dashboardRouter.navigateToCourseOutline( fm = fragmentManager, courseId = action.enrolledCourse.course.id, @@ -158,18 +159,18 @@ fun UsersCourseScreen( @OptIn(ExperimentalMaterialApi::class) @Composable -private fun UsersCourseScreen( +private fun PrimaryCourseScreen( uiMessage: UIMessage?, - uiState: UserCoursesUIState, + uiState: PrimaryCourseUIState, updating: Boolean, apiHostUrl: String, - onAction: (UserCoursesScreenAction) -> Unit, + onAction: (PrimaryCourseScreenAction) -> Unit, hasInternetConnection: Boolean ) { val scaffoldState = rememberScaffoldState() val pullRefreshState = rememberPullRefreshState( refreshing = updating, - onRefresh = { onAction(UserCoursesScreenAction.SwipeRefresh) } + onRefresh = { onAction(PrimaryCourseScreenAction.SwipeRefresh) } ) var isInternetConnectionShown by rememberSaveable { mutableStateOf(false) @@ -195,34 +196,34 @@ private fun UsersCourseScreen( .verticalScroll(rememberScrollState()), ) { when (uiState) { - is UserCoursesUIState.Loading -> { + is PrimaryCourseUIState.Loading -> { CircularProgressIndicator( modifier = Modifier.align(Alignment.Center), color = MaterialTheme.appColors.primary ) } - is UserCoursesUIState.Courses -> { + is PrimaryCourseUIState.Courses -> { UserCourses( modifier = Modifier.fillMaxSize(), userCourses = uiState.userCourses, apiHostUrl = apiHostUrl, openCourse = { - onAction(UserCoursesScreenAction.OpenCourse(it)) + onAction(PrimaryCourseScreenAction.OpenCourse(it)) }, onViewAllClick = { - onAction(UserCoursesScreenAction.ViewAll) + onAction(PrimaryCourseScreenAction.ViewAll) }, navigateToDates = { - onAction(UserCoursesScreenAction.NavigateToDates(it)) + onAction(PrimaryCourseScreenAction.NavigateToDates(it)) }, openBlock = { course, blockId -> - onAction(UserCoursesScreenAction.OpenBlock(course, blockId)) + onAction(PrimaryCourseScreenAction.OpenBlock(course, blockId)) } ) } - is UserCoursesUIState.Empty -> { + is PrimaryCourseUIState.Empty -> { EmptyState( modifier = Modifier.align(Alignment.Center) ) @@ -245,7 +246,7 @@ private fun UsersCourseScreen( }, onReloadClick = { isInternetConnectionShown = true - onAction(UserCoursesScreenAction.Reload) + onAction(PrimaryCourseScreenAction.Reload) } ) } @@ -400,8 +401,8 @@ private fun CourseListItem( AsyncImage( model = ImageRequest.Builder(LocalContext.current) .data(apiHostUrl + course.course.courseImage) - .error(org.openedx.core.R.drawable.core_no_image_course) - .placeholder(org.openedx.core.R.drawable.core_no_image_course) + .error(CoreR.drawable.core_no_image_course) + .placeholder(CoreR.drawable.core_no_image_course) .build(), contentDescription = null, contentScale = ContentScale.Crop, @@ -507,8 +508,8 @@ private fun PrimaryCourseCard( AsyncImage( model = ImageRequest.Builder(context) .data(apiHostUrl + primaryCourse.course.courseImage) - .error(org.openedx.core.R.drawable.core_no_image_course) - .placeholder(org.openedx.core.R.drawable.core_no_image_course) + .error(CoreR.drawable.core_no_image_course) + .placeholder(CoreR.drawable.core_no_image_course) .build(), contentDescription = null, contentScale = ContentScale.Crop, @@ -517,7 +518,7 @@ private fun PrimaryCourseCard( .height(140.dp) ) val progress: Float = try { - (primaryCourse.progress.numPointsEarned / primaryCourse.progress.numPointsPossible).toFloat() + (primaryCourse.progress.assignmentsCompleted / primaryCourse.progress.totalAssignmentsCount).toFloat() } catch (_: ArithmeticException) { 0f } @@ -607,11 +608,14 @@ private fun ResumeButton( horizontalArrangement = Arrangement.spacedBy(12.dp) ) { if (primaryCourse.courseStatus == null) { + Icon( + imageVector = Icons.Default.School, + tint = MaterialTheme.appColors.buttonText, + contentDescription = null + ) Text( - modifier = modifier - .fillMaxWidth(), + modifier = Modifier.weight(1f), text = stringResource(R.string.dashboard_start_course), - textAlign = TextAlign.Center, color = MaterialTheme.appColors.buttonText, style = MaterialTheme.appTypography.titleSmall ) @@ -622,6 +626,7 @@ private fun ResumeButton( contentDescription = null ) Column( + modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp) ) { Text( @@ -636,6 +641,12 @@ private fun ResumeButton( ) } } + Icon( + modifier = Modifier.size(16.dp), + imageVector = Icons.AutoMirrored.Filled.ArrowForwardIos, + tint = MaterialTheme.appColors.buttonText, + contentDescription = null + ) } } @@ -784,10 +795,10 @@ private fun ViewAllItemPreview() { @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() { +private fun PrimaryCourseScreenPreview() { OpenEdXTheme { - UsersCourseScreen( - uiState = UserCoursesUIState.Courses(mockUserCourses), + PrimaryCourseScreen( + uiState = PrimaryCourseUIState.Courses(mockUserCourses), apiHostUrl = "", uiMessage = null, updating = false, diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseUIState.kt b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseUIState.kt new file mode 100644 index 000000000..7e21139d0 --- /dev/null +++ b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseUIState.kt @@ -0,0 +1,9 @@ +package org.openedx.courses.presentation + +import org.openedx.courses.domain.model.UserCourses + +sealed class PrimaryCourseUIState { + data class Courses(val userCourses: UserCourses) : PrimaryCourseUIState() + data object Empty : PrimaryCourseUIState() + data object Loading : PrimaryCourseUIState() +} diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseViewModel.kt similarity index 82% rename from dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt rename to dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseViewModel.kt index 094112e5d..14b9b1db7 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseViewModel.kt @@ -23,7 +23,7 @@ import org.openedx.courses.domain.model.UserCourses import org.openedx.dashboard.domain.interactor.DashboardInteractor import org.openedx.dashboard.presentation.DashboardRouter -class UserCoursesViewModel( +class PrimaryCourseViewModel( private val config: Config, private val interactor: DashboardInteractor, private val resourceManager: ResourceManager, @@ -35,8 +35,8 @@ class UserCoursesViewModel( val apiHostUrl get() = config.getApiHostURL() - private val _uiState = MutableStateFlow(UserCoursesUIState.Loading) - val uiState: StateFlow + private val _uiState = MutableStateFlow(PrimaryCourseUIState.Loading) + val uiState: StateFlow get() = _uiState.asStateFlow() private val _uiMessage = MutableSharedFlow() @@ -61,16 +61,16 @@ class UserCoursesViewModel( if (networkConnection.isOnline()) { val response = interactor.getMainUserCourses() if (response.primary == null && response.enrollments.isNotEmpty()) { - _uiState.value = UserCoursesUIState.Empty + _uiState.value = PrimaryCourseUIState.Empty } else { - _uiState.value = UserCoursesUIState.Courses(response) + _uiState.value = PrimaryCourseUIState.Courses(response) } } else { val cachedUserCourses = fileUtil.getObjectFromFile() if (cachedUserCourses == null) { - _uiState.value = UserCoursesUIState.Empty + _uiState.value = PrimaryCourseUIState.Empty } else { - _uiState.value = UserCoursesUIState.Courses(cachedUserCourses) + _uiState.value = PrimaryCourseUIState.Courses(cachedUserCourses) } } } catch (e: Exception) { @@ -101,11 +101,11 @@ class UserCoursesViewModel( } } -interface UserCoursesScreenAction { - object SwipeRefresh : UserCoursesScreenAction - object ViewAll : UserCoursesScreenAction - object Reload : UserCoursesScreenAction - data class OpenBlock(val enrolledCourse: EnrolledCourse, val blockId: String) : UserCoursesScreenAction - data class OpenCourse(val enrolledCourse: EnrolledCourse) : UserCoursesScreenAction - data class NavigateToDates(val enrolledCourse: EnrolledCourse) : UserCoursesScreenAction +interface PrimaryCourseScreenAction { + object SwipeRefresh : PrimaryCourseScreenAction + object ViewAll : PrimaryCourseScreenAction + object Reload : PrimaryCourseScreenAction + data class OpenBlock(val enrolledCourse: EnrolledCourse, val blockId: String) : PrimaryCourseScreenAction + data class OpenCourse(val enrolledCourse: EnrolledCourse) : PrimaryCourseScreenAction + data class NavigateToDates(val enrolledCourse: EnrolledCourse) : PrimaryCourseScreenAction } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesUIState.kt b/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesUIState.kt deleted file mode 100644 index 2e0382d8d..000000000 --- a/dashboard/src/main/java/org/openedx/courses/presentation/UserCoursesUIState.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.openedx.courses.presentation - -import org.openedx.courses.domain.model.UserCourses - -sealed class UserCoursesUIState { - data class Courses(val userCourses: UserCourses) : UserCoursesUIState() - data object Empty : UserCoursesUIState() - data object Loading : UserCoursesUIState() -} diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/ListDashboardFragment.kt similarity index 98% rename from dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt rename to dashboard/src/main/java/org/openedx/dashboard/presentation/ListDashboardFragment.kt index ae01484f3..2e5ca4033 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/ListDashboardFragment.kt @@ -101,9 +101,9 @@ import org.openedx.dashboard.R import java.util.Date import org.openedx.core.R as CoreR -class DashboardFragment : Fragment() { +class ListDashboardFragment : Fragment() { - private val viewModel by viewModel() + private val viewModel by viewModel() private val router by inject() override fun onCreate(savedInstanceState: Bundle?) { @@ -126,7 +126,7 @@ class DashboardFragment : Fragment() { val canLoadMore by viewModel.canLoadMore.observeAsState(false) val appUpgradeEvent by viewModel.appUpgradeEvent.observeAsState() - MyCoursesScreen( + ListDashboardScreen( windowSize = windowSize, viewModel.apiHostUrl, uiState!!, @@ -169,7 +169,7 @@ class DashboardFragment : Fragment() { @OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class) @Composable -internal fun MyCoursesScreen( +internal fun ListDashboardScreen( windowSize: WindowSize, apiHostUrl: String, state: DashboardUIState, @@ -554,9 +554,9 @@ private fun CourseItemPreview() { @Preview(uiMode = UI_MODE_NIGHT_NO) @Preview(uiMode = UI_MODE_NIGHT_YES) @Composable -private fun MyCoursesScreenDay() { +private fun ListDashboardScreenPreview() { OpenEdXTheme { - MyCoursesScreen( + ListDashboardScreen( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), apiHostUrl = "http://localhost:8000", state = DashboardUIState.Courses( @@ -586,9 +586,9 @@ private fun MyCoursesScreenDay() { @Preview(uiMode = UI_MODE_NIGHT_NO, device = Devices.NEXUS_9) @Preview(uiMode = UI_MODE_NIGHT_YES, device = Devices.NEXUS_9) @Composable -private fun MyCoursesScreenTabletPreview() { +private fun ListDashboardScreenTabletPreview() { OpenEdXTheme { - MyCoursesScreen( + ListDashboardScreen( windowSize = WindowSize(WindowType.Medium, WindowType.Medium), apiHostUrl = "http://localhost:8000", state = DashboardUIState.Courses( diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardViewModel.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/ListDashboardViewModel.kt similarity index 99% rename from dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardViewModel.kt rename to dashboard/src/main/java/org/openedx/dashboard/presentation/ListDashboardViewModel.kt index 685e449d0..f2cd06090 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardViewModel.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/ListDashboardViewModel.kt @@ -20,7 +20,7 @@ import org.openedx.core.system.notifier.CourseDashboardUpdate import org.openedx.core.system.notifier.DiscoveryNotifier import org.openedx.dashboard.domain.interactor.DashboardInteractor -class DashboardViewModel( +class ListDashboardViewModel( private val config: Config, private val networkConnection: NetworkConnection, private val interactor: DashboardInteractor, diff --git a/dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt b/dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt index 8497cd290..fe5136dff 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt @@ -1,28 +1,8 @@ package org.openedx.learn.presentation import androidx.fragment.app.FragmentManager -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel -import org.openedx.core.R -import org.openedx.core.UIMessage import org.openedx.core.config.Config -import org.openedx.core.domain.model.EnrolledCourse -import org.openedx.core.extension.isInternetError -import org.openedx.core.system.ResourceManager -import org.openedx.core.system.connection.NetworkConnection -import org.openedx.core.system.notifier.CourseDashboardUpdate -import org.openedx.core.system.notifier.DiscoveryNotifier -import org.openedx.core.utils.FileUtil -import org.openedx.courses.domain.model.UserCourses -import org.openedx.dashboard.domain.interactor.DashboardInteractor import org.openedx.dashboard.presentation.DashboardRouter class LearnViewModel( diff --git a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt b/dashboard/src/main/java/org/openedx/learn/presentation/PrimaryCourseDashboardFragment.kt similarity index 88% rename from dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt rename to dashboard/src/main/java/org/openedx/learn/presentation/PrimaryCourseDashboardFragment.kt index 5720e6922..02397ef1c 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/PrimaryCourseDashboardFragment.kt @@ -1,7 +1,5 @@ package org.openedx.learn.presentation -import android.content.res.Configuration.UI_MODE_NIGHT_NO -import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup @@ -35,6 +33,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -42,7 +41,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -50,7 +48,6 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import org.koin.androidx.compose.koinViewModel import org.openedx.core.presentation.global.InDevelopmentScreen -import org.openedx.core.ui.PreviewFragmentManager import org.openedx.core.ui.crop import org.openedx.core.ui.displayCutoutForLandscape import org.openedx.core.ui.rememberWindowSize @@ -59,11 +56,11 @@ import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appTypography import org.openedx.core.ui.windowSizeValue -import org.openedx.courses.presentation.UsersCourseScreen +import org.openedx.courses.presentation.PrimaryCourseScreen import org.openedx.dashboard.R import org.openedx.learn.LearnType -class LearnFragment : Fragment() { +class PrimaryCourseDashboardFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, @@ -88,6 +85,7 @@ private fun LearnScreen( fragmentManager: FragmentManager, ) { val windowSize = rememberWindowSize() + val scope = rememberCoroutineScope() val scaffoldState = rememberScaffoldState() val pagerState = rememberPagerState { LearnType.entries.size @@ -134,19 +132,21 @@ private fun LearnScreen( .padding(horizontal = 16.dp), pagerState = pagerState ) - } - HorizontalPager( - modifier = Modifier - .fillMaxSize(), - state = pagerState, - userScrollEnabled = false - ) { page -> - when (page) { - 0 -> UsersCourseScreen(fragmentManager = fragmentManager) + HorizontalPager( + modifier = Modifier + .fillMaxSize(), + state = pagerState, + userScrollEnabled = false + ) { page -> + when (page) { + 0 -> PrimaryCourseScreen(fragmentManager = fragmentManager) - 1 -> InDevelopmentScreen() + 1 -> InDevelopmentScreen() + } } + } else { + PrimaryCourseScreen(fragmentManager = fragmentManager) } } } @@ -266,15 +266,24 @@ private fun LearnDropdownMenu( } } -@Preview(uiMode = UI_MODE_NIGHT_NO) -@Preview(uiMode = UI_MODE_NIGHT_YES) -@Preview(uiMode = UI_MODE_NIGHT_NO, device = Devices.NEXUS_9) -@Preview(uiMode = UI_MODE_NIGHT_YES, device = Devices.NEXUS_9) +@Preview +@Composable +private fun HeaderPreview() { + OpenEdXTheme { + Header( + label = stringResource(id = R.string.dashboard_learn), + onSearchClick = {} + ) + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Preview @Composable -private fun LearnScreenPreview() { +private fun LearnDropdownMenuPreview() { OpenEdXTheme { - LearnScreen( - fragmentManager = PreviewFragmentManager + LearnDropdownMenu( + pagerState = rememberPagerState { 2 } ) } } diff --git a/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt b/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt index 6fdfdec22..4fd957e12 100644 --- a/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt +++ b/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt @@ -77,7 +77,7 @@ class DashboardViewModelTest { @Test fun `getCourses no internet connection`() = runTest { - val viewModel = DashboardViewModel( + val viewModel = ListDashboardViewModel( config, networkConnection, interactor, @@ -101,7 +101,7 @@ class DashboardViewModelTest { @Test fun `getCourses unknown error`() = runTest { - val viewModel = DashboardViewModel( + val viewModel = ListDashboardViewModel( config, networkConnection, interactor, @@ -125,7 +125,7 @@ class DashboardViewModelTest { @Test fun `getCourses from network`() = runTest { - val viewModel = DashboardViewModel( + val viewModel = ListDashboardViewModel( config, networkConnection, interactor, @@ -149,7 +149,7 @@ class DashboardViewModelTest { @Test fun `getCourses from network with next page`() = runTest { - val viewModel = DashboardViewModel( + val viewModel = ListDashboardViewModel( config, networkConnection, interactor, @@ -183,7 +183,7 @@ class DashboardViewModelTest { fun `getCourses from cache`() = runTest { every { networkConnection.isOnline() } returns false coEvery { interactor.getEnrolledCoursesFromCache() } returns listOf(mockk()) - val viewModel = DashboardViewModel( + val viewModel = ListDashboardViewModel( config, networkConnection, interactor, @@ -207,7 +207,7 @@ class DashboardViewModelTest { fun `updateCourses no internet error`() = runTest { every { networkConnection.isOnline() } returns true coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList - val viewModel = DashboardViewModel( + val viewModel = ListDashboardViewModel( config, networkConnection, interactor, @@ -235,7 +235,7 @@ class DashboardViewModelTest { fun `updateCourses unknown exception`() = runTest { every { networkConnection.isOnline() } returns true coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList - val viewModel = DashboardViewModel( + val viewModel = ListDashboardViewModel( config, networkConnection, interactor, @@ -263,7 +263,7 @@ class DashboardViewModelTest { fun `updateCourses success`() = runTest { every { networkConnection.isOnline() } returns true coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList - val viewModel = DashboardViewModel( + val viewModel = ListDashboardViewModel( config, networkConnection, interactor, @@ -296,7 +296,7 @@ class DashboardViewModelTest { "" ) ) - val viewModel = DashboardViewModel( + val viewModel = ListDashboardViewModel( config, networkConnection, interactor, @@ -321,7 +321,7 @@ class DashboardViewModelTest { @Test fun `CourseDashboardUpdate notifier test`() = runTest { coEvery { discoveryNotifier.notifier } returns flow { emit(CourseDashboardUpdate()) } - val viewModel = DashboardViewModel( + val viewModel = ListDashboardViewModel( config, networkConnection, interactor, diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml index 8d683f0a0..6ddd394ab 100644 --- a/default_config/dev/config.yaml +++ b/default_config/dev/config.yaml @@ -23,11 +23,14 @@ DISCOVERY: PROGRAM_DETAIL_TEMPLATE: '' PROGRAM: - TYPE: 'native' + TYPE: 'webview' WEBVIEW: PROGRAM_URL: '' PROGRAM_DETAIL_URL_TEMPLATE: '' +DASHBOARD: + TYPE: 'primary_course' + FIREBASE: ENABLED: false ANALYTICS_SOURCE: '' # segment | none @@ -78,6 +81,3 @@ SOCIAL_AUTH_ENABLED: false #Course navigation feature flags COURSE_NESTED_LIST_ENABLED: false COURSE_UNIT_PROGRESS_ENABLED: false -#Dashboard feature flags -DASHBOARD_NEW_SCREEN_ENABLED: true - diff --git a/default_config/prod/config.yaml b/default_config/prod/config.yaml index b20ae3897..6c68daf93 100644 --- a/default_config/prod/config.yaml +++ b/default_config/prod/config.yaml @@ -28,6 +28,9 @@ PROGRAM: PROGRAM_URL: '' PROGRAM_DETAIL_URL_TEMPLATE: '' +DASHBOARD: + TYPE: 'primary_course' + FIREBASE: ENABLED: false ANALYTICS_SOURCE: '' # segment | none @@ -78,5 +81,3 @@ SOCIAL_AUTH_ENABLED: false #Course navigation feature flags COURSE_NESTED_LIST_ENABLED: false COURSE_UNIT_PROGRESS_ENABLED: false -#Dashboard feature flags -DASHBOARD_NEW_SCREEN_ENABLED: true diff --git a/default_config/stage/config.yaml b/default_config/stage/config.yaml index b20ae3897..6c68daf93 100644 --- a/default_config/stage/config.yaml +++ b/default_config/stage/config.yaml @@ -28,6 +28,9 @@ PROGRAM: PROGRAM_URL: '' PROGRAM_DETAIL_URL_TEMPLATE: '' +DASHBOARD: + TYPE: 'primary_course' + FIREBASE: ENABLED: false ANALYTICS_SOURCE: '' # segment | none @@ -78,5 +81,3 @@ SOCIAL_AUTH_ENABLED: false #Course navigation feature flags COURSE_NESTED_LIST_ENABLED: false COURSE_UNIT_PROGRESS_ENABLED: false -#Dashboard feature flags -DASHBOARD_NEW_SCREEN_ENABLED: true From f9069a09fa8bf6e0e4ae10b70b395b9637c43dc9 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Thu, 9 May 2024 20:16:08 +0300 Subject: [PATCH 13/23] feat: Added programs fragment to LearnFragment viewPager --- .../main/java/org/openedx/app/AppRouter.kt | 4 + .../main/java/org/openedx/app/MainFragment.kt | 10 +- .../core/adapter/NavigationFragmentAdapter.kt | 4 +- .../presentation/PrimaryCourseFragment.kt | 24 +++ .../dashboard/presentation/DashboardRouter.kt | 3 + ...eDashboardFragment.kt => LearnFragment.kt} | 150 ++++++++---------- .../src/main/res/layout/fragment_learn.xml | 24 +++ .../presentation/program/ProgramFragment.kt | 36 +++-- 8 files changed, 156 insertions(+), 99 deletions(-) rename app/src/main/java/org/openedx/app/adapter/MainNavigationFragmentAdapter.kt => core/src/main/java/org/openedx/core/adapter/NavigationFragmentAdapter.kt (74%) create mode 100644 dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseFragment.kt rename dashboard/src/main/java/org/openedx/learn/presentation/{PrimaryCourseDashboardFragment.kt => LearnFragment.kt} (68%) create mode 100644 dashboard/src/main/res/layout/fragment_learn.xml diff --git a/app/src/main/java/org/openedx/app/AppRouter.kt b/app/src/main/java/org/openedx/app/AppRouter.kt index ecabfed23..4cc840c4f 100644 --- a/app/src/main/java/org/openedx/app/AppRouter.kt +++ b/app/src/main/java/org/openedx/app/AppRouter.kt @@ -128,6 +128,10 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di replaceFragmentWithBackStack(fm, AllEnrolledCoursesFragment()) } + override fun getProgramFragmentInstance(): Fragment { + return ProgramFragment(myPrograms = true, isNestedFragment = true) + } + override fun navigateToCourseInfo( fm: FragmentManager, courseId: String, diff --git a/app/src/main/java/org/openedx/app/MainFragment.kt b/app/src/main/java/org/openedx/app/MainFragment.kt index b73982630..c1932ece0 100644 --- a/app/src/main/java/org/openedx/app/MainFragment.kt +++ b/app/src/main/java/org/openedx/app/MainFragment.kt @@ -11,8 +11,8 @@ import androidx.viewpager2.widget.ViewPager2 import kotlinx.coroutines.launch import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel -import org.openedx.app.adapter.MainNavigationFragmentAdapter import org.openedx.app.databinding.FragmentMainBinding +import org.openedx.core.adapter.NavigationFragmentAdapter import org.openedx.core.config.Config import org.openedx.core.config.DashboardConfig import org.openedx.core.presentation.global.app_upgrade.UpgradeRequiredFragment @@ -20,7 +20,7 @@ import org.openedx.core.presentation.global.viewBinding import org.openedx.dashboard.presentation.ListDashboardFragment import org.openedx.discovery.presentation.DiscoveryNavigator import org.openedx.discovery.presentation.DiscoveryRouter -import org.openedx.learn.presentation.PrimaryCourseDashboardFragment +import org.openedx.learn.presentation.LearnFragment import org.openedx.profile.presentation.profile.ProfileFragment class MainFragment : Fragment(R.layout.fragment_main) { @@ -30,7 +30,7 @@ class MainFragment : Fragment(R.layout.fragment_main) { private val router by inject() private val config by inject() - private lateinit var adapter: MainNavigationFragmentAdapter + private lateinit var adapter: NavigationFragmentAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -104,10 +104,10 @@ class MainFragment : Fragment(R.layout.fragment_main) { val discoveryFragment = DiscoveryNavigator(viewModel.isDiscoveryTypeWebView).getDiscoveryFragment() val dashboardFragment = when (config.getDashboardConfig().getType()) { DashboardConfig.DashboardType.LIST -> ListDashboardFragment() - DashboardConfig.DashboardType.PRIMARY_COURSE -> PrimaryCourseDashboardFragment() + DashboardConfig.DashboardType.PRIMARY_COURSE -> LearnFragment() } - adapter = MainNavigationFragmentAdapter(this).apply { + adapter = NavigationFragmentAdapter(this).apply { addFragment(dashboardFragment) addFragment(discoveryFragment) addFragment(ProfileFragment()) diff --git a/app/src/main/java/org/openedx/app/adapter/MainNavigationFragmentAdapter.kt b/core/src/main/java/org/openedx/core/adapter/NavigationFragmentAdapter.kt similarity index 74% rename from app/src/main/java/org/openedx/app/adapter/MainNavigationFragmentAdapter.kt rename to core/src/main/java/org/openedx/core/adapter/NavigationFragmentAdapter.kt index ccbe6f715..273c53427 100644 --- a/app/src/main/java/org/openedx/app/adapter/MainNavigationFragmentAdapter.kt +++ b/core/src/main/java/org/openedx/core/adapter/NavigationFragmentAdapter.kt @@ -1,9 +1,9 @@ -package org.openedx.app.adapter +package org.openedx.core.adapter import androidx.fragment.app.Fragment import androidx.viewpager2.adapter.FragmentStateAdapter -class MainNavigationFragmentAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) { +class NavigationFragmentAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) { private val fragments = ArrayList() diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseFragment.kt b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseFragment.kt new file mode 100644 index 000000000..8b366cde8 --- /dev/null +++ b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseFragment.kt @@ -0,0 +1,24 @@ +package org.openedx.courses.presentation + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.Fragment +import org.openedx.core.ui.theme.OpenEdXTheme + +class PrimaryCourseFragment : Fragment() { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ) = ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + OpenEdXTheme { + PrimaryCourseScreen(fragmentManager = requireActivity().supportFragmentManager) + } + } + } +} \ No newline at end of file diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt index 937091044..601f0fca7 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt @@ -1,5 +1,6 @@ package org.openedx.dashboard.presentation +import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import org.openedx.core.presentation.course.CourseContainerTab @@ -19,4 +20,6 @@ interface DashboardRouter { fun navigateToCourseSearch(fm: FragmentManager, querySearch: String) fun navigateToAllEnrolledCourses(fm: FragmentManager) + + fun getProgramFragmentInstance(): Fragment } diff --git a/dashboard/src/main/java/org/openedx/learn/presentation/PrimaryCourseDashboardFragment.kt b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt similarity index 68% rename from dashboard/src/main/java/org/openedx/learn/presentation/PrimaryCourseDashboardFragment.kt rename to dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt index 02397ef1c..af01e8253 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/PrimaryCourseDashboardFragment.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt @@ -1,20 +1,16 @@ package org.openedx.learn.presentation import android.os.Bundle -import android.view.LayoutInflater -import android.view.ViewGroup +import android.view.View import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.DropdownMenu @@ -22,32 +18,31 @@ import androidx.compose.material.DropdownMenuItem import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme -import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.Search -import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager +import androidx.viewpager2.widget.ViewPager2 +import org.koin.android.ext.android.inject import org.koin.androidx.compose.koinViewModel -import org.openedx.core.presentation.global.InDevelopmentScreen +import org.openedx.core.adapter.NavigationFragmentAdapter +import org.openedx.core.presentation.global.viewBinding import org.openedx.core.ui.crop import org.openedx.core.ui.displayCutoutForLandscape import org.openedx.core.ui.rememberWindowSize @@ -56,37 +51,56 @@ import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appTypography import org.openedx.core.ui.windowSizeValue -import org.openedx.courses.presentation.PrimaryCourseScreen +import org.openedx.courses.presentation.PrimaryCourseFragment import org.openedx.dashboard.R +import org.openedx.dashboard.databinding.FragmentLearnBinding +import org.openedx.dashboard.presentation.DashboardRouter import org.openedx.learn.LearnType -class PrimaryCourseDashboardFragment : Fragment() { +class LearnFragment : Fragment(R.layout.fragment_learn) { - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ) = ComposeView(requireContext()).apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setContent { + private val binding by viewBinding(FragmentLearnBinding::bind) + private val router by inject() + private lateinit var adapter: NavigationFragmentAdapter + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.header.setContent { OpenEdXTheme { - LearnScreen( - fragmentManager = requireParentFragment().parentFragmentManager + Header( + fragmentManager = requireParentFragment().parentFragmentManager, + viewPager = binding.viewPager ) } } + initViewPager() + } + + private fun initViewPager() { + binding.viewPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL + binding.viewPager.offscreenPageLimit = 2 + + adapter = NavigationFragmentAdapter(this).apply { + addFragment(PrimaryCourseFragment()) + addFragment(router.getProgramFragmentInstance()) + } + binding.viewPager.adapter = adapter + binding.viewPager.setUserInputEnabled(false) + } + + private fun setFragment() { + binding.viewPager.setCurrentItem(0, false) } } @OptIn(ExperimentalFoundationApi::class) @Composable -private fun LearnScreen( +private fun Header( viewModel: LearnViewModel = koinViewModel(), fragmentManager: FragmentManager, + viewPager: ViewPager2 ) { val windowSize = rememberWindowSize() - val scope = rememberCoroutineScope() - val scaffoldState = rememberScaffoldState() val pagerState = rememberPagerState { LearnType.entries.size } @@ -94,67 +108,40 @@ private fun LearnScreen( mutableStateOf( windowSize.windowSizeValue( expanded = Modifier.widthIn(Dp.Unspecified, 650.dp), - compact = Modifier.fillMaxSize(), + compact = Modifier.fillMaxWidth(), ) ) } - Scaffold( - scaffoldState = scaffoldState, - modifier = Modifier.fillMaxSize(), - backgroundColor = MaterialTheme.appColors.background - ) { paddingValues -> - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column( - modifier = Modifier - .padding(paddingValues) - .statusBarsInset() - .displayCutoutForLandscape() - .then(contentWidth), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Header( - modifier = Modifier - .padding(horizontal = 16.dp), - label = stringResource(id = R.string.dashboard_learn), - onSearchClick = { - viewModel.onSearchClick(fragmentManager) - } - ) - - if (viewModel.isProgramTypeWebView) { - LearnDropdownMenu( - modifier = Modifier - .align(Alignment.Start) - .padding(horizontal = 16.dp), - pagerState = pagerState - ) - - HorizontalPager( - modifier = Modifier - .fillMaxSize(), - state = pagerState, - userScrollEnabled = false - ) { page -> - when (page) { - 0 -> PrimaryCourseScreen(fragmentManager = fragmentManager) - - 1 -> InDevelopmentScreen() - } - } - } else { - PrimaryCourseScreen(fragmentManager = fragmentManager) - } + Column( + modifier = Modifier + .statusBarsInset() + .displayCutoutForLandscape() + .then(contentWidth), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Title( + modifier = Modifier + .padding(horizontal = 16.dp), + label = stringResource(id = R.string.dashboard_learn), + onSearchClick = { + viewModel.onSearchClick(fragmentManager) } + ) + + if (viewModel.isProgramTypeWebView) { + LearnDropdownMenu( + modifier = Modifier + .align(Alignment.Start) + .padding(horizontal = 16.dp), + viewPager = viewPager + ) } } } @Composable -private fun Header( +private fun Title( modifier: Modifier = Modifier, label: String, onSearchClick: () -> Unit @@ -189,17 +176,17 @@ private fun Header( @Composable private fun LearnDropdownMenu( modifier: Modifier = Modifier, - pagerState: PagerState + viewPager: ViewPager2 ) { var expanded by remember { mutableStateOf(false) } var currentValue by remember { mutableStateOf(LearnType.COURSES) } LaunchedEffect(currentValue) { - pagerState.scrollToPage( + viewPager.setCurrentItem( when (currentValue) { LearnType.COURSES -> 0 LearnType.PROGRAMS -> 1 - } + }, false ) } @@ -270,7 +257,7 @@ private fun LearnDropdownMenu( @Composable private fun HeaderPreview() { OpenEdXTheme { - Header( + Title( label = stringResource(id = R.string.dashboard_learn), onSearchClick = {} ) @@ -282,8 +269,9 @@ private fun HeaderPreview() { @Composable private fun LearnDropdownMenuPreview() { OpenEdXTheme { + val context = LocalContext.current LearnDropdownMenu( - pagerState = rememberPagerState { 2 } + viewPager = ViewPager2(context) ) } } diff --git a/dashboard/src/main/res/layout/fragment_learn.xml b/dashboard/src/main/res/layout/fragment_learn.xml new file mode 100644 index 000000000..c6556b364 --- /dev/null +++ b/dashboard/src/main/res/layout/fragment_learn.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramFragment.kt b/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramFragment.kt index ee3e04a3b..3b74dbc42 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramFragment.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramFragment.kt @@ -68,7 +68,10 @@ import org.openedx.discovery.presentation.catalog.WebViewLink import org.openedx.core.R as coreR import org.openedx.discovery.presentation.catalog.WebViewLink.Authority as linkAuthority -class ProgramFragment(private val myPrograms: Boolean = false) : Fragment() { +class ProgramFragment( + private val myPrograms: Boolean = false, + private val isNestedFragment: Boolean = false +) : Fragment() { private val viewModel by viewModel() @@ -127,6 +130,7 @@ class ProgramFragment(private val myPrograms: Boolean = false) : Fragment() { cookieManager = viewModel.cookieManager, canShowBackBtn = arguments?.getString(ARG_PATH_ID, "") ?.isNotEmpty() == true, + isNestedFragment = isNestedFragment, uriScheme = viewModel.uriScheme, hasInternetConnection = hasInternetConnection, checkInternetConnection = { @@ -224,6 +228,7 @@ private fun ProgramInfoScreen( cookieManager: AppCookieManager, uriScheme: String, canShowBackBtn: Boolean, + isNestedFragment: Boolean, hasInternetConnection: Boolean, checkInternetConnection: () -> Unit, onWebPageLoaded: () -> Unit, @@ -250,7 +255,7 @@ private fun ProgramInfoScreen( .fillMaxSize() .semantics { testTagsAsResourceId = true }, backgroundColor = MaterialTheme.appColors.background - ) { + ) { paddingValues -> val modifierScreenWidth by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( @@ -264,21 +269,29 @@ private fun ProgramInfoScreen( ) } + val statusBarPadding = if (isNestedFragment) { + Modifier + } else { + Modifier.statusBarsInset() + } + Column( modifier = Modifier .fillMaxSize() - .padding(it) - .statusBarsInset() + .padding(paddingValues) + .then(statusBarPadding) .displayCutoutForLandscape(), horizontalAlignment = Alignment.CenterHorizontally, ) { - Toolbar( - label = stringResource(id = R.string.discovery_programs), - canShowBackBtn = canShowBackBtn, - canShowSettingsIcon = !canShowBackBtn, - onBackClick = onBackClick, - onSettingsClick = onSettingsClick - ) + if (!isNestedFragment) { + Toolbar( + label = stringResource(id = R.string.discovery_programs), + canShowBackBtn = canShowBackBtn, + canShowSettingsIcon = !canShowBackBtn, + onBackClick = onBackClick, + onSettingsClick = onSettingsClick + ) + } Surface { Box( @@ -349,6 +362,7 @@ fun MyProgramsPreview() { cookieManager = koinViewModel().cookieManager, uriScheme = "", canShowBackBtn = false, + isNestedFragment = false, hasInternetConnection = false, checkInternetConnection = {}, onBackClick = {}, From 45aa9881ff8aa68d63f3d3300cfea48fd5d432ef Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Fri, 10 May 2024 13:02:55 +0300 Subject: [PATCH 14/23] feat: Empty states and settings button --- .../java/org/openedx/app/MainViewModel.kt | 13 +++- .../container/CourseContainerViewModelTest.kt | 7 ++ .../dates/CourseDatesViewModelTest.kt | 14 +++- .../outline/CourseOutlineViewModelTest.kt | 11 +++ .../videos/CourseVideoViewModelTest.kt | 10 ++- .../AllEnrolledCoursesFragment.kt | 39 +++++++--- .../presentation/AllEnrolledCoursesUIState.kt | 4 +- .../AllEnrolledCoursesViewModel.kt | 22 +++--- .../presentation/PrimaryCourseScreen.kt | 74 ++++++++++++++++--- .../presentation/PrimaryCourseViewModel.kt | 6 ++ .../learn/presentation/LearnFragment.kt | 27 +++---- .../learn/presentation/LearnViewModel.kt | 4 +- dashboard/src/main/res/values/strings.xml | 4 + 13 files changed, 175 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/org/openedx/app/MainViewModel.kt b/app/src/main/java/org/openedx/app/MainViewModel.kt index 3f90e1aa1..da681e8e1 100644 --- a/app/src/main/java/org/openedx/app/MainViewModel.kt +++ b/app/src/main/java/org/openedx/app/MainViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -31,13 +32,17 @@ class MainViewModel( val isDiscoveryTypeWebView get() = config.getDiscoveryConfig().isViewTypeWebView() + @OptIn(FlowPreview::class) override fun onCreate(owner: LifecycleOwner) { super.onCreate(owner) - notifier.notifier.onEach { - if (it is NavigationToDiscovery) { - _navigateToDiscovery.emit(true) + notifier.notifier + .onEach { + if (it is NavigationToDiscovery) { + _navigateToDiscovery.emit(true) + } } - }.distinctUntilChanged().launchIn(viewModelScope) + .distinctUntilChanged() + .launchIn(viewModelScope) } fun enableBottomBar(enable: Boolean) { diff --git a/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt index 63dce6272..46291af62 100644 --- a/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt @@ -134,6 +134,7 @@ class CourseContainerViewModelTest { "", "", "", + "", config, interactor, calendarManager, @@ -167,6 +168,7 @@ class CourseContainerViewModelTest { "", "", "", + "", config, interactor, calendarManager, @@ -200,6 +202,7 @@ class CourseContainerViewModelTest { "", "", "", + "", config, interactor, calendarManager, @@ -233,6 +236,7 @@ class CourseContainerViewModelTest { "", "", "", + "", config, interactor, calendarManager, @@ -267,6 +271,7 @@ class CourseContainerViewModelTest { "", "", "", + "", config, interactor, calendarManager, @@ -297,6 +302,7 @@ class CourseContainerViewModelTest { "", "", "", + "", config, interactor, calendarManager, @@ -327,6 +333,7 @@ class CourseContainerViewModelTest { "", "", "", + "", config, interactor, calendarManager, diff --git a/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt index 40a2d41c0..7d407de89 100644 --- a/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt @@ -44,6 +44,7 @@ import org.openedx.core.system.notifier.CourseLoading import org.openedx.core.system.notifier.CourseNotifier import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.CourseAnalytics +import org.openedx.course.presentation.CourseRouter import org.openedx.course.presentation.calendarsync.CalendarManager import java.net.UnknownHostException import java.util.Date @@ -62,6 +63,7 @@ class CourseDatesViewModelTest { private val corePreferences = mockk() private val analytics = mockk() private val config = mockk() + private val courseRouter = mockk() private val openEdx = "OpenEdx" private val calendarTitle = "OpenEdx - Abc" @@ -169,7 +171,8 @@ class CourseDatesViewModelTest { resourceManager, corePreferences, analytics, - config + config, + courseRouter ) coEvery { interactor.getCourseDates(any()) } throws UnknownHostException() val message = async { @@ -195,7 +198,8 @@ class CourseDatesViewModelTest { resourceManager, corePreferences, analytics, - config + config, + courseRouter ) coEvery { interactor.getCourseDates(any()) } throws Exception() val message = async { @@ -221,7 +225,8 @@ class CourseDatesViewModelTest { resourceManager, corePreferences, analytics, - config + config, + courseRouter ) coEvery { interactor.getCourseDates(any()) } returns mockedCourseDatesResult val message = async { @@ -247,7 +252,8 @@ class CourseDatesViewModelTest { resourceManager, corePreferences, analytics, - config + config, + courseRouter ) coEvery { interactor.getCourseDates(any()) } returns CourseDatesResult( datesSection = linkedMapOf(), diff --git a/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt index 098960a2a..955b3a85a 100644 --- a/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt @@ -56,6 +56,7 @@ import org.openedx.core.system.notifier.CourseNotifier import org.openedx.core.system.notifier.CourseStructureUpdated import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.CourseAnalytics +import org.openedx.course.presentation.CourseRouter import java.net.UnknownHostException import java.util.Date @@ -77,6 +78,7 @@ class CourseOutlineViewModelTest { private val workerController = mockk() private val analytics = mockk() private val coreAnalytics = mockk() + private val courseRouter = mockk() private val noInternet = "Slow or no internet connection" private val somethingWrong = "Something went wrong" @@ -233,6 +235,7 @@ class CourseOutlineViewModelTest { networkConnection, preferencesManager, analytics, + courseRouter, coreAnalytics, downloadDao, workerController, @@ -267,6 +270,7 @@ class CourseOutlineViewModelTest { networkConnection, preferencesManager, analytics, + courseRouter, coreAnalytics, downloadDao, workerController @@ -311,6 +315,7 @@ class CourseOutlineViewModelTest { networkConnection, preferencesManager, analytics, + courseRouter, coreAnalytics, downloadDao, workerController @@ -357,6 +362,7 @@ class CourseOutlineViewModelTest { networkConnection, preferencesManager, analytics, + courseRouter, coreAnalytics, downloadDao, workerController @@ -403,6 +409,7 @@ class CourseOutlineViewModelTest { networkConnection, preferencesManager, analytics, + courseRouter, coreAnalytics, downloadDao, workerController @@ -437,6 +444,7 @@ class CourseOutlineViewModelTest { networkConnection, preferencesManager, analytics, + courseRouter, coreAnalytics, downloadDao, workerController @@ -485,6 +493,7 @@ class CourseOutlineViewModelTest { networkConnection, preferencesManager, analytics, + courseRouter, coreAnalytics, downloadDao, workerController @@ -528,6 +537,7 @@ class CourseOutlineViewModelTest { networkConnection, preferencesManager, analytics, + courseRouter, coreAnalytics, downloadDao, workerController @@ -563,6 +573,7 @@ class CourseOutlineViewModelTest { networkConnection, preferencesManager, analytics, + courseRouter, coreAnalytics, downloadDao, workerController diff --git a/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt index 43d057a6c..78943d621 100644 --- a/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt @@ -53,6 +53,7 @@ import org.openedx.core.system.notifier.VideoNotifier import org.openedx.course.R import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.CourseAnalytics +import org.openedx.course.presentation.CourseRouter import java.util.Date @OptIn(ExperimentalCoroutinesApi::class) @@ -73,6 +74,7 @@ class CourseVideoViewModelTest { private val networkConnection = mockk() private val downloadDao = mockk() private val workerController = mockk() + private val courseRouter = mockk() private val cantDownload = "You can download content only from Wi-fi" @@ -196,9 +198,10 @@ class CourseVideoViewModelTest { courseNotifier, videoNotifier, analytics, + courseRouter, coreAnalytics, downloadDao, - workerController + workerController, ) viewModel.getVideos() @@ -227,6 +230,7 @@ class CourseVideoViewModelTest { courseNotifier, videoNotifier, analytics, + courseRouter, coreAnalytics, downloadDao, workerController @@ -267,6 +271,7 @@ class CourseVideoViewModelTest { courseNotifier, videoNotifier, analytics, + courseRouter, coreAnalytics, downloadDao, workerController @@ -308,6 +313,7 @@ class CourseVideoViewModelTest { courseNotifier, videoNotifier, analytics, + courseRouter, coreAnalytics, downloadDao, workerController @@ -344,6 +350,7 @@ class CourseVideoViewModelTest { courseNotifier, videoNotifier, analytics, + courseRouter, coreAnalytics, downloadDao, workerController @@ -384,6 +391,7 @@ class CourseVideoViewModelTest { courseNotifier, videoNotifier, analytics, + courseRouter, coreAnalytics, downloadDao, workerController 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 635029919..7f0d67ff0 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt @@ -400,7 +400,9 @@ private fun AllEnrolledCoursesScreen( .fillMaxHeight() .then(emptyStatePaddings) ) { - EmptyState() + EmptyState( + currentCourseStatus = CourseStatusFilter.entries[tabPagerState.currentPage] + ) } } } @@ -560,28 +562,33 @@ private fun Header( } @Composable -private fun EmptyState() { +private fun EmptyState( + currentCourseStatus: CourseStatusFilter +) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Column( - Modifier.width(185.dp), + Modifier.width(200.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Icon( - painter = painterResource(id = R.drawable.dashboard_ic_empty), - contentDescription = null, - tint = MaterialTheme.appColors.textFieldBorder + painter = painterResource(id = R.drawable.dashboard_ic_book), + tint = MaterialTheme.appColors.textFieldBorder, + contentDescription = null ) - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(4.dp)) Text( modifier = Modifier - .testTag("txt_empty_state_description") + .testTag("txt_empty_state_title") .fillMaxWidth(), - text = stringResource(id = R.string.dashboard_you_are_not_enrolled), - color = MaterialTheme.appColors.textPrimaryVariant, - style = MaterialTheme.appTypography.bodySmall, + text = stringResource( + id = R.string.dashboard_no_status_courses, + stringResource(currentCourseStatus.labelResId) + ), + color = MaterialTheme.appColors.textDark, + style = MaterialTheme.appTypography.titleMedium, textAlign = TextAlign.Center ) } @@ -616,6 +623,16 @@ private fun AllEnrolledCoursesPreview() { } } +@Preview +@Composable +private fun EmptyStatePreview() { + OpenEdXTheme { + EmptyState( + currentCourseStatus = CourseStatusFilter.COMPLETE + ) + } +} + private val mockCourseAssignments = CourseAssignments(null, emptyList()) private val mockCourseEnrolled = EnrolledCourse( auditAccessExpires = Date(), diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesUIState.kt b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesUIState.kt index 3ec4551f4..9c6166a9b 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesUIState.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesUIState.kt @@ -4,6 +4,6 @@ import org.openedx.core.domain.model.EnrolledCourse sealed class AllEnrolledCoursesUIState { data class Courses(val courses: List) : AllEnrolledCoursesUIState() - object Empty : AllEnrolledCoursesUIState() - object Loading : AllEnrolledCoursesUIState() + data object Empty : AllEnrolledCoursesUIState() + data object Loading : AllEnrolledCoursesUIState() } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt index 59368b04f..22e6914f8 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt @@ -1,6 +1,5 @@ package org.openedx.courses.presentation -import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -61,18 +60,8 @@ class AllEnrolledCoursesViewModel( private val currentFilter: MutableStateFlow = MutableStateFlow(CourseStatusFilter.ALL) - override fun onCreate(owner: LifecycleOwner) { - super.onCreate(owner) - viewModelScope.launch { - discoveryNotifier.notifier.collect { - if (it is CourseDashboardUpdate) { - updateCourses() - } - } - } - } - init { + collectDiscoveryNotifier() getCourses(currentFilter.value) } @@ -170,6 +159,15 @@ class AllEnrolledCoursesViewModel( analytics.dashboardCourseClickedEvent(courseId, courseName) } + private fun collectDiscoveryNotifier() { + viewModelScope.launch { + discoveryNotifier.notifier.collect { + if (it is CourseDashboardUpdate) { + updateCourses() + } + } + } + } } interface AllEnrolledCoursesAction { diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt index 1c4e102c9..14c332aa4 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt @@ -83,6 +83,7 @@ import org.openedx.core.domain.model.Progress import org.openedx.core.presentation.course.CourseContainerTab import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.OfflineModeDialog +import org.openedx.core.ui.OpenEdXButton import org.openedx.core.ui.TextIcon import org.openedx.core.ui.rememberWindowSize import org.openedx.core.ui.theme.OpenEdXTheme @@ -124,6 +125,10 @@ fun PrimaryCourseScreen( viewModel.getCourses() } + PrimaryCourseScreenAction.NavigateToDiscovery -> { + viewModel.navigateToDiscovery() + } + is PrimaryCourseScreenAction.OpenCourse -> { viewModel.dashboardRouter.navigateToCourseOutline( fm = fragmentManager, @@ -181,6 +186,7 @@ private fun PrimaryCourseScreen( modifier = Modifier.fillMaxSize(), backgroundColor = MaterialTheme.appColors.background ) { paddingValues -> + HandleUIMessage(uiMessage = uiMessage, scaffoldState = scaffoldState) Surface( @@ -224,8 +230,16 @@ private fun PrimaryCourseScreen( } is PrimaryCourseUIState.Empty -> { - EmptyState( - modifier = Modifier.align(Alignment.Center) + NoCoursesInfo( + modifier = Modifier + .align(Alignment.Center) + ) + FindACourseButton( + modifier = Modifier + .align(Alignment.BottomCenter), + findACourseClick = { + onAction(PrimaryCourseScreenAction.NavigateToDiscovery) + } ) } } @@ -692,7 +706,27 @@ private fun PrimaryCourseTitle( } @Composable -private fun EmptyState( +private fun FindACourseButton( + modifier: Modifier = Modifier, + findACourseClick: () -> Unit +) { + OpenEdXButton( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 20.dp), + onClick = { + findACourseClick() + } + ) { + Text( + color = MaterialTheme.appColors.buttonText, + text = stringResource(id = R.string.dashboard_find_a_course) + ) + } +} + +@Composable +private fun NoCoursesInfo( modifier: Modifier = Modifier ) { Box( @@ -700,22 +734,32 @@ private fun EmptyState( contentAlignment = Alignment.Center ) { Column( - Modifier.width(185.dp), + modifier = Modifier.width(200.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Icon( - painter = painterResource(id = R.drawable.dashboard_ic_empty), - contentDescription = null, - tint = MaterialTheme.appColors.textFieldBorder + painter = painterResource(id = R.drawable.dashboard_ic_book), + tint = MaterialTheme.appColors.textFieldBorder, + contentDescription = null + ) + Spacer(Modifier.height(4.dp)) + Text( + modifier = Modifier + .testTag("txt_empty_state_title") + .fillMaxWidth(), + text = stringResource(id = R.string.dashboard_all_courses_empty_title), + color = MaterialTheme.appColors.textDark, + style = MaterialTheme.appTypography.titleMedium, + textAlign = TextAlign.Center ) - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(12.dp)) Text( modifier = Modifier .testTag("txt_empty_state_description") .fillMaxWidth(), - text = stringResource(id = R.string.dashboard_you_are_not_enrolled), - color = MaterialTheme.appColors.textPrimaryVariant, - style = MaterialTheme.appTypography.bodySmall, + text = stringResource(id = R.string.dashboard_all_courses_empty_description), + color = MaterialTheme.appColors.textDark, + style = MaterialTheme.appTypography.labelMedium, textAlign = TextAlign.Center ) } @@ -807,3 +851,11 @@ private fun PrimaryCourseScreenPreview() { ) } } + +@Preview +@Composable +private fun NoCoursesInfoPreview() { + OpenEdXTheme { + NoCoursesInfo() + } +} diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseViewModel.kt index 14b9b1db7..a1362a7ab 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseViewModel.kt @@ -18,6 +18,7 @@ import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CourseDashboardUpdate import org.openedx.core.system.notifier.DiscoveryNotifier +import org.openedx.core.system.notifier.NavigationToDiscovery import org.openedx.core.utils.FileUtil import org.openedx.courses.domain.model.UserCourses import org.openedx.dashboard.domain.interactor.DashboardInteractor @@ -90,6 +91,10 @@ class PrimaryCourseViewModel( getCourses() } + fun navigateToDiscovery() { + viewModelScope.launch { discoveryNotifier.send(NavigationToDiscovery()) } + } + private fun collectDiscoveryNotifier() { viewModelScope.launch { discoveryNotifier.notifier.collect { @@ -105,6 +110,7 @@ interface PrimaryCourseScreenAction { object SwipeRefresh : PrimaryCourseScreenAction object ViewAll : PrimaryCourseScreenAction object Reload : PrimaryCourseScreenAction + object NavigateToDiscovery : PrimaryCourseScreenAction data class OpenBlock(val enrolledCourse: EnrolledCourse, val blockId: String) : PrimaryCourseScreenAction data class OpenCourse(val enrolledCourse: EnrolledCourse) : PrimaryCourseScreenAction data class NavigateToDates(val enrolledCourse: EnrolledCourse) : PrimaryCourseScreenAction 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 af01e8253..f1b9d07f3 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt @@ -21,7 +21,6 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ExpandMore -import androidx.compose.material.icons.filled.Search import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -32,6 +31,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp @@ -56,6 +56,7 @@ import org.openedx.dashboard.R import org.openedx.dashboard.databinding.FragmentLearnBinding import org.openedx.dashboard.presentation.DashboardRouter import org.openedx.learn.LearnType +import org.openedx.core.R as CoreR class LearnFragment : Fragment(R.layout.fragment_learn) { @@ -121,11 +122,9 @@ private fun Header( horizontalAlignment = Alignment.CenterHorizontally ) { Title( - modifier = Modifier - .padding(horizontal = 16.dp), label = stringResource(id = R.string.dashboard_learn), - onSearchClick = { - viewModel.onSearchClick(fragmentManager) + onSettingsClick = { + viewModel.onSettingsClick(fragmentManager) } ) @@ -144,13 +143,15 @@ private fun Header( private fun Title( modifier: Modifier = Modifier, label: String, - onSearchClick: () -> Unit + onSettingsClick: () -> Unit ) { Box( modifier = modifier.fillMaxWidth() ) { Text( - modifier = Modifier.align(Alignment.CenterStart), + modifier = Modifier + .align(Alignment.CenterStart) + .padding(start = 16.dp), text = label, color = MaterialTheme.appColors.textDark, style = MaterialTheme.appTypography.headlineBolt @@ -158,15 +159,15 @@ private fun Title( IconButton( modifier = Modifier .align(Alignment.CenterEnd) - .padding(start = 16.dp), + .padding(end = 12.dp), onClick = { - onSearchClick() + onSettingsClick() } ) { Icon( - imageVector = Icons.Filled.Search, - contentDescription = null, - tint = MaterialTheme.appColors.textDark + painter = painterResource(id = CoreR.drawable.core_ic_settings), + tint = MaterialTheme.appColors.primary, + contentDescription = stringResource(id = CoreR.string.core_accessibility_settings) ) } } @@ -259,7 +260,7 @@ private fun HeaderPreview() { OpenEdXTheme { Title( label = stringResource(id = R.string.dashboard_learn), - onSearchClick = {} + onSettingsClick = {} ) } } diff --git a/dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt b/dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt index fe5136dff..d2300f652 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt @@ -12,7 +12,7 @@ class LearnViewModel( val isProgramTypeWebView get() = config.getProgramConfig().isViewTypeWebView() - fun onSearchClick(fragmentManager: FragmentManager) { - dashboardRouter.navigateToCourseSearch(fragmentManager, "") + fun onSettingsClick(fragmentManager: FragmentManager) { + dashboardRouter.navigateToSettings(fragmentManager) } } diff --git a/dashboard/src/main/res/values/strings.xml b/dashboard/src/main/res/values/strings.xml index fdcd630d9..662bc4f8e 100644 --- a/dashboard/src/main/res/values/strings.xml +++ b/dashboard/src/main/res/values/strings.xml @@ -19,4 +19,8 @@ Expired All Courses All + No Courses + You are not currently enrolled in any courses, would you like to explore the course catalog? + Find a Course + No %1$s Courses From b22bb3088cb4ba389da1ea99b1473899d66b0deb Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Fri, 10 May 2024 13:08:23 +0300 Subject: [PATCH 15/23] fix: Number of courses --- .../org/openedx/courses/presentation/PrimaryCourseScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt index 14c332aa4..2e10cfe88 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt @@ -321,7 +321,7 @@ private fun SecondaryCourses( ) { TextIcon( modifier = Modifier.padding(horizontal = 18.dp), - text = stringResource(R.string.dashboard_view_all_with_count, courses.size), + text = stringResource(R.string.dashboard_view_all_with_count, courses.size + 1), textStyle = MaterialTheme.appTypography.titleSmall, icon = Icons.Default.ChevronRight, color = MaterialTheme.appColors.textDark, From e097f31b21da2a47848f0d8678ffbcd5a3b45152 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Fri, 10 May 2024 17:12:48 +0300 Subject: [PATCH 16/23] fix: Minor UI changes --- .../java/org/openedx/core/utils/TimeUtils.kt | 4 +- core/src/main/res/values-uk/strings.xml | 4 +- core/src/main/res/values/strings.xml | 4 +- .../container/CourseUnitContainerViewModel.kt | 30 ++++++------ .../src/main/java/org/openedx/DashboardUI.kt | 49 +++++++++++++++++++ .../AllEnrolledCoursesFragment.kt | 21 +------- .../AllEnrolledCoursesViewModel.kt | 6 ++- .../presentation/PrimaryCourseScreen.kt | 40 +++++++-------- .../learn/presentation/LearnFragment.kt | 10 +--- 9 files changed, 95 insertions(+), 73 deletions(-) create mode 100644 dashboard/src/main/java/org/openedx/DashboardUI.kt diff --git a/core/src/main/java/org/openedx/core/utils/TimeUtils.kt b/core/src/main/java/org/openedx/core/utils/TimeUtils.kt index 8e3b6fa1f..9ccfaebef 100644 --- a/core/src/main/java/org/openedx/core/utils/TimeUtils.kt +++ b/core/src/main/java/org/openedx/core/utils/TimeUtils.kt @@ -59,7 +59,7 @@ object TimeUtils { private fun dateToCourseDate(resourceManager: ResourceManager, date: Date?): String { return formatDate( - format = resourceManager.getString(R.string.core_date_format_MMMM_dd_yyyy), date = date + format = resourceManager.getString(R.string.core_date_format_MMM_dd_yyyy), date = date ) } @@ -152,7 +152,7 @@ object TimeUtils { ) } else { resourceManager.getString( - R.string.core_label_ending, dateToCourseDate(resourceManager, end) + R.string.core_label_ends, dateToCourseDate(resourceManager, end) ) } } diff --git a/core/src/main/res/values-uk/strings.xml b/core/src/main/res/values-uk/strings.xml index 3ff872c5a..2aab8871c 100644 --- a/core/src/main/res/values-uk/strings.xml +++ b/core/src/main/res/values-uk/strings.xml @@ -13,7 +13,7 @@ Виберіть значення Починається %1$s Закінчився %1$s - Закінчується %1$s + Закінчується %1$s Термін дії курсу закінчується %1$s Термін дії курсу закінчується %1$s Термін дії курсу минув %1$s @@ -31,7 +31,7 @@ Обліковий запис користувача не активовано. Будь ласка, спочатку активуйте свій обліковий запис. Надіслати електронний лист за допомогою ... Не встановлено жодного поштового клієнта - dd MMMM, yyyy + dd MMMM, yyyy dd MMM yyyy HH:mm Оновлення додатку Ми рекомендуємо вам оновитись до останньої версії. Оновіться зараз, щоб отримати останні функції та виправлення. diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 4410e4007..fc60e06d0 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -18,7 +18,7 @@ Select value Starting %1$s Ended %1$s - Ending %1$s + Ends %1$s Course access expires %1$s Course access expires on %1$s Course access expired %1$s @@ -46,7 +46,7 @@ OS version: Device model: Feedback - MMMM dd, yyyy + MMM dd, yyyy dd MMM yyyy hh:mm aaa App Update We recommend that you update to the latest version. Upgrade now to receive the latest features and fixes. diff --git a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt index 323adb7cb..48b7abf33 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt @@ -81,21 +81,6 @@ class CourseUnitContainerViewModel( private val _descendantsBlocks = MutableStateFlow>(listOf()) val descendantsBlocks = _descendantsBlocks.asStateFlow() - fun loadBlocks(mode: CourseViewMode) { - currentMode = mode - try { - val courseStructure = when (mode) { - CourseViewMode.FULL -> interactor.getCourseStructureFromCache() - CourseViewMode.VIDEOS -> interactor.getCourseStructureForVideos() - } - val blocks = courseStructure.blockData - courseName = courseStructure.name - this.blocks.clearAndAddAll(blocks) - } catch (e: Exception) { - //ignore e.printStackTrace() - } - } - init { _indexInContainer.value = 0 @@ -113,6 +98,21 @@ class CourseUnitContainerViewModel( } } + fun loadBlocks(mode: CourseViewMode) { + currentMode = mode + try { + val courseStructure = when (mode) { + CourseViewMode.FULL -> interactor.getCourseStructureFromCache() + CourseViewMode.VIDEOS -> interactor.getCourseStructureForVideos() + } + val blocks = courseStructure.blockData + courseName = courseStructure.name + this.blocks.clearAndAddAll(blocks) + } catch (e: Exception) { + //ignore e.printStackTrace() + } + } + fun setupCurrentIndex(componentId: String = "") { if (currentSectionIndex != -1) { return diff --git a/dashboard/src/main/java/org/openedx/DashboardUI.kt b/dashboard/src/main/java/org/openedx/DashboardUI.kt new file mode 100644 index 000000000..fcbc0d7bf --- /dev/null +++ b/dashboard/src/main/java/org/openedx/DashboardUI.kt @@ -0,0 +1,49 @@ +package org.openedx + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Lock +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.openedx.core.ui.theme.OpenEdXTheme +import org.openedx.core.ui.theme.appColors + +@Composable +fun Lock(modifier: Modifier = Modifier) { + Box( + modifier = modifier.fillMaxSize() + ) { + Icon( + modifier = Modifier + .size(32.dp) + .padding(top = 8.dp, end = 8.dp) + .background( + color = MaterialTheme.appColors.onPrimary.copy(0.5f), + shape = CircleShape + ) + .padding(4.dp) + .align(Alignment.TopEnd), + imageVector = Icons.Default.Lock, + contentDescription = null, + tint = MaterialTheme.appColors.onSurface + ) + } +} + +@Preview +@Composable +private fun LockPreview() { + OpenEdXTheme { + Lock() + } +} \ No newline at end of file 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 7f0d67ff0..4c946f6b9 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt @@ -6,7 +6,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -22,7 +21,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding 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.layout.widthIn import androidx.compose.foundation.lazy.grid.GridCells @@ -31,7 +29,6 @@ import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Card import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.ExperimentalMaterialApi @@ -43,7 +40,6 @@ import androidx.compose.material.Scaffold import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.Search import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh @@ -60,7 +56,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment 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.platform.ComposeView import androidx.compose.ui.platform.LocalContext @@ -82,6 +77,7 @@ import androidx.fragment.app.FragmentManager import coil.compose.AsyncImage import coil.request.ImageRequest import org.koin.androidx.compose.koinViewModel +import org.openedx.Lock import org.openedx.core.UIMessage import org.openedx.core.domain.model.Certificate import org.openedx.core.domain.model.CourseAssignments @@ -511,20 +507,7 @@ private fun CourseItem( ) } if (!course.course.coursewareAccess?.errorCode.isNullOrEmpty()) { - Icon( - modifier = Modifier - .size(32.dp) - .padding(top = 8.dp, end = 8.dp) - .background( - color = Color.White, - shape = CircleShape - ) - .padding(4.dp) - .align(Alignment.TopEnd), - imageVector = Icons.Default.Lock, - contentDescription = null, - tint = MaterialTheme.appColors.textWarning - ) + Lock() } } } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt index 22e6914f8..536a5f335 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt @@ -1,6 +1,7 @@ package org.openedx.courses.presentation import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -60,6 +61,8 @@ class AllEnrolledCoursesViewModel( private val currentFilter: MutableStateFlow = MutableStateFlow(CourseStatusFilter.ALL) + private var job: Job? = null + init { collectDiscoveryNotifier() getCourses(currentFilter.value) @@ -109,7 +112,8 @@ class AllEnrolledCoursesViewModel( page = 1 currentFilter.value = courseStatusFilter } - viewModelScope.launch { + job?.cancel() + job = viewModelScope.launch { try { isLoading = true val response = if (networkConnection.isOnline() || page > 1) { diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt index 2e10cfe88..3d271b79d 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt @@ -21,7 +21,6 @@ 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 import androidx.compose.material.Card import androidx.compose.material.CircularProgressIndicator @@ -36,7 +35,6 @@ import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos import androidx.compose.material.icons.filled.ChevronRight -import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.School import androidx.compose.material.icons.filled.Warning import androidx.compose.material.pullrefresh.PullRefreshIndicator @@ -51,7 +49,6 @@ 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.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.layout.ContentScale @@ -68,6 +65,7 @@ import androidx.fragment.app.FragmentManager import coil.compose.AsyncImage import coil.request.ImageRequest import org.koin.androidx.compose.koinViewModel +import org.openedx.Lock import org.openedx.core.UIMessage import org.openedx.core.domain.model.Certificate import org.openedx.core.domain.model.CourseAssignments @@ -292,12 +290,14 @@ private fun UserCourses( openCourse = openCourse ) } - SecondaryCourses( - courses = userCourses.enrollments, - apiHostUrl = apiHostUrl, - onCourseClick = openCourse, - onViewAllClick = onViewAllClick - ) + if (userCourses.enrollments.isNotEmpty()) { + SecondaryCourses( + courses = userCourses.enrollments, + apiHostUrl = apiHostUrl, + onCourseClick = openCourse, + onViewAllClick = onViewAllClick + ) + } } } @@ -437,20 +437,7 @@ private fun CourseListItem( ) } if (!course.course.coursewareAccess?.errorCode.isNullOrEmpty()) { - Icon( - modifier = Modifier - .size(32.dp) - .padding(top = 8.dp, end = 8.dp) - .background( - color = Color.White, - shape = CircleShape - ) - .padding(4.dp) - .align(Alignment.TopEnd), - imageVector = Icons.Default.Lock, - contentDescription = null, - tint = MaterialTheme.appColors.textWarning - ) + Lock() } } } @@ -477,6 +464,7 @@ private fun AssignmentItem( contentDescription = null ) Column( + modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp) ) { val infoTextStyle = if (title.isNullOrEmpty()) { @@ -497,6 +485,12 @@ private fun AssignmentItem( ) } } + Icon( + modifier = Modifier.size(16.dp), + imageVector = Icons.AutoMirrored.Filled.ArrowForwardIos, + tint = MaterialTheme.appColors.textDark, + contentDescription = null + ) } } 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 f1b9d07f3..736948417 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.DropdownMenu import androidx.compose.material.DropdownMenuItem @@ -88,13 +87,8 @@ class LearnFragment : Fragment(R.layout.fragment_learn) { binding.viewPager.adapter = adapter binding.viewPager.setUserInputEnabled(false) } - - private fun setFragment() { - binding.viewPager.setCurrentItem(0, false) - } } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun Header( viewModel: LearnViewModel = koinViewModel(), @@ -102,9 +96,6 @@ private fun Header( viewPager: ViewPager2 ) { val windowSize = rememberWindowSize() - val pagerState = rememberPagerState { - LearnType.entries.size - } val contentWidth by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( @@ -116,6 +107,7 @@ private fun Header( Column( modifier = Modifier + .background(MaterialTheme.appColors.background) .statusBarsInset() .displayCutoutForLandscape() .then(contentWidth), From c03832fb975fe8f47ced07d2f36a8d4de8d64976 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Tue, 14 May 2024 17:35:21 +0300 Subject: [PATCH 17/23] fix: Fixes according to designer feedback --- app/src/main/res/color/bottom_nav_color.xml | 5 +++++ app/src/main/res/layout/fragment_main.xml | 4 ++-- .../main/java/org/openedx/core/ui/ComposeCommon.kt | 5 +++-- core/src/main/res/values-night/colors.xml | 2 ++ core/src/main/res/values/colors.xml | 2 ++ .../courses/presentation/PrimaryCourseScreen.kt | 4 +++- .../org/openedx/learn/presentation/LearnFragment.kt | 13 ++++++++++--- dashboard/src/main/res/values/strings.xml | 4 ++-- 8 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 app/src/main/res/color/bottom_nav_color.xml diff --git a/app/src/main/res/color/bottom_nav_color.xml b/app/src/main/res/color/bottom_nav_color.xml new file mode 100644 index 000000000..07694e22e --- /dev/null +++ b/app/src/main/res/color/bottom_nav_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index eb6f37a6f..25a289a83 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -14,11 +14,12 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - - \ No newline at end of file diff --git a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt index aed1ba642..40d42971f 100644 --- a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt +++ b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt @@ -49,6 +49,7 @@ import androidx.compose.material.TextFieldDefaults import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.ManageAccounts import androidx.compose.material.icons.filled.Search import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -200,8 +201,8 @@ fun Toolbar( onClick = { onSettingsClick() } ) { Icon( - painter = painterResource(id = R.drawable.core_ic_settings), - tint = MaterialTheme.appColors.primary, + imageVector = Icons.Default.ManageAccounts, + tint = MaterialTheme.appColors.textAccent, contentDescription = stringResource(id = R.string.core_accessibility_settings) ) } diff --git a/core/src/main/res/values-night/colors.xml b/core/src/main/res/values-night/colors.xml index 5a7d9d3bd..4db689c7b 100644 --- a/core/src/main/res/values-night/colors.xml +++ b/core/src/main/res/values-night/colors.xml @@ -3,4 +3,6 @@ #FF19212F #5478F9 #19212F + #879FF5 + #8E9BAE \ No newline at end of file diff --git a/core/src/main/res/values/colors.xml b/core/src/main/res/values/colors.xml index d6d7f456d..d41c58bac 100644 --- a/core/src/main/res/values/colors.xml +++ b/core/src/main/res/values/colors.xml @@ -3,4 +3,6 @@ #FFFFFF #3C68FF #517BFE + #3C68FF + #97A5BB \ No newline at end of file diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt index 3d271b79d..c2a99811d 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt @@ -677,7 +677,9 @@ private fun PrimaryCourseTitle( modifier = Modifier.fillMaxWidth(), text = primaryCourse.course.name, style = MaterialTheme.appTypography.titleLarge, - color = MaterialTheme.appColors.textDark + color = MaterialTheme.appColors.textDark, + overflow = TextOverflow.Ellipsis, + maxLines = 3 ) Text( modifier = Modifier.fillMaxWidth(), 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 736948417..482e447ed 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt @@ -2,6 +2,7 @@ package org.openedx.learn.presentation import android.os.Bundle import android.view.View +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -20,6 +21,7 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.filled.ManageAccounts import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -28,9 +30,9 @@ 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.rotate import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp @@ -157,8 +159,8 @@ private fun Title( } ) { Icon( - painter = painterResource(id = CoreR.drawable.core_ic_settings), - tint = MaterialTheme.appColors.primary, + imageVector = Icons.Default.ManageAccounts, + tint = MaterialTheme.appColors.textAccent, contentDescription = stringResource(id = CoreR.string.core_accessibility_settings) ) } @@ -173,6 +175,10 @@ private fun LearnDropdownMenu( ) { var expanded by remember { mutableStateOf(false) } var currentValue by remember { mutableStateOf(LearnType.COURSES) } + val iconRotation by animateFloatAsState( + targetValue = if (expanded) 180f else 0f, + label = "" + ) LaunchedEffect(currentValue) { viewPager.setCurrentItem( @@ -199,6 +205,7 @@ private fun LearnDropdownMenu( style = MaterialTheme.appTypography.titleSmall ) Icon( + modifier = Modifier.rotate(iconRotation), imageVector = Icons.Default.ExpandMore, tint = MaterialTheme.appColors.textDark, contentDescription = null diff --git a/dashboard/src/main/res/values/strings.xml b/dashboard/src/main/res/values/strings.xml index 662bc4f8e..33f794904 100644 --- a/dashboard/src/main/res/values/strings.xml +++ b/dashboard/src/main/res/values/strings.xml @@ -9,8 +9,8 @@ Course %1$s Start course Resume Course - %1$d Past Due Assignment - View All (%1$d) + %1$d Past Due Assignments + View All Courses (%1$d) View All %1$s Due in %2$s All From e62b7128ccc1859c510da1515390b844256a22f6 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Wed, 15 May 2024 15:52:58 +0300 Subject: [PATCH 18/23] fix: Fixes after demo --- .../openedx/core/data/model/EnrolledCourse.kt | 2 +- .../AllEnrolledCoursesFragment.kt | 9 +- .../presentation/PrimaryCourseScreen.kt | 116 ++++++++++-------- .../presentation/PrimaryCourseViewModel.kt | 2 +- .../data/repository/DashboardRepository.kt | 4 +- .../domain/interactor/DashboardInteractor.kt | 2 +- 6 files changed, 74 insertions(+), 61 deletions(-) diff --git a/core/src/main/java/org/openedx/core/data/model/EnrolledCourse.kt b/core/src/main/java/org/openedx/core/data/model/EnrolledCourse.kt index 3cff7c74b..83f20717d 100644 --- a/core/src/main/java/org/openedx/core/data/model/EnrolledCourse.kt +++ b/core/src/main/java/org/openedx/core/data/model/EnrolledCourse.kt @@ -19,7 +19,7 @@ data class EnrolledCourse( val course: EnrolledCourseData?, @SerializedName("certificate") val certificate: Certificate?, - @SerializedName("progress") + @SerializedName("course_progress") val progress: Progress?, @SerializedName("course_status") val courseStatus: CourseStatus?, 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 4c946f6b9..4e6b745ce 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt @@ -447,7 +447,7 @@ private fun CourseItem( }, backgroundColor = MaterialTheme.appColors.background, shape = MaterialTheme.appShapes.courseImageShape, - elevation = 2.dp + elevation = 4.dp ) { Box { Column { @@ -463,11 +463,16 @@ private fun CourseItem( .fillMaxWidth() .height(90.dp) ) + val progress: Float = try { + course.progress.assignmentsCompleted.toFloat() / course.progress.totalAssignmentsCount.toFloat() + } catch (_: ArithmeticException) { + 0f + } LinearProgressIndicator( modifier = Modifier .fillMaxWidth() .height(8.dp), - progress = course.progress.assignmentsCompleted.toFloat(), + progress = progress, color = MaterialTheme.appColors.primary, backgroundColor = MaterialTheme.appColors.divider ) diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt index c2a99811d..f77514615 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt @@ -194,60 +194,63 @@ private fun PrimaryCourseScreen( color = MaterialTheme.appColors.background ) { Box( - Modifier - .fillMaxSize() - .pullRefresh(pullRefreshState) - .verticalScroll(rememberScrollState()), + Modifier.fillMaxSize() ) { - when (uiState) { - is PrimaryCourseUIState.Loading -> { - CircularProgressIndicator( - modifier = Modifier.align(Alignment.Center), - color = MaterialTheme.appColors.primary - ) - } + Box( + Modifier + .fillMaxSize() + .pullRefresh(pullRefreshState) + .verticalScroll(rememberScrollState()), + ) { + when (uiState) { + is PrimaryCourseUIState.Loading -> { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.appColors.primary + ) + } - is PrimaryCourseUIState.Courses -> { - UserCourses( - modifier = Modifier.fillMaxSize(), - userCourses = uiState.userCourses, - apiHostUrl = apiHostUrl, - openCourse = { - onAction(PrimaryCourseScreenAction.OpenCourse(it)) - }, - onViewAllClick = { - onAction(PrimaryCourseScreenAction.ViewAll) - }, - navigateToDates = { - onAction(PrimaryCourseScreenAction.NavigateToDates(it)) - }, - openBlock = { course, blockId -> - onAction(PrimaryCourseScreenAction.OpenBlock(course, blockId)) - } - ) - } + is PrimaryCourseUIState.Courses -> { + UserCourses( + modifier = Modifier.fillMaxSize(), + userCourses = uiState.userCourses, + apiHostUrl = apiHostUrl, + openCourse = { + onAction(PrimaryCourseScreenAction.OpenCourse(it)) + }, + onViewAllClick = { + onAction(PrimaryCourseScreenAction.ViewAll) + }, + navigateToDates = { + onAction(PrimaryCourseScreenAction.NavigateToDates(it)) + }, + openBlock = { course, blockId -> + onAction(PrimaryCourseScreenAction.OpenBlock(course, blockId)) + } + ) + } - is PrimaryCourseUIState.Empty -> { - NoCoursesInfo( - modifier = Modifier - .align(Alignment.Center) - ) - FindACourseButton( - modifier = Modifier - .align(Alignment.BottomCenter), - findACourseClick = { - onAction(PrimaryCourseScreenAction.NavigateToDiscovery) - } - ) + is PrimaryCourseUIState.Empty -> { + NoCoursesInfo( + modifier = Modifier + .align(Alignment.Center) + ) + FindACourseButton( + modifier = Modifier + .align(Alignment.BottomCenter), + findACourseClick = { + onAction(PrimaryCourseScreenAction.NavigateToDiscovery) + } + ) + } } - } - - PullRefreshIndicator( - updating, - pullRefreshState, - Modifier.align(Alignment.TopCenter) - ) + PullRefreshIndicator( + updating, + pullRefreshState, + Modifier.align(Alignment.TopCenter) + ) + } if (!isInternetConnectionShown && !hasInternetConnection) { OfflineModeDialog( Modifier @@ -258,7 +261,7 @@ private fun PrimaryCourseScreen( }, onReloadClick = { isInternetConnectionShown = true - onAction(PrimaryCourseScreenAction.Reload) + onAction(PrimaryCourseScreenAction.SwipeRefresh) } ) } @@ -369,7 +372,7 @@ private fun ViewAllItem( ), backgroundColor = MaterialTheme.appColors.cardViewBackground, shape = MaterialTheme.appShapes.courseImageShape, - elevation = 2.dp, + elevation = 4.dp, ) { Column( modifier = Modifier.fillMaxSize(), @@ -408,7 +411,7 @@ private fun CourseListItem( }, backgroundColor = MaterialTheme.appColors.background, shape = MaterialTheme.appShapes.courseImageShape, - elevation = 2.dp + elevation = 4.dp ) { Box { Column { @@ -512,7 +515,12 @@ private fun PrimaryCourseCard( shape = MaterialTheme.appShapes.courseImageShape, elevation = 4.dp ) { - Column { + Column( + modifier = Modifier + .clickable { + openCourse(primaryCourse) + } + ) { AsyncImage( model = ImageRequest.Builder(context) .data(apiHostUrl + primaryCourse.course.courseImage) @@ -526,7 +534,7 @@ private fun PrimaryCourseCard( .height(140.dp) ) val progress: Float = try { - (primaryCourse.progress.assignmentsCompleted / primaryCourse.progress.totalAssignmentsCount).toFloat() + primaryCourse.progress.assignmentsCompleted.toFloat() / primaryCourse.progress.totalAssignmentsCount.toFloat() } catch (_: ArithmeticException) { 0f } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseViewModel.kt index a1362a7ab..57066a9c3 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseViewModel.kt @@ -61,7 +61,7 @@ class PrimaryCourseViewModel( try { if (networkConnection.isOnline()) { val response = interactor.getMainUserCourses() - if (response.primary == null && response.enrollments.isNotEmpty()) { + if (response.primary == null && response.enrollments.isEmpty()) { _uiState.value = PrimaryCourseUIState.Empty } else { _uiState.value = PrimaryCourseUIState.Courses(response) diff --git a/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt b/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt index cd3ca27da..fe34f8bf2 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt @@ -38,7 +38,7 @@ class DashboardRepository( suspend fun getMainUserCourses(): UserCourses { val user = preferencesManager.user val result = api.getUserCourses( - username = user?.username ?: "" + username = user?.username ?: "", ) preferencesManager.appConfig = result.configs.mapToDomain() @@ -56,7 +56,7 @@ class DashboardRepository( username = user?.username ?: "", page = page, status = status?.key, - fields = listOf("progress") + fields = listOf("course_progress") ) preferencesManager.appConfig = result.configs.mapToDomain() diff --git a/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt b/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt index f8ec45ed8..5990edcca 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt @@ -16,7 +16,7 @@ class DashboardInteractor( suspend fun getEnrolledCoursesFromCache() = repository.getEnrolledCoursesFromCache() suspend fun getMainUserCourses(): UserCourses { - return repository.getMainUserCourses( ) + return repository.getMainUserCourses() } suspend fun getAllUserCourses(page: Int = 1, status: CourseStatusFilter? = null): DashboardCourseList { From 96b7795fd69d06b5eabd8cd7ceb133e9bddb27c3 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Thu, 16 May 2024 16:39:29 +0300 Subject: [PATCH 19/23] refactor: Move CourseContainerTab --- app/src/main/java/org/openedx/app/AppRouter.kt | 5 ++--- .../openedx/core/system/notifier/CourseNotifier.kt | 3 ++- .../openedx/core/system/notifier/CourseRefresh.kt | 5 ----- .../openedx/core/system/notifier/RefreshDates.kt | 3 +++ .../core/system/notifier/RefreshDiscussions.kt | 3 +++ .../presentation/container/CollapsingLayout.kt | 1 - .../container/CourseContainerFragment.kt | 14 ++++++++------ .../presentation/container}/CourseContainerTab.kt | 2 +- .../container/CourseContainerViewModel.kt | 8 ++++---- .../presentation/dates/CourseDatesViewModel.kt | 9 ++------- .../courses/presentation/PrimaryCourseScreen.kt | 3 +-- .../dashboard/presentation/DashboardRouter.kt | 3 +-- .../discovery/presentation/DiscoveryRouter.kt | 1 - .../topics/DiscussionTopicsViewModel.kt | 9 ++------- 14 files changed, 29 insertions(+), 40 deletions(-) delete mode 100644 core/src/main/java/org/openedx/core/system/notifier/CourseRefresh.kt create mode 100644 core/src/main/java/org/openedx/core/system/notifier/RefreshDates.kt create mode 100644 core/src/main/java/org/openedx/core/system/notifier/RefreshDiscussions.kt rename {core/src/main/java/org/openedx/core/presentation/course => course/src/main/java/org/openedx/course/presentation/container}/CourseContainerTab.kt (95%) diff --git a/app/src/main/java/org/openedx/app/AppRouter.kt b/app/src/main/java/org/openedx/app/AppRouter.kt index 4cc840c4f..277db69ad 100644 --- a/app/src/main/java/org/openedx/app/AppRouter.kt +++ b/app/src/main/java/org/openedx/app/AppRouter.kt @@ -9,7 +9,6 @@ import org.openedx.auth.presentation.restore.RestorePasswordFragment import org.openedx.auth.presentation.signin.SignInFragment import org.openedx.auth.presentation.signup.SignUpFragment import org.openedx.core.FragmentViewType -import org.openedx.core.presentation.course.CourseContainerTab import org.openedx.core.presentation.course.CourseViewMode import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRouter import org.openedx.core.presentation.global.app_upgrade.UpgradeRequiredFragment @@ -160,12 +159,12 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di courseId: String, courseTitle: String, enrollmentMode: String, - requiredTab: CourseContainerTab, + openDates: Boolean, openBlock: String ) { replaceFragmentWithBackStack( fm, - CourseContainerFragment.newInstance(courseId, courseTitle, enrollmentMode, requiredTab, openBlock) + CourseContainerFragment.newInstance(courseId, courseTitle, enrollmentMode, openDates, openBlock) ) } diff --git a/core/src/main/java/org/openedx/core/system/notifier/CourseNotifier.kt b/core/src/main/java/org/openedx/core/system/notifier/CourseNotifier.kt index f78732964..527a7ce51 100644 --- a/core/src/main/java/org/openedx/core/system/notifier/CourseNotifier.kt +++ b/core/src/main/java/org/openedx/core/system/notifier/CourseNotifier.kt @@ -18,6 +18,7 @@ class CourseNotifier { suspend fun send(event: CalendarSyncEvent) = channel.emit(event) suspend fun send(event: CourseDatesShifted) = channel.emit(event) suspend fun send(event: CourseLoading) = channel.emit(event) - suspend fun send(event: CourseRefresh) = channel.emit(event) suspend fun send(event: CourseOpenBlock) = channel.emit(event) + suspend fun send(event: RefreshDates) = channel.emit(event) + suspend fun send(event: RefreshDiscussions) = channel.emit(event) } diff --git a/core/src/main/java/org/openedx/core/system/notifier/CourseRefresh.kt b/core/src/main/java/org/openedx/core/system/notifier/CourseRefresh.kt deleted file mode 100644 index c85fc595d..000000000 --- a/core/src/main/java/org/openedx/core/system/notifier/CourseRefresh.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.openedx.core.system.notifier - -import org.openedx.core.presentation.course.CourseContainerTab - -data class CourseRefresh(val courseContainerTab: CourseContainerTab) : CourseEvent diff --git a/core/src/main/java/org/openedx/core/system/notifier/RefreshDates.kt b/core/src/main/java/org/openedx/core/system/notifier/RefreshDates.kt new file mode 100644 index 000000000..779d1b924 --- /dev/null +++ b/core/src/main/java/org/openedx/core/system/notifier/RefreshDates.kt @@ -0,0 +1,3 @@ +package org.openedx.core.system.notifier + +object RefreshDates : CourseEvent diff --git a/core/src/main/java/org/openedx/core/system/notifier/RefreshDiscussions.kt b/core/src/main/java/org/openedx/core/system/notifier/RefreshDiscussions.kt new file mode 100644 index 000000000..5c51f605b --- /dev/null +++ b/core/src/main/java/org/openedx/core/system/notifier/RefreshDiscussions.kt @@ -0,0 +1,3 @@ +package org.openedx.core.system.notifier + +object RefreshDiscussions : CourseEvent diff --git a/course/src/main/java/org/openedx/course/presentation/container/CollapsingLayout.kt b/course/src/main/java/org/openedx/course/presentation/container/CollapsingLayout.kt index 74c85873f..64ba858d8 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CollapsingLayout.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CollapsingLayout.kt @@ -64,7 +64,6 @@ import androidx.compose.ui.unit.dp import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import org.openedx.core.presentation.course.CourseContainerTab import org.openedx.core.ui.RoundTabsBar import org.openedx.core.ui.displayCutoutForLandscape import org.openedx.core.ui.rememberWindowSize diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt index be0e16879..3dc7a06b2 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt @@ -53,9 +53,7 @@ import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf -import org.openedx.core.extension.serializable import org.openedx.core.extension.takeIfNotEmpty -import org.openedx.core.presentation.course.CourseContainerTab import org.openedx.core.presentation.global.viewBinding import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.OfflineModeDialog @@ -258,13 +256,13 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { const val ARG_COURSE_ID = "courseId" const val ARG_TITLE = "title" const val ARG_ENROLLMENT_MODE = "enrollmentMode" - const val ARG_REQUIRED_TAB = "requiredTab" + const val ARG_OPEN_DATES = "open_dates" const val ARG_OPEN_BLOCK = "resume_block" fun newInstance( courseId: String, courseTitle: String, enrollmentMode: String, - requiredTab: CourseContainerTab = CourseContainerTab.HOME, + openDates: Boolean = false, openBlock: String = "" ): CourseContainerFragment { val fragment = CourseContainerFragment() @@ -272,7 +270,7 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { ARG_COURSE_ID to courseId, ARG_TITLE to courseTitle, ARG_ENROLLMENT_MODE to enrollmentMode, - ARG_REQUIRED_TAB to requiredTab, + ARG_OPEN_DATES to openDates, ARG_OPEN_BLOCK to openBlock ) return fragment @@ -304,7 +302,11 @@ fun CourseDashboard( val refreshing by viewModel.refreshing.collectAsState(true) val courseImage by viewModel.courseImage.collectAsState() val uiMessage by viewModel.uiMessage.collectAsState(null) - val requiredTab = bundle.serializable(CourseContainerFragment.ARG_REQUIRED_TAB) + val requiredTab = if (bundle.getBoolean(CourseContainerFragment.ARG_OPEN_DATES)) { + CourseContainerTab.DATES + } else { + CourseContainerTab.HOME + } val pagerState = rememberPagerState( initialPage = CourseContainerTab.entries.indexOf(requiredTab), pageCount = { CourseContainerTab.entries.size } diff --git a/core/src/main/java/org/openedx/core/presentation/course/CourseContainerTab.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerTab.kt similarity index 95% rename from core/src/main/java/org/openedx/core/presentation/course/CourseContainerTab.kt rename to course/src/main/java/org/openedx/course/presentation/container/CourseContainerTab.kt index 51d235c36..afeafcddc 100644 --- a/core/src/main/java/org/openedx/core/presentation/course/CourseContainerTab.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerTab.kt @@ -1,4 +1,4 @@ -package org.openedx.core.presentation.course +package org.openedx.course.presentation.container import androidx.annotation.StringRes import androidx.compose.material.icons.Icons diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt index b723c9396..310860933 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt @@ -25,7 +25,6 @@ import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences import org.openedx.core.exception.NoCachedDataException import org.openedx.core.extension.isInternetError -import org.openedx.core.presentation.course.CourseContainerTab import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CalendarSyncEvent.CheckCalendarSyncEvent @@ -35,8 +34,9 @@ import org.openedx.core.system.notifier.CourseDatesShifted import org.openedx.core.system.notifier.CourseLoading import org.openedx.core.system.notifier.CourseNotifier import org.openedx.core.system.notifier.CourseOpenBlock -import org.openedx.core.system.notifier.CourseRefresh import org.openedx.core.system.notifier.CourseStructureUpdated +import org.openedx.core.system.notifier.RefreshDates +import org.openedx.core.system.notifier.RefreshDiscussions import org.openedx.core.utils.TimeUtils import org.openedx.course.DatesShiftedSnackBar import org.openedx.course.R @@ -228,13 +228,13 @@ class CourseContainerViewModel( CourseContainerTab.DATES -> { viewModelScope.launch { - courseNotifier.send(CourseRefresh(courseContainerTab)) + courseNotifier.send(RefreshDates) } } CourseContainerTab.DISCUSSIONS -> { viewModelScope.launch { - courseNotifier.send(CourseRefresh(courseContainerTab)) + courseNotifier.send(RefreshDiscussions) } } diff --git a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt index a48284cb4..98478dd92 100644 --- a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt @@ -23,14 +23,13 @@ import org.openedx.core.domain.model.CourseStructure import org.openedx.core.extension.getSequentialBlocks import org.openedx.core.extension.getVerticalBlocks import org.openedx.core.extension.isInternetError -import org.openedx.core.presentation.course.CourseContainerTab import org.openedx.core.system.ResourceManager import org.openedx.core.system.notifier.CalendarSyncEvent.CheckCalendarSyncEvent import org.openedx.core.system.notifier.CalendarSyncEvent.CreateCalendarSyncEvent import org.openedx.core.system.notifier.CourseDatesShifted import org.openedx.core.system.notifier.CourseLoading import org.openedx.core.system.notifier.CourseNotifier -import org.openedx.core.system.notifier.CourseRefresh +import org.openedx.core.system.notifier.RefreshDates import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.CourseAnalyticsEvent @@ -88,11 +87,7 @@ class CourseDatesViewModel( _calendarSyncUIState.update { it.copy(isSynced = event.isSynced) } } - is CourseRefresh -> { - if (event.courseContainerTab == CourseContainerTab.DATES) { - loadingCourseDatesInternal() - } - } + is RefreshDates -> loadingCourseDatesInternal() } } } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt index f77514615..ce2850ecd 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt @@ -78,7 +78,6 @@ import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.domain.model.EnrolledCourseData import org.openedx.core.domain.model.Pagination import org.openedx.core.domain.model.Progress -import org.openedx.core.presentation.course.CourseContainerTab import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.OfflineModeDialog import org.openedx.core.ui.OpenEdXButton @@ -142,7 +141,7 @@ fun PrimaryCourseScreen( courseId = action.enrolledCourse.course.id, courseTitle = action.enrolledCourse.course.name, enrollmentMode = action.enrolledCourse.mode, - requiredTab = CourseContainerTab.DATES + openDates = true ) } diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt index 601f0fca7..b025c3748 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt @@ -2,7 +2,6 @@ package org.openedx.dashboard.presentation import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager -import org.openedx.core.presentation.course.CourseContainerTab interface DashboardRouter { @@ -11,7 +10,7 @@ interface DashboardRouter { courseId: String, courseTitle: String, enrollmentMode: String, - requiredTab: CourseContainerTab = CourseContainerTab.HOME, + openDates: Boolean = false, openBlock: String = "" ) diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryRouter.kt b/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryRouter.kt index 4d07b90ed..e1c4baa74 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryRouter.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryRouter.kt @@ -1,7 +1,6 @@ package org.openedx.discovery.presentation import androidx.fragment.app.FragmentManager -import org.openedx.core.presentation.course.CourseContainerTab interface DiscoveryRouter { diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModel.kt b/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModel.kt index 46552edc9..456eb79c2 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModel.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModel.kt @@ -11,11 +11,10 @@ import org.openedx.core.BaseViewModel import org.openedx.core.R import org.openedx.core.UIMessage import org.openedx.core.extension.isInternetError -import org.openedx.core.presentation.course.CourseContainerTab import org.openedx.core.system.ResourceManager import org.openedx.core.system.notifier.CourseLoading import org.openedx.core.system.notifier.CourseNotifier -import org.openedx.core.system.notifier.CourseRefresh +import org.openedx.core.system.notifier.RefreshDiscussions import org.openedx.discussion.domain.interactor.DiscussionInteractor import org.openedx.discussion.presentation.DiscussionAnalytics import org.openedx.discussion.presentation.DiscussionRouter @@ -81,11 +80,7 @@ class DiscussionTopicsViewModel( viewModelScope.launch { courseNotifier.notifier.collect { event -> when (event) { - is CourseRefresh -> { - if (event.courseContainerTab == CourseContainerTab.DISCUSSIONS) { - getCourseTopic() - } - } + is RefreshDiscussions -> getCourseTopic() } } } From 9454c60c6b302f52e95034b61d45720ea9e4cf06 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Wed, 22 May 2024 16:44:04 +0300 Subject: [PATCH 20/23] fix: Fixes according to PR feedback --- .../core/data/model/CourseAssignments.kt | 18 ++++++++-------- .../core/data/model/CourseDateBlock.kt | 3 ++- .../core/data/model/CourseEnrollments.kt | 7 +++++++ .../openedx/core/data/model/CourseStatus.kt | 14 ++++++------- .../openedx/core/data/model/EnrolledCourse.kt | 3 ++- .../org/openedx/core/data/model/Progress.kt | 11 +++++----- .../core/domain/model/CourseEnrollments.kt | 7 +++++++ .../global/InDevelopmentScreen.kt | 2 +- .../java/org/openedx/core/ui/theme/Type.kt | 4 ++-- .../java/org/openedx/core/utils/FileUtil.kt | 2 -- .../src/main/java/org/openedx/DashboardUI.kt | 2 +- .../courses/domain/model/UserCourses.kt | 8 ------- .../AllEnrolledCoursesFragment.kt | 2 +- .../presentation/PrimaryCourseFragment.kt | 2 +- .../presentation/PrimaryCourseScreen.kt | 21 ++++++++++++------- .../presentation/PrimaryCourseUIState.kt | 4 ++-- .../presentation/PrimaryCourseViewModel.kt | 10 ++++----- .../data/repository/DashboardRepository.kt | 12 ++++------- .../dashboard/domain/CourseStatusFilter.kt | 2 +- .../domain/interactor/DashboardInteractor.kt | 7 ++----- .../main/java/org/openedx/learn/LearnType.kt | 2 +- .../learn/presentation/LearnFragment.kt | 2 +- default_config/dev/config.yaml | 2 +- 23 files changed, 75 insertions(+), 72 deletions(-) create mode 100644 core/src/main/java/org/openedx/core/domain/model/CourseEnrollments.kt delete mode 100644 dashboard/src/main/java/org/openedx/courses/domain/model/UserCourses.kt diff --git a/core/src/main/java/org/openedx/core/data/model/CourseAssignments.kt b/core/src/main/java/org/openedx/core/data/model/CourseAssignments.kt index 66f468609..ca38de44b 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseAssignments.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseAssignments.kt @@ -2,6 +2,7 @@ package org.openedx.core.data.model import com.google.gson.annotations.SerializedName import org.openedx.core.data.model.room.discovery.CourseAssignmentsDb +import org.openedx.core.domain.model.CourseAssignments data class CourseAssignments( @SerializedName("future_assignments") @@ -9,15 +10,14 @@ data class CourseAssignments( @SerializedName("past_assignments") val pastAssignments: List? ) { - fun mapToDomain(): org.openedx.core.domain.model.CourseAssignments = - org.openedx.core.domain.model.CourseAssignments( - futureAssignments = futureAssignments?.map { - it.mapToDomain() - }, - pastAssignments = pastAssignments?.map { - it.mapToDomain() - } - ) + fun mapToDomain() = CourseAssignments( + futureAssignments = futureAssignments?.map { + it.mapToDomain() + }, + pastAssignments = pastAssignments?.map { + it.mapToDomain() + } + ) fun mapToRoomEntity() = CourseAssignmentsDb( futureAssignments = futureAssignments?.map { diff --git a/core/src/main/java/org/openedx/core/data/model/CourseDateBlock.kt b/core/src/main/java/org/openedx/core/data/model/CourseDateBlock.kt index 355595b75..520711e94 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseDateBlock.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseDateBlock.kt @@ -4,6 +4,7 @@ import android.os.Parcelable import com.google.gson.annotations.SerializedName import kotlinx.parcelize.Parcelize import org.openedx.core.data.model.room.discovery.CourseDateBlockDb +import org.openedx.core.domain.model.CourseDateBlock import org.openedx.core.utils.TimeUtils import java.util.Date @@ -31,7 +32,7 @@ data class CourseDateBlock( @SerializedName("first_component_block_id") val blockId: String = "", ): Parcelable { - fun mapToDomain() = org.openedx.core.domain.model.CourseDateBlock( + fun mapToDomain() = CourseDateBlock( complete = complete, date = TimeUtils.iso8601ToDate(date) ?: Date(), assignmentType = assignmentType, diff --git a/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt b/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt index 4f5f9332f..63025028c 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt @@ -7,6 +7,7 @@ import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.annotations.SerializedName import java.lang.reflect.Type +import org.openedx.core.domain.model.CourseEnrollments as DomainCourseEnrollments data class CourseEnrollments( @SerializedName("enrollments") @@ -18,6 +19,12 @@ data class CourseEnrollments( @SerializedName("primary") val primary: EnrolledCourse?, ) { + fun mapToDomain() = DomainCourseEnrollments( + enrollments = enrollments.mapToDomain(), + configs = configs.mapToDomain(), + primary = primary?.mapToDomain() + ) + class Deserializer : JsonDeserializer { override fun deserialize( json: JsonElement?, diff --git a/core/src/main/java/org/openedx/core/data/model/CourseStatus.kt b/core/src/main/java/org/openedx/core/data/model/CourseStatus.kt index 609459055..53caeb136 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseStatus.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseStatus.kt @@ -2,6 +2,7 @@ package org.openedx.core.data.model import com.google.gson.annotations.SerializedName import org.openedx.core.data.model.room.discovery.CourseStatusDb +import org.openedx.core.domain.model.CourseStatus data class CourseStatus( @SerializedName("last_visited_module_id") @@ -13,13 +14,12 @@ data class CourseStatus( @SerializedName("last_visited_unit_display_name") val lastVisitedUnitDisplayName: String? ) { - fun mapToDomain(): org.openedx.core.domain.model.CourseStatus = - org.openedx.core.domain.model.CourseStatus( - lastVisitedModuleId = lastVisitedModuleId ?: "", - lastVisitedModulePath = lastVisitedModulePath ?: emptyList(), - lastVisitedBlockId = lastVisitedBlockId ?: "", - lastVisitedUnitDisplayName = lastVisitedUnitDisplayName ?: "" - ) + fun mapToDomain() = CourseStatus( + lastVisitedModuleId = lastVisitedModuleId ?: "", + lastVisitedModulePath = lastVisitedModulePath ?: emptyList(), + lastVisitedBlockId = lastVisitedBlockId ?: "", + lastVisitedUnitDisplayName = lastVisitedUnitDisplayName ?: "" + ) fun mapToRoomEntity() = CourseStatusDb( lastVisitedModuleId = lastVisitedModuleId ?: "", diff --git a/core/src/main/java/org/openedx/core/data/model/EnrolledCourse.kt b/core/src/main/java/org/openedx/core/data/model/EnrolledCourse.kt index 83f20717d..edf8bbce3 100644 --- a/core/src/main/java/org/openedx/core/data/model/EnrolledCourse.kt +++ b/core/src/main/java/org/openedx/core/data/model/EnrolledCourse.kt @@ -5,6 +5,7 @@ import org.openedx.core.data.model.room.discovery.EnrolledCourseEntity import org.openedx.core.data.model.room.discovery.ProgressDb import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.utils.TimeUtils +import org.openedx.core.domain.model.Progress as ProgressDomain data class EnrolledCourse( @SerializedName("audit_access_expires") @@ -34,7 +35,7 @@ data class EnrolledCourse( isActive = isActive ?: false, course = course?.mapToDomain()!!, certificate = certificate?.mapToDomain(), - progress = progress?.mapToDomain() ?: org.openedx.core.domain.model.Progress.DEFAULT_PROGRESS, + progress = progress?.mapToDomain() ?: ProgressDomain.DEFAULT_PROGRESS, courseStatus = courseStatus?.mapToDomain(), courseAssignments = courseAssignments?.mapToDomain() ) diff --git a/core/src/main/java/org/openedx/core/data/model/Progress.kt b/core/src/main/java/org/openedx/core/data/model/Progress.kt index eab863b2e..932533e44 100644 --- a/core/src/main/java/org/openedx/core/data/model/Progress.kt +++ b/core/src/main/java/org/openedx/core/data/model/Progress.kt @@ -2,6 +2,7 @@ package org.openedx.core.data.model import com.google.gson.annotations.SerializedName import org.openedx.core.data.model.room.discovery.ProgressDb +import org.openedx.core.domain.model.Progress data class Progress( @SerializedName("assignments_completed") @@ -9,12 +10,10 @@ data class Progress( @SerializedName("total_assignments_count") val totalAssignmentsCount: Int? ) { - fun mapToDomain(): org.openedx.core.domain.model.Progress { - return org.openedx.core.domain.model.Progress( - assignmentsCompleted = assignmentsCompleted ?: 0, - totalAssignmentsCount = totalAssignmentsCount ?: 0 - ) - } + fun mapToDomain() = Progress( + assignmentsCompleted = assignmentsCompleted ?: 0, + totalAssignmentsCount = totalAssignmentsCount ?: 0 + ) fun mapToRoomEntity() = ProgressDb( assignmentsCompleted = assignmentsCompleted ?: 0, diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseEnrollments.kt b/core/src/main/java/org/openedx/core/domain/model/CourseEnrollments.kt new file mode 100644 index 000000000..6606902c2 --- /dev/null +++ b/core/src/main/java/org/openedx/core/domain/model/CourseEnrollments.kt @@ -0,0 +1,7 @@ +package org.openedx.core.domain.model + +data class CourseEnrollments( + val enrollments: DashboardCourseList, + val configs: AppConfig, + val primary: EnrolledCourse?, +) diff --git a/core/src/main/java/org/openedx/core/presentation/global/InDevelopmentScreen.kt b/core/src/main/java/org/openedx/core/presentation/global/InDevelopmentScreen.kt index 9cf2472f9..02f633704 100644 --- a/core/src/main/java/org/openedx/core/presentation/global/InDevelopmentScreen.kt +++ b/core/src/main/java/org/openedx/core/presentation/global/InDevelopmentScreen.kt @@ -28,4 +28,4 @@ fun InDevelopmentScreen( style = MaterialTheme.appTypography.headlineMedium ) } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/ui/theme/Type.kt b/core/src/main/java/org/openedx/core/ui/theme/Type.kt index 88371c284..deb8bad6a 100644 --- a/core/src/main/java/org/openedx/core/ui/theme/Type.kt +++ b/core/src/main/java/org/openedx/core/ui/theme/Type.kt @@ -17,7 +17,7 @@ data class AppTypography( val displayLarge: TextStyle, val displayMedium: TextStyle, val displaySmall: TextStyle, - val headlineBolt: TextStyle, + val headlineBold: TextStyle, val headlineLarge: TextStyle, val headlineMedium: TextStyle, val headlineSmall: TextStyle, @@ -74,7 +74,7 @@ internal val LocalTypography = staticCompositionLocalOf { letterSpacing = 0.sp, fontFamily = fontFamily ), - headlineBolt = TextStyle( + headlineBold = TextStyle( fontSize = 34.sp, lineHeight = 24.sp, fontWeight = FontWeight.Bold, diff --git a/core/src/main/java/org/openedx/core/utils/FileUtil.kt b/core/src/main/java/org/openedx/core/utils/FileUtil.kt index d1ae17c04..b7d22f929 100644 --- a/core/src/main/java/org/openedx/core/utils/FileUtil.kt +++ b/core/src/main/java/org/openedx/core/utils/FileUtil.kt @@ -31,8 +31,6 @@ class FileUtil(val context: Context) { null } } - - } enum class Directories { diff --git a/dashboard/src/main/java/org/openedx/DashboardUI.kt b/dashboard/src/main/java/org/openedx/DashboardUI.kt index fcbc0d7bf..13a3f42d1 100644 --- a/dashboard/src/main/java/org/openedx/DashboardUI.kt +++ b/dashboard/src/main/java/org/openedx/DashboardUI.kt @@ -46,4 +46,4 @@ private fun LockPreview() { OpenEdXTheme { Lock() } -} \ No newline at end of file +} diff --git a/dashboard/src/main/java/org/openedx/courses/domain/model/UserCourses.kt b/dashboard/src/main/java/org/openedx/courses/domain/model/UserCourses.kt deleted file mode 100644 index 5371b1cc3..000000000 --- a/dashboard/src/main/java/org/openedx/courses/domain/model/UserCourses.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.openedx.courses.domain.model - -import org.openedx.core.domain.model.EnrolledCourse - -data class UserCourses( - val enrollments: List, - val primary: EnrolledCourse? -) 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 4e6b745ce..d2af43f3c 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt @@ -530,7 +530,7 @@ private fun Header( modifier = Modifier.align(Alignment.CenterStart), text = stringResource(id = R.string.dashboard_all_courses), color = MaterialTheme.appColors.textDark, - style = MaterialTheme.appTypography.headlineBolt + style = MaterialTheme.appTypography.headlineBold ) IconButton( modifier = Modifier diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseFragment.kt b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseFragment.kt index 8b366cde8..e911248f9 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseFragment.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseFragment.kt @@ -21,4 +21,4 @@ class PrimaryCourseFragment : Fragment() { } } } -} \ No newline at end of file +} diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt index ce2850ecd..ac98d29db 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt @@ -67,9 +67,12 @@ import coil.request.ImageRequest import org.koin.androidx.compose.koinViewModel import org.openedx.Lock import org.openedx.core.UIMessage +import org.openedx.core.domain.model.AppConfig import org.openedx.core.domain.model.Certificate import org.openedx.core.domain.model.CourseAssignments import org.openedx.core.domain.model.CourseDateBlock +import org.openedx.core.domain.model.CourseDatesCalendarSync +import org.openedx.core.domain.model.CourseEnrollments import org.openedx.core.domain.model.CourseSharingUtmParameters import org.openedx.core.domain.model.CourseStatus import org.openedx.core.domain.model.CoursewareAccess @@ -88,7 +91,6 @@ import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography import org.openedx.core.utils.TimeUtils -import org.openedx.courses.domain.model.UserCourses import org.openedx.dashboard.R import java.util.Date import org.openedx.core.R as CoreR @@ -272,7 +274,7 @@ private fun PrimaryCourseScreen( @Composable private fun UserCourses( modifier: Modifier = Modifier, - userCourses: UserCourses, + userCourses: CourseEnrollments, apiHostUrl: String, openCourse: (EnrolledCourse) -> Unit, navigateToDates: (EnrolledCourse) -> Unit, @@ -283,18 +285,19 @@ private fun UserCourses( modifier = modifier .padding(vertical = 12.dp) ) { - if (userCourses.primary != null) { + val primaryCourse = userCourses.primary + if (primaryCourse != null) { PrimaryCourseCard( - primaryCourse = userCourses.primary, + primaryCourse = primaryCourse, apiHostUrl = apiHostUrl, navigateToDates = navigateToDates, openBlock = openBlock, openCourse = openCourse ) } - if (userCourses.enrollments.isNotEmpty()) { + if (userCourses.enrollments.courses.isNotEmpty()) { SecondaryCourses( - courses = userCourses.enrollments, + courses = userCourses.enrollments.courses, apiHostUrl = apiHostUrl, onCourseClick = openCourse, onViewAllClick = onViewAllClick @@ -821,8 +824,10 @@ private val mockDashboardCourseList = DashboardCourseList( pagination = mockPagination, courses = listOf(mockCourse, mockCourse, mockCourse, mockCourse, mockCourse, mockCourse) ) -private val mockUserCourses = UserCourses( - enrollments = mockDashboardCourseList.courses, + +private val mockUserCourses = CourseEnrollments( + enrollments = mockDashboardCourseList, + configs = AppConfig(CourseDatesCalendarSync(true, true, true, true)), primary = mockCourse ) diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseUIState.kt b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseUIState.kt index 7e21139d0..f30771688 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseUIState.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseUIState.kt @@ -1,9 +1,9 @@ package org.openedx.courses.presentation -import org.openedx.courses.domain.model.UserCourses +import org.openedx.core.domain.model.CourseEnrollments sealed class PrimaryCourseUIState { - data class Courses(val userCourses: UserCourses) : PrimaryCourseUIState() + data class Courses(val userCourses: CourseEnrollments) : PrimaryCourseUIState() data object Empty : PrimaryCourseUIState() data object Loading : PrimaryCourseUIState() } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseViewModel.kt index 57066a9c3..326adc837 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseViewModel.kt @@ -12,6 +12,7 @@ import org.openedx.core.BaseViewModel import org.openedx.core.R import org.openedx.core.UIMessage import org.openedx.core.config.Config +import org.openedx.core.data.model.CourseEnrollments import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.extension.isInternetError import org.openedx.core.system.ResourceManager @@ -20,7 +21,6 @@ import org.openedx.core.system.notifier.CourseDashboardUpdate import org.openedx.core.system.notifier.DiscoveryNotifier import org.openedx.core.system.notifier.NavigationToDiscovery import org.openedx.core.utils.FileUtil -import org.openedx.courses.domain.model.UserCourses import org.openedx.dashboard.domain.interactor.DashboardInteractor import org.openedx.dashboard.presentation.DashboardRouter @@ -61,17 +61,17 @@ class PrimaryCourseViewModel( try { if (networkConnection.isOnline()) { val response = interactor.getMainUserCourses() - if (response.primary == null && response.enrollments.isEmpty()) { + if (response.primary == null && response.enrollments.courses.isEmpty()) { _uiState.value = PrimaryCourseUIState.Empty } else { _uiState.value = PrimaryCourseUIState.Courses(response) } } else { - val cachedUserCourses = fileUtil.getObjectFromFile() - if (cachedUserCourses == null) { + val courseEnrollments = fileUtil.getObjectFromFile() + if (courseEnrollments == null) { _uiState.value = PrimaryCourseUIState.Empty } else { - _uiState.value = PrimaryCourseUIState.Courses(cachedUserCourses) + _uiState.value = PrimaryCourseUIState.Courses(courseEnrollments.mapToDomain()) } } } catch (e: Exception) { diff --git a/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt b/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt index fe34f8bf2..5844e8415 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt @@ -2,10 +2,10 @@ package org.openedx.dashboard.data.repository import org.openedx.core.data.api.CourseApi import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.domain.model.CourseEnrollments import org.openedx.core.domain.model.DashboardCourseList import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.utils.FileUtil -import org.openedx.courses.domain.model.UserCourses import org.openedx.dashboard.data.DashboardDao import org.openedx.dashboard.domain.CourseStatusFilter @@ -35,19 +35,15 @@ class DashboardRepository( return list.map { it.mapToDomain() } } - suspend fun getMainUserCourses(): UserCourses { + suspend fun getMainUserCourses(): CourseEnrollments { val user = preferencesManager.user val result = api.getUserCourses( username = user?.username ?: "", ) preferencesManager.appConfig = result.configs.mapToDomain() - val userCourses = UserCourses( - enrollments = result.enrollments.mapToDomain().courses, - primary = result.primary?.mapToDomain() - ) - fileUtil.saveObjectToFile(userCourses) - return userCourses + fileUtil.saveObjectToFile(result) + return result.mapToDomain() } suspend fun getAllUserCourses(page: Int, status: CourseStatusFilter?): DashboardCourseList { diff --git a/dashboard/src/main/java/org/openedx/dashboard/domain/CourseStatusFilter.kt b/dashboard/src/main/java/org/openedx/dashboard/domain/CourseStatusFilter.kt index e53d7fb88..a61fc2a1f 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/domain/CourseStatusFilter.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/domain/CourseStatusFilter.kt @@ -15,4 +15,4 @@ enum class CourseStatusFilter( IN_PROGRESS("in_progress", R.string.dashboard_course_filter_in_progress), COMPLETE("completed", R.string.dashboard_course_filter_completed), EXPIRED("expired", R.string.dashboard_course_filter_expired) -} \ No newline at end of file +} diff --git a/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt b/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt index 5990edcca..04146c103 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt @@ -1,7 +1,6 @@ package org.openedx.dashboard.domain.interactor import org.openedx.core.domain.model.DashboardCourseList -import org.openedx.courses.domain.model.UserCourses import org.openedx.dashboard.data.repository.DashboardRepository import org.openedx.dashboard.domain.CourseStatusFilter @@ -15,9 +14,7 @@ class DashboardInteractor( suspend fun getEnrolledCoursesFromCache() = repository.getEnrolledCoursesFromCache() - suspend fun getMainUserCourses(): UserCourses { - return repository.getMainUserCourses() - } + suspend fun getMainUserCourses() = repository.getMainUserCourses() suspend fun getAllUserCourses(page: Int = 1, status: CourseStatusFilter? = null): DashboardCourseList { return repository.getAllUserCourses( @@ -25,4 +22,4 @@ class DashboardInteractor( status ) } -} \ No newline at end of file +} diff --git a/dashboard/src/main/java/org/openedx/learn/LearnType.kt b/dashboard/src/main/java/org/openedx/learn/LearnType.kt index f9cd26b36..08100ef35 100644 --- a/dashboard/src/main/java/org/openedx/learn/LearnType.kt +++ b/dashboard/src/main/java/org/openedx/learn/LearnType.kt @@ -6,4 +6,4 @@ import org.openedx.dashboard.R enum class LearnType(@StringRes val title: Int) { COURSES(R.string.dashboard_courses), PROGRAMS(R.string.dashboard_programs) -} \ No newline at end of file +} 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 482e447ed..a4127d37e 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt @@ -148,7 +148,7 @@ private fun Title( .padding(start = 16.dp), text = label, color = MaterialTheme.appColors.textDark, - style = MaterialTheme.appTypography.headlineBolt + style = MaterialTheme.appTypography.headlineBold ) IconButton( modifier = Modifier diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml index 6ddd394ab..6c68daf93 100644 --- a/default_config/dev/config.yaml +++ b/default_config/dev/config.yaml @@ -23,7 +23,7 @@ DISCOVERY: PROGRAM_DETAIL_TEMPLATE: '' PROGRAM: - TYPE: 'webview' + TYPE: 'native' WEBVIEW: PROGRAM_URL: '' PROGRAM_DETAIL_URL_TEMPLATE: '' From 31b38432cbfa2eb45e59fcc5195b760c39d0eb30 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Thu, 23 May 2024 18:16:24 +0300 Subject: [PATCH 21/23] fix: Fixes according to PR feedback --- .../main/java/org/openedx/app/AppRouter.kt | 8 +- .../main/java/org/openedx/app/MainFragment.kt | 16 +- .../java/org/openedx/app/MainViewModel.kt | 7 +- .../java/org/openedx/app/di/ScreenModule.kt | 12 +- app/src/main/res/color/bottom_nav_color.xml | 6 +- app/src/main/res/drawable/app_ic_rows.xml | 8 +- app/src/main/res/layout/fragment_main.xml | 2 +- app/src/main/res/menu/bottom_view_menu.xml | 16 +- app/src/main/res/values-uk/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- .../openedx/core/config/DashboardConfig.kt | 4 +- .../core/data/model/CourseAssignments.kt | 8 +- .../core/data/model/CourseDateBlock.kt | 56 +++-- .../room/discovery/EnrolledCourseEntity.kt | 6 +- .../openedx/core/domain/model/CourseStatus.kt | 2 +- .../org/openedx/core/domain/model/Progress.kt | 2 +- .../org/openedx/core/module/DownloadWorker.kt | 7 +- .../openedx/core/module/TranscriptManager.kt | 3 +- .../global/InDevelopmentScreen.kt | 31 --- .../java/org/openedx/core/ui/ComposeCommon.kt | 1 + .../java/org/openedx/core/utils/FileUtil.kt | 2 +- .../main/res/drawable/core_ic_settings.xml | 20 -- .../res/drawable/ic_core_chapter_icon.xml | 49 ++-- core/src/main/res/values-night/colors.xml | 2 +- core/src/main/res/values/colors.xml | 2 +- core/src/main/res/values/strings.xml | 6 - .../container/CourseContainerFragment.kt | 28 ++- .../container/CourseContainerTab.kt | 12 +- .../container/CourseContainerViewModel.kt | 6 +- .../dates/CourseDatesViewModel.kt | 4 +- .../outline/CourseOutlineScreen.kt | 16 +- .../outline/CourseOutlineViewModel.kt | 8 +- .../course/presentation/ui/CourseVideosUI.kt | 14 +- course/src/main/res/values/strings.xml | 6 + .../presentation/MyCoursesScreenTest.kt | 6 +- .../AllEnrolledCoursesFragment.kt | 220 ++---------------- .../presentation/AllEnrolledCoursesUIState.kt | 11 +- .../presentation/AllEnrolledCoursesView.kt | 194 +++++++++++++++ .../AllEnrolledCoursesViewModel.kt | 68 +++--- ...ragment.kt => DashboardGalleryFragment.kt} | 4 +- .../presentation/DashboardGalleryUIState.kt | 9 + ...ourseScreen.kt => DashboardGalleryView.kt} | 96 ++++---- ...wModel.kt => DashboardGalleryViewModel.kt} | 57 +++-- .../presentation/PrimaryCourseUIState.kt | 9 - .../data/repository/DashboardRepository.kt | 3 +- ...rdFragment.kt => DashboardListFragment.kt} | 16 +- ...ViewModel.kt => DashboardListViewModel.kt} | 2 +- .../dashboard/presentation/DashboardRouter.kt | 4 +- .../learn/presentation/LearnFragment.kt | 7 +- .../main/res/drawable/dashboard_ic_book.xml | 30 +-- dashboard/src/main/res/values/strings.xml | 3 +- .../presentation/DashboardViewModelTest.kt | 20 +- default_config/prod/config.yaml | 2 +- default_config/stage/config.yaml | 2 +- 54 files changed, 553 insertions(+), 584 deletions(-) delete mode 100644 core/src/main/java/org/openedx/core/presentation/global/InDevelopmentScreen.kt delete mode 100644 core/src/main/res/drawable/core_ic_settings.xml create mode 100644 dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesView.kt rename dashboard/src/main/java/org/openedx/courses/presentation/{PrimaryCourseFragment.kt => DashboardGalleryFragment.kt} (82%) create mode 100644 dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryUIState.kt rename dashboard/src/main/java/org/openedx/courses/presentation/{PrimaryCourseScreen.kt => DashboardGalleryView.kt} (89%) rename dashboard/src/main/java/org/openedx/courses/presentation/{PrimaryCourseViewModel.kt => DashboardGalleryViewModel.kt} (67%) delete mode 100644 dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseUIState.kt rename dashboard/src/main/java/org/openedx/dashboard/presentation/{ListDashboardFragment.kt => DashboardListFragment.kt} (98%) rename dashboard/src/main/java/org/openedx/dashboard/presentation/{ListDashboardViewModel.kt => DashboardListViewModel.kt} (99%) diff --git a/app/src/main/java/org/openedx/app/AppRouter.kt b/app/src/main/java/org/openedx/app/AppRouter.kt index 277db69ad..6d439fd10 100644 --- a/app/src/main/java/org/openedx/app/AppRouter.kt +++ b/app/src/main/java/org/openedx/app/AppRouter.kt @@ -159,17 +159,17 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di courseId: String, courseTitle: String, enrollmentMode: String, - openDates: Boolean, - openBlock: String + openTab: String, + resumeBlockId: String ) { replaceFragmentWithBackStack( fm, - CourseContainerFragment.newInstance(courseId, courseTitle, enrollmentMode, openDates, openBlock) + CourseContainerFragment.newInstance(courseId, courseTitle, enrollmentMode, openTab, resumeBlockId) ) } override fun navigateToEnrolledProgramInfo(fm: FragmentManager, pathId: String) { - replaceFragmentWithBackStack(fm, ProgramFragment(true)) + replaceFragmentWithBackStack(fm, ProgramFragment.newInstance(pathId)) } override fun navigateToNoAccess( diff --git a/app/src/main/java/org/openedx/app/MainFragment.kt b/app/src/main/java/org/openedx/app/MainFragment.kt index c1932ece0..3db56e2b3 100644 --- a/app/src/main/java/org/openedx/app/MainFragment.kt +++ b/app/src/main/java/org/openedx/app/MainFragment.kt @@ -13,11 +13,10 @@ import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.app.databinding.FragmentMainBinding import org.openedx.core.adapter.NavigationFragmentAdapter -import org.openedx.core.config.Config import org.openedx.core.config.DashboardConfig import org.openedx.core.presentation.global.app_upgrade.UpgradeRequiredFragment import org.openedx.core.presentation.global.viewBinding -import org.openedx.dashboard.presentation.ListDashboardFragment +import org.openedx.dashboard.presentation.DashboardListFragment import org.openedx.discovery.presentation.DiscoveryNavigator import org.openedx.discovery.presentation.DiscoveryRouter import org.openedx.learn.presentation.LearnFragment @@ -28,7 +27,6 @@ class MainFragment : Fragment(R.layout.fragment_main) { private val binding by viewBinding(FragmentMainBinding::bind) private val viewModel by viewModel() private val router by inject() - private val config by inject() private lateinit var adapter: NavigationFragmentAdapter @@ -53,7 +51,7 @@ class MainFragment : Fragment(R.layout.fragment_main) { binding.viewPager.setCurrentItem(0, false) } - R.id.fragmentHome -> { + R.id.fragmentDiscover -> { viewModel.logDiscoveryTabClickedEvent() binding.viewPager.setCurrentItem(1, false) } @@ -75,7 +73,7 @@ class MainFragment : Fragment(R.layout.fragment_main) { viewLifecycleOwner.lifecycleScope.launch { viewModel.navigateToDiscovery.collect { shouldNavigateToDiscovery -> if (shouldNavigateToDiscovery) { - binding.bottomNavView.selectedItemId = R.id.fragmentHome + binding.bottomNavView.selectedItemId = R.id.fragmentDiscover } } } @@ -84,7 +82,7 @@ class MainFragment : Fragment(R.layout.fragment_main) { getString(ARG_COURSE_ID).takeIf { it.isNullOrBlank().not() }?.let { courseId -> val infoType = getString(ARG_INFO_TYPE) - if (config.getDiscoveryConfig().isViewTypeWebView() && infoType != null) { + if (viewModel.isDiscoveryTypeWebView && infoType != null) { router.navigateToCourseInfo(parentFragmentManager, courseId, infoType) } else { router.navigateToCourseDetail(parentFragmentManager, courseId) @@ -102,9 +100,9 @@ class MainFragment : Fragment(R.layout.fragment_main) { binding.viewPager.offscreenPageLimit = 4 val discoveryFragment = DiscoveryNavigator(viewModel.isDiscoveryTypeWebView).getDiscoveryFragment() - val dashboardFragment = when (config.getDashboardConfig().getType()) { - DashboardConfig.DashboardType.LIST -> ListDashboardFragment() - DashboardConfig.DashboardType.PRIMARY_COURSE -> LearnFragment() + val dashboardFragment = when (viewModel.dashboardType) { + DashboardConfig.DashboardType.LIST -> DashboardListFragment() + DashboardConfig.DashboardType.GALLERY -> LearnFragment() } adapter = NavigationFragmentAdapter(this).apply { diff --git a/app/src/main/java/org/openedx/app/MainViewModel.kt b/app/src/main/java/org/openedx/app/MainViewModel.kt index da681e8e1..eed901039 100644 --- a/app/src/main/java/org/openedx/app/MainViewModel.kt +++ b/app/src/main/java/org/openedx/app/MainViewModel.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -31,8 +30,8 @@ class MainViewModel( get() = _navigateToDiscovery.asSharedFlow() val isDiscoveryTypeWebView get() = config.getDiscoveryConfig().isViewTypeWebView() + val dashboardType get() = config.getDashboardConfig().getType() - @OptIn(FlowPreview::class) override fun onCreate(owner: LifecycleOwner) { super.onCreate(owner) notifier.notifier @@ -57,10 +56,6 @@ class MainViewModel( logEvent(AppAnalyticsEvent.MY_COURSES) } - fun logMyProgramsTabClickedEvent() { - logEvent(AppAnalyticsEvent.MY_PROGRAMS) - } - fun logProfileTabClickedEvent() { logEvent(AppAnalyticsEvent.PROFILE) } diff --git a/app/src/main/java/org/openedx/app/di/ScreenModule.kt b/app/src/main/java/org/openedx/app/di/ScreenModule.kt index 204f49bad..41eeb97a0 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -30,10 +30,10 @@ import org.openedx.course.presentation.unit.video.VideoViewModel import org.openedx.course.presentation.videos.CourseVideoViewModel import org.openedx.course.settings.download.DownloadQueueViewModel import org.openedx.courses.presentation.AllEnrolledCoursesViewModel -import org.openedx.courses.presentation.PrimaryCourseViewModel +import org.openedx.courses.presentation.DashboardGalleryViewModel import org.openedx.dashboard.data.repository.DashboardRepository import org.openedx.dashboard.domain.interactor.DashboardInteractor -import org.openedx.dashboard.presentation.ListDashboardViewModel +import org.openedx.dashboard.presentation.DashboardListViewModel import org.openedx.discovery.data.repository.DiscoveryRepository import org.openedx.discovery.domain.interactor.DiscoveryInteractor import org.openedx.discovery.presentation.NativeDiscoveryViewModel @@ -119,8 +119,8 @@ val screenModule = module { factory { DashboardRepository(get(), get(), get(), get()) } factory { DashboardInteractor(get()) } - viewModel { ListDashboardViewModel(get(), get(), get(), get(), get(), get(), get()) } - viewModel { PrimaryCourseViewModel(get(), get(), get(), get(), get(), get(), get()) } + viewModel { DashboardListViewModel(get(), get(), get(), get(), get(), get(), get()) } + viewModel { DashboardGalleryViewModel(get(), get(), get(), get(), get(), get(), get()) } viewModel { AllEnrolledCoursesViewModel(get(), get(), get(), get(), get(), get(), get()) } viewModel { LearnViewModel(get(), get()) } @@ -198,11 +198,11 @@ val screenModule = module { get() ) } - viewModel { (courseId: String, courseTitle: String, enrollmentMode: String, openBlock: String) -> + viewModel { (courseId: String, courseTitle: String, enrollmentMode: String, resumeBlockId: String) -> CourseContainerViewModel( courseId, courseTitle, - openBlock, + resumeBlockId, enrollmentMode, get(), get(), diff --git a/app/src/main/res/color/bottom_nav_color.xml b/app/src/main/res/color/bottom_nav_color.xml index 07694e22e..4e2851e90 100644 --- a/app/src/main/res/color/bottom_nav_color.xml +++ b/app/src/main/res/color/bottom_nav_color.xml @@ -1,5 +1,5 @@ - - - \ No newline at end of file + + + diff --git a/app/src/main/res/drawable/app_ic_rows.xml b/app/src/main/res/drawable/app_ic_rows.xml index e068a37a6..eabe550d3 100644 --- a/app/src/main/res/drawable/app_ic_rows.xml +++ b/app/src/main/res/drawable/app_ic_rows.xml @@ -3,8 +3,8 @@ android:height="17dp" android:viewportWidth="20" android:viewportHeight="17"> - + diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index 25a289a83..a9c1fd34c 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -27,4 +27,4 @@ app:layout_constraintStart_toStartOf="parent" app:menu="@menu/bottom_view_menu" /> - \ No newline at end of file + diff --git a/app/src/main/res/menu/bottom_view_menu.xml b/app/src/main/res/menu/bottom_view_menu.xml index 6285572db..f97e849f7 100644 --- a/app/src/main/res/menu/bottom_view_menu.xml +++ b/app/src/main/res/menu/bottom_view_menu.xml @@ -3,20 +3,20 @@ + android:icon="@drawable/app_ic_rows" + android:title="@string/app_navigation_learn" /> + android:icon="@drawable/app_ic_home" + android:title="@string/app_navigation_discovery" /> + android:icon="@drawable/app_ic_profile" + android:title="@string/app_navigation_profile" /> - \ No newline at end of file + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index c5f93a68a..17d58ded3 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -8,4 +8,4 @@ Мої курси Програми Профіль - \ No newline at end of file + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index df82406af..baa1c2a89 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,4 +7,4 @@ Learn Programs Profile - \ No newline at end of file + diff --git a/core/src/main/java/org/openedx/core/config/DashboardConfig.kt b/core/src/main/java/org/openedx/core/config/DashboardConfig.kt index 1a1b33105..9aa081aff 100644 --- a/core/src/main/java/org/openedx/core/config/DashboardConfig.kt +++ b/core/src/main/java/org/openedx/core/config/DashboardConfig.kt @@ -4,13 +4,13 @@ import com.google.gson.annotations.SerializedName data class DashboardConfig( @SerializedName("TYPE") - private val viewType: String = DashboardType.PRIMARY_COURSE.name, + private val viewType: String = DashboardType.GALLERY.name, ) { fun getType(): DashboardType { return DashboardType.valueOf(viewType.uppercase()) } enum class DashboardType { - LIST, PRIMARY_COURSE + LIST, GALLERY } } diff --git a/core/src/main/java/org/openedx/core/data/model/CourseAssignments.kt b/core/src/main/java/org/openedx/core/data/model/CourseAssignments.kt index ca38de44b..165868d6d 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseAssignments.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseAssignments.kt @@ -11,19 +11,19 @@ data class CourseAssignments( val pastAssignments: List? ) { fun mapToDomain() = CourseAssignments( - futureAssignments = futureAssignments?.map { + futureAssignments = futureAssignments?.mapNotNull { it.mapToDomain() }, - pastAssignments = pastAssignments?.map { + pastAssignments = pastAssignments?.mapNotNull { it.mapToDomain() } ) fun mapToRoomEntity() = CourseAssignmentsDb( - futureAssignments = futureAssignments?.map { + futureAssignments = futureAssignments?.mapNotNull { it.mapToRoomEntity() }, - pastAssignments = pastAssignments?.map { + pastAssignments = pastAssignments?.mapNotNull { it.mapToRoomEntity() } ) diff --git a/core/src/main/java/org/openedx/core/data/model/CourseDateBlock.kt b/core/src/main/java/org/openedx/core/data/model/CourseDateBlock.kt index 520711e94..d29e7a7ea 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseDateBlock.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseDateBlock.kt @@ -6,7 +6,6 @@ import kotlinx.parcelize.Parcelize import org.openedx.core.data.model.room.discovery.CourseDateBlockDb import org.openedx.core.domain.model.CourseDateBlock import org.openedx.core.utils.TimeUtils -import java.util.Date @Parcelize data class CourseDateBlock( @@ -31,27 +30,36 @@ data class CourseDateBlock( // component blockId in-case of navigating inside the app for component available in mobile @SerializedName("first_component_block_id") val blockId: String = "", -): Parcelable { - fun mapToDomain() = CourseDateBlock( - complete = complete, - date = TimeUtils.iso8601ToDate(date) ?: Date(), - assignmentType = assignmentType, - dateType = dateType, - description = description, - learnerHasAccess = learnerHasAccess, - link = link, - title = title, - blockId = blockId - ) - fun mapToRoomEntity() = CourseDateBlockDb( - complete = complete, - date = date, - assignmentType = assignmentType, - dateType = dateType, - description = description, - learnerHasAccess = learnerHasAccess, - link = link, - title = title, - blockId = blockId - ) +) : Parcelable { + fun mapToDomain(): CourseDateBlock? { + TimeUtils.iso8601ToDate(date)?.let { + return CourseDateBlock( + complete = complete, + date = it, + assignmentType = assignmentType, + dateType = dateType, + description = description, + learnerHasAccess = learnerHasAccess, + link = link, + title = title, + blockId = blockId + ) + } ?: return null + } + + fun mapToRoomEntity(): CourseDateBlockDb? { + TimeUtils.iso8601ToDate(date)?.let { + return CourseDateBlockDb( + complete = complete, + date = it, + assignmentType = assignmentType, + dateType = dateType, + description = description, + learnerHasAccess = learnerHasAccess, + link = link, + title = title, + blockId = blockId + ) + } ?: return null + } } diff --git a/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt b/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt index 4c1553dcd..a6cd1d93e 100644 --- a/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt +++ b/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt @@ -225,8 +225,8 @@ data class CourseDateBlockDb( val learnerHasAccess: Boolean = false, @ColumnInfo("complete") val complete: Boolean = false, - @ColumnInfo("date") - val date: String, + @Embedded + val date: Date, @ColumnInfo("dateType") val dateType: DateType = DateType.NONE, @ColumnInfo("assignmentType") @@ -239,7 +239,7 @@ data class CourseDateBlockDb( blockId = blockId, learnerHasAccess = learnerHasAccess, complete = complete, - date = TimeUtils.iso8601ToDate(date) ?: Date(), + date = date, dateType = dateType, assignmentType = assignmentType ) diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseStatus.kt b/core/src/main/java/org/openedx/core/domain/model/CourseStatus.kt index e25490430..134695c72 100644 --- a/core/src/main/java/org/openedx/core/domain/model/CourseStatus.kt +++ b/core/src/main/java/org/openedx/core/domain/model/CourseStatus.kt @@ -9,4 +9,4 @@ data class CourseStatus( val lastVisitedModulePath: List, val lastVisitedBlockId: String, val lastVisitedUnitDisplayName: String -): Parcelable +) : Parcelable diff --git a/core/src/main/java/org/openedx/core/domain/model/Progress.kt b/core/src/main/java/org/openedx/core/domain/model/Progress.kt index 9f021aa52..5d8ea19f8 100644 --- a/core/src/main/java/org/openedx/core/domain/model/Progress.kt +++ b/core/src/main/java/org/openedx/core/domain/model/Progress.kt @@ -9,6 +9,6 @@ data class Progress( val totalAssignmentsCount: Int, ) : Parcelable { companion object { - val DEFAULT_PROGRESS = Progress(0,0) + val DEFAULT_PROGRESS = Progress(0, 0) } } diff --git a/core/src/main/java/org/openedx/core/module/DownloadWorker.kt b/core/src/main/java/org/openedx/core/module/DownloadWorker.kt index 9234ec023..29f3f48ba 100644 --- a/core/src/main/java/org/openedx/core/module/DownloadWorker.kt +++ b/core/src/main/java/org/openedx/core/module/DownloadWorker.kt @@ -23,6 +23,7 @@ import org.openedx.core.module.download.CurrentProgress import org.openedx.core.module.download.FileDownloader import org.openedx.core.system.notifier.DownloadNotifier import org.openedx.core.system.notifier.DownloadProgressChanged +import org.openedx.core.utils.FileUtil import java.io.File class DownloadWorker( @@ -41,11 +42,7 @@ class DownloadWorker( private var downloadEnqueue = listOf() - private val folder = File( - context.externalCacheDir.toString() + File.separator + - context.getString(R.string.app_name) - .replace(Regex("\\s"), "_") - ) + private val folder = FileUtil(context).getExternalAppDir() private var currentDownload: DownloadModel? = null private var lastUpdateTime = 0L diff --git a/core/src/main/java/org/openedx/core/module/TranscriptManager.kt b/core/src/main/java/org/openedx/core/module/TranscriptManager.kt index ba67d1a54..0b8b3e1e1 100644 --- a/core/src/main/java/org/openedx/core/module/TranscriptManager.kt +++ b/core/src/main/java/org/openedx/core/module/TranscriptManager.kt @@ -125,5 +125,4 @@ class TranscriptManager( } return null } - -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/presentation/global/InDevelopmentScreen.kt b/core/src/main/java/org/openedx/core/presentation/global/InDevelopmentScreen.kt deleted file mode 100644 index 02f633704..000000000 --- a/core/src/main/java/org/openedx/core/presentation/global/InDevelopmentScreen.kt +++ /dev/null @@ -1,31 +0,0 @@ -package org.openedx.core.presentation.global - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import org.openedx.core.ui.theme.appColors -import org.openedx.core.ui.theme.appTypography - -@Composable -fun InDevelopmentScreen( - modifier: Modifier = Modifier -) { - Box( - modifier = modifier - .fillMaxSize() - .background(MaterialTheme.appColors.secondary), - contentAlignment = Alignment.Center - ) { - Text( - modifier = Modifier.testTag("txt_in_development"), - text = "Will be available soon", - style = MaterialTheme.appTypography.headlineMedium - ) - } -} diff --git a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt index 40d42971f..062179500 100644 --- a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt +++ b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt @@ -1244,6 +1244,7 @@ fun RoundTabsBar( .clickable { scope.launch { pagerState.scrollToPage(index) + rowState.animateScrollToItem(index) onTabClicked(index) } } diff --git a/core/src/main/java/org/openedx/core/utils/FileUtil.kt b/core/src/main/java/org/openedx/core/utils/FileUtil.kt index b7d22f929..2f5c2b2e5 100644 --- a/core/src/main/java/org/openedx/core/utils/FileUtil.kt +++ b/core/src/main/java/org/openedx/core/utils/FileUtil.kt @@ -15,7 +15,7 @@ class FileUtil(val context: Context) { return file } - inline fun < reified T> saveObjectToFile(obj: T, fileName: String = "${T::class.java.simpleName}.json") { + inline fun saveObjectToFile(obj: T, fileName: String = "${T::class.java.simpleName}.json") { val gson: Gson = GsonBuilder().setPrettyPrinting().create() val jsonString = gson.toJson(obj) File(getExternalAppDir().path + fileName).writeText(jsonString) diff --git a/core/src/main/res/drawable/core_ic_settings.xml b/core/src/main/res/drawable/core_ic_settings.xml deleted file mode 100644 index a86316516..000000000 --- a/core/src/main/res/drawable/core_ic_settings.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - diff --git a/core/src/main/res/drawable/ic_core_chapter_icon.xml b/core/src/main/res/drawable/ic_core_chapter_icon.xml index eaf899ce2..9ee00fed7 100644 --- a/core/src/main/res/drawable/ic_core_chapter_icon.xml +++ b/core/src/main/res/drawable/ic_core_chapter_icon.xml @@ -3,29 +3,28 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24"> - - - - - - + + + + + + diff --git a/core/src/main/res/values-night/colors.xml b/core/src/main/res/values-night/colors.xml index 4db689c7b..d6f9f1a14 100644 --- a/core/src/main/res/values-night/colors.xml +++ b/core/src/main/res/values-night/colors.xml @@ -5,4 +5,4 @@ #19212F #879FF5 #8E9BAE - \ No newline at end of file + diff --git a/core/src/main/res/values/colors.xml b/core/src/main/res/values/colors.xml index d41c58bac..57a25d9ed 100644 --- a/core/src/main/res/values/colors.xml +++ b/core/src/main/res/values/colors.xml @@ -5,4 +5,4 @@ #517BFE #3C68FF #97A5BB - \ No newline at end of file + diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index fc60e06d0..9538b8ed3 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -131,10 +131,4 @@ Video streaming quality Video download quality Manage Account - - Home - Videos - Discussions - More - Dates diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt index 3dc7a06b2..d39d74a6c 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt @@ -84,7 +84,7 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { requireArguments().getString(ARG_COURSE_ID, ""), requireArguments().getString(ARG_TITLE, ""), requireArguments().getString(ARG_ENROLLMENT_MODE, ""), - requireArguments().getString(ARG_OPEN_BLOCK, "") + requireArguments().getString(ARG_RESUME_BLOCK, "") ) } @@ -256,22 +256,23 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { const val ARG_COURSE_ID = "courseId" const val ARG_TITLE = "title" const val ARG_ENROLLMENT_MODE = "enrollmentMode" - const val ARG_OPEN_DATES = "open_dates" - const val ARG_OPEN_BLOCK = "resume_block" + const val ARG_OPEN_TAB = "open_tab" + const val ARG_RESUME_BLOCK = "resume_block" + const val DEFAULT_TAB = "home" fun newInstance( courseId: String, courseTitle: String, enrollmentMode: String, - openDates: Boolean = false, - openBlock: String = "" + openTab: String = DEFAULT_TAB, + resumeBlockId: String = "" ): CourseContainerFragment { val fragment = CourseContainerFragment() fragment.arguments = bundleOf( ARG_COURSE_ID to courseId, ARG_TITLE to courseTitle, ARG_ENROLLMENT_MODE to enrollmentMode, - ARG_OPEN_DATES to openDates, - ARG_OPEN_BLOCK to openBlock + ARG_OPEN_TAB to openTab, + ARG_RESUME_BLOCK to resumeBlockId ) return fragment } @@ -302,11 +303,16 @@ fun CourseDashboard( val refreshing by viewModel.refreshing.collectAsState(true) val courseImage by viewModel.courseImage.collectAsState() val uiMessage by viewModel.uiMessage.collectAsState(null) - val requiredTab = if (bundle.getBoolean(CourseContainerFragment.ARG_OPEN_DATES)) { - CourseContainerTab.DATES - } else { - CourseContainerTab.HOME + val openTab = bundle.getString(CourseContainerFragment.ARG_OPEN_TAB, CourseContainerFragment.DEFAULT_TAB) + val requiredTab = when (openTab.uppercase()) { + CourseContainerTab.HOME.name -> CourseContainerTab.HOME + CourseContainerTab.VIDEOS.name -> CourseContainerTab.VIDEOS + CourseContainerTab.DATES.name -> CourseContainerTab.DATES + CourseContainerTab.DISCUSSIONS.name -> CourseContainerTab.DISCUSSIONS + CourseContainerTab.MORE.name -> CourseContainerTab.MORE + else -> CourseContainerTab.HOME } + val pagerState = rememberPagerState( initialPage = CourseContainerTab.entries.indexOf(requiredTab), pageCount = { CourseContainerTab.entries.size } diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerTab.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerTab.kt index afeafcddc..abd5babdc 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerTab.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerTab.kt @@ -8,17 +8,17 @@ import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.outlined.CalendarMonth import androidx.compose.material.icons.rounded.PlayCircleFilled import androidx.compose.ui.graphics.vector.ImageVector -import org.openedx.core.R import org.openedx.core.ui.TabItem +import org.openedx.course.R enum class CourseContainerTab( @StringRes override val labelResId: Int, override val icon: ImageVector ) : TabItem { - HOME(R.string.core_course_container_nav_home, Icons.Default.Home), - VIDEOS(R.string.core_course_container_nav_videos, Icons.Rounded.PlayCircleFilled), - DATES(R.string.core_course_container_nav_dates, Icons.Outlined.CalendarMonth), - DISCUSSIONS(R.string.core_course_container_nav_discussions, Icons.AutoMirrored.Filled.Chat), - MORE(R.string.core_course_container_nav_more, Icons.AutoMirrored.Filled.TextSnippet) + HOME(R.string.course_container_nav_home, Icons.Default.Home), + VIDEOS(R.string.course_container_nav_videos, Icons.Rounded.PlayCircleFilled), + DATES(R.string.course_container_nav_dates, Icons.Outlined.CalendarMonth), + DISCUSSIONS(R.string.course_container_nav_discussions, Icons.AutoMirrored.Filled.Chat), + MORE(R.string.course_container_nav_more, Icons.AutoMirrored.Filled.TextSnippet) } diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt index 310860933..af209660e 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt @@ -58,7 +58,7 @@ import org.openedx.core.R as CoreR class CourseContainerViewModel( val courseId: String, var courseName: String, - private var openBlock: String, + private var resumeBlockId: String, private val enrollmentMode: String, private val config: Config, private val interactor: CourseInteractor, @@ -182,9 +182,9 @@ class CourseContainerViewModel( } isReady } - if (_dataReady.value == true && openBlock.isNotEmpty()) { + if (_dataReady.value == true && resumeBlockId.isNotEmpty()) { delay(500L) - courseNotifier.send(CourseOpenBlock(openBlock)) + courseNotifier.send(CourseOpenBlock(resumeBlockId)) } } catch (e: Exception) { if (e.isInternetError() || e is NoCachedDataException) { diff --git a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt index 98478dd92..ae9754535 100644 --- a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt @@ -87,7 +87,9 @@ class CourseDatesViewModel( _calendarSyncUIState.update { it.copy(isSynced = event.isSynced) } } - is RefreshDates -> loadingCourseDatesInternal() + is RefreshDates -> { + loadingCourseDatesInternal() + } } } } diff --git a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineScreen.kt b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineScreen.kt index b767f1918..4981d12df 100644 --- a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineScreen.kt +++ b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineScreen.kt @@ -62,13 +62,13 @@ import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appTypography import org.openedx.core.ui.windowSizeValue +import org.openedx.core.utils.FileUtil import org.openedx.course.R import org.openedx.course.presentation.ui.CourseDatesBanner import org.openedx.course.presentation.ui.CourseDatesBannerTablet import org.openedx.course.presentation.ui.CourseExpandableChapterCard import org.openedx.course.presentation.ui.CourseSectionCard import org.openedx.course.presentation.ui.CourseSubSectionItem -import java.io.File import java.util.Date import org.openedx.core.R as CoreR @@ -81,12 +81,12 @@ fun CourseOutlineScreen( ) { val uiState by viewModel.uiState.collectAsState() val uiMessage by viewModel.uiMessage.collectAsState(null) - val openBlock by viewModel.openBlock.collectAsState("") + val resumeBlockId by viewModel.resumeBlockId.collectAsState("") val context = LocalContext.current - LaunchedEffect(openBlock) { - if (openBlock.isNotEmpty()) { - viewModel.openBlock(fragmentManager, openBlock) + LaunchedEffect(resumeBlockId) { + if (resumeBlockId.isNotEmpty()) { + viewModel.openBlock(fragmentManager, resumeBlockId) } } @@ -146,11 +146,7 @@ fun CourseOutlineScreen( viewModel.removeDownloadModels(it.id) } else { viewModel.saveDownloadModels( - context.externalCacheDir.toString() + - File.separator + - context - .getString(CoreR.string.app_name) - .replace(Regex("\\s"), "_"), it.id + FileUtil(context).getExternalAppDir().path, it.id ) } }, diff --git a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt index 9616ebcef..1a5c14b53 100644 --- a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt @@ -74,9 +74,9 @@ class CourseOutlineViewModel( val uiMessage: SharedFlow get() = _uiMessage.asSharedFlow() - private val _openBlock = MutableSharedFlow() - val openBlock: SharedFlow - get() = _openBlock.asSharedFlow() + private val _resumeBlockId = MutableSharedFlow() + val resumeBlockId: SharedFlow + get() = _resumeBlockId.asSharedFlow() private var resumeSectionBlock: Block? = null private var resumeVerticalBlock: Block? = null @@ -98,7 +98,7 @@ class CourseOutlineViewModel( } is CourseOpenBlock -> { - _openBlock.emit(event.blockId) + _resumeBlockId.emit(event.blockId) } } } diff --git a/course/src/main/java/org/openedx/course/presentation/ui/CourseVideosUI.kt b/course/src/main/java/org/openedx/course/presentation/ui/CourseVideosUI.kt index 0c085fa53..b4446e0ad 100644 --- a/course/src/main/java/org/openedx/course/presentation/ui/CourseVideosUI.kt +++ b/course/src/main/java/org/openedx/course/presentation/ui/CourseVideosUI.kt @@ -76,10 +76,10 @@ import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appTypography import org.openedx.core.ui.windowSizeValue +import org.openedx.core.utils.FileUtil import org.openedx.course.R import org.openedx.course.presentation.videos.CourseVideoViewModel import org.openedx.course.presentation.videos.CourseVideosUIState -import java.io.File import java.util.Date @Composable @@ -136,11 +136,7 @@ fun CourseVideosScreen( viewModel.removeDownloadModels(it.id) } else { viewModel.saveDownloadModels( - context.externalCacheDir.toString() + - File.separator + - context - .getString(org.openedx.core.R.string.app_name) - .replace(Regex("\\s"), "_"), it.id + FileUtil(context).getExternalAppDir().path, it.id ) } }, @@ -150,11 +146,7 @@ fun CourseVideosScreen( viewModel.removeAllDownloadModels() } else { viewModel.saveAllDownloadModels( - context.externalCacheDir.toString() + - File.separator + - context - .getString(org.openedx.core.R.string.app_name) - .replace(Regex("\\s"), "_") + FileUtil(context).getExternalAppDir().path ) } }, diff --git a/course/src/main/res/values/strings.xml b/course/src/main/res/values/strings.xml index c6b370267..8c260bb3b 100644 --- a/course/src/main/res/values/strings.xml +++ b/course/src/main/res/values/strings.xml @@ -78,6 +78,12 @@ Assignment Due + Home + Videos + Discussions + More + Dates + diff --git a/dashboard/src/androidTest/java/org/openedx/dashboard/presentation/MyCoursesScreenTest.kt b/dashboard/src/androidTest/java/org/openedx/dashboard/presentation/MyCoursesScreenTest.kt index cc14b481a..f3b6a5aee 100644 --- a/dashboard/src/androidTest/java/org/openedx/dashboard/presentation/MyCoursesScreenTest.kt +++ b/dashboard/src/androidTest/java/org/openedx/dashboard/presentation/MyCoursesScreenTest.kt @@ -68,7 +68,7 @@ class MyCoursesScreenTest { @Test fun dashboardScreenLoading() { composeTestRule.setContent { - ListDashboardScreen( + DashboardListView( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), apiHostUrl = "http://localhost:8000", state = DashboardUIState.Courses(listOf(mockCourseEnrolled, mockCourseEnrolled)), @@ -101,7 +101,7 @@ class MyCoursesScreenTest { @Test fun dashboardScreenLoaded() { composeTestRule.setContent { - ListDashboardScreen( + DashboardListView( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), apiHostUrl = "http://localhost:8000", state = DashboardUIState.Courses(listOf(mockCourseEnrolled, mockCourseEnrolled)), @@ -127,7 +127,7 @@ class MyCoursesScreenTest { @Test fun dashboardScreenRefreshing() { composeTestRule.setContent { - ListDashboardScreen( + DashboardListView( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), apiHostUrl = "http://localhost:8000", state = DashboardUIState.Courses(listOf(mockCourseEnrolled, mockCourseEnrolled)), 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 d2af43f3c..64ce4baa4 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt @@ -6,12 +6,10 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup import androidx.compose.foundation.ExperimentalFoundationApi -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.Spacer import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxHeight @@ -19,9 +17,7 @@ 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.offset import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid @@ -29,18 +25,11 @@ import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material.Card import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.LinearProgressIndicator import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Search import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState @@ -56,28 +45,18 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId -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.compose.ui.unit.dp import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager -import coil.compose.AsyncImage -import coil.request.ImageRequest import org.koin.androidx.compose.koinViewModel -import org.openedx.Lock import org.openedx.core.UIMessage import org.openedx.core.domain.model.Certificate import org.openedx.core.domain.model.CourseAssignments @@ -98,13 +77,9 @@ import org.openedx.core.ui.statusBarsInset import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes -import org.openedx.core.ui.theme.appTypography import org.openedx.core.ui.windowSizeValue -import org.openedx.core.utils.TimeUtils -import org.openedx.dashboard.R import org.openedx.dashboard.domain.CourseStatusFilter import java.util.Date -import org.openedx.core.R as CoreR class AllEnrolledCoursesFragment : Fragment() { @@ -116,7 +91,7 @@ class AllEnrolledCoursesFragment : Fragment() { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { OpenEdXTheme { - AllEnrolledCoursesScreen( + AllEnrolledCoursesView( fragmentManager = requireActivity().supportFragmentManager ) } @@ -125,21 +100,17 @@ class AllEnrolledCoursesFragment : Fragment() { } @Composable -private fun AllEnrolledCoursesScreen( +private fun AllEnrolledCoursesView( viewModel: AllEnrolledCoursesViewModel = koinViewModel(), fragmentManager: FragmentManager ) { val uiState by viewModel.uiState.collectAsState() val uiMessage by viewModel.uiMessage.collectAsState(null) - val refreshing by viewModel.updating.collectAsState(false) - val canLoadMore by viewModel.canLoadMore.collectAsState(false) - AllEnrolledCoursesScreen( + AllEnrolledCoursesView( apiHostUrl = viewModel.apiHostUrl, state = uiState, uiMessage = uiMessage, - canLoadMore = canLoadMore, - refreshing = refreshing, hasInternetConnection = viewModel.hasInternetConnection, onAction = { action -> when (action) { @@ -160,15 +131,12 @@ private fun AllEnrolledCoursesScreen( } AllEnrolledCoursesAction.Search -> { - viewModel.dashboardRouter.navigateToCourseSearch( - fragmentManager, "" - ) + viewModel.navigateToCourseSearch(fragmentManager) } is AllEnrolledCoursesAction.OpenCourse -> { with(action.enrolledCourse) { - viewModel.dashboardCourseClickedEvent(course.id, course.name) - viewModel.dashboardRouter.navigateToCourseOutline( + viewModel.navigateToCourseOutline( fragmentManager, course.id, course.name, @@ -187,12 +155,10 @@ private fun AllEnrolledCoursesScreen( @OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) @Composable -private fun AllEnrolledCoursesScreen( +private fun AllEnrolledCoursesView( apiHostUrl: String, state: AllEnrolledCoursesUIState, uiMessage: UIMessage?, - canLoadMore: Boolean, - refreshing: Boolean, hasInternetConnection: Boolean, onAction: (AllEnrolledCoursesAction) -> Unit ) { @@ -202,7 +168,7 @@ private fun AllEnrolledCoursesScreen( val scrollState = rememberLazyGridState() val columns = if (windowSize.isTablet) 3 else 2 val pullRefreshState = rememberPullRefreshState( - refreshing = refreshing, + refreshing = state.refreshing, onRefresh = { onAction(AllEnrolledCoursesAction.SwipeRefresh) } ) val tabPagerState = rememberPagerState(pageCount = { @@ -324,8 +290,8 @@ private fun AllEnrolledCoursesScreen( onAction(AllEnrolledCoursesAction.FilterChange(newFilter)) } ) - when (state) { - is AllEnrolledCoursesUIState.Loading -> { + when { + state.showProgress -> { Box( modifier = Modifier .fillMaxSize(), @@ -335,7 +301,7 @@ private fun AllEnrolledCoursesScreen( } } - is AllEnrolledCoursesUIState.Courses -> { + !state.courses.isNullOrEmpty() -> { Box( modifier = Modifier .fillMaxSize() @@ -364,7 +330,7 @@ private fun AllEnrolledCoursesScreen( ) } item { - if (canLoadMore) { + if (state.canLoadMore) { Box( modifier = Modifier .fillMaxWidth() @@ -386,7 +352,7 @@ private fun AllEnrolledCoursesScreen( } } - is AllEnrolledCoursesUIState.Empty -> { + state.courses?.isEmpty() == true -> { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center @@ -405,7 +371,7 @@ private fun AllEnrolledCoursesScreen( } } PullRefreshIndicator( - refreshing, + state.refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter) ) @@ -431,158 +397,6 @@ private fun AllEnrolledCoursesScreen( } } -@Composable -private fun CourseItem( - modifier: Modifier = Modifier, - course: EnrolledCourse, - apiHostUrl: String, - onClick: (EnrolledCourse) -> Unit, -) { - Card( - modifier = modifier - .width(170.dp) - .height(180.dp) - .clickable { - onClick(course) - }, - backgroundColor = MaterialTheme.appColors.background, - shape = MaterialTheme.appShapes.courseImageShape, - elevation = 4.dp - ) { - Box { - Column { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(apiHostUrl + course.course.courseImage) - .error(CoreR.drawable.core_no_image_course) - .placeholder(CoreR.drawable.core_no_image_course) - .build(), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxWidth() - .height(90.dp) - ) - val progress: Float = try { - course.progress.assignmentsCompleted.toFloat() / course.progress.totalAssignmentsCount.toFloat() - } catch (_: ArithmeticException) { - 0f - } - LinearProgressIndicator( - modifier = Modifier - .fillMaxWidth() - .height(8.dp), - progress = progress, - color = MaterialTheme.appColors.primary, - backgroundColor = MaterialTheme.appColors.divider - ) - - Text( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp) - .padding(top = 4.dp), - style = MaterialTheme.appTypography.labelMedium, - color = MaterialTheme.appColors.textFieldHint, - overflow = TextOverflow.Ellipsis, - minLines = 1, - maxLines = 2, - text = stringResource( - R.string.dashboard_course_date, - TimeUtils.getCourseFormattedDate( - LocalContext.current, - Date(), - course.auditAccessExpires, - course.course.start, - course.course.end, - course.course.startType, - course.course.startDisplay - ) - ) - ) - Text( - modifier = Modifier - .padding(horizontal = 8.dp, vertical = 4.dp), - text = course.course.name, - style = MaterialTheme.appTypography.titleSmall, - color = MaterialTheme.appColors.textDark, - overflow = TextOverflow.Ellipsis, - minLines = 1, - maxLines = 2 - ) - } - if (!course.course.coursewareAccess?.errorCode.isNullOrEmpty()) { - Lock() - } - } - } -} - -@Composable -private fun Header( - modifier: Modifier = Modifier, - onSearchClick: () -> Unit -) { - Box( - modifier = modifier.fillMaxWidth() - ) { - Text( - modifier = Modifier.align(Alignment.CenterStart), - text = stringResource(id = R.string.dashboard_all_courses), - color = MaterialTheme.appColors.textDark, - style = MaterialTheme.appTypography.headlineBold - ) - IconButton( - modifier = Modifier - .align(Alignment.CenterEnd) - .offset(x = 12.dp), - onClick = { - onSearchClick() - } - ) { - Icon( - imageVector = Icons.Filled.Search, - contentDescription = null, - tint = MaterialTheme.appColors.textDark - ) - } - } -} - -@Composable -private fun EmptyState( - currentCourseStatus: CourseStatusFilter -) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column( - Modifier.width(200.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Icon( - painter = painterResource(id = R.drawable.dashboard_ic_book), - tint = MaterialTheme.appColors.textFieldBorder, - contentDescription = null - ) - Spacer(Modifier.height(4.dp)) - Text( - modifier = Modifier - .testTag("txt_empty_state_title") - .fillMaxWidth(), - text = stringResource( - id = R.string.dashboard_no_status_courses, - stringResource(currentCourseStatus.labelResId) - ), - color = MaterialTheme.appColors.textDark, - style = MaterialTheme.appTypography.titleMedium, - textAlign = TextAlign.Center - ) - } - } -} - @Preview(uiMode = UI_MODE_NIGHT_NO) @Preview(uiMode = UI_MODE_NIGHT_YES) @Preview(uiMode = UI_MODE_NIGHT_NO, device = Devices.NEXUS_9) @@ -590,10 +404,10 @@ private fun EmptyState( @Composable private fun AllEnrolledCoursesPreview() { OpenEdXTheme { - AllEnrolledCoursesScreen( + AllEnrolledCoursesView( apiHostUrl = "http://localhost:8000", - state = AllEnrolledCoursesUIState.Courses( - listOf( + state = AllEnrolledCoursesUIState( + courses = listOf( mockCourseEnrolled, mockCourseEnrolled, mockCourseEnrolled, @@ -604,8 +418,6 @@ private fun AllEnrolledCoursesPreview() { ), uiMessage = null, hasInternetConnection = true, - refreshing = false, - canLoadMore = false, onAction = {} ) } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesUIState.kt b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesUIState.kt index 9c6166a9b..2d7efb51b 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesUIState.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesUIState.kt @@ -2,8 +2,9 @@ package org.openedx.courses.presentation import org.openedx.core.domain.model.EnrolledCourse -sealed class AllEnrolledCoursesUIState { - data class Courses(val courses: List) : AllEnrolledCoursesUIState() - data object Empty : AllEnrolledCoursesUIState() - data object Loading : AllEnrolledCoursesUIState() -} +data class AllEnrolledCoursesUIState( + val courses: List? = null, + val refreshing: Boolean = false, + val canLoadMore: Boolean = false, + val showProgress: Boolean = false, +) diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesView.kt b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesView.kt new file mode 100644 index 000000000..5c13705be --- /dev/null +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesView.kt @@ -0,0 +1,194 @@ +package org.openedx.courses.presentation + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.Card +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.LinearProgressIndicator +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Search +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +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.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import org.openedx.Lock +import org.openedx.core.R +import org.openedx.core.domain.model.EnrolledCourse +import org.openedx.core.ui.theme.appColors +import org.openedx.core.ui.theme.appShapes +import org.openedx.core.ui.theme.appTypography +import org.openedx.core.utils.TimeUtils +import org.openedx.dashboard.domain.CourseStatusFilter +import java.util.Date + +@Composable +fun CourseItem( + modifier: Modifier = Modifier, + course: EnrolledCourse, + apiHostUrl: String, + onClick: (EnrolledCourse) -> Unit, +) { + Card( + modifier = modifier + .width(170.dp) + .height(180.dp) + .clickable { + onClick(course) + }, + backgroundColor = MaterialTheme.appColors.background, + shape = MaterialTheme.appShapes.courseImageShape, + elevation = 4.dp + ) { + Box { + Column { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(apiHostUrl + course.course.courseImage) + .error(R.drawable.core_no_image_course) + .placeholder(R.drawable.core_no_image_course) + .build(), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .height(90.dp) + ) + val progress: Float = try { + course.progress.assignmentsCompleted.toFloat() / course.progress.totalAssignmentsCount.toFloat() + } catch (_: ArithmeticException) { + 0f + } + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .height(8.dp), + progress = progress, + color = MaterialTheme.appColors.primary, + backgroundColor = MaterialTheme.appColors.divider + ) + + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + .padding(top = 4.dp), + style = MaterialTheme.appTypography.labelMedium, + color = MaterialTheme.appColors.textFieldHint, + overflow = TextOverflow.Ellipsis, + minLines = 1, + maxLines = 2, + text = stringResource( + org.openedx.dashboard.R.string.dashboard_course_date, + TimeUtils.getCourseFormattedDate( + LocalContext.current, + Date(), + course.auditAccessExpires, + course.course.start, + course.course.end, + course.course.startType, + course.course.startDisplay + ) + ) + ) + Text( + modifier = Modifier + .padding(horizontal = 8.dp, vertical = 4.dp), + text = course.course.name, + style = MaterialTheme.appTypography.titleSmall, + color = MaterialTheme.appColors.textDark, + overflow = TextOverflow.Ellipsis, + minLines = 1, + maxLines = 2 + ) + } + if (!course.course.coursewareAccess?.errorCode.isNullOrEmpty()) { + Lock() + } + } + } +} + +@Composable +fun Header( + modifier: Modifier = Modifier, + onSearchClick: () -> Unit +) { + Box( + modifier = modifier.fillMaxWidth() + ) { + Text( + modifier = Modifier.align(Alignment.CenterStart), + text = stringResource(id = org.openedx.dashboard.R.string.dashboard_all_courses), + color = MaterialTheme.appColors.textDark, + style = MaterialTheme.appTypography.headlineBold + ) + IconButton( + modifier = Modifier + .align(Alignment.CenterEnd) + .offset(x = 12.dp), + onClick = { + onSearchClick() + } + ) { + Icon( + imageVector = Icons.Filled.Search, + contentDescription = null, + tint = MaterialTheme.appColors.textDark + ) + } + } +} + +@Composable +fun EmptyState( + currentCourseStatus: CourseStatusFilter +) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + Modifier.width(200.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + painter = painterResource(id = org.openedx.dashboard.R.drawable.dashboard_ic_book), + tint = MaterialTheme.appColors.textFieldBorder, + contentDescription = null + ) + Spacer(Modifier.height(4.dp)) + Text( + modifier = Modifier + .testTag("txt_empty_state_title") + .fillMaxWidth(), + text = stringResource( + id = org.openedx.dashboard.R.string.dashboard_no_status_courses, + stringResource(currentCourseStatus.labelResId) + ), + color = MaterialTheme.appColors.textDark, + style = MaterialTheme.appTypography.titleMedium, + textAlign = TextAlign.Center + ) + } + } +} \ No newline at end of file diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt index 536a5f335..59d910bd7 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt @@ -1,5 +1,6 @@ package org.openedx.courses.presentation +import androidx.fragment.app.FragmentManager import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableSharedFlow @@ -8,6 +9,7 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel import org.openedx.core.R @@ -31,7 +33,7 @@ class AllEnrolledCoursesViewModel( private val resourceManager: ResourceManager, private val discoveryNotifier: DiscoveryNotifier, private val analytics: DashboardAnalytics, - val dashboardRouter: DashboardRouter + private val dashboardRouter: DashboardRouter ) : BaseViewModel() { val apiHostUrl get() = config.getApiHostURL() @@ -42,7 +44,7 @@ class AllEnrolledCoursesViewModel( private var page = 1 private var isLoading = false - private val _uiState = MutableStateFlow(AllEnrolledCoursesUIState.Loading) + private val _uiState = MutableStateFlow(AllEnrolledCoursesUIState()) val uiState: StateFlow get() = _uiState.asStateFlow() @@ -50,15 +52,6 @@ class AllEnrolledCoursesViewModel( val uiMessage: SharedFlow get() = _uiMessage.asSharedFlow() - private val _updating = MutableStateFlow(false) - val updating: StateFlow - get() = _updating.asStateFlow() - - - private val _canLoadMore = MutableStateFlow(false) - val canLoadMore: StateFlow - get() = _canLoadMore.asStateFlow() - private val currentFilter: MutableStateFlow = MutableStateFlow(CourseStatusFilter.ALL) private var job: Job? = null @@ -69,7 +62,7 @@ class AllEnrolledCoursesViewModel( } fun getCourses(courseStatusFilter: CourseStatusFilter? = null) { - _uiState.value = AllEnrolledCoursesUIState.Loading + _uiState.update { it.copy(showProgress = true) } coursesList.clear() internalLoadingCourses(courseStatusFilter ?: currentFilter.value) } @@ -77,24 +70,20 @@ class AllEnrolledCoursesViewModel( fun updateCourses() { viewModelScope.launch { try { - _updating.value = true + _uiState.update { it.copy(refreshing = true) } isLoading = true page = 1 val response = interactor.getAllUserCourses(page, currentFilter.value) if (response.pagination.next.isNotEmpty() && page != response.pagination.numPages) { - _canLoadMore.value = true + _uiState.update { it.copy(canLoadMore = true) } page++ } else { - _canLoadMore.value = false + _uiState.update { it.copy(canLoadMore = false) } page = -1 } coursesList.clear() coursesList.addAll(response.courses) - if (coursesList.isEmpty()) { - _uiState.value = AllEnrolledCoursesUIState.Empty - } else { - _uiState.value = AllEnrolledCoursesUIState.Courses(ArrayList(coursesList)) - } + _uiState.update { it.copy(courses = coursesList) } } catch (e: Exception) { if (e.isInternetError()) { _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection))) @@ -102,7 +91,7 @@ class AllEnrolledCoursesViewModel( _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error))) } } - _updating.value = false + _uiState.update { it.copy(refreshing = false, showProgress = false) } isLoading = false } } @@ -123,24 +112,20 @@ class AllEnrolledCoursesViewModel( } if (response != null) { if (response.pagination.next.isNotEmpty() && page != response.pagination.numPages) { - _canLoadMore.value = true + _uiState.update { it.copy(canLoadMore = true) } page++ } else { - _canLoadMore.value = false + _uiState.update { it.copy(canLoadMore = false) } page = -1 } coursesList.addAll(response.courses) } else { val cachedList = interactor.getEnrolledCoursesFromCache() - _canLoadMore.value = false + _uiState.update { it.copy(canLoadMore = false) } page = -1 coursesList.addAll(cachedList) } - if (coursesList.isEmpty()) { - _uiState.value = AllEnrolledCoursesUIState.Empty - } else { - _uiState.value = AllEnrolledCoursesUIState.Courses(ArrayList(coursesList)) - } + _uiState.update { it.copy(courses = coursesList) } } catch (e: Exception) { if (e.isInternetError()) { _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection))) @@ -148,7 +133,7 @@ class AllEnrolledCoursesViewModel( _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error))) } } - _updating.value = false + _uiState.update { it.copy(refreshing = false, showProgress = false) } isLoading = false } } @@ -159,7 +144,7 @@ class AllEnrolledCoursesViewModel( } } - fun dashboardCourseClickedEvent(courseId: String, courseName: String) { + private fun dashboardCourseClickedEvent(courseId: String, courseName: String) { analytics.dashboardCourseClickedEvent(courseId, courseName) } @@ -172,6 +157,27 @@ class AllEnrolledCoursesViewModel( } } } + + fun navigateToCourseSearch(fragmentManager: FragmentManager) { + dashboardRouter.navigateToCourseSearch( + fragmentManager, "" + ) + } + + fun navigateToCourseOutline( + fragmentManager: FragmentManager, + courseId: String, + courseName: String, + mode: String + ) { + dashboardCourseClickedEvent(courseId, courseName) + dashboardRouter.navigateToCourseOutline( + fragmentManager, + courseId, + courseName, + mode + ) + } } interface AllEnrolledCoursesAction { diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseFragment.kt b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryFragment.kt similarity index 82% rename from dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseFragment.kt rename to dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryFragment.kt index e911248f9..b0309785c 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseFragment.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryFragment.kt @@ -8,7 +8,7 @@ import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment import org.openedx.core.ui.theme.OpenEdXTheme -class PrimaryCourseFragment : Fragment() { +class DashboardGalleryFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -17,7 +17,7 @@ class PrimaryCourseFragment : Fragment() { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { OpenEdXTheme { - PrimaryCourseScreen(fragmentManager = requireActivity().supportFragmentManager) + DashboardGalleryView(fragmentManager = requireActivity().supportFragmentManager) } } } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryUIState.kt b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryUIState.kt new file mode 100644 index 000000000..c4049f463 --- /dev/null +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryUIState.kt @@ -0,0 +1,9 @@ +package org.openedx.courses.presentation + +import org.openedx.core.domain.model.CourseEnrollments + +sealed class DashboardGalleryUIState { + data class Courses(val userCourses: CourseEnrollments) : DashboardGalleryUIState() + data object Empty : DashboardGalleryUIState() + data object Loading : DashboardGalleryUIState() +} diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt similarity index 89% rename from dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt rename to dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt index ac98d29db..14dd3b1b6 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseScreen.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt @@ -96,15 +96,15 @@ import java.util.Date import org.openedx.core.R as CoreR @Composable -fun PrimaryCourseScreen( - viewModel: PrimaryCourseViewModel = koinViewModel(), +fun DashboardGalleryView( + viewModel: DashboardGalleryViewModel = koinViewModel(), fragmentManager: FragmentManager, ) { val updating by viewModel.updating.collectAsState(false) val uiMessage by viewModel.uiMessage.collectAsState(null) - val uiState by viewModel.uiState.collectAsState(PrimaryCourseUIState.Loading) + val uiState by viewModel.uiState.collectAsState(DashboardGalleryUIState.Loading) - PrimaryCourseScreen( + DashboardGalleryView( uiMessage = uiMessage, uiState = uiState, updating = updating, @@ -112,48 +112,42 @@ fun PrimaryCourseScreen( hasInternetConnection = viewModel.hasInternetConnection, onAction = { action -> when (action) { - PrimaryCourseScreenAction.SwipeRefresh -> { + DashboardGalleryScreenAction.SwipeRefresh -> { viewModel.updateCourses() } - PrimaryCourseScreenAction.ViewAll -> { - viewModel.dashboardRouter.navigateToAllEnrolledCourses(fragmentManager) + DashboardGalleryScreenAction.ViewAll -> { + viewModel.navigateToAllEnrolledCourses(fragmentManager) } - PrimaryCourseScreenAction.Reload -> { + DashboardGalleryScreenAction.Reload -> { viewModel.getCourses() } - PrimaryCourseScreenAction.NavigateToDiscovery -> { + DashboardGalleryScreenAction.NavigateToDiscovery -> { viewModel.navigateToDiscovery() } - is PrimaryCourseScreenAction.OpenCourse -> { - viewModel.dashboardRouter.navigateToCourseOutline( - fm = fragmentManager, - courseId = action.enrolledCourse.course.id, - courseTitle = action.enrolledCourse.course.name, - enrollmentMode = action.enrolledCourse.mode + is DashboardGalleryScreenAction.OpenCourse -> { + viewModel.navigateToCourseOutline( + fragmentManager = fragmentManager, + enrolledCourse = action.enrolledCourse ) } - is PrimaryCourseScreenAction.NavigateToDates -> { - viewModel.dashboardRouter.navigateToCourseOutline( - fm = fragmentManager, - courseId = action.enrolledCourse.course.id, - courseTitle = action.enrolledCourse.course.name, - enrollmentMode = action.enrolledCourse.mode, + is DashboardGalleryScreenAction.NavigateToDates -> { + viewModel.navigateToCourseOutline( + fragmentManager = fragmentManager, + enrolledCourse = action.enrolledCourse, openDates = true ) } - is PrimaryCourseScreenAction.OpenBlock -> { - viewModel.dashboardRouter.navigateToCourseOutline( - fm = fragmentManager, - courseId = action.enrolledCourse.course.id, - courseTitle = action.enrolledCourse.course.name, - enrollmentMode = action.enrolledCourse.mode, - openBlock = action.blockId + is DashboardGalleryScreenAction.OpenBlock -> { + viewModel.navigateToCourseOutline( + fragmentManager = fragmentManager, + enrolledCourse = action.enrolledCourse, + resumeBlockId = action.blockId ) } } @@ -163,18 +157,18 @@ fun PrimaryCourseScreen( @OptIn(ExperimentalMaterialApi::class) @Composable -private fun PrimaryCourseScreen( +private fun DashboardGalleryView( uiMessage: UIMessage?, - uiState: PrimaryCourseUIState, + uiState: DashboardGalleryUIState, updating: Boolean, apiHostUrl: String, - onAction: (PrimaryCourseScreenAction) -> Unit, + onAction: (DashboardGalleryScreenAction) -> Unit, hasInternetConnection: Boolean ) { val scaffoldState = rememberScaffoldState() val pullRefreshState = rememberPullRefreshState( refreshing = updating, - onRefresh = { onAction(PrimaryCourseScreenAction.SwipeRefresh) } + onRefresh = { onAction(DashboardGalleryScreenAction.SwipeRefresh) } ) var isInternetConnectionShown by rememberSaveable { mutableStateOf(false) @@ -204,34 +198,34 @@ private fun PrimaryCourseScreen( .verticalScroll(rememberScrollState()), ) { when (uiState) { - is PrimaryCourseUIState.Loading -> { + is DashboardGalleryUIState.Loading -> { CircularProgressIndicator( modifier = Modifier.align(Alignment.Center), color = MaterialTheme.appColors.primary ) } - is PrimaryCourseUIState.Courses -> { + is DashboardGalleryUIState.Courses -> { UserCourses( modifier = Modifier.fillMaxSize(), userCourses = uiState.userCourses, apiHostUrl = apiHostUrl, openCourse = { - onAction(PrimaryCourseScreenAction.OpenCourse(it)) + onAction(DashboardGalleryScreenAction.OpenCourse(it)) }, onViewAllClick = { - onAction(PrimaryCourseScreenAction.ViewAll) + onAction(DashboardGalleryScreenAction.ViewAll) }, navigateToDates = { - onAction(PrimaryCourseScreenAction.NavigateToDates(it)) + onAction(DashboardGalleryScreenAction.NavigateToDates(it)) }, - openBlock = { course, blockId -> - onAction(PrimaryCourseScreenAction.OpenBlock(course, blockId)) + resumeBlockId = { course, blockId -> + onAction(DashboardGalleryScreenAction.OpenBlock(course, blockId)) } ) } - is PrimaryCourseUIState.Empty -> { + is DashboardGalleryUIState.Empty -> { NoCoursesInfo( modifier = Modifier .align(Alignment.Center) @@ -240,7 +234,7 @@ private fun PrimaryCourseScreen( modifier = Modifier .align(Alignment.BottomCenter), findACourseClick = { - onAction(PrimaryCourseScreenAction.NavigateToDiscovery) + onAction(DashboardGalleryScreenAction.NavigateToDiscovery) } ) } @@ -262,7 +256,7 @@ private fun PrimaryCourseScreen( }, onReloadClick = { isInternetConnectionShown = true - onAction(PrimaryCourseScreenAction.SwipeRefresh) + onAction(DashboardGalleryScreenAction.SwipeRefresh) } ) } @@ -279,7 +273,7 @@ private fun UserCourses( openCourse: (EnrolledCourse) -> Unit, navigateToDates: (EnrolledCourse) -> Unit, onViewAllClick: () -> Unit, - openBlock: (enrolledCourse: EnrolledCourse, blockId: String) -> Unit, + resumeBlockId: (enrolledCourse: EnrolledCourse, blockId: String) -> Unit, ) { Column( modifier = modifier @@ -291,7 +285,7 @@ private fun UserCourses( primaryCourse = primaryCourse, apiHostUrl = apiHostUrl, navigateToDates = navigateToDates, - openBlock = openBlock, + resumeBlockId = resumeBlockId, openCourse = openCourse ) } @@ -504,7 +498,7 @@ private fun PrimaryCourseCard( primaryCourse: EnrolledCourse, apiHostUrl: String, navigateToDates: (EnrolledCourse) -> Unit, - openBlock: (enrolledCourse: EnrolledCourse, blockId: String) -> Unit, + resumeBlockId: (enrolledCourse: EnrolledCourse, blockId: String) -> Unit, openCourse: (EnrolledCourse) -> Unit, ) { val context = LocalContext.current @@ -563,7 +557,7 @@ private fun PrimaryCourseCard( AssignmentItem( modifier = Modifier.clickable { if (pastAssignments.size == 1) { - openBlock(primaryCourse, nearestAssignment.blockId) + resumeBlockId(primaryCourse, nearestAssignment.blockId) } else { navigateToDates(primaryCourse) } @@ -581,7 +575,7 @@ private fun PrimaryCourseCard( AssignmentItem( modifier = Modifier.clickable { if (futureAssignments.size == 1) { - openBlock(primaryCourse, nearestAssignment.blockId) + resumeBlockId(primaryCourse, nearestAssignment.blockId) } else { navigateToDates(primaryCourse) } @@ -601,7 +595,7 @@ private fun PrimaryCourseCard( if (primaryCourse.courseStatus == null) { openCourse(primaryCourse) } else { - openBlock(primaryCourse, primaryCourse.courseStatus?.lastVisitedBlockId ?: "") + resumeBlockId(primaryCourse, primaryCourse.courseStatus?.lastVisitedBlockId ?: "") } } ) @@ -847,10 +841,10 @@ private fun ViewAllItemPreview() { @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 PrimaryCourseScreenPreview() { +private fun DashboardGalleryViewPreview() { OpenEdXTheme { - PrimaryCourseScreen( - uiState = PrimaryCourseUIState.Courses(mockUserCourses), + DashboardGalleryView( + uiState = DashboardGalleryUIState.Courses(mockUserCourses), apiHostUrl = "", uiMessage = null, updating = false, diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt similarity index 67% rename from dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseViewModel.kt rename to dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt index 326adc837..69b5dc4d5 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt @@ -1,5 +1,6 @@ package org.openedx.courses.presentation +import androidx.fragment.app.FragmentManager import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -24,20 +25,24 @@ import org.openedx.core.utils.FileUtil import org.openedx.dashboard.domain.interactor.DashboardInteractor import org.openedx.dashboard.presentation.DashboardRouter -class PrimaryCourseViewModel( +class DashboardGalleryViewModel( private val config: Config, private val interactor: DashboardInteractor, private val resourceManager: ResourceManager, private val discoveryNotifier: DiscoveryNotifier, private val networkConnection: NetworkConnection, private val fileUtil: FileUtil, - val dashboardRouter: DashboardRouter + private val dashboardRouter: DashboardRouter ) : BaseViewModel() { + companion object { + private const val DATES_TAB = "dates" + } + val apiHostUrl get() = config.getApiHostURL() - private val _uiState = MutableStateFlow(PrimaryCourseUIState.Loading) - val uiState: StateFlow + private val _uiState = MutableStateFlow(DashboardGalleryUIState.Loading) + val uiState: StateFlow get() = _uiState.asStateFlow() private val _uiMessage = MutableSharedFlow() @@ -62,16 +67,16 @@ class PrimaryCourseViewModel( if (networkConnection.isOnline()) { val response = interactor.getMainUserCourses() if (response.primary == null && response.enrollments.courses.isEmpty()) { - _uiState.value = PrimaryCourseUIState.Empty + _uiState.value = DashboardGalleryUIState.Empty } else { - _uiState.value = PrimaryCourseUIState.Courses(response) + _uiState.value = DashboardGalleryUIState.Courses(response) } } else { val courseEnrollments = fileUtil.getObjectFromFile() if (courseEnrollments == null) { - _uiState.value = PrimaryCourseUIState.Empty + _uiState.value = DashboardGalleryUIState.Empty } else { - _uiState.value = PrimaryCourseUIState.Courses(courseEnrollments.mapToDomain()) + _uiState.value = DashboardGalleryUIState.Courses(courseEnrollments.mapToDomain()) } } } catch (e: Exception) { @@ -95,6 +100,26 @@ class PrimaryCourseViewModel( viewModelScope.launch { discoveryNotifier.send(NavigationToDiscovery()) } } + fun navigateToAllEnrolledCourses(fragmentManager: FragmentManager) { + dashboardRouter.navigateToAllEnrolledCourses(fragmentManager) + } + + fun navigateToCourseOutline( + fragmentManager: FragmentManager, + enrolledCourse: EnrolledCourse, + openDates: Boolean = false, + resumeBlockId: String = "" + ) { + dashboardRouter.navigateToCourseOutline( + fm = fragmentManager, + courseId = enrolledCourse.course.id, + courseTitle = enrolledCourse.course.name, + enrollmentMode = enrolledCourse.mode, + openTab = if (openDates) DATES_TAB else "", + resumeBlockId = resumeBlockId + ) + } + private fun collectDiscoveryNotifier() { viewModelScope.launch { discoveryNotifier.notifier.collect { @@ -106,12 +131,12 @@ class PrimaryCourseViewModel( } } -interface PrimaryCourseScreenAction { - object SwipeRefresh : PrimaryCourseScreenAction - object ViewAll : PrimaryCourseScreenAction - object Reload : PrimaryCourseScreenAction - object NavigateToDiscovery : PrimaryCourseScreenAction - data class OpenBlock(val enrolledCourse: EnrolledCourse, val blockId: String) : PrimaryCourseScreenAction - data class OpenCourse(val enrolledCourse: EnrolledCourse) : PrimaryCourseScreenAction - data class NavigateToDates(val enrolledCourse: EnrolledCourse) : PrimaryCourseScreenAction +interface DashboardGalleryScreenAction { + object SwipeRefresh : DashboardGalleryScreenAction + object ViewAll : DashboardGalleryScreenAction + object Reload : DashboardGalleryScreenAction + object NavigateToDiscovery : DashboardGalleryScreenAction + data class OpenBlock(val enrolledCourse: EnrolledCourse, val blockId: String) : DashboardGalleryScreenAction + data class OpenCourse(val enrolledCourse: EnrolledCourse) : DashboardGalleryScreenAction + data class NavigateToDates(val enrolledCourse: EnrolledCourse) : DashboardGalleryScreenAction } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseUIState.kt b/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseUIState.kt deleted file mode 100644 index f30771688..000000000 --- a/dashboard/src/main/java/org/openedx/courses/presentation/PrimaryCourseUIState.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.openedx.courses.presentation - -import org.openedx.core.domain.model.CourseEnrollments - -sealed class PrimaryCourseUIState { - data class Courses(val userCourses: CourseEnrollments) : PrimaryCourseUIState() - data object Empty : PrimaryCourseUIState() - data object Loading : PrimaryCourseUIState() -} diff --git a/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt b/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt index 5844e8415..22637f48c 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt @@ -36,9 +36,8 @@ class DashboardRepository( } suspend fun getMainUserCourses(): CourseEnrollments { - val user = preferencesManager.user val result = api.getUserCourses( - username = user?.username ?: "", + username = preferencesManager.user?.username ?: "", ) preferencesManager.appConfig = result.configs.mapToDomain() diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/ListDashboardFragment.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt similarity index 98% rename from dashboard/src/main/java/org/openedx/dashboard/presentation/ListDashboardFragment.kt rename to dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt index 2e5ca4033..597958e51 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/ListDashboardFragment.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt @@ -101,9 +101,9 @@ import org.openedx.dashboard.R import java.util.Date import org.openedx.core.R as CoreR -class ListDashboardFragment : Fragment() { +class DashboardListFragment : Fragment() { - private val viewModel by viewModel() + private val viewModel by viewModel() private val router by inject() override fun onCreate(savedInstanceState: Bundle?) { @@ -126,7 +126,7 @@ class ListDashboardFragment : Fragment() { val canLoadMore by viewModel.canLoadMore.observeAsState(false) val appUpgradeEvent by viewModel.appUpgradeEvent.observeAsState() - ListDashboardScreen( + DashboardListView( windowSize = windowSize, viewModel.apiHostUrl, uiState!!, @@ -169,7 +169,7 @@ class ListDashboardFragment : Fragment() { @OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class) @Composable -internal fun ListDashboardScreen( +internal fun DashboardListView( windowSize: WindowSize, apiHostUrl: String, state: DashboardUIState, @@ -554,9 +554,9 @@ private fun CourseItemPreview() { @Preview(uiMode = UI_MODE_NIGHT_NO) @Preview(uiMode = UI_MODE_NIGHT_YES) @Composable -private fun ListDashboardScreenPreview() { +private fun DashboardListViewPreview() { OpenEdXTheme { - ListDashboardScreen( + DashboardListView( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), apiHostUrl = "http://localhost:8000", state = DashboardUIState.Courses( @@ -586,9 +586,9 @@ private fun ListDashboardScreenPreview() { @Preview(uiMode = UI_MODE_NIGHT_NO, device = Devices.NEXUS_9) @Preview(uiMode = UI_MODE_NIGHT_YES, device = Devices.NEXUS_9) @Composable -private fun ListDashboardScreenTabletPreview() { +private fun DashboardListViewTabletPreview() { OpenEdXTheme { - ListDashboardScreen( + DashboardListView( windowSize = WindowSize(WindowType.Medium, WindowType.Medium), apiHostUrl = "http://localhost:8000", state = DashboardUIState.Courses( diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/ListDashboardViewModel.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt similarity index 99% rename from dashboard/src/main/java/org/openedx/dashboard/presentation/ListDashboardViewModel.kt rename to dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt index f2cd06090..812e52f2e 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/ListDashboardViewModel.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt @@ -20,7 +20,7 @@ import org.openedx.core.system.notifier.CourseDashboardUpdate import org.openedx.core.system.notifier.DiscoveryNotifier import org.openedx.dashboard.domain.interactor.DashboardInteractor -class ListDashboardViewModel( +class DashboardListViewModel( private val config: Config, private val networkConnection: NetworkConnection, private val interactor: DashboardInteractor, diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt index b025c3748..4d9b5cdbc 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt @@ -10,8 +10,8 @@ interface DashboardRouter { courseId: String, courseTitle: String, enrollmentMode: String, - openDates: Boolean = false, - openBlock: String = "" + openTab: String = "", + resumeBlockId: String = "" ) fun navigateToSettings(fm: FragmentManager) 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 a4127d37e..521d933c3 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt @@ -3,7 +3,6 @@ package org.openedx.learn.presentation import android.os.Bundle import android.view.View import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -52,7 +51,7 @@ import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appTypography import org.openedx.core.ui.windowSizeValue -import org.openedx.courses.presentation.PrimaryCourseFragment +import org.openedx.courses.presentation.DashboardGalleryFragment import org.openedx.dashboard.R import org.openedx.dashboard.databinding.FragmentLearnBinding import org.openedx.dashboard.presentation.DashboardRouter @@ -83,7 +82,7 @@ class LearnFragment : Fragment(R.layout.fragment_learn) { binding.viewPager.offscreenPageLimit = 2 adapter = NavigationFragmentAdapter(this).apply { - addFragment(PrimaryCourseFragment()) + addFragment(DashboardGalleryFragment()) addFragment(router.getProgramFragmentInstance()) } binding.viewPager.adapter = adapter @@ -167,7 +166,6 @@ private fun Title( } } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun LearnDropdownMenu( modifier: Modifier = Modifier, @@ -264,7 +262,6 @@ private fun HeaderPreview() { } } -@OptIn(ExperimentalFoundationApi::class) @Preview @Composable private fun LearnDropdownMenuPreview() { diff --git a/dashboard/src/main/res/drawable/dashboard_ic_book.xml b/dashboard/src/main/res/drawable/dashboard_ic_book.xml index a26c83ec7..dd802ee92 100644 --- a/dashboard/src/main/res/drawable/dashboard_ic_book.xml +++ b/dashboard/src/main/res/drawable/dashboard_ic_book.xml @@ -6,39 +6,39 @@ + android:strokeLineCap="round" + android:strokeLineJoin="round" /> + android:strokeLineCap="round" + android:strokeLineJoin="round" /> + android:strokeLineCap="round" + android:strokeLineJoin="round" /> + android:strokeLineCap="round" + android:strokeLineJoin="round" /> + android:strokeLineCap="round" + android:strokeLineJoin="round" /> diff --git a/dashboard/src/main/res/values/strings.xml b/dashboard/src/main/res/values/strings.xml index 33f794904..4ca0c4fce 100644 --- a/dashboard/src/main/res/values/strings.xml +++ b/dashboard/src/main/res/values/strings.xml @@ -7,7 +7,7 @@ Learn Programs Course %1$s - Start course + Start Course Resume Course %1$d Past Due Assignments View All Courses (%1$d) @@ -18,7 +18,6 @@ Completed Expired All Courses - All No Courses You are not currently enrolled in any courses, would you like to explore the course catalog? Find a Course diff --git a/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt b/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt index 4fd957e12..6ca20a255 100644 --- a/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt +++ b/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt @@ -77,7 +77,7 @@ class DashboardViewModelTest { @Test fun `getCourses no internet connection`() = runTest { - val viewModel = ListDashboardViewModel( + val viewModel = DashboardListViewModel( config, networkConnection, interactor, @@ -101,7 +101,7 @@ class DashboardViewModelTest { @Test fun `getCourses unknown error`() = runTest { - val viewModel = ListDashboardViewModel( + val viewModel = DashboardListViewModel( config, networkConnection, interactor, @@ -125,7 +125,7 @@ class DashboardViewModelTest { @Test fun `getCourses from network`() = runTest { - val viewModel = ListDashboardViewModel( + val viewModel = DashboardListViewModel( config, networkConnection, interactor, @@ -149,7 +149,7 @@ class DashboardViewModelTest { @Test fun `getCourses from network with next page`() = runTest { - val viewModel = ListDashboardViewModel( + val viewModel = DashboardListViewModel( config, networkConnection, interactor, @@ -183,7 +183,7 @@ class DashboardViewModelTest { fun `getCourses from cache`() = runTest { every { networkConnection.isOnline() } returns false coEvery { interactor.getEnrolledCoursesFromCache() } returns listOf(mockk()) - val viewModel = ListDashboardViewModel( + val viewModel = DashboardListViewModel( config, networkConnection, interactor, @@ -207,7 +207,7 @@ class DashboardViewModelTest { fun `updateCourses no internet error`() = runTest { every { networkConnection.isOnline() } returns true coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList - val viewModel = ListDashboardViewModel( + val viewModel = DashboardListViewModel( config, networkConnection, interactor, @@ -235,7 +235,7 @@ class DashboardViewModelTest { fun `updateCourses unknown exception`() = runTest { every { networkConnection.isOnline() } returns true coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList - val viewModel = ListDashboardViewModel( + val viewModel = DashboardListViewModel( config, networkConnection, interactor, @@ -263,7 +263,7 @@ class DashboardViewModelTest { fun `updateCourses success`() = runTest { every { networkConnection.isOnline() } returns true coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList - val viewModel = ListDashboardViewModel( + val viewModel = DashboardListViewModel( config, networkConnection, interactor, @@ -296,7 +296,7 @@ class DashboardViewModelTest { "" ) ) - val viewModel = ListDashboardViewModel( + val viewModel = DashboardListViewModel( config, networkConnection, interactor, @@ -321,7 +321,7 @@ class DashboardViewModelTest { @Test fun `CourseDashboardUpdate notifier test`() = runTest { coEvery { discoveryNotifier.notifier } returns flow { emit(CourseDashboardUpdate()) } - val viewModel = ListDashboardViewModel( + val viewModel = DashboardListViewModel( config, networkConnection, interactor, diff --git a/default_config/prod/config.yaml b/default_config/prod/config.yaml index 6c68daf93..139652ca6 100644 --- a/default_config/prod/config.yaml +++ b/default_config/prod/config.yaml @@ -29,7 +29,7 @@ PROGRAM: PROGRAM_DETAIL_URL_TEMPLATE: '' DASHBOARD: - TYPE: 'primary_course' + TYPE: 'gallery' FIREBASE: ENABLED: false diff --git a/default_config/stage/config.yaml b/default_config/stage/config.yaml index 6c68daf93..139652ca6 100644 --- a/default_config/stage/config.yaml +++ b/default_config/stage/config.yaml @@ -29,7 +29,7 @@ PROGRAM: PROGRAM_DETAIL_URL_TEMPLATE: '' DASHBOARD: - TYPE: 'primary_course' + TYPE: 'gallery' FIREBASE: ENABLED: false From 3fd665717a2e4cdaa5e9f134ec9e4fe0c526c207 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Wed, 29 May 2024 10:32:42 +0300 Subject: [PATCH 22/23] feat: added a patch from Omer Habib --- app/src/main/res/layout/fragment_main.xml | 2 +- .../core/data/model/CourseAssignments.kt | 2 +- .../core/data/model/CourseEnrollments.kt | 2 +- .../openedx/core/data/model/CourseStatus.kt | 2 +- .../org/openedx/core/data/model/Progress.kt | 2 +- .../room/discovery/EnrolledCourseEntity.kt | 12 +++++------ .../openedx/core/domain/model/CourseStatus.kt | 2 +- .../org/openedx/core/module/DownloadWorker.kt | 2 +- .../openedx/core/module/TranscriptManager.kt | 6 ++++-- .../container/CourseContainerFragment.kt | 5 ++--- .../container/CourseContainerTab.kt | 2 +- .../container/CourseContainerViewModel.kt | 2 +- .../AllEnrolledCoursesFragment.kt | 6 +++--- .../openedx/courses/presentation/CourseTab.kt | 5 +++++ .../presentation/DashboardGalleryView.kt | 2 +- .../presentation/DashboardGalleryViewModel.kt | 20 +++++++++---------- .../dashboard/domain/CourseStatusFilter.kt | 2 +- .../domain/interactor/DashboardInteractor.kt | 7 +++++-- .../learn/presentation/LearnFragment.kt | 2 +- default_config/dev/config.yaml | 2 +- 20 files changed, 48 insertions(+), 39 deletions(-) create mode 100644 dashboard/src/main/java/org/openedx/courses/presentation/CourseTab.kt diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index 76242b72b..9794b7bd7 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -18,9 +18,9 @@ android:id="@+id/bottom_nav_view" android:layout_width="match_parent" android:layout_height="wrap_content" + android:background="@color/background" app:itemIconTint="@color/bottom_nav_color" app:itemTextColor="@color/bottom_nav_color" - android:background="@color/background" app:labelVisibilityMode="labeled" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/core/src/main/java/org/openedx/core/data/model/CourseAssignments.kt b/core/src/main/java/org/openedx/core/data/model/CourseAssignments.kt index 165868d6d..ed8de3a4e 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseAssignments.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseAssignments.kt @@ -8,7 +8,7 @@ data class CourseAssignments( @SerializedName("future_assignments") val futureAssignments: List?, @SerializedName("past_assignments") - val pastAssignments: List? + val pastAssignments: List?, ) { fun mapToDomain() = CourseAssignments( futureAssignments = futureAssignments?.mapNotNull { diff --git a/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt b/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt index 63025028c..ca28740fe 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt @@ -29,7 +29,7 @@ data class CourseEnrollments( override fun deserialize( json: JsonElement?, typeOfT: Type?, - context: JsonDeserializationContext? + context: JsonDeserializationContext?, ): CourseEnrollments { val enrollments = deserializeEnrollments(json) val appConfig = deserializeAppConfig(json) diff --git a/core/src/main/java/org/openedx/core/data/model/CourseStatus.kt b/core/src/main/java/org/openedx/core/data/model/CourseStatus.kt index 53caeb136..53cb028b4 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseStatus.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseStatus.kt @@ -12,7 +12,7 @@ data class CourseStatus( @SerializedName("last_visited_block_id") val lastVisitedBlockId: String?, @SerializedName("last_visited_unit_display_name") - val lastVisitedUnitDisplayName: String? + val lastVisitedUnitDisplayName: String?, ) { fun mapToDomain() = CourseStatus( lastVisitedModuleId = lastVisitedModuleId ?: "", diff --git a/core/src/main/java/org/openedx/core/data/model/Progress.kt b/core/src/main/java/org/openedx/core/data/model/Progress.kt index 932533e44..d4813c14c 100644 --- a/core/src/main/java/org/openedx/core/data/model/Progress.kt +++ b/core/src/main/java/org/openedx/core/data/model/Progress.kt @@ -8,7 +8,7 @@ data class Progress( @SerializedName("assignments_completed") val assignmentsCompleted: Int?, @SerializedName("total_assignments_count") - val totalAssignmentsCount: Int? + val totalAssignmentsCount: Int?, ) { fun mapToDomain() = Progress( assignmentsCompleted = assignmentsCompleted ?: 0, diff --git a/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt b/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt index a6cd1d93e..e019f6300 100644 --- a/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt +++ b/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt @@ -40,7 +40,7 @@ data class EnrolledCourseEntity( @Embedded val courseStatus: CourseStatusDb?, @Embedded - val courseAssignments: CourseAssignmentsDb? + val courseAssignments: CourseAssignmentsDb?, ) { fun mapToDomain(): EnrolledCourse { @@ -98,7 +98,7 @@ data class EnrolledCourseDataDb( @ColumnInfo("videoOutline") val videoOutline: String, @ColumnInfo("isSelfPaced") - val isSelfPaced: Boolean + val isSelfPaced: Boolean, ) { fun mapToDomain(): EnrolledCourseData { return EnrolledCourseData( @@ -138,7 +138,7 @@ data class CoursewareAccessDb( @ColumnInfo("additionalContextUserMessage") val additionalContextUserMessage: String, @ColumnInfo("userFragment") - val userFragment: String + val userFragment: String, ) { fun mapToDomain(): CoursewareAccess { @@ -156,7 +156,7 @@ data class CoursewareAccessDb( data class CertificateDb( @ColumnInfo("certificateURL") - val certificateURL: String? + val certificateURL: String?, ) { fun mapToDomain() = Certificate(certificateURL) } @@ -165,7 +165,7 @@ data class CourseSharingUtmParametersDb( @ColumnInfo("facebook") val facebook: String, @ColumnInfo("twitter") - val twitter: String + val twitter: String, ) { fun mapToDomain() = CourseSharingUtmParameters( facebook, twitter @@ -204,7 +204,7 @@ data class CourseAssignmentsDb( @ColumnInfo("futureAssignments") val futureAssignments: List?, @ColumnInfo("pastAssignments") - val pastAssignments: List? + val pastAssignments: List?, ) { fun mapToDomain() = CourseAssignments( futureAssignments = futureAssignments?.map { it.mapToDomain() }, diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseStatus.kt b/core/src/main/java/org/openedx/core/domain/model/CourseStatus.kt index 134695c72..aef245f67 100644 --- a/core/src/main/java/org/openedx/core/domain/model/CourseStatus.kt +++ b/core/src/main/java/org/openedx/core/domain/model/CourseStatus.kt @@ -8,5 +8,5 @@ data class CourseStatus( val lastVisitedModuleId: String, val lastVisitedModulePath: List, val lastVisitedBlockId: String, - val lastVisitedUnitDisplayName: String + val lastVisitedUnitDisplayName: String, ) : Parcelable diff --git a/core/src/main/java/org/openedx/core/module/DownloadWorker.kt b/core/src/main/java/org/openedx/core/module/DownloadWorker.kt index 29f3f48ba..736a1b1ce 100644 --- a/core/src/main/java/org/openedx/core/module/DownloadWorker.kt +++ b/core/src/main/java/org/openedx/core/module/DownloadWorker.kt @@ -28,7 +28,7 @@ import java.io.File class DownloadWorker( val context: Context, - parameters: WorkerParameters + parameters: WorkerParameters, ) : CoroutineWorker(context, parameters), CoroutineScope { private val notificationManager = diff --git a/core/src/main/java/org/openedx/core/module/TranscriptManager.kt b/core/src/main/java/org/openedx/core/module/TranscriptManager.kt index 0b8b3e1e1..c08870a33 100644 --- a/core/src/main/java/org/openedx/core/module/TranscriptManager.kt +++ b/core/src/main/java/org/openedx/core/module/TranscriptManager.kt @@ -17,7 +17,7 @@ import java.nio.charset.Charset import java.util.concurrent.TimeUnit class TranscriptManager( - val context: Context + val context: Context, ) { private val transcriptDownloader = object : AbstractDownloader() { @@ -31,7 +31,9 @@ class TranscriptManager( val transcriptDir = getTranscriptDir() ?: return false val hash = Sha1Util.SHA1(url) val file = File(transcriptDir, hash) - return file.exists() && System.currentTimeMillis() - file.lastModified() < TimeUnit.HOURS.toMillis(5) + return file.exists() && System.currentTimeMillis() - file.lastModified() < TimeUnit.HOURS.toMillis( + 5 + ) } fun get(url: String): String? { diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt index d39d74a6c..f05999188 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt @@ -258,12 +258,11 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { const val ARG_ENROLLMENT_MODE = "enrollmentMode" const val ARG_OPEN_TAB = "open_tab" const val ARG_RESUME_BLOCK = "resume_block" - const val DEFAULT_TAB = "home" fun newInstance( courseId: String, courseTitle: String, enrollmentMode: String, - openTab: String = DEFAULT_TAB, + openTab: String = CourseContainerTab.HOME.name, resumeBlockId: String = "" ): CourseContainerFragment { val fragment = CourseContainerFragment() @@ -303,7 +302,7 @@ fun CourseDashboard( val refreshing by viewModel.refreshing.collectAsState(true) val courseImage by viewModel.courseImage.collectAsState() val uiMessage by viewModel.uiMessage.collectAsState(null) - val openTab = bundle.getString(CourseContainerFragment.ARG_OPEN_TAB, CourseContainerFragment.DEFAULT_TAB) + val openTab = bundle.getString(CourseContainerFragment.ARG_OPEN_TAB, CourseContainerTab.HOME.name) val requiredTab = when (openTab.uppercase()) { CourseContainerTab.HOME.name -> CourseContainerTab.HOME CourseContainerTab.VIDEOS.name -> CourseContainerTab.VIDEOS diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerTab.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerTab.kt index abd5babdc..fbdbb60fc 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerTab.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerTab.kt @@ -14,7 +14,7 @@ import org.openedx.course.R enum class CourseContainerTab( @StringRes override val labelResId: Int, - override val icon: ImageVector + override val icon: ImageVector, ) : TabItem { HOME(R.string.course_container_nav_home, Icons.Default.Home), VIDEOS(R.string.course_container_nav_videos, Icons.Rounded.PlayCircleFilled), diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt index af209660e..96945e7bc 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt @@ -70,7 +70,7 @@ class CourseContainerViewModel( private val coursePreferences: CoursePreferences, private val courseAnalytics: CourseAnalytics, private val imageProcessor: ImageProcessor, - val courseRouter: CourseRouter + val courseRouter: CourseRouter, ) : BaseViewModel() { private val _dataReady = MutableLiveData() 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 64ce4baa4..a81118f80 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt @@ -101,9 +101,9 @@ class AllEnrolledCoursesFragment : Fragment() { @Composable private fun AllEnrolledCoursesView( - viewModel: AllEnrolledCoursesViewModel = koinViewModel(), fragmentManager: FragmentManager ) { + val viewModel: AllEnrolledCoursesViewModel = koinViewModel() val uiState by viewModel.uiState.collectAsState() val uiMessage by viewModel.uiMessage.collectAsState(null) @@ -455,8 +455,8 @@ private val mockCourseEnrolled = EnrolledCourse( dynamicUpgradeDeadline = "", subscriptionId = "", coursewareAccess = CoursewareAccess( - true, - "", + false, + "204", "", "", "", diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/CourseTab.kt b/dashboard/src/main/java/org/openedx/courses/presentation/CourseTab.kt new file mode 100644 index 000000000..f0da7c186 --- /dev/null +++ b/dashboard/src/main/java/org/openedx/courses/presentation/CourseTab.kt @@ -0,0 +1,5 @@ +package org.openedx.courses.presentation + +enum class CourseTab { + HOME, VIDEOS, DATES, DISCUSSIONS, MORE +} diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt index e39537427..c4ea029b9 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt @@ -97,9 +97,9 @@ import org.openedx.core.R as CoreR @Composable fun DashboardGalleryView( - viewModel: DashboardGalleryViewModel = koinViewModel(), fragmentManager: FragmentManager, ) { + val viewModel: DashboardGalleryViewModel = koinViewModel() val updating by viewModel.updating.collectAsState(false) val uiMessage by viewModel.uiMessage.collectAsState(null) val uiState by viewModel.uiState.collectAsState(DashboardGalleryUIState.Loading) diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt index 69b5dc4d5..8520f9a77 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt @@ -32,16 +32,13 @@ class DashboardGalleryViewModel( private val discoveryNotifier: DiscoveryNotifier, private val networkConnection: NetworkConnection, private val fileUtil: FileUtil, - private val dashboardRouter: DashboardRouter + private val dashboardRouter: DashboardRouter, ) : BaseViewModel() { - companion object { - private const val DATES_TAB = "dates" - } - val apiHostUrl get() = config.getApiHostURL() - private val _uiState = MutableStateFlow(DashboardGalleryUIState.Loading) + private val _uiState = + MutableStateFlow(DashboardGalleryUIState.Loading) val uiState: StateFlow get() = _uiState.asStateFlow() @@ -76,7 +73,8 @@ class DashboardGalleryViewModel( if (courseEnrollments == null) { _uiState.value = DashboardGalleryUIState.Empty } else { - _uiState.value = DashboardGalleryUIState.Courses(courseEnrollments.mapToDomain()) + _uiState.value = + DashboardGalleryUIState.Courses(courseEnrollments.mapToDomain()) } } } catch (e: Exception) { @@ -108,14 +106,14 @@ class DashboardGalleryViewModel( fragmentManager: FragmentManager, enrolledCourse: EnrolledCourse, openDates: Boolean = false, - resumeBlockId: String = "" + resumeBlockId: String = "", ) { dashboardRouter.navigateToCourseOutline( fm = fragmentManager, courseId = enrolledCourse.course.id, courseTitle = enrolledCourse.course.name, enrollmentMode = enrolledCourse.mode, - openTab = if (openDates) DATES_TAB else "", + openTab = if (openDates) CourseTab.DATES.name else CourseTab.HOME.name, resumeBlockId = resumeBlockId ) } @@ -136,7 +134,9 @@ interface DashboardGalleryScreenAction { object ViewAll : DashboardGalleryScreenAction object Reload : DashboardGalleryScreenAction object NavigateToDiscovery : DashboardGalleryScreenAction - data class OpenBlock(val enrolledCourse: EnrolledCourse, val blockId: String) : DashboardGalleryScreenAction + data class OpenBlock(val enrolledCourse: EnrolledCourse, val blockId: String) : + DashboardGalleryScreenAction + data class OpenCourse(val enrolledCourse: EnrolledCourse) : DashboardGalleryScreenAction data class NavigateToDates(val enrolledCourse: EnrolledCourse) : DashboardGalleryScreenAction } diff --git a/dashboard/src/main/java/org/openedx/dashboard/domain/CourseStatusFilter.kt b/dashboard/src/main/java/org/openedx/dashboard/domain/CourseStatusFilter.kt index a61fc2a1f..79a19b89d 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/domain/CourseStatusFilter.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/domain/CourseStatusFilter.kt @@ -9,7 +9,7 @@ enum class CourseStatusFilter( val key: String, @StringRes override val labelResId: Int, - override val icon: ImageVector? = null + override val icon: ImageVector? = null, ) : TabItem { ALL("all", R.string.dashboard_course_filter_all), IN_PROGRESS("in_progress", R.string.dashboard_course_filter_in_progress), diff --git a/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt b/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt index 04146c103..ae2e94d93 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt @@ -5,7 +5,7 @@ import org.openedx.dashboard.data.repository.DashboardRepository import org.openedx.dashboard.domain.CourseStatusFilter class DashboardInteractor( - private val repository: DashboardRepository + private val repository: DashboardRepository, ) { suspend fun getEnrolledCourses(page: Int): DashboardCourseList { @@ -16,7 +16,10 @@ class DashboardInteractor( suspend fun getMainUserCourses() = repository.getMainUserCourses() - suspend fun getAllUserCourses(page: Int = 1, status: CourseStatusFilter? = null): DashboardCourseList { + suspend fun getAllUserCourses( + page: Int = 1, + status: CourseStatusFilter? = null, + ): DashboardCourseList { return repository.getAllUserCourses( page, status 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 929edae7d..b2de66cd4 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt @@ -92,10 +92,10 @@ class LearnFragment : Fragment(R.layout.fragment_learn) { @Composable private fun Header( - viewModel: LearnViewModel = koinViewModel(), fragmentManager: FragmentManager, viewPager: ViewPager2 ) { + val viewModel: LearnViewModel = koinViewModel() val windowSize = rememberWindowSize() val contentWidth by remember(key1 = windowSize) { mutableStateOf( diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml index 6c68daf93..139652ca6 100644 --- a/default_config/dev/config.yaml +++ b/default_config/dev/config.yaml @@ -29,7 +29,7 @@ PROGRAM: PROGRAM_DETAIL_URL_TEMPLATE: '' DASHBOARD: - TYPE: 'primary_course' + TYPE: 'gallery' FIREBASE: ENABLED: false From 465bafa6f891e919b2a84acaae386f2aaa3baeb6 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Wed, 29 May 2024 10:32:42 +0300 Subject: [PATCH 23/23] fix: Fixes according to PR feedback --- .../main/java/org/openedx/app/MainFragment.kt | 9 +- app/src/main/res/layout/fragment_main.xml | 2 +- .../core/data/model/CourseAssignments.kt | 2 +- .../core/data/model/CourseEnrollments.kt | 2 +- .../openedx/core/data/model/CourseStatus.kt | 2 +- .../org/openedx/core/data/model/Progress.kt | 2 +- .../room/discovery/EnrolledCourseEntity.kt | 12 +- .../openedx/core/domain/model/CourseStatus.kt | 2 +- .../org/openedx/core/module/DownloadWorker.kt | 2 +- .../openedx/core/module/TranscriptManager.kt | 6 +- .../container/CourseContainerFragment.kt | 5 +- .../container/CourseContainerTab.kt | 2 +- .../container/CourseContainerViewModel.kt | 2 +- .../section/CourseSectionFragment.kt | 8 +- .../java/org/openedx/DashboardNavigator.kt | 17 + .../presentation/AllEnrolledCoursesAction.kt | 14 + .../AllEnrolledCoursesFragment.kt | 448 ------------------ .../presentation/AllEnrolledCoursesView.kt | 447 ++++++++++++++++- .../AllEnrolledCoursesViewModel.kt | 10 - .../openedx/courses/presentation/CourseTab.kt | 5 + .../DashboardGalleryScreenAction.kt | 13 + .../presentation/DashboardGalleryView.kt | 2 +- .../presentation/DashboardGalleryViewModel.kt | 26 +- .../dashboard/domain/CourseStatusFilter.kt | 2 +- .../domain/interactor/DashboardInteractor.kt | 7 +- .../learn/presentation/LearnFragment.kt | 2 +- default_config/dev/config.yaml | 2 +- 27 files changed, 536 insertions(+), 517 deletions(-) create mode 100644 dashboard/src/main/java/org/openedx/DashboardNavigator.kt create mode 100644 dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesAction.kt create mode 100644 dashboard/src/main/java/org/openedx/courses/presentation/CourseTab.kt create mode 100644 dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryScreenAction.kt diff --git a/app/src/main/java/org/openedx/app/MainFragment.kt b/app/src/main/java/org/openedx/app/MainFragment.kt index 3db56e2b3..fc4fb1b22 100644 --- a/app/src/main/java/org/openedx/app/MainFragment.kt +++ b/app/src/main/java/org/openedx/app/MainFragment.kt @@ -11,15 +11,13 @@ import androidx.viewpager2.widget.ViewPager2 import kotlinx.coroutines.launch import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel +import org.openedx.DashboardNavigator import org.openedx.app.databinding.FragmentMainBinding import org.openedx.core.adapter.NavigationFragmentAdapter -import org.openedx.core.config.DashboardConfig import org.openedx.core.presentation.global.app_upgrade.UpgradeRequiredFragment import org.openedx.core.presentation.global.viewBinding -import org.openedx.dashboard.presentation.DashboardListFragment import org.openedx.discovery.presentation.DiscoveryNavigator import org.openedx.discovery.presentation.DiscoveryRouter -import org.openedx.learn.presentation.LearnFragment import org.openedx.profile.presentation.profile.ProfileFragment class MainFragment : Fragment(R.layout.fragment_main) { @@ -100,10 +98,7 @@ class MainFragment : Fragment(R.layout.fragment_main) { binding.viewPager.offscreenPageLimit = 4 val discoveryFragment = DiscoveryNavigator(viewModel.isDiscoveryTypeWebView).getDiscoveryFragment() - val dashboardFragment = when (viewModel.dashboardType) { - DashboardConfig.DashboardType.LIST -> DashboardListFragment() - DashboardConfig.DashboardType.GALLERY -> LearnFragment() - } + val dashboardFragment = DashboardNavigator(viewModel.dashboardType).getDashboardFragment() adapter = NavigationFragmentAdapter(this).apply { addFragment(dashboardFragment) diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index 76242b72b..9794b7bd7 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -18,9 +18,9 @@ android:id="@+id/bottom_nav_view" android:layout_width="match_parent" android:layout_height="wrap_content" + android:background="@color/background" app:itemIconTint="@color/bottom_nav_color" app:itemTextColor="@color/bottom_nav_color" - android:background="@color/background" app:labelVisibilityMode="labeled" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/core/src/main/java/org/openedx/core/data/model/CourseAssignments.kt b/core/src/main/java/org/openedx/core/data/model/CourseAssignments.kt index 165868d6d..ed8de3a4e 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseAssignments.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseAssignments.kt @@ -8,7 +8,7 @@ data class CourseAssignments( @SerializedName("future_assignments") val futureAssignments: List?, @SerializedName("past_assignments") - val pastAssignments: List? + val pastAssignments: List?, ) { fun mapToDomain() = CourseAssignments( futureAssignments = futureAssignments?.mapNotNull { diff --git a/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt b/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt index 63025028c..ca28740fe 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt @@ -29,7 +29,7 @@ data class CourseEnrollments( override fun deserialize( json: JsonElement?, typeOfT: Type?, - context: JsonDeserializationContext? + context: JsonDeserializationContext?, ): CourseEnrollments { val enrollments = deserializeEnrollments(json) val appConfig = deserializeAppConfig(json) diff --git a/core/src/main/java/org/openedx/core/data/model/CourseStatus.kt b/core/src/main/java/org/openedx/core/data/model/CourseStatus.kt index 53caeb136..53cb028b4 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseStatus.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseStatus.kt @@ -12,7 +12,7 @@ data class CourseStatus( @SerializedName("last_visited_block_id") val lastVisitedBlockId: String?, @SerializedName("last_visited_unit_display_name") - val lastVisitedUnitDisplayName: String? + val lastVisitedUnitDisplayName: String?, ) { fun mapToDomain() = CourseStatus( lastVisitedModuleId = lastVisitedModuleId ?: "", diff --git a/core/src/main/java/org/openedx/core/data/model/Progress.kt b/core/src/main/java/org/openedx/core/data/model/Progress.kt index 932533e44..d4813c14c 100644 --- a/core/src/main/java/org/openedx/core/data/model/Progress.kt +++ b/core/src/main/java/org/openedx/core/data/model/Progress.kt @@ -8,7 +8,7 @@ data class Progress( @SerializedName("assignments_completed") val assignmentsCompleted: Int?, @SerializedName("total_assignments_count") - val totalAssignmentsCount: Int? + val totalAssignmentsCount: Int?, ) { fun mapToDomain() = Progress( assignmentsCompleted = assignmentsCompleted ?: 0, diff --git a/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt b/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt index a6cd1d93e..e019f6300 100644 --- a/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt +++ b/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt @@ -40,7 +40,7 @@ data class EnrolledCourseEntity( @Embedded val courseStatus: CourseStatusDb?, @Embedded - val courseAssignments: CourseAssignmentsDb? + val courseAssignments: CourseAssignmentsDb?, ) { fun mapToDomain(): EnrolledCourse { @@ -98,7 +98,7 @@ data class EnrolledCourseDataDb( @ColumnInfo("videoOutline") val videoOutline: String, @ColumnInfo("isSelfPaced") - val isSelfPaced: Boolean + val isSelfPaced: Boolean, ) { fun mapToDomain(): EnrolledCourseData { return EnrolledCourseData( @@ -138,7 +138,7 @@ data class CoursewareAccessDb( @ColumnInfo("additionalContextUserMessage") val additionalContextUserMessage: String, @ColumnInfo("userFragment") - val userFragment: String + val userFragment: String, ) { fun mapToDomain(): CoursewareAccess { @@ -156,7 +156,7 @@ data class CoursewareAccessDb( data class CertificateDb( @ColumnInfo("certificateURL") - val certificateURL: String? + val certificateURL: String?, ) { fun mapToDomain() = Certificate(certificateURL) } @@ -165,7 +165,7 @@ data class CourseSharingUtmParametersDb( @ColumnInfo("facebook") val facebook: String, @ColumnInfo("twitter") - val twitter: String + val twitter: String, ) { fun mapToDomain() = CourseSharingUtmParameters( facebook, twitter @@ -204,7 +204,7 @@ data class CourseAssignmentsDb( @ColumnInfo("futureAssignments") val futureAssignments: List?, @ColumnInfo("pastAssignments") - val pastAssignments: List? + val pastAssignments: List?, ) { fun mapToDomain() = CourseAssignments( futureAssignments = futureAssignments?.map { it.mapToDomain() }, diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseStatus.kt b/core/src/main/java/org/openedx/core/domain/model/CourseStatus.kt index 134695c72..aef245f67 100644 --- a/core/src/main/java/org/openedx/core/domain/model/CourseStatus.kt +++ b/core/src/main/java/org/openedx/core/domain/model/CourseStatus.kt @@ -8,5 +8,5 @@ data class CourseStatus( val lastVisitedModuleId: String, val lastVisitedModulePath: List, val lastVisitedBlockId: String, - val lastVisitedUnitDisplayName: String + val lastVisitedUnitDisplayName: String, ) : Parcelable diff --git a/core/src/main/java/org/openedx/core/module/DownloadWorker.kt b/core/src/main/java/org/openedx/core/module/DownloadWorker.kt index 29f3f48ba..736a1b1ce 100644 --- a/core/src/main/java/org/openedx/core/module/DownloadWorker.kt +++ b/core/src/main/java/org/openedx/core/module/DownloadWorker.kt @@ -28,7 +28,7 @@ import java.io.File class DownloadWorker( val context: Context, - parameters: WorkerParameters + parameters: WorkerParameters, ) : CoroutineWorker(context, parameters), CoroutineScope { private val notificationManager = diff --git a/core/src/main/java/org/openedx/core/module/TranscriptManager.kt b/core/src/main/java/org/openedx/core/module/TranscriptManager.kt index 0b8b3e1e1..c08870a33 100644 --- a/core/src/main/java/org/openedx/core/module/TranscriptManager.kt +++ b/core/src/main/java/org/openedx/core/module/TranscriptManager.kt @@ -17,7 +17,7 @@ import java.nio.charset.Charset import java.util.concurrent.TimeUnit class TranscriptManager( - val context: Context + val context: Context, ) { private val transcriptDownloader = object : AbstractDownloader() { @@ -31,7 +31,9 @@ class TranscriptManager( val transcriptDir = getTranscriptDir() ?: return false val hash = Sha1Util.SHA1(url) val file = File(transcriptDir, hash) - return file.exists() && System.currentTimeMillis() - file.lastModified() < TimeUnit.HOURS.toMillis(5) + return file.exists() && System.currentTimeMillis() - file.lastModified() < TimeUnit.HOURS.toMillis( + 5 + ) } fun get(url: String): String? { diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt index d39d74a6c..f05999188 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt @@ -258,12 +258,11 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { const val ARG_ENROLLMENT_MODE = "enrollmentMode" const val ARG_OPEN_TAB = "open_tab" const val ARG_RESUME_BLOCK = "resume_block" - const val DEFAULT_TAB = "home" fun newInstance( courseId: String, courseTitle: String, enrollmentMode: String, - openTab: String = DEFAULT_TAB, + openTab: String = CourseContainerTab.HOME.name, resumeBlockId: String = "" ): CourseContainerFragment { val fragment = CourseContainerFragment() @@ -303,7 +302,7 @@ fun CourseDashboard( val refreshing by viewModel.refreshing.collectAsState(true) val courseImage by viewModel.courseImage.collectAsState() val uiMessage by viewModel.uiMessage.collectAsState(null) - val openTab = bundle.getString(CourseContainerFragment.ARG_OPEN_TAB, CourseContainerFragment.DEFAULT_TAB) + val openTab = bundle.getString(CourseContainerFragment.ARG_OPEN_TAB, CourseContainerTab.HOME.name) val requiredTab = when (openTab.uppercase()) { CourseContainerTab.HOME.name -> CourseContainerTab.HOME CourseContainerTab.VIDEOS.name -> CourseContainerTab.VIDEOS diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerTab.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerTab.kt index abd5babdc..fbdbb60fc 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerTab.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerTab.kt @@ -14,7 +14,7 @@ import org.openedx.course.R enum class CourseContainerTab( @StringRes override val labelResId: Int, - override val icon: ImageVector + override val icon: ImageVector, ) : TabItem { HOME(R.string.course_container_nav_home, Icons.Default.Home), VIDEOS(R.string.course_container_nav_videos, Icons.Rounded.PlayCircleFilled), diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt index af209660e..96945e7bc 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt @@ -70,7 +70,7 @@ class CourseContainerViewModel( private val coursePreferences: CoursePreferences, private val courseAnalytics: CourseAnalytics, private val imageProcessor: ImageProcessor, - val courseRouter: CourseRouter + val courseRouter: CourseRouter, ) : BaseViewModel() { private val _dataReady = MutableLiveData() diff --git a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt index 7896e2dca..6a1a1bf9e 100644 --- a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt @@ -78,10 +78,10 @@ import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography import org.openedx.core.ui.windowSizeValue +import org.openedx.core.utils.FileUtil import org.openedx.course.R import org.openedx.course.presentation.CourseRouter import org.openedx.course.presentation.ui.CardArrow -import java.io.File import org.openedx.core.R as CoreR class CourseSectionFragment : Fragment() { @@ -134,11 +134,7 @@ class CourseSectionFragment : Fragment() { viewModel.removeDownloadModels(it.id) } else { viewModel.saveDownloadModels( - requireContext().externalCacheDir.toString() + - File.separator + - requireContext() - .getString(org.openedx.core.R.string.app_name) - .replace(Regex("\\s"), "_"), it.id + FileUtil(context).getExternalAppDir().path, it.id ) } } diff --git a/dashboard/src/main/java/org/openedx/DashboardNavigator.kt b/dashboard/src/main/java/org/openedx/DashboardNavigator.kt new file mode 100644 index 000000000..9e5f4c900 --- /dev/null +++ b/dashboard/src/main/java/org/openedx/DashboardNavigator.kt @@ -0,0 +1,17 @@ +package org.openedx + +import androidx.fragment.app.Fragment +import org.openedx.core.config.DashboardConfig +import org.openedx.dashboard.presentation.DashboardListFragment +import org.openedx.learn.presentation.LearnFragment + +class DashboardNavigator( + private val dashboardType: DashboardConfig.DashboardType, +) { + fun getDashboardFragment(): Fragment { + return when (dashboardType) { + DashboardConfig.DashboardType.GALLERY -> LearnFragment() + else -> DashboardListFragment() + } + } +} diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesAction.kt b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesAction.kt new file mode 100644 index 000000000..7655fd6a2 --- /dev/null +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesAction.kt @@ -0,0 +1,14 @@ +package org.openedx.courses.presentation + +import org.openedx.core.domain.model.EnrolledCourse +import org.openedx.dashboard.domain.CourseStatusFilter + +interface AllEnrolledCoursesAction { + object Reload : AllEnrolledCoursesAction + object SwipeRefresh : AllEnrolledCoursesAction + object EndOfPage : AllEnrolledCoursesAction + object Back : AllEnrolledCoursesAction + object Search : AllEnrolledCoursesAction + data class OpenCourse(val enrolledCourse: EnrolledCourse) : AllEnrolledCoursesAction + data class FilterChange(val courseStatusFilter: CourseStatusFilter?) : AllEnrolledCoursesAction +} 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 64ce4baa4..e59a73fde 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt @@ -1,85 +1,12 @@ package org.openedx.courses.presentation -import android.content.res.Configuration.UI_MODE_NIGHT_NO -import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup -import androidx.compose.foundation.ExperimentalFoundationApi -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.calculateEndPadding -import androidx.compose.foundation.layout.calculateStartPadding -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.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items -import androidx.compose.foundation.lazy.grid.rememberLazyGridState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Scaffold -import androidx.compose.material.Surface -import androidx.compose.material.pullrefresh.PullRefreshIndicator -import androidx.compose.material.pullrefresh.pullRefresh -import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material.rememberScaffoldState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.testTagsAsResourceId -import androidx.compose.ui.tooling.preview.Devices -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import org.koin.androidx.compose.koinViewModel -import org.openedx.core.UIMessage -import org.openedx.core.domain.model.Certificate -import org.openedx.core.domain.model.CourseAssignments -import org.openedx.core.domain.model.CourseSharingUtmParameters -import org.openedx.core.domain.model.CourseStatus -import org.openedx.core.domain.model.CoursewareAccess -import org.openedx.core.domain.model.EnrolledCourse -import org.openedx.core.domain.model.EnrolledCourseData -import org.openedx.core.domain.model.Progress -import org.openedx.core.ui.BackBtn -import org.openedx.core.ui.HandleUIMessage -import org.openedx.core.ui.OfflineModeDialog -import org.openedx.core.ui.RoundTabsBar -import org.openedx.core.ui.displayCutoutForLandscape -import org.openedx.core.ui.rememberWindowSize -import org.openedx.core.ui.shouldLoadMore -import org.openedx.core.ui.statusBarsInset import org.openedx.core.ui.theme.OpenEdXTheme -import org.openedx.core.ui.theme.appColors -import org.openedx.core.ui.theme.appShapes -import org.openedx.core.ui.windowSizeValue -import org.openedx.dashboard.domain.CourseStatusFilter -import java.util.Date class AllEnrolledCoursesFragment : Fragment() { @@ -98,378 +25,3 @@ class AllEnrolledCoursesFragment : Fragment() { } } } - -@Composable -private fun AllEnrolledCoursesView( - viewModel: AllEnrolledCoursesViewModel = koinViewModel(), - fragmentManager: FragmentManager -) { - val uiState by viewModel.uiState.collectAsState() - val uiMessage by viewModel.uiMessage.collectAsState(null) - - AllEnrolledCoursesView( - apiHostUrl = viewModel.apiHostUrl, - state = uiState, - uiMessage = uiMessage, - hasInternetConnection = viewModel.hasInternetConnection, - onAction = { action -> - when (action) { - AllEnrolledCoursesAction.Reload -> { - viewModel.getCourses() - } - - AllEnrolledCoursesAction.SwipeRefresh -> { - viewModel.updateCourses() - } - - AllEnrolledCoursesAction.EndOfPage -> { - viewModel.fetchMore() - } - - AllEnrolledCoursesAction.Back -> { - fragmentManager.popBackStack() - } - - AllEnrolledCoursesAction.Search -> { - viewModel.navigateToCourseSearch(fragmentManager) - } - - is AllEnrolledCoursesAction.OpenCourse -> { - with(action.enrolledCourse) { - viewModel.navigateToCourseOutline( - fragmentManager, - course.id, - course.name, - mode - ) - } - } - - is AllEnrolledCoursesAction.FilterChange -> { - viewModel.getCourses(action.courseStatusFilter) - } - } - } - ) -} - -@OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) -@Composable -private fun AllEnrolledCoursesView( - apiHostUrl: String, - state: AllEnrolledCoursesUIState, - uiMessage: UIMessage?, - hasInternetConnection: Boolean, - 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 = state.refreshing, - onRefresh = { onAction(AllEnrolledCoursesAction.SwipeRefresh) } - ) - val tabPagerState = rememberPagerState(pageCount = { - CourseStatusFilter.entries.size - }) - var isInternetConnectionShown by rememberSaveable { - mutableStateOf(false) - } - val firstVisibleIndex = remember { - mutableIntStateOf(scrollState.firstVisibleItemIndex) - } - - Scaffold( - scaffoldState = scaffoldState, - modifier = Modifier - .fillMaxSize() - .semantics { - testTagsAsResourceId = true - }, - backgroundColor = MaterialTheme.appColors.background - ) { paddingValues -> - val contentPaddings by remember(key1 = windowSize) { - mutableStateOf( - windowSize.windowSizeValue( - expanded = PaddingValues( - 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( - expanded = Modifier.padding( - top = 32.dp, - bottom = 40.dp - ), - compact = Modifier.padding(horizontal = 24.dp, vertical = 24.dp) - ) - ) - } - - val contentWidth by remember(key1 = windowSize) { - mutableStateOf( - windowSize.windowSizeValue( - expanded = Modifier.widthIn(Dp.Unspecified, 650.dp), - compact = Modifier.fillMaxWidth(), - ) - ) - } - - HandleUIMessage(uiMessage = uiMessage, scaffoldState = scaffoldState) - Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - contentAlignment = Alignment.TopCenter - ) { - Column( - modifier = Modifier - .statusBarsInset() - .displayCutoutForLandscape() - .then(contentWidth), - horizontalAlignment = Alignment.CenterHorizontally - ) { - 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), - ) { - Column( - modifier = Modifier - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Header( - modifier = Modifier - .padding( - start = contentPaddings.calculateStartPadding(layoutDirection), - end = contentPaddings.calculateEndPadding(layoutDirection) - ), - onSearchClick = { - onAction(AllEnrolledCoursesAction.Search) - } - ) - 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)) - } - ) - when { - state.showProgress -> { - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator(color = MaterialTheme.appColors.primary) - } - } - - !state.courses.isNullOrEmpty() -> { - 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 (state.canLoadMore) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(180.dp), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator( - color = MaterialTheme.appColors.primary - ) - } - } - } - } - ) - } - if (scrollState.shouldLoadMore(firstVisibleIndex, 4)) { - onAction(AllEnrolledCoursesAction.EndOfPage) - } - } - } - - state.courses?.isEmpty() == true -> { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column( - modifier = Modifier - .fillMaxHeight() - .then(emptyStatePaddings) - ) { - EmptyState( - currentCourseStatus = CourseStatusFilter.entries[tabPagerState.currentPage] - ) - } - } - } - } - } - PullRefreshIndicator( - state.refreshing, - pullRefreshState, - Modifier.align(Alignment.TopCenter) - ) - - if (!isInternetConnectionShown && !hasInternetConnection) { - OfflineModeDialog( - Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter), - onDismissCLick = { - isInternetConnectionShown = true - }, - onReloadClick = { - isInternetConnectionShown = true - onAction(AllEnrolledCoursesAction.Reload) - } - ) - } - } - } - } - } - } -} - -@Preview(uiMode = UI_MODE_NIGHT_NO) -@Preview(uiMode = UI_MODE_NIGHT_YES) -@Preview(uiMode = UI_MODE_NIGHT_NO, device = Devices.NEXUS_9) -@Preview(uiMode = UI_MODE_NIGHT_YES, device = Devices.NEXUS_9) -@Composable -private fun AllEnrolledCoursesPreview() { - OpenEdXTheme { - AllEnrolledCoursesView( - apiHostUrl = "http://localhost:8000", - state = AllEnrolledCoursesUIState( - courses = listOf( - mockCourseEnrolled, - mockCourseEnrolled, - mockCourseEnrolled, - mockCourseEnrolled, - mockCourseEnrolled, - mockCourseEnrolled - ) - ), - uiMessage = null, - hasInternetConnection = true, - onAction = {} - ) - } -} - -@Preview -@Composable -private fun EmptyStatePreview() { - OpenEdXTheme { - EmptyState( - currentCourseStatus = CourseStatusFilter.COMPLETE - ) - } -} - -private val mockCourseAssignments = CourseAssignments(null, emptyList()) -private val mockCourseEnrolled = EnrolledCourse( - auditAccessExpires = Date(), - created = "created", - certificate = Certificate(""), - mode = "mode", - isActive = true, - progress = Progress.DEFAULT_PROGRESS, - courseStatus = CourseStatus("", emptyList(), "", ""), - courseAssignments = mockCourseAssignments, - course = EnrolledCourseData( - id = "id", - name = "name", - number = "", - org = "Org", - start = Date(), - startDisplay = "", - startType = "", - end = Date(), - dynamicUpgradeDeadline = "", - subscriptionId = "", - coursewareAccess = CoursewareAccess( - true, - "", - "", - "", - "", - "" - ), - media = null, - courseImage = "", - courseAbout = "", - courseSharingUtmParameters = CourseSharingUtmParameters("", ""), - courseUpdates = "", - courseHandouts = "", - discussionUrl = "", - videoOutline = "", - isSelfPaced = false - ) -) diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesView.kt b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesView.kt index 5c13705be..3392ed7bd 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesView.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesView.kt @@ -1,46 +1,401 @@ package org.openedx.courses.presentation +import android.content.res.Configuration +import androidx.compose.foundation.ExperimentalFoundationApi 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.Spacer +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +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.navigationBarsPadding import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.Card +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.LinearProgressIndicator import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Search +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId 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.compose.ui.unit.dp +import androidx.fragment.app.FragmentManager import coil.compose.AsyncImage import coil.request.ImageRequest +import org.koin.androidx.compose.koinViewModel import org.openedx.Lock import org.openedx.core.R +import org.openedx.core.UIMessage +import org.openedx.core.domain.model.Certificate +import org.openedx.core.domain.model.CourseAssignments +import org.openedx.core.domain.model.CourseSharingUtmParameters +import org.openedx.core.domain.model.CourseStatus +import org.openedx.core.domain.model.CoursewareAccess import org.openedx.core.domain.model.EnrolledCourse +import org.openedx.core.domain.model.EnrolledCourseData +import org.openedx.core.domain.model.Progress +import org.openedx.core.ui.BackBtn +import org.openedx.core.ui.HandleUIMessage +import org.openedx.core.ui.OfflineModeDialog +import org.openedx.core.ui.RoundTabsBar +import org.openedx.core.ui.displayCutoutForLandscape +import org.openedx.core.ui.rememberWindowSize +import org.openedx.core.ui.shouldLoadMore +import org.openedx.core.ui.statusBarsInset +import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography +import org.openedx.core.ui.windowSizeValue import org.openedx.core.utils.TimeUtils import org.openedx.dashboard.domain.CourseStatusFilter import java.util.Date +@Composable +fun AllEnrolledCoursesView( + fragmentManager: FragmentManager +) { + val viewModel: AllEnrolledCoursesViewModel = koinViewModel() + val uiState by viewModel.uiState.collectAsState() + val uiMessage by viewModel.uiMessage.collectAsState(null) + + AllEnrolledCoursesView( + apiHostUrl = viewModel.apiHostUrl, + state = uiState, + uiMessage = uiMessage, + hasInternetConnection = viewModel.hasInternetConnection, + onAction = { action -> + when (action) { + AllEnrolledCoursesAction.Reload -> { + viewModel.getCourses() + } + + AllEnrolledCoursesAction.SwipeRefresh -> { + viewModel.updateCourses() + } + + AllEnrolledCoursesAction.EndOfPage -> { + viewModel.fetchMore() + } + + AllEnrolledCoursesAction.Back -> { + fragmentManager.popBackStack() + } + + AllEnrolledCoursesAction.Search -> { + viewModel.navigateToCourseSearch(fragmentManager) + } + + is AllEnrolledCoursesAction.OpenCourse -> { + with(action.enrolledCourse) { + viewModel.navigateToCourseOutline( + fragmentManager, + course.id, + course.name, + mode + ) + } + } + + is AllEnrolledCoursesAction.FilterChange -> { + viewModel.getCourses(action.courseStatusFilter) + } + } + } + ) +} + +@OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) +@Composable +private fun AllEnrolledCoursesView( + apiHostUrl: String, + state: AllEnrolledCoursesUIState, + uiMessage: UIMessage?, + hasInternetConnection: Boolean, + 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 = state.refreshing, + onRefresh = { onAction(AllEnrolledCoursesAction.SwipeRefresh) } + ) + val tabPagerState = rememberPagerState(pageCount = { + CourseStatusFilter.entries.size + }) + var isInternetConnectionShown by rememberSaveable { + mutableStateOf(false) + } + val firstVisibleIndex = remember { + mutableIntStateOf(scrollState.firstVisibleItemIndex) + } + + Scaffold( + scaffoldState = scaffoldState, + modifier = Modifier + .fillMaxSize() + .semantics { + testTagsAsResourceId = true + }, + backgroundColor = MaterialTheme.appColors.background + ) { paddingValues -> + val contentPaddings by remember(key1 = windowSize) { + mutableStateOf( + windowSize.windowSizeValue( + expanded = PaddingValues( + 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( + expanded = Modifier.padding( + top = 32.dp, + bottom = 40.dp + ), + compact = Modifier.padding(horizontal = 24.dp, vertical = 24.dp) + ) + ) + } + + val contentWidth by remember(key1 = windowSize) { + mutableStateOf( + windowSize.windowSizeValue( + expanded = Modifier.widthIn(Dp.Unspecified, 650.dp), + compact = Modifier.fillMaxWidth(), + ) + ) + } + + HandleUIMessage(uiMessage = uiMessage, scaffoldState = scaffoldState) + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + contentAlignment = Alignment.TopCenter + ) { + Column( + modifier = Modifier + .statusBarsInset() + .displayCutoutForLandscape() + .then(contentWidth), + horizontalAlignment = Alignment.CenterHorizontally + ) { + 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), + ) { + Column( + modifier = Modifier + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Header( + modifier = Modifier + .padding( + start = contentPaddings.calculateStartPadding(layoutDirection), + end = contentPaddings.calculateEndPadding(layoutDirection) + ), + onSearchClick = { + onAction(AllEnrolledCoursesAction.Search) + } + ) + 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)) + } + ) + when { + state.showProgress -> { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = MaterialTheme.appColors.primary) + } + } + + !state.courses.isNullOrEmpty() -> { + 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 (state.canLoadMore) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(180.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + color = MaterialTheme.appColors.primary + ) + } + } + } + } + ) + } + if (scrollState.shouldLoadMore(firstVisibleIndex, 4)) { + onAction(AllEnrolledCoursesAction.EndOfPage) + } + } + } + + state.courses?.isEmpty() == true -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier + .fillMaxHeight() + .then(emptyStatePaddings) + ) { + EmptyState( + currentCourseStatus = CourseStatusFilter.entries[tabPagerState.currentPage] + ) + } + } + } + } + } + PullRefreshIndicator( + state.refreshing, + pullRefreshState, + Modifier.align(Alignment.TopCenter) + ) + + if (!isInternetConnectionShown && !hasInternetConnection) { + OfflineModeDialog( + Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter), + onDismissCLick = { + isInternetConnectionShown = true + }, + onReloadClick = { + isInternetConnectionShown = true + onAction(AllEnrolledCoursesAction.Reload) + } + ) + } + } + } + } + } + } +} + @Composable fun CourseItem( modifier: Modifier = Modifier, @@ -191,4 +546,94 @@ fun EmptyState( ) } } -} \ No newline at end of file +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun CourseItemPreview() { + OpenEdXTheme { + CourseItem( + course = mockCourseEnrolled, + apiHostUrl = "", + onClick = {} + ) + } +} + +@Preview +@Composable +private fun EmptyStatePreview() { + OpenEdXTheme { + EmptyState( + currentCourseStatus = CourseStatusFilter.COMPLETE + ) + } +} + +@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 AllEnrolledCoursesPreview() { + OpenEdXTheme { + AllEnrolledCoursesView( + apiHostUrl = "http://localhost:8000", + state = AllEnrolledCoursesUIState( + courses = listOf( + mockCourseEnrolled, + mockCourseEnrolled, + mockCourseEnrolled, + mockCourseEnrolled, + mockCourseEnrolled, + mockCourseEnrolled + ) + ), + uiMessage = null, + hasInternetConnection = true, + onAction = {} + ) + } +} + +private val mockCourseAssignments = CourseAssignments(null, emptyList()) +private val mockCourseEnrolled = EnrolledCourse( + auditAccessExpires = Date(), + created = "created", + certificate = Certificate(""), + mode = "mode", + isActive = true, + progress = Progress.DEFAULT_PROGRESS, + courseStatus = CourseStatus("", emptyList(), "", ""), + courseAssignments = mockCourseAssignments, + course = EnrolledCourseData( + id = "id", + name = "name", + number = "", + org = "Org", + start = Date(), + startDisplay = "", + startType = "", + end = Date(), + dynamicUpgradeDeadline = "", + subscriptionId = "", + coursewareAccess = CoursewareAccess( + false, + "204", + "", + "", + "", + "" + ), + media = null, + courseImage = "", + courseAbout = "", + courseSharingUtmParameters = CourseSharingUtmParameters("", ""), + courseUpdates = "", + courseHandouts = "", + discussionUrl = "", + videoOutline = "", + isSelfPaced = false + ) +) diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt index 59d910bd7..6f3f96ebf 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt @@ -179,13 +179,3 @@ class AllEnrolledCoursesViewModel( ) } } - -interface AllEnrolledCoursesAction { - object Reload : AllEnrolledCoursesAction - object SwipeRefresh : AllEnrolledCoursesAction - object EndOfPage : AllEnrolledCoursesAction - object Back : AllEnrolledCoursesAction - object Search : AllEnrolledCoursesAction - data class OpenCourse(val enrolledCourse: EnrolledCourse) : AllEnrolledCoursesAction - data class FilterChange(val courseStatusFilter: CourseStatusFilter?) : AllEnrolledCoursesAction -} diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/CourseTab.kt b/dashboard/src/main/java/org/openedx/courses/presentation/CourseTab.kt new file mode 100644 index 000000000..f0da7c186 --- /dev/null +++ b/dashboard/src/main/java/org/openedx/courses/presentation/CourseTab.kt @@ -0,0 +1,5 @@ +package org.openedx.courses.presentation + +enum class CourseTab { + HOME, VIDEOS, DATES, DISCUSSIONS, MORE +} diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryScreenAction.kt b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryScreenAction.kt new file mode 100644 index 000000000..f612a5289 --- /dev/null +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryScreenAction.kt @@ -0,0 +1,13 @@ +package org.openedx.courses.presentation + +import org.openedx.core.domain.model.EnrolledCourse + +interface DashboardGalleryScreenAction { + object SwipeRefresh : DashboardGalleryScreenAction + object ViewAll : DashboardGalleryScreenAction + object Reload : DashboardGalleryScreenAction + object NavigateToDiscovery : DashboardGalleryScreenAction + data class OpenBlock(val enrolledCourse: EnrolledCourse, val blockId: String) : DashboardGalleryScreenAction + data class OpenCourse(val enrolledCourse: EnrolledCourse) : DashboardGalleryScreenAction + data class NavigateToDates(val enrolledCourse: EnrolledCourse) : DashboardGalleryScreenAction +} diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt index e39537427..c4ea029b9 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt @@ -97,9 +97,9 @@ import org.openedx.core.R as CoreR @Composable fun DashboardGalleryView( - viewModel: DashboardGalleryViewModel = koinViewModel(), fragmentManager: FragmentManager, ) { + val viewModel: DashboardGalleryViewModel = koinViewModel() val updating by viewModel.updating.collectAsState(false) val uiMessage by viewModel.uiMessage.collectAsState(null) val uiState by viewModel.uiState.collectAsState(DashboardGalleryUIState.Loading) diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt index 69b5dc4d5..6ff7ba3fd 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt @@ -32,16 +32,13 @@ class DashboardGalleryViewModel( private val discoveryNotifier: DiscoveryNotifier, private val networkConnection: NetworkConnection, private val fileUtil: FileUtil, - private val dashboardRouter: DashboardRouter + private val dashboardRouter: DashboardRouter, ) : BaseViewModel() { - companion object { - private const val DATES_TAB = "dates" - } - val apiHostUrl get() = config.getApiHostURL() - private val _uiState = MutableStateFlow(DashboardGalleryUIState.Loading) + private val _uiState = + MutableStateFlow(DashboardGalleryUIState.Loading) val uiState: StateFlow get() = _uiState.asStateFlow() @@ -76,7 +73,8 @@ class DashboardGalleryViewModel( if (courseEnrollments == null) { _uiState.value = DashboardGalleryUIState.Empty } else { - _uiState.value = DashboardGalleryUIState.Courses(courseEnrollments.mapToDomain()) + _uiState.value = + DashboardGalleryUIState.Courses(courseEnrollments.mapToDomain()) } } } catch (e: Exception) { @@ -108,14 +106,14 @@ class DashboardGalleryViewModel( fragmentManager: FragmentManager, enrolledCourse: EnrolledCourse, openDates: Boolean = false, - resumeBlockId: String = "" + resumeBlockId: String = "", ) { dashboardRouter.navigateToCourseOutline( fm = fragmentManager, courseId = enrolledCourse.course.id, courseTitle = enrolledCourse.course.name, enrollmentMode = enrolledCourse.mode, - openTab = if (openDates) DATES_TAB else "", + openTab = if (openDates) CourseTab.DATES.name else CourseTab.HOME.name, resumeBlockId = resumeBlockId ) } @@ -130,13 +128,3 @@ class DashboardGalleryViewModel( } } } - -interface DashboardGalleryScreenAction { - object SwipeRefresh : DashboardGalleryScreenAction - object ViewAll : DashboardGalleryScreenAction - object Reload : DashboardGalleryScreenAction - object NavigateToDiscovery : DashboardGalleryScreenAction - data class OpenBlock(val enrolledCourse: EnrolledCourse, val blockId: String) : DashboardGalleryScreenAction - data class OpenCourse(val enrolledCourse: EnrolledCourse) : DashboardGalleryScreenAction - data class NavigateToDates(val enrolledCourse: EnrolledCourse) : DashboardGalleryScreenAction -} diff --git a/dashboard/src/main/java/org/openedx/dashboard/domain/CourseStatusFilter.kt b/dashboard/src/main/java/org/openedx/dashboard/domain/CourseStatusFilter.kt index a61fc2a1f..79a19b89d 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/domain/CourseStatusFilter.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/domain/CourseStatusFilter.kt @@ -9,7 +9,7 @@ enum class CourseStatusFilter( val key: String, @StringRes override val labelResId: Int, - override val icon: ImageVector? = null + override val icon: ImageVector? = null, ) : TabItem { ALL("all", R.string.dashboard_course_filter_all), IN_PROGRESS("in_progress", R.string.dashboard_course_filter_in_progress), diff --git a/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt b/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt index 04146c103..ae2e94d93 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/domain/interactor/DashboardInteractor.kt @@ -5,7 +5,7 @@ import org.openedx.dashboard.data.repository.DashboardRepository import org.openedx.dashboard.domain.CourseStatusFilter class DashboardInteractor( - private val repository: DashboardRepository + private val repository: DashboardRepository, ) { suspend fun getEnrolledCourses(page: Int): DashboardCourseList { @@ -16,7 +16,10 @@ class DashboardInteractor( suspend fun getMainUserCourses() = repository.getMainUserCourses() - suspend fun getAllUserCourses(page: Int = 1, status: CourseStatusFilter? = null): DashboardCourseList { + suspend fun getAllUserCourses( + page: Int = 1, + status: CourseStatusFilter? = null, + ): DashboardCourseList { return repository.getAllUserCourses( page, status 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 929edae7d..b2de66cd4 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt @@ -92,10 +92,10 @@ class LearnFragment : Fragment(R.layout.fragment_learn) { @Composable private fun Header( - viewModel: LearnViewModel = koinViewModel(), fragmentManager: FragmentManager, viewPager: ViewPager2 ) { + val viewModel: LearnViewModel = koinViewModel() val windowSize = rememberWindowSize() val contentWidth by remember(key1 = windowSize) { mutableStateOf( diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml index 6c68daf93..139652ca6 100644 --- a/default_config/dev/config.yaml +++ b/default_config/dev/config.yaml @@ -29,7 +29,7 @@ PROGRAM: PROGRAM_DETAIL_URL_TEMPLATE: '' DASHBOARD: - TYPE: 'primary_course' + TYPE: 'gallery' FIREBASE: ENABLED: false