From a986adf65b42b51bde968b738eb972d21ce3eb42 Mon Sep 17 00:00:00 2001 From: hryh27 <128455389+hryh27@users.noreply.github.com> Date: Wed, 22 Mar 2023 17:15:35 +0200 Subject: [PATCH 01/20] discussion button bottom padding (#1) Co-authored-by: Serh Hryhorchuk --- .../presentation/threads/DiscussionThreadsFragment.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsFragment.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsFragment.kt index 6601ea4aa..ef7e7bf17 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsFragment.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsFragment.kt @@ -437,7 +437,9 @@ private fun DiscussionThreadsScreen( } } NewEdxButton( - width = Modifier.widthIn(184.dp, Dp.Unspecified), + width = Modifier + .padding(bottom = 24.dp) + .widthIn(184.dp, Dp.Unspecified), text = stringResource(id = discussionR.string.discussion_create_post), onClick = { onCreatePostClick() @@ -456,7 +458,6 @@ private fun DiscussionThreadsScreen( ) } ) - Spacer(Modifier.height(24.dp)) } } is DiscussionThreadsUIState.Loading -> { From 32f620df85bff4cf65865d5e241e51cc507970d1 Mon Sep 17 00:00:00 2001 From: hryh27 <128455389+hryh27@users.noreply.github.com> Date: Wed, 22 Mar 2023 17:17:15 +0200 Subject: [PATCH 02/20] text field cursor position (#2) Co-authored-by: Serh Hryhorchuk --- .../presentation/signin/SignInFragment.kt | 9 +++-- .../auth/presentation/ui/AuthUI.kt | 13 ++++--- .../com/raccoongang/core/ui/ComposeCommon.kt | 34 ++++++++++++------- .../search/CourseSearchFragment.kt | 17 +++++----- .../search/DiscussionSearchThreadFragment.kt | 17 +++++----- 5 files changed, 52 insertions(+), 38 deletions(-) diff --git a/auth/src/main/java/com/raccoongang/auth/presentation/signin/SignInFragment.kt b/auth/src/main/java/com/raccoongang/auth/presentation/signin/SignInFragment.kt index d5e4c00bf..494becbd8 100644 --- a/auth/src/main/java/com/raccoongang/auth/presentation/signin/SignInFragment.kt +++ b/auth/src/main/java/com/raccoongang/auth/presentation/signin/SignInFragment.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp @@ -274,7 +275,11 @@ private fun PasswordTextField( onValueChanged: (String) -> Unit, onPressDone: () -> Unit, ) { - var passwordTextFieldValue by rememberSaveable { mutableStateOf("") } + var passwordTextFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf( + TextFieldValue("") + ) + } val focusManager = LocalFocusManager.current Text( modifier = Modifier.fillMaxWidth(), @@ -288,7 +293,7 @@ private fun PasswordTextField( value = passwordTextFieldValue, onValueChange = { passwordTextFieldValue = it - onValueChanged(it.trim()) + onValueChanged(it.text.trim()) }, colors = TextFieldDefaults.outlinedTextFieldColors( unfocusedBorderColor = MaterialTheme.appColors.textFieldBorder, diff --git a/auth/src/main/java/com/raccoongang/auth/presentation/ui/AuthUI.kt b/auth/src/main/java/com/raccoongang/auth/presentation/ui/AuthUI.kt index f07973dc8..415fd7ad2 100644 --- a/auth/src/main/java/com/raccoongang/auth/presentation/ui/AuthUI.kt +++ b/auth/src/main/java/com/raccoongang/auth/presentation/ui/AuthUI.kt @@ -25,10 +25,7 @@ import androidx.compose.ui.focus.focusTarget import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.input.* import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.raccoongang.auth.R @@ -194,8 +191,10 @@ fun LoginTextField( imeAction: ImeAction = ImeAction.Next, keyboardActions: (FocusManager) -> Unit = { it.moveFocus(FocusDirection.Down) } ) { - var loginTextFieldValue by rememberSaveable { - mutableStateOf("") + var loginTextFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf( + TextFieldValue("") + ) } val focusManager = LocalFocusManager.current Text( @@ -209,7 +208,7 @@ fun LoginTextField( value = loginTextFieldValue, onValueChange = { loginTextFieldValue = it - onValueChanged(it.trim()) + onValueChanged(it.text.trim()) }, colors = TextFieldDefaults.outlinedTextFieldColors( unfocusedBorderColor = MaterialTheme.appColors.textFieldBorder, diff --git a/core/src/main/java/com/raccoongang/core/ui/ComposeCommon.kt b/core/src/main/java/com/raccoongang/core/ui/ComposeCommon.kt index 28f759104..83d63b68f 100644 --- a/core/src/main/java/com/raccoongang/core/ui/ComposeCommon.kt +++ b/core/src/main/java/com/raccoongang/core/ui/ComposeCommon.kt @@ -35,15 +35,12 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.* import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp @@ -105,11 +102,11 @@ fun StaticSearchBar( @Composable fun SearchBar( modifier: Modifier, - searchValue: String, + searchValue: TextFieldValue, requestFocus: Boolean = false, label: String = stringResource(id = R.string.core_search), keyboardActions: () -> Unit, - onValueChanged: (String) -> Unit = {}, + onValueChanged: (TextFieldValue) -> Unit = {}, onClearValue: () -> Unit, ) { val keyboardController = LocalSoftwareKeyboardController.current @@ -123,6 +120,9 @@ fun SearchBar( var isFocused by rememberSaveable { mutableStateOf(false) } + var textFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(searchValue) + } OutlinedTextField( modifier = Modifier .focusRequester(focusRequester) @@ -132,9 +132,14 @@ fun SearchBar( .clip(MaterialTheme.appShapes.textFieldShape) .then(modifier), shape = MaterialTheme.appShapes.textFieldShape, - value = searchValue, + value = textFieldValue, onValueChange = { - onValueChanged(it) + if (it.text != textFieldValue.text) { + textFieldValue = it + onValueChanged(textFieldValue) + } else { + textFieldValue = it + } }, colors = TextFieldDefaults.outlinedTextFieldColors( textColor = MaterialTheme.appColors.textPrimary, @@ -161,8 +166,9 @@ fun SearchBar( ) }, trailingIcon = { - if (searchValue.isNotEmpty()) { + if (searchValue.text.isNotEmpty()) { IconButton(onClick = { + textFieldValue = TextFieldValue("") onClearValue() }) { Icon( @@ -476,8 +482,10 @@ fun NewEdxOutlinedTextField( keyboardActions: (FocusManager) -> Unit = { it.moveFocus(FocusDirection.Down) }, onValueChanged: (String) -> Unit, ) { - var inputFieldValue by rememberSaveable { - mutableStateOf("") + var inputFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf( + TextFieldValue("") + ) } val focusManager = LocalFocusManager.current @@ -502,7 +510,7 @@ fun NewEdxOutlinedTextField( value = inputFieldValue, onValueChange = { inputFieldValue = it - onValueChanged(it) + onValueChanged(it.text) }, colors = TextFieldDefaults.outlinedTextFieldColors( unfocusedBorderColor = MaterialTheme.appColors.textFieldBorder, @@ -859,7 +867,7 @@ private fun SearchBarPreview() { modifier = Modifier .fillMaxWidth() .height(48.dp), - searchValue = "", + searchValue = TextFieldValue(), keyboardActions = {}, onClearValue = {} ) diff --git a/discovery/src/main/java/com/raccoongang/discovery/presentation/search/CourseSearchFragment.kt b/discovery/src/main/java/com/raccoongang/discovery/presentation/search/CourseSearchFragment.kt index 4526c6fb2..ce2c5897b 100644 --- a/discovery/src/main/java/com/raccoongang/discovery/presentation/search/CourseSearchFragment.kt +++ b/discovery/src/main/java/com/raccoongang/discovery/presentation/search/CourseSearchFragment.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Devices @@ -123,8 +124,8 @@ private fun CourseSearchScreen( val pullRefreshState = rememberPullRefreshState(refreshing = refreshing, onRefresh = { onSwipeRefresh() }) - var searchText by rememberSaveable { - mutableStateOf("") + var textFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue("")) } val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current @@ -217,17 +218,17 @@ private fun CourseSearchScreen( .then(searchTab), label = "", requestFocus = true, - searchValue = searchText, + searchValue = textFieldValue, keyboardActions = { focusManager.clearFocus() }, onValueChanged = { text -> - searchText = text - onSearchTextChanged(searchText) + textFieldValue = text + onSearchTextChanged(textFieldValue.text) }, onClearValue = { - searchText = "" - onSearchTextChanged(searchText) + textFieldValue = TextFieldValue("") + onSearchTextChanged(textFieldValue.text) } ) Spacer(modifier = Modifier.height(20.dp)) @@ -241,7 +242,7 @@ private fun CourseSearchScreen( .pullRefresh(pullRefreshState) ) { val typingText = - if (searchText.isEmpty()) { + if (textFieldValue.text.isEmpty()) { stringResource(id = discoveryR.string.discovery_start_typing_to_find) } else { pluralStringResource( diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/search/DiscussionSearchThreadFragment.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/search/DiscussionSearchThreadFragment.kt index af31a97e2..38f801022 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/search/DiscussionSearchThreadFragment.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/search/DiscussionSearchThreadFragment.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Devices @@ -142,8 +143,8 @@ private fun DiscussionSearchThreadScreen( val pullRefreshState = rememberPullRefreshState(refreshing = refreshing, onRefresh = { onSwipeRefresh() }) - var searchText by rememberSaveable { - mutableStateOf("") + var textFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue("")) } val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current @@ -234,17 +235,17 @@ private fun DiscussionSearchThreadScreen( .then(searchTab), label = "", requestFocus = true, - searchValue = searchText, + searchValue = textFieldValue, keyboardActions = { focusManager.clearFocus() }, onValueChanged = { text -> - searchText = text - onSearchTextChanged(searchText) + textFieldValue = text + onSearchTextChanged(textFieldValue.text) }, onClearValue = { - searchText = "" - onSearchTextChanged(searchText) + textFieldValue = TextFieldValue("") + onSearchTextChanged(textFieldValue.text) } ) Spacer(modifier = Modifier.height(20.dp)) @@ -258,7 +259,7 @@ private fun DiscussionSearchThreadScreen( .pullRefresh(pullRefreshState) ) { val typingText = - if (searchText.isEmpty()) { + if (textFieldValue.text.isEmpty()) { stringResource(id = discussionR.string.discussion_start_typing_to_find) } else { pluralStringResource( From 7b340cd9c3d46515a001d58fc1cf2d7c713bea9c Mon Sep 17 00:00:00 2001 From: hryh27 <128455389+hryh27@users.noreply.github.com> Date: Thu, 23 Mar 2023 16:48:48 +0200 Subject: [PATCH 03/20] bug fix (#3) * fix bugs * fix video player --- app/build.gradle | 1 - .../com/raccoongang/newedx/di/ScreenModule.kt | 2 +- build.gradle | 2 + core/build.gradle | 1 + .../raccoongang/core/extension/FragmentExt.kt | 28 ++++++++ .../core/system/notifier/CourseNotifier.kt | 1 + .../core/system/notifier/CoursePauseVideo.kt | 3 + .../detail/CourseDetailsFragment.kt | 22 ++++++- .../presentation/handouts/WebViewFragment.kt | 19 ++++++ .../outline/CourseOutlineFragment.kt | 7 +- .../container/CourseUnitContainerFragment.kt | 11 ++-- .../container/CourseUnitContainerViewModel.kt | 11 ++++ .../unit/html/HtmlUnitFragment.kt | 19 +++++- .../unit/video/VideoUnitFragment.kt | 66 +++++++++---------- .../unit/video/VideoUnitViewModel.kt | 9 ++- .../unit/video/YoutubeVideoUnitFragment.kt | 15 ++++- .../videos/CourseVideosFragment.kt | 3 +- 17 files changed, 166 insertions(+), 54 deletions(-) create mode 100644 core/src/main/java/com/raccoongang/core/extension/FragmentExt.kt create mode 100644 core/src/main/java/com/raccoongang/core/system/notifier/CoursePauseVideo.kt diff --git a/app/build.gradle b/app/build.gradle index 52af57186..44305da69 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -76,7 +76,6 @@ dependencies { kapt "androidx.room:room-compiler:$room_version" - implementation "androidx.window:window:1.0.0" implementation 'androidx.core:core-splashscreen:1.0.0' testImplementation 'junit:junit:4.13.2' diff --git a/app/src/main/java/com/raccoongang/newedx/di/ScreenModule.kt b/app/src/main/java/com/raccoongang/newedx/di/ScreenModule.kt index ed8b390b6..506e5a522 100644 --- a/app/src/main/java/com/raccoongang/newedx/di/ScreenModule.kt +++ b/app/src/main/java/com/raccoongang/newedx/di/ScreenModule.kt @@ -81,7 +81,7 @@ val screenModule = module { viewModel { (courseId: String) -> CourseOutlineViewModel(courseId, get(), get(), get(), get(), get(), get(), get()) } viewModel { CourseUnitsViewModel(get(), get(),get(), get(), get()) } viewModel { (courseId: String) -> CourseSectionViewModel(get(), get(), get(), get(), get(), get(), courseId) } - viewModel { (courseId: String) -> CourseUnitContainerViewModel(get(), courseId) } + viewModel { (courseId: String) -> CourseUnitContainerViewModel(get(), get(), courseId) } viewModel { (courseId: String) -> CourseVideoViewModel(courseId, get(), get(), get(), get(), get(), get(), get()) } viewModel { (courseId: String) -> VideoViewModel(courseId, get(), get(), get()) } viewModel { (courseId: String) -> VideoUnitViewModel(courseId, get(), get(), get(), get()) } diff --git a/build.gradle b/build.gradle index 589d82e24..2831e9883 100644 --- a/build.gradle +++ b/build.gradle @@ -41,6 +41,8 @@ ext { work_version = '2.8.0' + window_version = '1.0.0' + //testing mockk_version = '1.13.3' android_arch_version = '2.1.0' diff --git a/core/build.gradle b/core/build.gradle index b738d79db..ddabafb98 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -114,6 +114,7 @@ dependencies { api "androidx.fragment:fragment-ktx:$fragment_version" api "androidx.constraintlayout:constraintlayout:$constraintlayout_version" api "androidx.viewpager2:viewpager2:$viewpager2_version" + api "androidx.window:window:$window_version" //Android Jetpack api "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" diff --git a/core/src/main/java/com/raccoongang/core/extension/FragmentExt.kt b/core/src/main/java/com/raccoongang/core/extension/FragmentExt.kt new file mode 100644 index 000000000..adc559577 --- /dev/null +++ b/core/src/main/java/com/raccoongang/core/extension/FragmentExt.kt @@ -0,0 +1,28 @@ +package com.raccoongang.core.extension + +import androidx.fragment.app.Fragment +import androidx.window.layout.WindowMetricsCalculator +import com.raccoongang.core.ui.WindowSize +import com.raccoongang.core.ui.WindowType + +fun Fragment.computeWindowSizeClasses(): WindowSize { + val metrics = WindowMetricsCalculator.getOrCreate() + .computeCurrentWindowMetrics(requireActivity()) + + val widthDp = metrics.bounds.width() / + resources.displayMetrics.density + val widthWindowSize = when { + widthDp < 600f -> WindowType.Compact + widthDp < 840f -> WindowType.Medium + else -> WindowType.Expanded + } + + val heightDp = metrics.bounds.height() / + resources.displayMetrics.density + val heightWindowSize = when { + heightDp < 480f -> WindowType.Compact + heightDp < 900f -> WindowType.Medium + else -> WindowType.Expanded + } + return WindowSize(widthWindowSize, heightWindowSize) +} \ No newline at end of file diff --git a/core/src/main/java/com/raccoongang/core/system/notifier/CourseNotifier.kt b/core/src/main/java/com/raccoongang/core/system/notifier/CourseNotifier.kt index 444a70ef5..196bf379e 100644 --- a/core/src/main/java/com/raccoongang/core/system/notifier/CourseNotifier.kt +++ b/core/src/main/java/com/raccoongang/core/system/notifier/CourseNotifier.kt @@ -13,5 +13,6 @@ class CourseNotifier { suspend fun send(event: CourseVideoPositionChanged) = channel.emit(event) suspend fun send(event: CourseStructureUpdated) = channel.emit(event) suspend fun send(event: CourseDashboardUpdate) = channel.emit(event) + suspend fun send(event: CoursePauseVideo) = channel.emit(event) } \ No newline at end of file diff --git a/core/src/main/java/com/raccoongang/core/system/notifier/CoursePauseVideo.kt b/core/src/main/java/com/raccoongang/core/system/notifier/CoursePauseVideo.kt new file mode 100644 index 000000000..b47475da9 --- /dev/null +++ b/core/src/main/java/com/raccoongang/core/system/notifier/CoursePauseVideo.kt @@ -0,0 +1,3 @@ +package com.raccoongang.core.system.notifier + +class CoursePauseVideo: CourseEvent \ No newline at end of file diff --git a/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt index b42a03d0b..ac7257c41 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt @@ -1,10 +1,13 @@ package com.raccoongang.course.presentation.detail import android.annotation.SuppressLint +import android.content.Intent import android.content.res.Configuration.* +import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup +import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient import androidx.compose.foundation.* @@ -344,8 +347,7 @@ private fun CourseDetailNativeContent( Column { CourseImageHeader( modifier = Modifier - .fillMaxWidth() - .height(imageHeight) + .aspectRatio(1.86f) .padding(6.dp), courseImage = course.media.image?.large, courseCertificate = null @@ -523,6 +525,22 @@ private fun CourseDescription( super.onPageCommitVisible(view, url) onWebPageLoaded() } + + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest? + ): Boolean { + val clickUrl = request?.url?.toString() ?: "" + return if (clickUrl.isNotEmpty() && + (clickUrl.startsWith("http://") || + clickUrl.startsWith("https://")) + ) { + context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) + true + } else { + false + } + } } with(settings) { javaScriptEnabled = true diff --git a/course/src/main/java/com/raccoongang/course/presentation/handouts/WebViewFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/handouts/WebViewFragment.kt index 2dd5433b4..ef302f27e 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/handouts/WebViewFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/handouts/WebViewFragment.kt @@ -1,10 +1,13 @@ package com.raccoongang.course.presentation.handouts import android.annotation.SuppressLint +import android.content.Intent import android.content.res.Configuration +import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup +import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient import androidx.compose.foundation.background @@ -196,6 +199,22 @@ private fun HandoutsContent(body: String, onWebPageLoaded: () -> Unit) { super.onPageCommitVisible(view, url) onWebPageLoaded() } + + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest? + ): Boolean { + val clickUrl = request?.url?.toString() ?: "" + return if (clickUrl.isNotEmpty() && + (clickUrl.startsWith("http://") || + clickUrl.startsWith("https://")) + ) { + context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) + true + } else { + false + } + } } with(settings) { javaScriptEnabled = true diff --git a/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt index f901419a8..b4bef6484 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt @@ -212,8 +212,8 @@ internal fun CourseOutlineScreen( val imageHeight by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( - expanded = 260.dp, - compact = 200.dp + expanded = 300.dp, + compact = 250.dp ) ) } @@ -289,8 +289,7 @@ internal fun CourseOutlineScreen( ) { CourseImageHeader( modifier = Modifier - .fillMaxWidth() - .height(imageHeight) + .aspectRatio(1.86f) .padding(6.dp), courseImage = courseImage, courseCertificate = courseCertificate diff --git a/course/src/main/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerFragment.kt index 5ffc3a9ed..fca1f3e21 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerFragment.kt @@ -15,10 +15,9 @@ import com.raccoongang.core.domain.model.Block import com.raccoongang.core.extension.serializable import com.raccoongang.core.presentation.course.CourseViewMode import com.raccoongang.core.presentation.global.InsetHolder -import com.raccoongang.core.presentation.global.WindowSizeHolder import com.raccoongang.core.presentation.global.viewBinding import com.raccoongang.core.ui.BackBtn -import com.raccoongang.core.ui.WindowSize +import com.raccoongang.core.ui.rememberWindowSize import com.raccoongang.core.ui.theme.NewEdxTheme import com.raccoongang.course.R import com.raccoongang.course.databinding.FragmentCourseUnitContainerBinding @@ -41,8 +40,6 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta parametersOf(requireArguments().getString(ARG_COURSE_ID, "")) } - private var windowSize: WindowSize? = null - private var blockId: String = "" override fun onCreate(savedInstanceState: Bundle?) { @@ -51,7 +48,6 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta blockId = requireArguments().getString(ARG_BLOCK_ID, "") viewModel.loadBlocks(requireArguments().serializable(ARG_MODE)!!) viewModel.setupCurrentIndex(blockId) - windowSize = (requireActivity() as WindowSizeHolder).windowSize } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -85,6 +81,8 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta mutableStateOf(viewModel.hasNextBlock) } + val windowSize = rememberWindowSize() + updateNavigationButtons { prev, next, bool -> prevButtonText = prev nextButtonText = next @@ -92,7 +90,7 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta } NavigationUnitsButtons( - windowSize = windowSize!!, + windowSize = windowSize, prevButtonText = prevButtonText, nextButtonText = nextButtonText, hasNextBlock = hasNextBlock, @@ -120,6 +118,7 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta hasNextBlock = bool } } else { + viewModel.sendEventPauseVideo() val dialog = ChapterEndFragmentDialog.newInstance(block.displayName) dialog.show( requireActivity().supportFragmentManager, diff --git a/course/src/main/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerViewModel.kt b/course/src/main/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerViewModel.kt index e9190d9ae..9aa274b39 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerViewModel.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerViewModel.kt @@ -1,18 +1,23 @@ package com.raccoongang.course.presentation.unit.container +import androidx.lifecycle.viewModelScope import com.raccoongang.core.BaseViewModel import com.raccoongang.core.BlockType import com.raccoongang.core.domain.model.Block import com.raccoongang.core.module.db.DownloadModel import com.raccoongang.core.module.db.DownloadedState import com.raccoongang.core.presentation.course.CourseViewMode +import com.raccoongang.core.system.notifier.CourseNotifier +import com.raccoongang.core.system.notifier.CoursePauseVideo import com.raccoongang.course.domain.interactor.CourseInteractor import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking class CourseUnitContainerViewModel( private val interactor: CourseInteractor, + private val notifier: CourseNotifier, val courseId: String ) : BaseViewModel() { @@ -89,6 +94,12 @@ class CourseUnitContainerViewModel( return null } + fun sendEventPauseVideo() { + viewModelScope.launch { + notifier.send(CoursePauseVideo()) + } + } + private fun updateVerticalIndex(blockId: String) { currentVerticalIndex = blocks.indexOfFirst { it.type == BlockType.VERTICAL && it.descendants.contains(blockId) } diff --git a/course/src/main/java/com/raccoongang/course/presentation/unit/html/HtmlUnitFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/unit/html/HtmlUnitFragment.kt index bf3d7667c..19eb144b0 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/unit/html/HtmlUnitFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/unit/html/HtmlUnitFragment.kt @@ -1,6 +1,8 @@ package com.raccoongang.course.presentation.unit.html import android.annotation.SuppressLint +import android.content.Intent +import android.net.Uri import android.os.Bundle import android.util.Log import android.view.LayoutInflater @@ -12,7 +14,6 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalContext @@ -162,6 +163,22 @@ private fun HTMLContentView( onWebPageLoaded() } + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest? + ): Boolean { + val clickUrl = request?.url?.toString() ?: "" + return if (clickUrl.isNotEmpty() && + (clickUrl.startsWith("http://") || + clickUrl.startsWith("https://")) + ) { + context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) + true + } else { + false + } + } + override fun onReceivedHttpError( view: WebView, request: WebResourceRequest, diff --git a/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitFragment.kt index f28cfa3a6..112675da5 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitFragment.kt @@ -15,8 +15,8 @@ import androidx.fragment.app.Fragment import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.Player +import com.raccoongang.core.extension.computeWindowSizeClasses import com.raccoongang.core.extension.dpToPixel -import com.raccoongang.core.presentation.global.WindowSizeHolder import com.raccoongang.core.presentation.global.viewBinding import com.raccoongang.core.ui.WindowSize import com.raccoongang.core.ui.theme.NewEdxTheme @@ -48,7 +48,7 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - windowSize = (requireActivity() as WindowSizeHolder).windowSize + windowSize = computeWindowSizeClasses() lifecycle.addObserver(viewModel) viewModel.videoUrl = requireArguments().getString(ARG_VIDEO_URL, "") viewModel.isDownloaded = requireArguments().getBoolean(ARG_DOWNLOADED) @@ -73,6 +73,10 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { } } } + + viewModel.isVideoPaused.observe(this) { + exoPlayer?.pause() + } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -139,39 +143,34 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { if (exoPlayer == null) { exoPlayer = ExoPlayer.Builder(requireContext()) .build() - playerView.player = exoPlayer - playerView.setShowNextButton(false) - playerView.setShowPreviousButton(false) - val mediaItem = MediaItem.fromUri(viewModel.videoUrl) - exoPlayer?.setMediaItem(mediaItem, viewModel.currentVideoTime.toLong()) - exoPlayer?.prepare() - exoPlayer?.playWhenReady = true - - playerView.setFullscreenButtonClickListener { isFullScreen -> - router.navigateToFullScreenVideo( - requireActivity().supportFragmentManager, - viewModel.videoUrl, - exoPlayer?.currentPosition ?: 0L, - blockId, - viewModel.courseId - ) - viewModel.fullscreenHandled = true - } + } + playerView.player = exoPlayer + playerView.setShowNextButton(false) + playerView.setShowPreviousButton(false) + val mediaItem = MediaItem.fromUri(viewModel.videoUrl) + exoPlayer?.setMediaItem(mediaItem, viewModel.currentVideoTime) + exoPlayer?.prepare() + exoPlayer?.playWhenReady = !(viewModel.isVideoPaused.value ?: false) + + playerView.setFullscreenButtonClickListener { isFullScreen -> + router.navigateToFullScreenVideo( + requireActivity().supportFragmentManager, + viewModel.videoUrl, + exoPlayer?.currentPosition ?: 0L, + blockId, + viewModel.courseId + ) + viewModel.fullscreenHandled = true + } - exoPlayer?.addListener(object : Player.Listener { - override fun onPlaybackStateChanged(playbackState: Int) { - super.onPlaybackStateChanged(playbackState) - if (playbackState == Player.STATE_ENDED) { - viewModel.markBlockCompleted(blockId) - } + exoPlayer?.addListener(object : Player.Listener { + override fun onPlaybackStateChanged(playbackState: Int) { + super.onPlaybackStateChanged(playbackState) + if (playbackState == Player.STATE_ENDED) { + viewModel.markBlockCompleted(blockId) } - }) - } else { - val mediaItem = MediaItem.fromUri(viewModel.videoUrl) - exoPlayer?.setMediaItem(mediaItem, viewModel.currentVideoTime.toLong()) - exoPlayer?.prepare() - exoPlayer?.playWhenReady = true - } + } + }) } } @@ -190,6 +189,7 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { override fun onDestroyView() { exoPlayer?.release() + exoPlayer = null super.onDestroyView() } diff --git a/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitViewModel.kt b/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitViewModel.kt index 06b3cc155..ed98a2304 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitViewModel.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitViewModel.kt @@ -8,6 +8,7 @@ import com.raccoongang.core.BaseViewModel import com.raccoongang.core.data.storage.PreferencesManager import com.raccoongang.core.system.connection.NetworkConnection import com.raccoongang.core.system.notifier.CourseNotifier +import com.raccoongang.core.system.notifier.CoursePauseVideo import com.raccoongang.core.system.notifier.CourseVideoPositionChanged import com.raccoongang.course.data.repository.CourseRepository import kotlinx.coroutines.delay @@ -36,6 +37,10 @@ class VideoUnitViewModel( val isPopUpViewShow: LiveData get() = _isPopUpViewShow + private val _isVideoPaused = MutableLiveData() + val isVideoPaused: LiveData + get() = _isVideoPaused + val hasInternetConnection: Boolean get() = networkConnection.isOnline() @@ -50,10 +55,12 @@ class VideoUnitViewModel( super.onCreate(owner) viewModelScope.launch { notifier.notifier.collect { - _isUpdated.value = false if (it is CourseVideoPositionChanged && videoUrl == it.videoUrl) { + _isUpdated.value = false currentVideoTime = it.videoTime _isUpdated.value = true + } else if (it is CoursePauseVideo) { + _isVideoPaused.value = true } } } diff --git a/course/src/main/java/com/raccoongang/course/presentation/unit/video/YoutubeVideoUnitFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/unit/video/YoutubeVideoUnitFragment.kt index bf5836a36..84f611d39 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/unit/video/YoutubeVideoUnitFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/unit/video/YoutubeVideoUnitFragment.kt @@ -15,7 +15,7 @@ import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.AbstractYouTubePlayerListener import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.options.IFramePlayerOptions import com.pierfrancescosoffritti.androidyoutubeplayer.core.ui.DefaultPlayerUiController -import com.raccoongang.core.presentation.global.WindowSizeHolder +import com.raccoongang.core.extension.computeWindowSizeClasses import com.raccoongang.core.presentation.global.viewBinding import com.raccoongang.core.ui.WindowSize import com.raccoongang.core.ui.theme.NewEdxTheme @@ -40,12 +40,13 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) private var windowSize: WindowSize? = null private var orientationListener: OrientationEventListener? = null + private var _youTubePlayer: YouTubePlayer? = null private var blockId = "" override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - windowSize = (requireActivity() as WindowSizeHolder).windowSize + windowSize = computeWindowSizeClasses() lifecycle.addObserver(viewModel) viewModel.videoUrl = requireArguments().getString(ARG_VIDEO_URL, "") blockId = requireArguments().getString(ARG_BLOCK_ID, "") @@ -69,6 +70,10 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) } } } + + viewModel.isVideoPaused.observe(this) { + _youTubePlayer?.pause() + } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -125,6 +130,7 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) override fun onReady(youTubePlayer: YouTubePlayer) { super.onReady(youTubePlayer) + _youTubePlayer = youTubePlayer val defPlayerUiController = DefaultPlayerUiController( binding.youtubePlayerView, youTubePlayer @@ -143,6 +149,9 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) val videoId = viewModel.videoUrl.split("watch?v=")[1] youTubePlayer.loadVideo(videoId, viewModel.currentVideoTime.toFloat()) + if (viewModel.isVideoPaused.value == true) { + youTubePlayer.pause() + } } } @@ -150,7 +159,7 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) viewModel.isPopUpViewShow.observe(viewLifecycleOwner) { if (windowSize?.isTablet != true) { - binding.cvRotateHelper?.isVisible = it + binding.cvRotateHelper.isVisible = it } } } diff --git a/course/src/main/java/com/raccoongang/course/presentation/videos/CourseVideosFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/videos/CourseVideosFragment.kt index 2c3250838..602e7a2fa 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/videos/CourseVideosFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/videos/CourseVideosFragment.kt @@ -279,8 +279,7 @@ private fun CourseVideosScreen( is CourseVideosUIState.CourseData -> { CourseImageHeader( modifier = Modifier - .fillMaxWidth() - .height(imageHeight) + .aspectRatio(1.86f) .padding(6.dp), courseImage = courseImage, courseCertificate = courseCertificate From 08dbed760676681fad5988d57793045e70d13ef6 Mon Sep 17 00:00:00 2001 From: Serhii <128455389+hryh27@users.noreply.github.com> Date: Thu, 23 Mar 2023 19:04:06 +0200 Subject: [PATCH 04/20] fix bug with incorrect link onClick inside WebView (#4) --- .../course/presentation/detail/CourseDetailsFragment.kt | 2 +- .../raccoongang/course/presentation/handouts/WebViewFragment.kt | 2 +- .../course/presentation/unit/html/HtmlUnitFragment.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt index ac7257c41..1c16a747c 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt @@ -535,7 +535,7 @@ private fun CourseDescription( (clickUrl.startsWith("http://") || clickUrl.startsWith("https://")) ) { - context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) + context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(clickUrl))) true } else { false diff --git a/course/src/main/java/com/raccoongang/course/presentation/handouts/WebViewFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/handouts/WebViewFragment.kt index ef302f27e..18b1df27f 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/handouts/WebViewFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/handouts/WebViewFragment.kt @@ -209,7 +209,7 @@ private fun HandoutsContent(body: String, onWebPageLoaded: () -> Unit) { (clickUrl.startsWith("http://") || clickUrl.startsWith("https://")) ) { - context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) + context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(clickUrl))) true } else { false diff --git a/course/src/main/java/com/raccoongang/course/presentation/unit/html/HtmlUnitFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/unit/html/HtmlUnitFragment.kt index 19eb144b0..7537af832 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/unit/html/HtmlUnitFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/unit/html/HtmlUnitFragment.kt @@ -172,7 +172,7 @@ private fun HTMLContentView( (clickUrl.startsWith("http://") || clickUrl.startsWith("https://")) ) { - context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) + context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(clickUrl))) true } else { false From f789cb0b14e991b5f5700a7ba337248ff15614d5 Mon Sep 17 00:00:00 2001 From: Serhii <128455389+hryh27@users.noreply.github.com> Date: Fri, 24 Mar 2023 18:31:08 +0200 Subject: [PATCH 05/20] Fix/profile issues (#5) * fix profile issues * AND-258, 252, 250, 253, 254, 256, 260, 259 --- .../com/raccoongang/newedx/AppActivity.kt | 7 +- core/build.gradle | 3 + .../core/presentation/global/AppDataHolder.kt | 9 + .../com/raccoongang/core/ui/ComposeCommon.kt | 17 +- .../com/raccoongang/core/ui/theme/Color.kt | 1 + .../com/raccoongang/core/ui/theme/Theme.kt | 6 +- .../com/raccoongang/core/utils/EmailUtil.kt | 66 +++++++ core/src/main/res/values-uk/strings.xml | 8 +- core/src/main/res/values/strings.xml | 7 + .../delete/DeleteProfileFragment.kt | 3 +- .../presentation/edit/EditProfileFragment.kt | 18 +- .../presentation/profile/ProfileFragment.kt | 166 +++++++++++------- .../res/drawable-night/profile_delete_box.xml | 50 ++++++ profile/src/main/res/values-uk/strings.xml | 1 + profile/src/main/res/values/strings.xml | 3 +- 15 files changed, 279 insertions(+), 86 deletions(-) create mode 100644 core/src/main/java/com/raccoongang/core/presentation/global/AppDataHolder.kt create mode 100644 core/src/main/java/com/raccoongang/core/utils/EmailUtil.kt create mode 100644 profile/src/main/res/drawable-night/profile_delete_box.xml diff --git a/app/src/main/java/com/raccoongang/newedx/AppActivity.kt b/app/src/main/java/com/raccoongang/newedx/AppActivity.kt index 6ce983701..b70455c1e 100644 --- a/app/src/main/java/com/raccoongang/newedx/AppActivity.kt +++ b/app/src/main/java/com/raccoongang/newedx/AppActivity.kt @@ -15,6 +15,8 @@ import androidx.window.layout.WindowMetricsCalculator import com.raccoongang.auth.presentation.signin.SignInFragment import com.raccoongang.core.data.storage.PreferencesManager import com.raccoongang.core.extension.requestApplyInsetsWhenAttached +import com.raccoongang.core.presentation.global.AppData +import com.raccoongang.core.presentation.global.AppDataHolder import com.raccoongang.core.presentation.global.InsetHolder import com.raccoongang.core.presentation.global.WindowSizeHolder import com.raccoongang.core.ui.WindowSize @@ -24,7 +26,7 @@ import com.raccoongang.profile.presentation.ProfileRouter import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel -class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { +class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder, AppDataHolder { override val topInset: Int get() = _insetTop @@ -34,6 +36,9 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { override val windowSize: WindowSize get() = _windowSize + override val appData: AppData + get() = AppData(BuildConfig.VERSION_NAME) + private lateinit var binding: ActivityAppBinding private val preferencesManager by inject() private val viewModel by viewModel() diff --git a/core/build.gradle b/core/build.gradle index ddabafb98..c3e03c43a 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -47,6 +47,7 @@ android { resValue "string", "privacy_policy_link", envUrls.privacyPolicy resValue "string", "terms_of_service_link", envUrls.termsOfService resValue "string", "contact_us_link", envUrls.contactUs + resValue "string", "feedback_email_address", envUrls.FEEDBACK_EMAIL_ADDRESS } develop { def envMap = config.environments.find { it.key == "DEV" } @@ -60,6 +61,7 @@ android { resValue "string", "privacy_policy_link", envUrls.privacyPolicy resValue "string", "terms_of_service_link", envUrls.termsOfService resValue "string", "contact_us_link", envUrls.contactUs + resValue "string", "feedback_email_address", envUrls.FEEDBACK_EMAIL_ADDRESS } stage { def envMap = config.environments.find { it.key == "STAGE" } @@ -73,6 +75,7 @@ android { resValue "string", "privacy_policy_link", envUrls.privacyPolicy resValue "string", "terms_of_service_link", envUrls.termsOfService resValue "string", "contact_us_link", envUrls.contactUs + resValue "string", "feedback_email_address", envUrls.FEEDBACK_EMAIL_ADDRESS } } diff --git a/core/src/main/java/com/raccoongang/core/presentation/global/AppDataHolder.kt b/core/src/main/java/com/raccoongang/core/presentation/global/AppDataHolder.kt new file mode 100644 index 000000000..4a82a928f --- /dev/null +++ b/core/src/main/java/com/raccoongang/core/presentation/global/AppDataHolder.kt @@ -0,0 +1,9 @@ +package com.raccoongang.core.presentation.global + +interface AppDataHolder { + val appData: AppData +} + +data class AppData( + val versionName: String +) \ No newline at end of file diff --git a/core/src/main/java/com/raccoongang/core/ui/ComposeCommon.kt b/core/src/main/java/com/raccoongang/core/ui/ComposeCommon.kt index 83d63b68f..a23ecf397 100644 --- a/core/src/main/java/com/raccoongang/core/ui/ComposeCommon.kt +++ b/core/src/main/java/com/raccoongang/core/ui/ComposeCommon.kt @@ -517,7 +517,7 @@ fun NewEdxOutlinedTextField( textColor = MaterialTheme.appColors.textFieldText, backgroundColor = MaterialTheme.appColors.textFieldBackground, errorBorderColor = MaterialTheme.appColors.error, - ), + ), shape = MaterialTheme.appShapes.textFieldShape, placeholder = { Text( @@ -678,19 +678,20 @@ fun IconText( @Composable fun IconText( + modifier: Modifier = Modifier, text: String, painter: Painter, color: Color, textStyle: TextStyle = MaterialTheme.appTypography.bodySmall, onClick: (() -> Unit)? = null, ) { - val modifier = if (onClick == null) { + val modifierClickable = if (onClick == null) { Modifier } else { Modifier.noRippleClickable { onClick.invoke() } } Row( - modifier = modifier, + modifier = modifier.then(modifierClickable), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp) ) { @@ -782,15 +783,17 @@ fun NewEdxButton( backgroundColor: Color = MaterialTheme.appColors.buttonBackground, content: (@Composable RowScope.() -> Unit)? = null ) { - Button(modifier = Modifier - .then(width) - .height(42.dp), + Button( + modifier = Modifier + .then(width) + .height(42.dp), shape = MaterialTheme.appShapes.buttonShape, colors = ButtonDefaults.buttonColors( backgroundColor = backgroundColor ), enabled = enabled, - onClick = onClick) { + onClick = onClick + ) { if (content == null) { Text( text = text, diff --git a/core/src/main/java/com/raccoongang/core/ui/theme/Color.kt b/core/src/main/java/com/raccoongang/core/ui/theme/Color.kt index adfcaf8cb..d183f5159 100644 --- a/core/src/main/java/com/raccoongang/core/ui/theme/Color.kt +++ b/core/src/main/java/com/raccoongang/core/ui/theme/Color.kt @@ -23,6 +23,7 @@ data class AppColors( val cardViewBackground: Color, val cardViewBorder: Color, + val divider: Color, val certificateForeground: Color, val bottomSheetToggle: Color, diff --git a/core/src/main/java/com/raccoongang/core/ui/theme/Theme.kt b/core/src/main/java/com/raccoongang/core/ui/theme/Theme.kt index 06cafdc86..c51e3236c 100644 --- a/core/src/main/java/com/raccoongang/core/ui/theme/Theme.kt +++ b/core/src/main/java/com/raccoongang/core/ui/theme/Theme.kt @@ -41,8 +41,9 @@ private val DarkColorPalette = AppColors( buttonBackground = Color(0xFF5478F9), buttonText = Color.White, - cardViewBackground = Color(0xE2242A38), + cardViewBackground = Color(0xFF273346), cardViewBorder = Color(0xFF273346), + divider = Color(0xFF4E5A70), certificateForeground = Color(0xD92EB865), bottomSheetToggle = Color(0xFF4E5A70), @@ -81,8 +82,9 @@ private val LightColorPalette = AppColors( buttonBackground = Color(0xFF3C68FF), buttonText = Color.White, - cardViewBackground = Color.White, + cardViewBackground = Color(0xFFF9FAFB), cardViewBorder = Color(0xFFCCD4E0), + divider = Color(0xFFCCD4E0), certificateForeground = Color(0xD94BD191), bottomSheetToggle = Color(0xFF4E5A70), diff --git a/core/src/main/java/com/raccoongang/core/utils/EmailUtil.kt b/core/src/main/java/com/raccoongang/core/utils/EmailUtil.kt new file mode 100644 index 000000000..c43d6fb2d --- /dev/null +++ b/core/src/main/java/com/raccoongang/core/utils/EmailUtil.kt @@ -0,0 +1,66 @@ +package com.raccoongang.core.utils + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.os.Build +import android.widget.Toast +import com.raccoongang.core.R + +object EmailUtil { + + fun showFeedbackScreen( + context: Context, + subject: String, + appVersion: String + ) { + val NEW_LINE = "\n" + val to = context.getString(R.string.feedback_email_address) + val body = StringBuilder() + with(body) { + append("${context.getString(R.string.core_android_os_version)} ${Build.VERSION.RELEASE}") + append(NEW_LINE) + append("${context.getString(R.string.core_app_version)} $appVersion") + append(NEW_LINE) + append("${context.getString(R.string.core_android_device_model)} ${Build.MODEL}") + append(NEW_LINE) + append(NEW_LINE) + append(context.getString(R.string.core_insert_feedback)) + } + sendEmailIntent(context, to, subject, body.toString()) + } + + private fun sendEmailIntent( + context: Context?, + to: String, + subject: String, + email: String + ) { + val emailIntent = Intent(Intent.ACTION_SEND) + emailIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf(to)) + emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject) + emailIntent.putExtra(Intent.EXTRA_TEXT, email) + emailIntent.type = "plain/text" + try { + emailIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context?.let { + // add flag to make sure this call works from non-activity context + val targetIntent = Intent.createChooser( + emailIntent, + it.getString(R.string.core_email_chooser_header) + ) + targetIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + it.startActivity(targetIntent) + } + } catch (ex: ActivityNotFoundException) { + //There is no activity which can perform the intended share Intent + context?.let { + Toast.makeText( + it, it.getString(R.string.core_email_client_not_present), + Toast.LENGTH_SHORT + ).show() + } + } + } + +} \ No newline at end of file diff --git a/core/src/main/res/values-uk/strings.xml b/core/src/main/res/values-uk/strings.xml index b9511606d..8de28cc5b 100644 --- a/core/src/main/res/values-uk/strings.xml +++ b/core/src/main/res/values-uk/strings.xml @@ -29,7 +29,13 @@ Перезавантажити Завантаження у процесі. Обліковий запис користувача не активовано. Будь ласка, спочатку активуйте свій обліковий запис. - + Надіслати електронний лист за допомогою ... + Не встановлено жодного поштового клієнта + Версія додатку: + Android OS: + Модель пристрою: + Відгук: + Відгук клієнта dd MMMM dd MMM yyyy HH:mm diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 44aae1c99..7ce2d1303 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -31,6 +31,13 @@ 540p 720p (Best quality) User account is not activated. Please activate your account first. + Send email using... + No e-mail clients installed + App Version: + Android OS: + Device Model: + Feedback: + Customer Feedback MMMM dd dd MMM yyyy hh:mm aaa diff --git a/profile/src/main/java/com/raccoongang/profile/presentation/delete/DeleteProfileFragment.kt b/profile/src/main/java/com/raccoongang/profile/presentation/delete/DeleteProfileFragment.kt index 73e8bb716..03f769fcb 100644 --- a/profile/src/main/java/com/raccoongang/profile/presentation/delete/DeleteProfileFragment.kt +++ b/profile/src/main/java/com/raccoongang/profile/presentation/delete/DeleteProfileFragment.kt @@ -248,7 +248,8 @@ fun DeleteProfileScreen( Spacer(Modifier.height(38.dp)) NewEdxButton( text = stringResource(id = profileR.string.profile_yes_delete_account), - enabled = uiState !is DeleteProfileFragmentUIState.Loading, + enabled = uiState !is DeleteProfileFragmentUIState.Loading && password.isNotEmpty(), + backgroundColor = MaterialTheme.appColors.error, onClick = { onDeleteClick(password) } diff --git a/profile/src/main/java/com/raccoongang/profile/presentation/edit/EditProfileFragment.kt b/profile/src/main/java/com/raccoongang/profile/presentation/edit/EditProfileFragment.kt index 466ab6f2c..c8f91eeff 100644 --- a/profile/src/main/java/com/raccoongang/profile/presentation/edit/EditProfileFragment.kt +++ b/profile/src/main/java/com/raccoongang/profile/presentation/edit/EditProfileFragment.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalComposeUiApi::class) + package com.raccoongang.profile.presentation.edit import android.content.res.Configuration @@ -36,7 +38,6 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow -import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.* @@ -611,7 +612,8 @@ private fun EditProfileScreen( onValueChanged = { mapFields[BIO] = it }, - mapFields = mapFields + mapFields = mapFields, + onDoneClick = { onSaveClick(mapFields.toMap()) } ) Spacer(Modifier.height(40.dp)) IconText( @@ -780,6 +782,7 @@ private fun ProfileFields( mapFields: MutableMap, onFieldClick: (String) -> Unit, onValueChanged: (String) -> Unit, + onDoneClick: () -> Unit ) { val languageProficiency = (mapFields[LANGUAGE] as List) val lang = if (languageProficiency.isNotEmpty()) { @@ -814,7 +817,8 @@ private fun ProfileFields( .height(132.dp), name = stringResource(id = profileR.string.profile_about_me), initialValue = mapFields[BIO].toString(), - onValueChanged = onValueChanged + onValueChanged = onValueChanged, + onDoneClick = onDoneClick ) } } @@ -887,7 +891,9 @@ private fun InputEditField( initialValue: String, disabled: Boolean = false, onValueChanged: (String) -> Unit, + onDoneClick: () -> Unit ) { + val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current val keyboardType = KeyboardType.Text @@ -919,10 +925,12 @@ private fun InputEditField( }, keyboardOptions = KeyboardOptions.Default.copy( keyboardType = keyboardType, - imeAction = ImeAction.Next + imeAction = ImeAction.Done ), keyboardActions = KeyboardActions { - focusManager.moveFocus(FocusDirection.Down) + keyboardController?.hide() + focusManager.clearFocus() + onDoneClick() }, textStyle = MaterialTheme.appTypography.bodyMedium, modifier = modifier diff --git a/profile/src/main/java/com/raccoongang/profile/presentation/profile/ProfileFragment.kt b/profile/src/main/java/com/raccoongang/profile/presentation/profile/ProfileFragment.kt index e9fc5e465..33cc19caa 100644 --- a/profile/src/main/java/com/raccoongang/profile/presentation/profile/ProfileFragment.kt +++ b/profile/src/main/java/com/raccoongang/profile/presentation/profile/ProfileFragment.kt @@ -25,6 +25,8 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview @@ -37,11 +39,14 @@ import com.raccoongang.core.R import com.raccoongang.core.UIMessage import com.raccoongang.core.domain.model.Account import com.raccoongang.core.domain.model.ProfileImage +import com.raccoongang.core.presentation.global.AppData +import com.raccoongang.core.presentation.global.AppDataHolder import com.raccoongang.core.ui.* import com.raccoongang.core.ui.theme.NewEdxTheme import com.raccoongang.core.ui.theme.appColors import com.raccoongang.core.ui.theme.appShapes import com.raccoongang.core.ui.theme.appTypography +import com.raccoongang.core.utils.EmailUtil import com.raccoongang.profile.presentation.ProfileRouter import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel @@ -73,6 +78,7 @@ class ProfileFragment : Fragment() { windowSize = windowSize, uiState = uiState!!, uiMessage = uiMessage, + appData = (requireActivity() as AppDataHolder).appData, logout = { viewModel.logout() }, @@ -104,6 +110,7 @@ class ProfileFragment : Fragment() { private fun ProfileScreen( windowSize: WindowSize, uiState: ProfileUIState, + appData: AppData, uiMessage: UIMessage?, onVideoSettingsClick: () -> Unit, logout: () -> Unit, @@ -172,20 +179,20 @@ private fun ProfileScreen( style = MaterialTheme.appTypography.titleMedium ) - IconButton( + IconText( modifier = Modifier - .padding(end = 8.dp), + .height(48.dp) + .padding(end = 24.dp), + text = stringResource(com.raccoongang.profile.R.string.profile_edit), + painter = painterResource(id = R.drawable.core_ic_edit), + textStyle = MaterialTheme.appTypography.labelLarge, + color = MaterialTheme.appColors.primary, onClick = { if (uiState is ProfileUIState.Data) { editAccountClicked(uiState.account) } - }) { - Icon( - painter = painterResource(id = R.drawable.core_ic_edit), - tint = MaterialTheme.appColors.onBackground, - contentDescription = null - ) - } + } + ) } Column( modifier = Modifier @@ -255,7 +262,7 @@ private fun ProfileScreen( Spacer(modifier = Modifier.height(24.dp)) - SupportInfoSection() + SupportInfoSection(appData) Spacer(modifier = Modifier.height(24.dp)) @@ -276,46 +283,71 @@ private fun ProfileScreen( @Composable private fun ProfileInfoSection(account: Account) { - Column { - Text( - text = stringResource(id = com.raccoongang.profile.R.string.profile_prof_info), - style = MaterialTheme.appTypography.labelLarge, - color = MaterialTheme.appColors.textSecondary - ) - Spacer(modifier = Modifier.height(14.dp)) - Card( - modifier = Modifier.border( - 1.dp, - MaterialTheme.appColors.cardViewBorder, - MaterialTheme.appShapes.cardShape - ), - shape = MaterialTheme.appShapes.cardShape, - backgroundColor = MaterialTheme.appColors.cardViewBackground - ) { - Column( - Modifier - .fillMaxWidth() - .padding(20.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) + + if (account.yearOfBirth != null || account.bio.isNotEmpty()) { + Column { + Text( + text = stringResource(id = com.raccoongang.profile.R.string.profile_prof_info), + style = MaterialTheme.appTypography.labelLarge, + color = MaterialTheme.appColors.textSecondary + ) + Spacer(modifier = Modifier.height(14.dp)) + Card( + modifier = Modifier, + shape = MaterialTheme.appShapes.cardShape, + elevation = 0.dp, + backgroundColor = MaterialTheme.appColors.cardViewBackground ) { - Text( - text = stringResource( - id = com.raccoongang.profile.R.string.profile_year_of_birth, - if (account.yearOfBirth != null) { - account.yearOfBirth.toString() - } else "" - ), - style = MaterialTheme.appTypography.titleMedium, - color = MaterialTheme.appColors.textPrimary - ) - Text( - text = stringResource( - id = com.raccoongang.profile.R.string.profile_bio, - account.bio - ), - style = MaterialTheme.appTypography.titleMedium, - color = MaterialTheme.appColors.textPrimary - ) + Column( + Modifier + .fillMaxWidth() + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + if (account.yearOfBirth != null) { + Text( + text = buildAnnotatedString { + val value = if (account.yearOfBirth != null) { + account.yearOfBirth.toString() + } else "" + val text = stringResource( + id = com.raccoongang.profile.R.string.profile_year_of_birth, + value + ) + append(text) + addStyle( + style = SpanStyle( + color = MaterialTheme.appColors.textPrimaryVariant + ), + start = 0, + end = text.length - value.length + ) + }, + style = MaterialTheme.appTypography.titleMedium, + color = MaterialTheme.appColors.textPrimary + ) + } + if (account.bio.isNotEmpty()) { + Text( + text = buildAnnotatedString { + val text = stringResource( + id = com.raccoongang.profile.R.string.profile_bio, + account.bio + ) + append(text) + addStyle( + style = SpanStyle( + color = MaterialTheme.appColors.textPrimaryVariant + ), + start = 0, + end = text.length - account.bio.length + ) + }, + style = MaterialTheme.appTypography.titleMedium, + color = MaterialTheme.appColors.textPrimary + ) + } + } } } } @@ -331,12 +363,9 @@ fun SettingsSection(onVideoSettingsClick: () -> Unit) { ) Spacer(modifier = Modifier.height(14.dp)) Card( - modifier = Modifier.border( - 1.dp, - MaterialTheme.appColors.cardViewBorder, - MaterialTheme.appShapes.cardShape - ), + modifier = Modifier, shape = MaterialTheme.appShapes.cardShape, + elevation = 0.dp, backgroundColor = MaterialTheme.appColors.cardViewBackground ) { Column( @@ -355,7 +384,7 @@ fun SettingsSection(onVideoSettingsClick: () -> Unit) { } @Composable -private fun SupportInfoSection() { +private fun SupportInfoSection(appData: AppData) { val uriHandler = LocalUriHandler.current val context = LocalContext.current Column { @@ -366,12 +395,9 @@ private fun SupportInfoSection() { ) Spacer(modifier = Modifier.height(14.dp)) Card( - modifier = Modifier.border( - 1.dp, - MaterialTheme.appColors.cardViewBorder, - MaterialTheme.appShapes.cardShape - ), + modifier = Modifier, shape = MaterialTheme.appShapes.cardShape, + elevation = 0.dp, backgroundColor = MaterialTheme.appColors.cardViewBackground ) { Column( @@ -383,15 +409,21 @@ private fun SupportInfoSection() { ProfileInfoItem( text = stringResource(id = com.raccoongang.profile.R.string.profile_contact_support), onClick = { - uriHandler.openUri(context.getString(R.string.contact_us_link)) + EmailUtil.showFeedbackScreen( + context, + context.getString(R.string.core_email_subject), + appData.versionName + ) } ) + Divider(color = MaterialTheme.appColors.divider) ProfileInfoItem( text = stringResource(id = R.string.core_terms_of_use), onClick = { uriHandler.openUri(context.getString(R.string.terms_of_service_link)) } ) + Divider(color = MaterialTheme.appColors.divider) ProfileInfoItem( text = stringResource(id = R.string.core_privacy_policy), onClick = { @@ -408,15 +440,11 @@ private fun LogoutButton(onClick: () -> Unit) { Card( modifier = Modifier .fillMaxWidth() - .border( - 1.dp, - MaterialTheme.appColors.cardViewBorder, - MaterialTheme.appShapes.cardShape - ) .clickable { onClick() }, shape = MaterialTheme.appShapes.cardShape, + elevation = 0.dp, backgroundColor = MaterialTheme.appColors.cardViewBackground ) { Row( @@ -543,7 +571,8 @@ private fun ProfileScreenPreview() { uiMessage = null, logout = {}, editAccountClicked = {}, - onVideoSettingsClick = {} + onVideoSettingsClick = {}, + appData = AppData("1") ) } } @@ -560,7 +589,8 @@ private fun ProfileScreenTabletPreview() { uiMessage = null, logout = {}, editAccountClicked = {}, - onVideoSettingsClick = {} + onVideoSettingsClick = {}, + appData = AppData("1") ) } } diff --git a/profile/src/main/res/drawable-night/profile_delete_box.xml b/profile/src/main/res/drawable-night/profile_delete_box.xml new file mode 100644 index 000000000..e1e54cf43 --- /dev/null +++ b/profile/src/main/res/drawable-night/profile_delete_box.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + diff --git a/profile/src/main/res/values-uk/strings.xml b/profile/src/main/res/values-uk/strings.xml index 691a8fed1..123d4aee6 100644 --- a/profile/src/main/res/values-uk/strings.xml +++ b/profile/src/main/res/values-uk/strings.xml @@ -11,6 +11,7 @@ Повний профіль Обмежений профіль Редагувати профіль + Редагувати Зберегти Видалити профіль Вам повинно бути не менше 13 років, щоб мати повний доступ до інформації в профілі diff --git a/profile/src/main/res/values/strings.xml b/profile/src/main/res/values/strings.xml index 9800d9820..b28806bd4 100644 --- a/profile/src/main/res/values/strings.xml +++ b/profile/src/main/res/values/strings.xml @@ -11,6 +11,7 @@ Full Profile Limited Profile Edit Profile + Edit Save Delete Account You must be over 13 years old to have a profile with full access to information @@ -41,6 +42,6 @@ Leave profile? Leave Keep editing - Changes you may made may not be saved. + Changes you have made may not be saved. \ No newline at end of file From 68a1a17664e23aeef1a64aa421435d37b70b4205 Mon Sep 17 00:00:00 2001 From: Serhii <128455389+hryh27@users.noreply.github.com> Date: Mon, 3 Apr 2023 12:37:24 +0300 Subject: [PATCH 06/20] AND-262, 263, 264, 266, 267, 268 (#6) * AND-262, 263, 264, 266, 267, 268 --- .../com/raccoongang/core/AppDataConstants.kt | 2 +- .../raccoongang/core/domain/model/Account.kt | 8 + .../presentation/edit/EditProfileFragment.kt | 69 +++---- .../presentation/edit/EditProfileUIState.kt | 6 +- .../presentation/edit/EditProfileViewModel.kt | 27 ++- .../presentation/profile/ProfileFragment.kt | 169 +++++++++++------- .../presentation/profile/ProfileViewModel.kt | 11 ++ profile/src/main/res/values/strings.xml | 12 +- 8 files changed, 189 insertions(+), 115 deletions(-) diff --git a/core/src/main/java/com/raccoongang/core/AppDataConstants.kt b/core/src/main/java/com/raccoongang/core/AppDataConstants.kt index dc7b7bf4b..d41752b3a 100644 --- a/core/src/main/java/com/raccoongang/core/AppDataConstants.kt +++ b/core/src/main/java/com/raccoongang/core/AppDataConstants.kt @@ -3,7 +3,7 @@ package com.raccoongang.core import java.util.* object AppDataConstants { - const val USER_MIN_YEAR = 18 + const val USER_MIN_YEAR = 13 const val USER_MAX_YEAR = 77 const val DEFAULT_MIME_TYPE = "image/jpeg" val defaultLocale = Locale("en") diff --git a/core/src/main/java/com/raccoongang/core/domain/model/Account.kt b/core/src/main/java/com/raccoongang/core/domain/model/Account.kt index 384f8f7ba..944b88a80 100644 --- a/core/src/main/java/com/raccoongang/core/domain/model/Account.kt +++ b/core/src/main/java/com/raccoongang/core/domain/model/Account.kt @@ -2,6 +2,7 @@ package com.raccoongang.core.domain.model import android.os.Parcelable import com.google.gson.annotations.SerializedName +import com.raccoongang.core.AppDataConstants.USER_MIN_YEAR import kotlinx.parcelize.Parcelize import java.util.* @@ -48,4 +49,11 @@ data class Account( ALL_USERS } + fun isLimited() = accountPrivacy == Privacy.PRIVATE + + fun isOlderThanMinAge() : Boolean { + val currentYear = Calendar.getInstance().get(Calendar.YEAR) + return yearOfBirth != null && currentYear - yearOfBirth > USER_MIN_YEAR + } + } diff --git a/profile/src/main/java/com/raccoongang/profile/presentation/edit/EditProfileFragment.kt b/profile/src/main/java/com/raccoongang/profile/presentation/edit/EditProfileFragment.kt index c8f91eeff..7086db6b4 100644 --- a/profile/src/main/java/com/raccoongang/profile/presentation/edit/EditProfileFragment.kt +++ b/profile/src/main/java/com/raccoongang/profile/presentation/edit/EditProfileFragment.kt @@ -79,6 +79,8 @@ import java.io.File import java.io.FileOutputStream import com.raccoongang.profile.R as profileR +private const val BIO_TEXT_FIELD_LIMIT = 300 + class EditProfileFragment : Fragment() { private val viewModel by viewModel { @@ -119,7 +121,12 @@ class EditProfileFragment : Fragment() { NewEdxTheme { val windowSize = rememberWindowSize() - val uiState by viewModel.uiState.observeAsState(EditProfileUIState(viewModel.account)) + val uiState by viewModel.uiState.observeAsState( + EditProfileUIState( + viewModel.account, + isLimited = viewModel.isLimitedProfile + ) + ) val uiMessage by viewModel.uiMessage.observeAsState() val selectedImageUri by viewModel.selectedImageUri.observeAsState() val isImageDeleted by viewModel.deleteImage.observeAsState(false) @@ -175,6 +182,9 @@ class EditProfileFragment : Fragment() { }, onDeleteImageClick = { viewModel.deleteImage() + }, + onLimitedProfileChange = { + viewModel.isLimitedProfile = it } ) } @@ -264,6 +274,7 @@ private fun EditProfileScreen( leaveDialog: Boolean, onKeepEdit: () -> Unit, onDataChanged: (Boolean) -> Unit, + onLimitedProfileChange: (Boolean) -> Unit, onBackClick: (Boolean) -> Unit, onSaveClick: (Map) -> Unit, onDeleteClick: () -> Unit, @@ -273,12 +284,7 @@ private fun EditProfileScreen( val scaffoldState = rememberScaffoldState() val coroutine = rememberCoroutineScope() val configuration = LocalConfiguration.current - var accountPrivacy by rememberSaveable { - mutableStateOf(uiState.account.accountPrivacy) - } - var isLimited by rememberSaveable { - mutableStateOf(uiState.account.accountPrivacy == Account.Privacy.PRIVATE) - } + val bottomSheetScaffoldState = rememberModalBottomSheetState( initialValue = ModalBottomSheetValue.Hidden ) @@ -300,7 +306,7 @@ private fun EditProfileScreen( Pair(LANGUAGE, uiState.account.languageProficiencies), Pair(COUNTRY, uiState.account.country), Pair(BIO, uiState.account.bio), - Pair(ACCOUNT_PRIVACY, accountPrivacy.name.lowercase()) + Pair(ACCOUNT_PRIVACY, uiState.account.accountPrivacy.name.lowercase()) ) } @@ -310,7 +316,7 @@ private fun EditProfileScreen( && uiState.account.bio == mapFields[BIO] && selectedImageUri == null && !isImageDeleted - && uiState.account.accountPrivacy == accountPrivacy) + && uiState.isLimited == uiState.account.isLimited()) onDataChanged(saveButtonEnabled) val serverFieldName = rememberSaveable { @@ -416,15 +422,11 @@ private fun EditProfileScreen( HandleUIMessage(uiMessage = uiMessage, scaffoldState = scaffoldState) - if (isOpenChangeImageDialogState) { + if (isOpenChangeImageDialogState && uiState.account.isOlderThanMinAge()) { ChangeImageDialog( onSelectFromGalleryClick = { isOpenChangeImageDialogState = false - if (!isLimited) { - onSelectImageClick() - } else { - openDialog = true - } + onSelectImageClick() }, onRemoveImageClick = { onDeleteImageClick() @@ -434,6 +436,8 @@ private fun EditProfileScreen( isOpenChangeImageDialogState = false } ) + } else { + isOpenChangeImageDialogState = false } if (leaveDialog) { @@ -509,7 +513,7 @@ private fun EditProfileScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = stringResource(if (isLimited) profileR.string.profile_limited_profile else profileR.string.profile_full_profile), + text = stringResource(if (uiState.isLimited) profileR.string.profile_limited_profile else profileR.string.profile_full_profile), color = MaterialTheme.appColors.textSecondary, style = MaterialTheme.appTypography.titleSmall ) @@ -558,25 +562,24 @@ private fun EditProfileScreen( Text( modifier = Modifier.clickable { if (!LocaleUtils.isProfileLimited(mapFields[YEAR_OF_BIRTH].toString())) { - isLimited = !isLimited - accountPrivacy = - if (accountPrivacy == Account.Privacy.PRIVATE) { - Account.Privacy.ALL_USERS - } else { - Account.Privacy.PRIVATE - } - mapFields[ACCOUNT_PRIVACY] = accountPrivacy + val privacy = if (uiState.isLimited) { + Account.Privacy.ALL_USERS + } else { + Account.Privacy.PRIVATE + } + mapFields[ACCOUNT_PRIVACY] = privacy + onLimitedProfileChange(!uiState.isLimited) } else { openDialog = true } }, - text = stringResource(if (isLimited) profileR.string.profile_switch_to_full else profileR.string.profile_switch_to_limited), + text = stringResource(if (uiState.isLimited) profileR.string.profile_switch_to_full else profileR.string.profile_switch_to_limited), color = MaterialTheme.appColors.textAccent, style = MaterialTheme.appTypography.labelLarge ) Spacer(modifier = Modifier.height(20.dp)) ProfileFields( - disabled = isLimited, + disabled = uiState.isLimited, onFieldClick = { it -> if (it == YEAR_OF_BIRTH) { serverFieldName.value = YEAR_OF_BIRTH @@ -817,7 +820,9 @@ private fun ProfileFields( .height(132.dp), name = stringResource(id = profileR.string.profile_about_me), initialValue = mapFields[BIO].toString(), - onValueChanged = onValueChanged, + onValueChanged = { + onValueChanged(it.take(BIO_TEXT_FIELD_LIMIT)) + }, onDoneClick = onDoneClick ) } @@ -1020,7 +1025,7 @@ private fun EditProfileScreenPreview() { NewEdxTheme { EditProfileScreen( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), - uiState = EditProfileUIState(account = mockAccount, isUpdating = false), + uiState = EditProfileUIState(account = mockAccount, isUpdating = false, false), selectedImageUri = null, uiMessage = null, isImageDeleted = true, @@ -1031,7 +1036,8 @@ private fun EditProfileScreenPreview() { onSelectImageClick = {}, onDeleteImageClick = {}, onDataChanged = {}, - onKeepEdit = {} + onKeepEdit = {}, + onLimitedProfileChange = {} ) } } @@ -1043,7 +1049,7 @@ private fun EditProfileScreenTabletPreview() { NewEdxTheme { EditProfileScreen( windowSize = WindowSize(WindowType.Medium, WindowType.Medium), - uiState = EditProfileUIState(account = mockAccount, isUpdating = false), + uiState = EditProfileUIState(account = mockAccount, isUpdating = false, false), selectedImageUri = null, uiMessage = null, isImageDeleted = true, @@ -1054,7 +1060,8 @@ private fun EditProfileScreenTabletPreview() { onSelectImageClick = {}, onDeleteImageClick = {}, onDataChanged = {}, - onKeepEdit = {} + onKeepEdit = {}, + onLimitedProfileChange = {} ) } } diff --git a/profile/src/main/java/com/raccoongang/profile/presentation/edit/EditProfileUIState.kt b/profile/src/main/java/com/raccoongang/profile/presentation/edit/EditProfileUIState.kt index 17ed241ec..01337c0d0 100644 --- a/profile/src/main/java/com/raccoongang/profile/presentation/edit/EditProfileUIState.kt +++ b/profile/src/main/java/com/raccoongang/profile/presentation/edit/EditProfileUIState.kt @@ -2,6 +2,10 @@ package com.raccoongang.profile.presentation.edit import com.raccoongang.core.domain.model.Account -data class EditProfileUIState(val account: Account, val isUpdating :Boolean = false) +data class EditProfileUIState( + val account: Account, + val isUpdating: Boolean = false, + val isLimited: Boolean +) diff --git a/profile/src/main/java/com/raccoongang/profile/presentation/edit/EditProfileViewModel.kt b/profile/src/main/java/com/raccoongang/profile/presentation/edit/EditProfileViewModel.kt index c0f0d8bfd..dbcf8ca16 100644 --- a/profile/src/main/java/com/raccoongang/profile/presentation/edit/EditProfileViewModel.kt +++ b/profile/src/main/java/com/raccoongang/profile/presentation/edit/EditProfileViewModel.kt @@ -43,6 +43,11 @@ class EditProfileViewModel( get() = _deleteImage var profileDataChanged = false + var isLimitedProfile: Boolean = account.isLimited() + set(value) { + field = value + _uiState.value = EditProfileUIState(account, isLimited = value) + } private val _showLeaveDialog = MutableLiveData() val showLeaveDialog: LiveData @@ -50,18 +55,22 @@ class EditProfileViewModel( fun updateAccount(fields: Map) { - _uiState.value = EditProfileUIState(account, true) + _uiState.value = EditProfileUIState(account, true, isLimitedProfile) viewModelScope.launch { try { if (deleteImage.value == true) { interactor.deleteProfileImage() } - val account = interactor.updateAccount(fields) - _uiState.value = EditProfileUIState(account, isUpdating = false) + val updatedAccount = interactor.updateAccount(fields) + account = updatedAccount + isLimitedProfile = updatedAccount.isLimited() + _uiState.value = + EditProfileUIState(updatedAccount, isUpdating = false, isLimitedProfile) sendAccountUpdated() _deleteImage.value = false + _selectedImageUri.value = null } catch (e: Exception) { - _uiState.value = EditProfileUIState(account.copy()) + _uiState.value = EditProfileUIState(account.copy(), isLimited = isLimitedProfile) if (e.isInternetError()) { _uiMessage.value = UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) @@ -74,16 +83,19 @@ class EditProfileViewModel( } fun updateAccountAndImage(fields: Map, file: File, mimeType: String) { - _uiState.value = EditProfileUIState(account, true) + _uiState.value = EditProfileUIState(account, true, isLimitedProfile) viewModelScope.launch { try { interactor.setProfileImage(file, mimeType) val updatedAccount = interactor.updateAccount(fields) - _uiState.value = EditProfileUIState(updatedAccount, isUpdating = false) + account = updatedAccount + isLimitedProfile = updatedAccount.isLimited() + _uiState.value = + EditProfileUIState(updatedAccount, isUpdating = false, isLimitedProfile) _selectedImageUri.value = null sendAccountUpdated() } catch (e: Exception) { - _uiState.value = EditProfileUIState(account.copy()) + _uiState.value = EditProfileUIState(account.copy(), isLimited = isLimitedProfile) if (e.isInternetError()) { _uiMessage.value = UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) @@ -109,7 +121,6 @@ class EditProfileViewModel( _showLeaveDialog.value = value } - private suspend fun sendAccountUpdated() { notifier.send(AccountUpdated()) } diff --git a/profile/src/main/java/com/raccoongang/profile/presentation/profile/ProfileFragment.kt b/profile/src/main/java/com/raccoongang/profile/presentation/profile/ProfileFragment.kt index 33cc19caa..831605716 100644 --- a/profile/src/main/java/com/raccoongang/profile/presentation/profile/ProfileFragment.kt +++ b/profile/src/main/java/com/raccoongang/profile/presentation/profile/ProfileFragment.kt @@ -12,6 +12,9 @@ import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowForwardIos import androidx.compose.material.icons.filled.ExitToApp +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.saveable.rememberSaveable @@ -74,11 +77,14 @@ class ProfileFragment : Fragment() { val uiState by viewModel.uiState.observeAsState() val logoutSuccess by viewModel.successLogout.observeAsState(false) val uiMessage by viewModel.uiMessage.observeAsState() + val refreshing by viewModel.isUpdating.observeAsState(false) + ProfileScreen( windowSize = windowSize, uiState = uiState!!, uiMessage = uiMessage, appData = (requireActivity() as AppDataHolder).appData, + refreshing = refreshing, logout = { viewModel.logout() }, @@ -88,6 +94,9 @@ class ProfileFragment : Fragment() { it ) }, + onSwipeRefresh = { + viewModel.updateAccount() + }, onVideoSettingsClick = { router.navigateToVideoSettings( requireParentFragment().parentFragmentManager @@ -106,19 +115,25 @@ class ProfileFragment : Fragment() { } +@OptIn(ExperimentalMaterialApi::class) @Composable private fun ProfileScreen( windowSize: WindowSize, uiState: ProfileUIState, appData: AppData, uiMessage: UIMessage?, + refreshing: Boolean, onVideoSettingsClick: () -> Unit, logout: () -> Unit, + onSwipeRefresh: () -> Unit, editAccountClicked: (Account) -> Unit ) { val scaffoldState = rememberScaffoldState() var showLogoutDialog by rememberSaveable { mutableStateOf(false) } + val pullRefreshState = + rememberPullRefreshState(refreshing = refreshing, onRefresh = { onSwipeRefresh() }) + Scaffold( modifier = Modifier.fillMaxSize(), scaffoldState = scaffoldState @@ -194,87 +209,101 @@ private fun ProfileScreen( } ) } - Column( - modifier = Modifier - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally + Surface( + color = MaterialTheme.appColors.background ) { - when (uiState) { - is ProfileUIState.Loading -> { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } - } - is ProfileUIState.Data -> { - Column( - Modifier - .fillMaxHeight() - .then(contentWidth) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Image( - painter = rememberAsyncImagePainter( - model = uiState.account.profileImage.imageUrlFull, - placeholder = painterResource(id = R.drawable.core_ic_default_profile_picture), - error = painterResource(id = R.drawable.core_ic_default_profile_picture) - ), - contentDescription = null, - modifier = Modifier - .border( - 2.dp, - MaterialTheme.appColors.onSurface, - CircleShape + Box( + modifier = Modifier.pullRefresh(pullRefreshState), + contentAlignment = Alignment.TopCenter + ) { + Column( + modifier = Modifier + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + when (uiState) { + is ProfileUIState.Loading -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + is ProfileUIState.Data -> { + Column( + Modifier + .fillMaxHeight() + .then(contentWidth) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = rememberAsyncImagePainter( + model = uiState.account.profileImage.imageUrlFull, + placeholder = painterResource(id = R.drawable.core_ic_default_profile_picture), + error = painterResource(id = R.drawable.core_ic_default_profile_picture) + ), + contentDescription = null, + modifier = Modifier + .border( + 2.dp, + MaterialTheme.appColors.onSurface, + CircleShape + ) + .padding(2.dp) + .size(100.dp) + .clip(CircleShape) + .background(Color.Gray) ) - .padding(2.dp) - .size(100.dp) - .clip(CircleShape) - .background(Color.Gray) - ) - Spacer(modifier = Modifier.height(20.dp)) - Text( - text = uiState.account.name, - color = MaterialTheme.appColors.textPrimary, - style = MaterialTheme.appTypography.headlineSmall - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = "@${uiState.account.username}", - color = MaterialTheme.appColors.textPrimaryVariant, - style = MaterialTheme.appTypography.labelLarge - ) - Spacer(modifier = Modifier.height(36.dp)) + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = uiState.account.name, + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.headlineSmall + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "@${uiState.account.username}", + color = MaterialTheme.appColors.textPrimaryVariant, + style = MaterialTheme.appTypography.labelLarge + ) + Spacer(modifier = Modifier.height(36.dp)) - Column( - Modifier - .fillMaxWidth() - ) { - ProfileInfoSection(uiState.account) + Column( + Modifier + .fillMaxWidth() + ) { + ProfileInfoSection(uiState.account) - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(24.dp)) - SettingsSection(onVideoSettingsClick = { - onVideoSettingsClick() - }) + SettingsSection(onVideoSettingsClick = { + onVideoSettingsClick() + }) - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(24.dp)) - SupportInfoSection(appData) + SupportInfoSection(appData) - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(24.dp)) - LogoutButton( - onClick = { showLogoutDialog = true } - ) + LogoutButton( + onClick = { showLogoutDialog = true } + ) - Spacer(Modifier.height(30.dp)) - } + Spacer(Modifier.height(30.dp)) + } + } + } } } + PullRefreshIndicator( + refreshing, + pullRefreshState, + Modifier.align(Alignment.TopCenter) + ) } } } @@ -569,7 +598,9 @@ private fun ProfileScreenPreview() { windowSize = WindowSize(WindowType.Compact, WindowType.Compact), uiState = ProfileUIState.Data(mockAccount), uiMessage = null, + refreshing = false, logout = {}, + onSwipeRefresh = {}, editAccountClicked = {}, onVideoSettingsClick = {}, appData = AppData("1") @@ -587,7 +618,9 @@ private fun ProfileScreenTabletPreview() { windowSize = WindowSize(WindowType.Medium, WindowType.Medium), uiState = ProfileUIState.Data(mockAccount), uiMessage = null, + refreshing = false, logout = {}, + onSwipeRefresh = {}, editAccountClicked = {}, onVideoSettingsClick = {}, appData = AppData("1") diff --git a/profile/src/main/java/com/raccoongang/profile/presentation/profile/ProfileViewModel.kt b/profile/src/main/java/com/raccoongang/profile/presentation/profile/ProfileViewModel.kt index b7312b61c..74da654e9 100644 --- a/profile/src/main/java/com/raccoongang/profile/presentation/profile/ProfileViewModel.kt +++ b/profile/src/main/java/com/raccoongang/profile/presentation/profile/ProfileViewModel.kt @@ -42,6 +42,10 @@ class ProfileViewModel( val uiMessage: LiveData get() = _uiMessage + private val _isUpdating = MutableLiveData() + val isUpdating: LiveData + get() = _isUpdating + init { getAccount() } @@ -74,10 +78,17 @@ class ProfileViewModel( _uiMessage.value = UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) } + } finally { + _isUpdating.value = false } } } + fun updateAccount() { + _isUpdating.value = true + getAccount() + } + fun logout() { viewModelScope.launch { try { diff --git a/profile/src/main/res/values/strings.xml b/profile/src/main/res/values/strings.xml index b28806bd4..9ad0c47c9 100644 --- a/profile/src/main/res/values/strings.xml +++ b/profile/src/main/res/values/strings.xml @@ -8,17 +8,17 @@ Year of birth: %1$s Log out? Are you sure you want to log out? - Full Profile - Limited Profile - Edit Profile + Full profile + Limited profile + Edit profile Edit Save - Delete Account + Delete account You must be over 13 years old to have a profile with full access to information Year of birth Location - About Me - Spoken Language + About me + Spoken language Switch to full profile Switch to limited profile Oh, sorry From da32ace3b1c36fe03ee599d6323caea24c96f5f6 Mon Sep 17 00:00:00 2001 From: Serhii <128455389+hryh27@users.noreply.github.com> Date: Mon, 3 Apr 2023 13:09:34 +0300 Subject: [PATCH 07/20] fix bugs (#8) * fix bugs --- .../java/com/raccoongang/newedx/AppRouter.kt | 8 +++- .../com/raccoongang/core/ui/ComposeCommon.kt | 26 +++++++++-- .../com/raccoongang/core/ui/theme/Theme.kt | 2 +- .../detail/CourseDetailsFragment.kt | 8 +++- .../data/model/response/ThreadsResponse.kt | 15 ++++-- .../discussion/domain/model/Thread.kt | 15 ++++-- .../presentation/DiscussionRouter.kt | 3 +- .../comments/DiscussionCommentsFragment.kt | 36 ++++++++++----- .../comments/DiscussionCommentsViewModel.kt | 2 +- .../responses/DiscussionResponsesFragment.kt | 42 ++++++++++++----- .../responses/DiscussionResponsesViewModel.kt | 2 + .../search/DiscussionSearchThreadFragment.kt | 5 +- .../threads/DiscussionAddThreadFragment.kt | 13 +++--- .../threads/DiscussionThreadsFragment.kt | 5 +- .../presentation/ui/DiscussionUI.kt | 46 +++++++++++-------- .../res/drawable/discussion_ic_report.xml | 17 +++++++ .../res/drawable/discussion_star_filled.xml | 17 +++++++ discussion/src/main/res/values-uk/strings.xml | 1 + discussion/src/main/res/values/strings.xml | 1 + .../presentation/edit/EditProfileFragment.kt | 2 +- .../presentation/profile/ProfileFragment.kt | 1 - 21 files changed, 196 insertions(+), 71 deletions(-) create mode 100644 discussion/src/main/res/drawable/discussion_ic_report.xml create mode 100644 discussion/src/main/res/drawable/discussion_star_filled.xml diff --git a/app/src/main/java/com/raccoongang/newedx/AppRouter.kt b/app/src/main/java/com/raccoongang/newedx/AppRouter.kt index aeb0a17e3..22d8b089e 100644 --- a/app/src/main/java/com/raccoongang/newedx/AppRouter.kt +++ b/app/src/main/java/com/raccoongang/newedx/AppRouter.kt @@ -194,10 +194,14 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di ) } - override fun navigateToDiscussionResponses(fm: FragmentManager, comment: DiscussionComment) { + override fun navigateToDiscussionResponses( + fm: FragmentManager, + comment: DiscussionComment, + isClosed: Boolean + ) { replaceFragmentWithBackStack( fm, - DiscussionResponsesFragment.newInstance(comment) + DiscussionResponsesFragment.newInstance(comment, isClosed) ) } diff --git a/core/src/main/java/com/raccoongang/core/ui/ComposeCommon.kt b/core/src/main/java/com/raccoongang/core/ui/ComposeCommon.kt index a23ecf397..6b534c86d 100644 --- a/core/src/main/java/com/raccoongang/core/ui/ComposeCommon.kt +++ b/core/src/main/java/com/raccoongang/core/ui/ComposeCommon.kt @@ -91,7 +91,7 @@ fun StaticSearchBar( Text( modifier = Modifier.fillMaxWidth(), text = text, - color = MaterialTheme.appColors.textSecondary + color = MaterialTheme.appColors.textFieldHint ) } } @@ -285,6 +285,7 @@ fun HyperlinkText( @Composable fun HyperlinkImageText( modifier: Modifier = Modifier, + title: String = "", imageText: LinkedImageText, textStyle: TextStyle = TextStyle.Default, linkTextColor: Color = MaterialTheme.appColors.primary, @@ -295,6 +296,10 @@ fun HyperlinkImageText( val fullText = imageText.text val hyperLinks = imageText.links val annotatedString = buildAnnotatedString { + if(title.isNotEmpty()) { + append(title) + append("\n\n") + } append(fullText) addStyle( style = SpanStyle( @@ -302,11 +307,11 @@ fun HyperlinkImageText( fontSize = fontSize ), start = 0, - end = fullText.length + end = this.length ) for ((key, value) in hyperLinks) { - val startIndex = fullText.indexOf(key) + val startIndex = this.toString().indexOf(key) if (startIndex == -1) continue val endIndex = startIndex + key.length addStyle( @@ -326,8 +331,19 @@ fun HyperlinkImageText( end = endIndex ) } + if (title.isNotEmpty()) { + addStyle( + style = SpanStyle( + color = MaterialTheme.appColors.textPrimary, + fontSize = MaterialTheme.appTypography.titleLarge.fontSize, + fontWeight = MaterialTheme.appTypography.titleLarge.fontWeight + ), + start = 0, + end = title.length + ) + } for (item in imageText.headers) { - val startIndex = fullText.indexOf(item) + val startIndex = this.toString().indexOf(item) if (startIndex == -1) continue val endIndex = startIndex + item.length addStyle( @@ -345,7 +361,7 @@ fun HyperlinkImageText( fontSize = fontSize ), start = 0, - end = fullText.length + end = this.length ) } diff --git a/core/src/main/java/com/raccoongang/core/ui/theme/Theme.kt b/core/src/main/java/com/raccoongang/core/ui/theme/Theme.kt index c51e3236c..0967ebac3 100644 --- a/core/src/main/java/com/raccoongang/core/ui/theme/Theme.kt +++ b/core/src/main/java/com/raccoongang/core/ui/theme/Theme.kt @@ -42,7 +42,7 @@ private val DarkColorPalette = AppColors( buttonText = Color.White, cardViewBackground = Color(0xFF273346), - cardViewBorder = Color(0xFF273346), + cardViewBorder = Color(0xFF4E5A70), divider = Color(0xFF4E5A70), certificateForeground = Color(0xD92EB865), diff --git a/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt index 1c16a747c..8ec9db7ef 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt @@ -361,6 +361,7 @@ private fun CourseDetailNativeContent( val enrollmentEnd = course.enrollmentEnd if (enrollmentEnd != null && Date() > enrollmentEnd) { EnrollOverLabel() + Spacer(Modifier.height(24.dp)) } Text( text = course.shortDescription, @@ -469,6 +470,11 @@ private fun CourseDetailNativeContentLandscape( @Composable private fun EnrollOverLabel() { + val borderColor = if (!isSystemInDarkTheme()) { + MaterialTheme.appColors.cardViewBorder + } else { + MaterialTheme.appColors.surface + } Box( Modifier .fillMaxWidth() @@ -482,7 +488,7 @@ private fun EnrollOverLabel() { ) .border( 1.dp, - MaterialTheme.appColors.cardViewBorder, + borderColor, MaterialTheme.appShapes.material.medium ) ) { diff --git a/discussion/src/main/java/com/raccoongang/discussion/data/model/response/ThreadsResponse.kt b/discussion/src/main/java/com/raccoongang/discussion/data/model/response/ThreadsResponse.kt index 595d8797b..e8c699318 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/data/model/response/ThreadsResponse.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/data/model/response/ThreadsResponse.kt @@ -19,7 +19,7 @@ data class ThreadsResponse( @SerializedName("id") val id: String, @SerializedName("author") - val author: String, + val author: String?, @SerializedName("author_label") val authorLabel: String?, @SerializedName("created_at") @@ -40,6 +40,10 @@ data class ThreadsResponse( val editableFields: List, @SerializedName("can_delete") val canDelete: Boolean, + @SerializedName("anonymous") + val anonymous: Boolean, + @SerializedName("anonymous_to_peers") + val anonymousToPeers: Boolean, @SerializedName("course_id") val courseId: String, @SerializedName("topic_id") @@ -70,6 +74,8 @@ data class ThreadsResponse( val read: Boolean, @SerializedName("has_endorsed") val hasEndorsed: Boolean, + @SerializedName("response_count") + val responseCount: Int, @SerializedName("users") val users: Map? ) { @@ -92,7 +98,7 @@ data class ThreadsResponse( fun mapToDomain(): com.raccoongang.discussion.domain.model.Thread { return com.raccoongang.discussion.domain.model.Thread( id, - author, + author ?: "", authorLabel ?: "", createdAt, updatedAt, @@ -119,7 +125,10 @@ data class ThreadsResponse( unreadCommentCount, read, hasEndorsed, - users?.entries?.associate { it.key to it.value.mapToDomain() } + users?.entries?.associate { it.key to it.value.mapToDomain() }, + responseCount, + anonymous, + anonymousToPeers ) } diff --git a/discussion/src/main/java/com/raccoongang/discussion/domain/model/Thread.kt b/discussion/src/main/java/com/raccoongang/discussion/domain/model/Thread.kt index 98886e483..c37a67600 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/domain/model/Thread.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/domain/model/Thread.kt @@ -3,6 +3,7 @@ package com.raccoongang.discussion.domain.model import android.os.Parcelable import com.raccoongang.core.domain.model.ProfileImage import com.raccoongang.core.extension.LinkedImageText +import com.raccoongang.discussion.R import kotlinx.parcelize.Parcelize @Parcelize @@ -35,7 +36,10 @@ data class Thread( val unreadCommentCount: Int, val read: Boolean, val hasEndorsed: Boolean, - val users: Map? + val users: Map?, + val responseCount: Int, + val anonymous: Boolean, + val anonymousToPeers: Boolean ) : Parcelable @Parcelize @@ -43,7 +47,10 @@ data class DiscussionProfile( val image: ProfileImage? ) : Parcelable -enum class DiscussionType(val value: String) { - QUESTION("question"), - DISCUSSION("discussion") +enum class DiscussionType( + val value: String, + val resId: Int +) { + QUESTION("question", R.string.discussion_question), + DISCUSSION("discussion", R.string.discussion_discussion) } diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/DiscussionRouter.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/DiscussionRouter.kt index a834a7441..45f094211 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/DiscussionRouter.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/DiscussionRouter.kt @@ -22,7 +22,8 @@ interface DiscussionRouter { fun navigateToDiscussionResponses( fm: FragmentManager, - comment: DiscussionComment + comment: DiscussionComment, + isClosed: Boolean ) fun navigateToAddThread( diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsFragment.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsFragment.kt index 98af93450..9acd60635 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsFragment.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsFragment.kt @@ -8,6 +8,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -44,6 +45,7 @@ import com.raccoongang.core.extension.parcelable import com.raccoongang.core.ui.* import com.raccoongang.core.ui.theme.NewEdxTheme import com.raccoongang.core.ui.theme.appColors +import com.raccoongang.core.ui.theme.appShapes import com.raccoongang.core.ui.theme.appTypography import com.raccoongang.discussion.domain.model.DiscussionComment import com.raccoongang.discussion.domain.model.DiscussionType @@ -98,17 +100,22 @@ class DiscussionCommentsFragment : Fragment() { viewModel.fetchMore() }, onItemClick = { action, id, bool -> - when (action) { - ACTION_UPVOTE_COMMENT -> viewModel.setCommentUpvoted(id, bool) - ACTION_REPORT_COMMENT -> viewModel.setCommentReported(id, bool) - ACTION_UPVOTE_THREAD -> viewModel.setThreadUpvoted(bool) - ACTION_REPORT_THREAD -> viewModel.setThreadReported(bool) - ACTION_FOLLOW_THREAD -> viewModel.setThreadFollowed(bool) + if (!viewModel.thread.closed) { + when (action) { + ACTION_UPVOTE_COMMENT -> viewModel.setCommentUpvoted(id, bool) + ACTION_UPVOTE_THREAD -> viewModel.setThreadUpvoted(bool) + ACTION_FOLLOW_THREAD -> viewModel.setThreadFollowed(bool) + } + } else { + when (action) { + ACTION_REPORT_COMMENT -> viewModel.setCommentReported(id, bool) + ACTION_REPORT_THREAD -> viewModel.setThreadReported(bool) + } } }, onCommentClick = { router.navigateToDiscussionResponses( - requireActivity().supportFragmentManager, it + requireActivity().supportFragmentManager, it, viewModel.thread.closed ) }, onAddResponseClick = { @@ -178,7 +185,7 @@ private fun DiscussionCommentsScreen( } val iconButtonColor = if (responseValue.isEmpty()) { - MaterialTheme.appColors.cardViewBorder + MaterialTheme.appColors.textFieldBackgroundVariant } else { Color.White } @@ -326,7 +333,9 @@ private fun DiscussionCommentsScreen( if (scrollState.shouldLoadMore(firstVisibleIndex, 4)) { paginationCallBack() } - Divider(color = MaterialTheme.appColors.cardViewBorder) + if (!isSystemInDarkTheme()) { + Divider(color = MaterialTheme.appColors.cardViewBorder) + } Box( Modifier .fillMaxWidth() @@ -350,6 +359,7 @@ private fun DiscussionCommentsScreen( onValueChange = { str -> responseValue = str }, + shape = MaterialTheme.appShapes.buttonShape, textStyle = MaterialTheme.appTypography.labelLarge, maxLines = 3, placeholder = { @@ -363,7 +373,8 @@ private fun DiscussionCommentsScreen( backgroundColor = MaterialTheme.appColors.textFieldBackgroundVariant, unfocusedBorderColor = MaterialTheme.appColors.textFieldBorder, textColor = MaterialTheme.appColors.textFieldText - ) + ), + enabled = !uiState.thread.closed ) Box( modifier = Modifier @@ -498,7 +509,10 @@ private val mockThread = com.raccoongang.discussion.domain.model.Thread( 4, false, false, - mapOf() + mapOf(), + 10, + false, + false ) private val mockComment = DiscussionComment( diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsViewModel.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsViewModel.kt index 0c447bd97..fa53644b5 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsViewModel.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsViewModel.kt @@ -29,7 +29,7 @@ class DiscussionCommentsViewModel( thread: com.raccoongang.discussion.domain.model.Thread, ) : BaseViewModel() { - val title = thread.title + val title = resourceManager.getString(thread.type.resId) var thread: com.raccoongang.discussion.domain.model.Thread private set diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/responses/DiscussionResponsesFragment.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/responses/DiscussionResponsesFragment.kt index 21234bca2..c20821c6c 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/responses/DiscussionResponsesFragment.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/responses/DiscussionResponsesFragment.kt @@ -8,6 +8,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -44,6 +45,7 @@ import com.raccoongang.core.extension.parcelable import com.raccoongang.core.ui.* import com.raccoongang.core.ui.theme.NewEdxTheme import com.raccoongang.core.ui.theme.appColors +import com.raccoongang.core.ui.theme.appShapes import com.raccoongang.core.ui.theme.appTypography import com.raccoongang.discussion.domain.model.DiscussionComment import com.raccoongang.discussion.presentation.comments.DiscussionCommentsFragment @@ -61,6 +63,7 @@ class DiscussionResponsesFragment : Fragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycle.addObserver(viewModel) + viewModel.isThreadClosed = requireArguments().getBoolean(ARG_IS_CLOSED, false) } override fun onCreateView( @@ -84,6 +87,7 @@ class DiscussionResponsesFragment : Fragment() { uiMessage = uiMessage, canLoadMore = canLoadMore, refreshing = refreshing, + isClosed = viewModel.isThreadClosed, onSwipeRefresh = { viewModel.updateCommentResponses() }, @@ -93,10 +97,12 @@ class DiscussionResponsesFragment : Fragment() { onItemClick = { action, id, bool -> when (action) { DiscussionCommentsFragment.ACTION_UPVOTE_COMMENT -> { - viewModel.setCommentUpvoted( - id, - bool - ) + if (!viewModel.isThreadClosed) { + viewModel.setCommentUpvoted( + id, + bool + ) + } } DiscussionCommentsFragment.ACTION_REPORT_COMMENT -> { viewModel.setCommentReported( @@ -119,11 +125,16 @@ class DiscussionResponsesFragment : Fragment() { companion object { private const val ARG_COMMENT = "comment" + private const val ARG_IS_CLOSED = "isClosed" - fun newInstance(comment: DiscussionComment): DiscussionResponsesFragment { + fun newInstance( + comment: DiscussionComment, + isClosed: Boolean + ): DiscussionResponsesFragment { val fragment = DiscussionResponsesFragment() fragment.arguments = bundleOf( - ARG_COMMENT to comment + ARG_COMMENT to comment, + ARG_IS_CLOSED to isClosed ) return fragment } @@ -138,11 +149,12 @@ private fun DiscussionResponsesScreen( uiMessage: UIMessage?, canLoadMore: Boolean, refreshing: Boolean, + isClosed: Boolean, onSwipeRefresh: () -> Unit, paginationCallBack: () -> Unit, onItemClick: (String, String, Boolean) -> Unit, addCommentClick: (String) -> Unit, - onBackClick: () -> Unit + onBackClick: () -> Unit, ) { val scaffoldState = rememberScaffoldState() val scrollState = rememberLazyListState() @@ -162,7 +174,7 @@ private fun DiscussionResponsesScreen( } val iconButtonColor = if (commentValue.isEmpty()) { - MaterialTheme.appColors.cardViewBorder + MaterialTheme.appColors.textFieldBackgroundVariant } else { Color.White } @@ -337,7 +349,9 @@ private fun DiscussionResponsesScreen( paginationCallBack() } } - Divider(color = MaterialTheme.appColors.cardViewBorder) + if (!isSystemInDarkTheme()) { + Divider(color = MaterialTheme.appColors.cardViewBorder) + } Box( Modifier .fillMaxWidth() @@ -363,6 +377,7 @@ private fun DiscussionResponsesScreen( }, textStyle = MaterialTheme.appTypography.labelLarge, maxLines = 3, + shape = MaterialTheme.appShapes.buttonShape, placeholder = { Text( text = stringResource(id = com.raccoongang.discussion.R.string.discussion_add_comment), @@ -374,7 +389,8 @@ private fun DiscussionResponsesScreen( backgroundColor = MaterialTheme.appColors.textFieldBackgroundVariant, unfocusedBorderColor = MaterialTheme.appColors.textFieldBorder, textColor = MaterialTheme.appColors.textFieldText - ) + ), + enabled = !isClosed ) Box( modifier = Modifier @@ -438,6 +454,7 @@ private fun DiscussionResponsesScreenPreview() { uiMessage = null, canLoadMore = false, refreshing = false, + onSwipeRefresh = {}, paginationCallBack = { }, onItemClick = { _, _, _ -> @@ -446,7 +463,7 @@ private fun DiscussionResponsesScreenPreview() { }, onBackClick = {}, - onSwipeRefresh = {} + isClosed = false ) } } @@ -467,6 +484,7 @@ private fun DiscussionResponsesScreenTabletPreview() { uiMessage = null, canLoadMore = false, refreshing = false, + onSwipeRefresh = {}, paginationCallBack = { }, onItemClick = { _, _, _ -> @@ -475,7 +493,7 @@ private fun DiscussionResponsesScreenTabletPreview() { }, onBackClick = {}, - onSwipeRefresh = {} + isClosed = false ) } } diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/responses/DiscussionResponsesViewModel.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/responses/DiscussionResponsesViewModel.kt index c2e1f2e4a..04584d065 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/responses/DiscussionResponsesViewModel.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/responses/DiscussionResponsesViewModel.kt @@ -41,6 +41,8 @@ class DiscussionResponsesViewModel( val isUpdating: LiveData get() = _isUpdating + var isThreadClosed: Boolean = false + private val comments = mutableListOf() private var page = 1 private var isLoading = false diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/search/DiscussionSearchThreadFragment.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/search/DiscussionSearchThreadFragment.kt index 38f801022..7c72b86d2 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/search/DiscussionSearchThreadFragment.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/search/DiscussionSearchThreadFragment.kt @@ -409,5 +409,8 @@ private val mockThread = com.raccoongang.discussion.domain.model.Thread( 4, false, false, - mapOf() + mapOf(), + 10, + false, + false ) \ No newline at end of file diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionAddThreadFragment.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionAddThreadFragment.kt index de576c9b1..9eac99c70 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionAddThreadFragment.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionAddThreadFragment.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalComposeUiApi::class) + package com.raccoongang.discussion.presentation.threads import android.content.res.Configuration @@ -16,11 +18,13 @@ import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.saveable.rememberSaveable 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.platform.ComposeView import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction @@ -121,6 +125,7 @@ private fun DiscussionAddThreadScreen( ) { val scaffoldState = rememberScaffoldState() val configuration = LocalConfiguration.current + val keyboardController = LocalSoftwareKeyboardController.current val bottomSheetScaffoldState = rememberModalBottomSheetState( initialValue = ModalBottomSheetValue.Hidden ) @@ -334,13 +339,7 @@ private fun DiscussionAddThreadScreen( imeAction = ImeAction.Done, keyboardActions = { focusManager -> focusManager.clearFocus() - onPostDiscussionClick( - discussionType, - topicData.second, - titleValue, - discussionValue, - followPost - ) + keyboardController?.hide() }, onValueChanged = { value -> discussionValue = value diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsFragment.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsFragment.kt index ef7e7bf17..3e80f5355 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsFragment.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsFragment.kt @@ -557,5 +557,8 @@ private val mockThread = com.raccoongang.discussion.domain.model.Thread( 4, false, false, - mapOf() + mapOf(), + 10, + false, + false ) \ No newline at end of file diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/ui/DiscussionUI.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/ui/DiscussionUI.kt index 04a723a55..2261e7e8d 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/ui/DiscussionUI.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/ui/DiscussionUI.kt @@ -13,16 +13,12 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ChevronRight -import androidx.compose.material.icons.filled.Star -import androidx.compose.material.icons.outlined.Flag import androidx.compose.material.icons.outlined.HelpOutline -import androidx.compose.material.icons.outlined.StarRate import androidx.compose.runtime.Composable 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.graphics.Shape import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.rememberVectorPainter @@ -86,12 +82,14 @@ fun ThreadMainItem( ) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Image( - painter = rememberAsyncImagePainter(profileImageUrl), + painter = rememberAsyncImagePainter( + model = profileImageUrl, + error = painterResource(id = com.raccoongang.core.R.drawable.core_ic_default_profile_picture) + ), contentDescription = null, modifier = Modifier .size(48.dp) .clip(MaterialTheme.appShapes.material.medium) - .background(Color.Gray) ) Spacer(Modifier.width(16.dp)) Column( @@ -99,19 +97,19 @@ fun ThreadMainItem( verticalArrangement = Arrangement.spacedBy(4.dp) ) { Text( - text = thread.author, + text = thread.author.ifEmpty { stringResource(id = R.string.discussion_anonymous) }, color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.titleMedium ) Text( text = TimeUtils.iso8601ToDateWithTime(context, thread.createdAt), style = MaterialTheme.appTypography.labelSmall, - color = MaterialTheme.appColors.textSecondary + color = MaterialTheme.appColors.textPrimaryVariant ) } IconText( text = stringResource(id = R.string.discussion_follow), - icon = if (thread.following) Icons.Filled.Star else Icons.Outlined.StarRate, + painter = painterResource(if (thread.following) R.drawable.discussion_star_filled else R.drawable.discussion_star), textStyle = MaterialTheme.appTypography.labelLarge, color = MaterialTheme.appColors.textPrimaryVariant, onClick = { @@ -120,6 +118,7 @@ fun ThreadMainItem( } Spacer(modifier = Modifier.height(24.dp)) HyperlinkImageText( + title = thread.title, imageText = thread.parsedRenderedBody, linkTextColor = MaterialTheme.appColors.primary ) @@ -145,7 +144,7 @@ fun ThreadMainItem( ) IconText( text = reportText, - icon = Icons.Outlined.Flag, + painter = painterResource(id = R.drawable.discussion_ic_report), textStyle = MaterialTheme.appTypography.labelLarge, color = reportColor, onClick = { @@ -215,12 +214,15 @@ fun CommentItem( verticalAlignment = Alignment.CenterVertically ) { Image( - painter = rememberAsyncImagePainter(profileImageUrl), + painter = rememberAsyncImagePainter( + model = profileImageUrl, + error = painterResource(id = com.raccoongang.core.R.drawable.core_ic_default_profile_picture) + ), contentDescription = null, modifier = Modifier .size(32.dp) .clip(CircleShape) - .background(Color.Gray) + ) Spacer(Modifier.width(12.dp)) Column( @@ -235,12 +237,12 @@ fun CommentItem( Text( text = TimeUtils.iso8601ToDateWithTime(context, comment.createdAt), style = MaterialTheme.appTypography.labelSmall, - color = MaterialTheme.appColors.textSecondary + color = MaterialTheme.appColors.textPrimaryVariant ) } IconText( text = reportText, - icon = Icons.Outlined.Flag, + painter = painterResource(id = R.drawable.discussion_ic_report), textStyle = MaterialTheme.appTypography.labelMedium, color = reportColor, onClick = { @@ -348,12 +350,15 @@ fun CommentMainItem( verticalAlignment = Alignment.CenterVertically ) { Image( - painter = rememberAsyncImagePainter(profileImageUrl), + painter = rememberAsyncImagePainter( + model = profileImageUrl, + error = painterResource(id = com.raccoongang.core.R.drawable.core_ic_default_profile_picture) + ), contentDescription = null, modifier = Modifier .size(32.dp) .clip(CircleShape) - .background(Color.Gray) + ) Spacer(Modifier.width(12.dp)) Column( @@ -368,7 +373,7 @@ fun CommentMainItem( Text( text = TimeUtils.iso8601ToDateWithTime(context, comment.createdAt), style = MaterialTheme.appTypography.labelSmall, - color = MaterialTheme.appColors.textSecondary + color = MaterialTheme.appColors.textPrimaryVariant ) } } @@ -403,7 +408,7 @@ fun CommentMainItem( ) IconText( text = reportText, - icon = Icons.Outlined.Flag, + painter = painterResource(id = R.drawable.discussion_ic_report), textStyle = MaterialTheme.appTypography.labelLarge, color = reportColor, onClick = { @@ -686,7 +691,10 @@ private val mockThread = com.raccoongang.discussion.domain.model.Thread( 4, false, false, - mapOf() + mapOf(), + 10, + false, + false ) private val mockTopic = Topic( diff --git a/discussion/src/main/res/drawable/discussion_ic_report.xml b/discussion/src/main/res/drawable/discussion_ic_report.xml new file mode 100644 index 000000000..7989a4e84 --- /dev/null +++ b/discussion/src/main/res/drawable/discussion_ic_report.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/discussion/src/main/res/drawable/discussion_star_filled.xml b/discussion/src/main/res/drawable/discussion_star_filled.xml new file mode 100644 index 000000000..a33d84d99 --- /dev/null +++ b/discussion/src/main/res/drawable/discussion_star_filled.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/discussion/src/main/res/values-uk/strings.xml b/discussion/src/main/res/values-uk/strings.xml index e21357a82..a396d06c4 100644 --- a/discussion/src/main/res/values-uk/strings.xml +++ b/discussion/src/main/res/values-uk/strings.xml @@ -32,6 +32,7 @@ Тема Результати пошуку Почніть вводити, щоб знайти тему + anonymous %1$d голос diff --git a/discussion/src/main/res/values/strings.xml b/discussion/src/main/res/values/strings.xml index 913f4fca8..a4f18d95c 100644 --- a/discussion/src/main/res/values/strings.xml +++ b/discussion/src/main/res/values/strings.xml @@ -31,6 +31,7 @@ Topic Search results Start typing to find the thread + anonymous diff --git a/profile/src/main/java/com/raccoongang/profile/presentation/edit/EditProfileFragment.kt b/profile/src/main/java/com/raccoongang/profile/presentation/edit/EditProfileFragment.kt index 7086db6b4..30fa34734 100644 --- a/profile/src/main/java/com/raccoongang/profile/presentation/edit/EditProfileFragment.kt +++ b/profile/src/main/java/com/raccoongang/profile/presentation/edit/EditProfileFragment.kt @@ -536,7 +536,7 @@ private fun EditProfileScreen( .padding(2.dp) .size(100.dp) .clip(CircleShape) - .background(Color.Gray) + .noRippleClickable { isOpenChangeImageDialogState = true } diff --git a/profile/src/main/java/com/raccoongang/profile/presentation/profile/ProfileFragment.kt b/profile/src/main/java/com/raccoongang/profile/presentation/profile/ProfileFragment.kt index 831605716..360b26ba3 100644 --- a/profile/src/main/java/com/raccoongang/profile/presentation/profile/ProfileFragment.kt +++ b/profile/src/main/java/com/raccoongang/profile/presentation/profile/ProfileFragment.kt @@ -254,7 +254,6 @@ private fun ProfileScreen( .padding(2.dp) .size(100.dp) .clip(CircleShape) - .background(Color.Gray) ) Spacer(modifier = Modifier.height(20.dp)) Text( From 77c24c8b9903cfb9ab2d0b223ae3e4fd9ad0e535 Mon Sep 17 00:00:00 2001 From: Serhii <128455389+hryh27@users.noreply.github.com> Date: Mon, 3 Apr 2023 17:56:40 +0300 Subject: [PATCH 08/20] Feature/api improvements (#7) * is_enrolled flag for CourseDetails * discussion improvements * outline improvements * handouts fix * dashboard pagination * course search improvements --- .../java/com/raccoongang/newedx/AppRouter.kt | 31 +++--- .../raccoongang/newedx/room/AppDatabase.kt | 4 +- .../raccoongang/core/data/api/CourseApi.kt | 26 +++-- .../core/data/model/CourseDetails.kt | 5 +- .../core/data/model/CourseStructureModel.kt | 65 ++++++++++++- .../core/data/model/DashboardCourseList.kt | 33 +++++++ .../core/data/model/EnrolledCourse.kt | 3 +- .../core/data/model/room/BlockDb.kt | 18 +--- .../core/data/model/room/CourseEntity.kt | 8 +- .../data/model/room/CourseStructureEntity.kt | 64 ++++++++++++ .../raccoongang/core/domain/model/Course.kt | 3 +- .../core/domain/model/CourseStructure.kt | 18 +++- .../core/domain/model/DashboardCourseList.kt | 6 ++ .../data/repository/CourseRepository.kt | 45 +++------ .../course/data/storage/CourseConverter.kt | 12 +-- .../course/data/storage/CourseDao.kt | 15 ++- .../domain/interactor/CourseInteractor.kt | 15 ++- .../course/presentation/CourseRouter.kt | 9 +- .../container/CourseContainerFragment.kt | 45 ++++----- .../container/CourseContainerViewModel.kt | 8 +- .../detail/CourseDetailsFragment.kt | 41 +++----- .../detail/CourseDetailsUIState.kt | 4 +- .../detail/CourseDetailsViewModel.kt | 34 ++----- .../handouts/HandoutsViewModel.kt | 9 +- .../outline/CourseOutlineFragment.kt | 97 ++++++------------- .../outline/CourseOutlineUIState.kt | 3 +- .../outline/CourseOutlineViewModel.kt | 15 +-- .../section/CourseSectionViewModel.kt | 3 +- .../container/CourseUnitContainerViewModel.kt | 3 +- .../units/CourseUnitsViewModel.kt | 3 +- .../videos/CourseVideoViewModel.kt | 23 ++--- .../videos/CourseVideosFragment.kt | 72 +++++++------- .../videos/CourseVideosUIState.kt | 4 +- .../data/repository/DashboardRepository.kt | 14 +-- .../domain/interactor/DashboardInteractor.kt | 6 +- .../presentation/DashboardFragment.kt | 44 +++++++-- .../dashboard/presentation/DashboardRouter.kt | 9 +- .../presentation/DashboardViewModel.kt | 53 ++++++++-- .../presentation/DiscoveryFragment.kt | 3 +- .../search/CourseSearchFragment.kt | 3 +- .../discussion/data/api/DiscussionApi.kt | 6 +- .../data/model/response/CommentsResponse.kt | 4 + .../data/repository/DiscussionRepository.kt | 3 +- .../domain/interactor/DiscussionInteractor.kt | 17 +++- .../domain/model/DiscussionComment.kt | 2 + .../comments/DiscussionCommentsFragment.kt | 4 +- .../comments/DiscussionCommentsViewModel.kt | 11 +-- .../responses/DiscussionResponsesFragment.kt | 4 +- .../responses/DiscussionResponsesViewModel.kt | 11 +-- .../threads/DiscussionThreadsFragment.kt | 51 +++++++++- .../threads/DiscussionThreadsViewModel.kt | 96 ++++++++++++------ .../presentation/ui/DiscussionUI.kt | 10 +- 52 files changed, 667 insertions(+), 428 deletions(-) create mode 100644 core/src/main/java/com/raccoongang/core/data/model/DashboardCourseList.kt rename course/src/main/java/com/raccoongang/course/data/model/BlockDbEntity.kt => core/src/main/java/com/raccoongang/core/data/model/room/BlockDb.kt (93%) create mode 100644 core/src/main/java/com/raccoongang/core/data/model/room/CourseStructureEntity.kt create mode 100644 core/src/main/java/com/raccoongang/core/domain/model/DashboardCourseList.kt diff --git a/app/src/main/java/com/raccoongang/newedx/AppRouter.kt b/app/src/main/java/com/raccoongang/newedx/AppRouter.kt index 22d8b089e..756643997 100644 --- a/app/src/main/java/com/raccoongang/newedx/AppRouter.kt +++ b/app/src/main/java/com/raccoongang/newedx/AppRouter.kt @@ -8,13 +8,12 @@ import com.raccoongang.auth.presentation.signin.SignInFragment import com.raccoongang.auth.presentation.signup.SignUpFragment import com.raccoongang.core.FragmentViewType import com.raccoongang.core.domain.model.Account -import com.raccoongang.core.domain.model.Certificate import com.raccoongang.core.domain.model.CoursewareAccess import com.raccoongang.core.presentation.course.CourseViewMode import com.raccoongang.course.presentation.CourseRouter import com.raccoongang.course.presentation.container.CourseContainerFragment import com.raccoongang.course.presentation.container.NoAccessCourseContainerFragment -import com.raccoongang.course.presentation.detail.CourseDetailFragment +import com.raccoongang.course.presentation.detail.CourseDetailsFragment import com.raccoongang.course.presentation.handouts.HandoutsType import com.raccoongang.course.presentation.handouts.WebViewFragment import com.raccoongang.discovery.presentation.search.CourseSearchFragment @@ -38,7 +37,7 @@ import com.raccoongang.profile.presentation.delete.DeleteProfileFragment import com.raccoongang.profile.presentation.edit.EditProfileFragment import com.raccoongang.profile.presentation.settings.video.VideoQualityFragment import com.raccoongang.profile.presentation.settings.video.VideoSettingsFragment -import java.util.Date +import java.util.* class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, DiscussionRouter, ProfileRouter { @@ -62,7 +61,7 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di //region DiscoveryRouter override fun navigateToCourseDetail(fm: FragmentManager, courseId: String) { - replaceFragmentWithBackStack(fm, CourseDetailFragment.newInstance(courseId)) + replaceFragmentWithBackStack(fm, CourseDetailsFragment.newInstance(courseId)) } override fun navigateToCourseSearch(fm: FragmentManager) { @@ -71,26 +70,26 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di //endregion //region DashboardRouter + override fun navigateToCourseOutline( fm: FragmentManager, courseId: String, - title: String, - image: String, - certificate: Certificate, - coursewareAccess: CoursewareAccess, - auditAccessExpires: Date? + courseTitle: String ) { - val destinationFragment = if (coursewareAccess.hasAccess) { - CourseContainerFragment.newInstance(courseId, title, image, certificate) - } else { - NoAccessCourseContainerFragment.newInstance(title, coursewareAccess, auditAccessExpires) - } - replaceFragmentWithBackStack( fm, - destinationFragment + CourseContainerFragment.newInstance(courseId, courseTitle) ) } + + override fun navigateToNoAccess( + fm: FragmentManager, + title: String, + coursewareAccess: CoursewareAccess, + auditAccessExpires: Date? + ) { + replaceFragment(fm, NoAccessCourseContainerFragment.newInstance(title,coursewareAccess, auditAccessExpires)) + } //endregion //region CourseRouter diff --git a/app/src/main/java/com/raccoongang/newedx/room/AppDatabase.kt b/app/src/main/java/com/raccoongang/newedx/room/AppDatabase.kt index 6457f5889..e8acf7994 100644 --- a/app/src/main/java/com/raccoongang/newedx/room/AppDatabase.kt +++ b/app/src/main/java/com/raccoongang/newedx/room/AppDatabase.kt @@ -4,10 +4,10 @@ import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters import com.raccoongang.core.data.model.room.CourseEntity +import com.raccoongang.core.data.model.room.CourseStructureEntity import com.raccoongang.core.data.model.room.discovery.EnrolledCourseEntity import com.raccoongang.core.module.db.DownloadDao import com.raccoongang.core.module.db.DownloadModelEntity -import com.raccoongang.course.data.model.BlockDbEntity import com.raccoongang.course.data.storage.CourseConverter import com.raccoongang.course.data.storage.CourseDao import com.raccoongang.dashboard.data.DashboardDao @@ -21,7 +21,7 @@ const val DATABASE_NAME = "newEdx_db" entities = [ CourseEntity::class, EnrolledCourseEntity::class, - BlockDbEntity::class, + CourseStructureEntity::class, DownloadModelEntity::class ], version = DATABASE_VERSION, diff --git a/core/src/main/java/com/raccoongang/core/data/api/CourseApi.kt b/core/src/main/java/com/raccoongang/core/data/api/CourseApi.kt index 7a1149264..2dfe736d6 100644 --- a/core/src/main/java/com/raccoongang/core/data/api/CourseApi.kt +++ b/core/src/main/java/com/raccoongang/core/data/api/CourseApi.kt @@ -6,31 +6,37 @@ import retrofit2.http.* interface CourseApi { - @GET("/api/mobile/v1/users/{username}/course_enrollments") + @GET("/mobile_api_extensions/v1/users/{username}/course_enrollments") suspend fun getEnrolledCourses( @Header("Cache-Control") cacheControlHeaderParam: String? = null, @Path("username") username: String, @Query("org") org: String? = null, - ): List + @Query("page") page: Int + ): DashboardCourseList - @GET("/api/courses/v1/courses/") + @GET("/mobile_api_extensions/courses/v1/courses/") suspend fun getCourseList( @Query("search_term") searchQuery: String? = null, @Query("page") page: Int, @Query("mobile") mobile: Boolean, @Query("username") username: String? = null, @Query("org") org: String? = null, - @Query("permissions") permission: List = listOf("enroll","see_in_catalog","see_about_page") + @Query("permissions") permission: List = listOf( + "enroll", + "see_in_catalog", + "see_about_page" + ) ): CourseList - @GET("/api/courses/v1/courses/{course_id}") + @GET("/mobile_api_extensions/v1/courses/{course_id}") suspend fun getCourseDetail( @Path("course_id") courseId: String?, @Query("username") username: String? = null, + @Query("is_enrolled") isEnrolled: Boolean = true, ): CourseDetails @GET( - "/api/courses/{api_version}/blocks/?" + + "/mobile_api_extensions/{api_version}/blocks/?" + "depth=all&" + "requested_fields=contains_gated_content,show_gated_sections,special_exam_info,graded,format,student_view_multi_device,due,completion&" + "student_view_data=video,discussion&" + @@ -59,9 +65,9 @@ interface CourseApi { blocksCompletionBody: BlocksCompletionBody ) - @GET - suspend fun getHandouts(@Url url: String): HandoutsModel + @GET("/api/mobile/v1/course_info/{course_id}/handouts") + suspend fun getHandouts(@Path("course_id") courseId: String): HandoutsModel - @GET - suspend fun getAnnouncements(@Url url: String): List + @GET("/api/mobile/v1/course_info/{course_id}/updates") + suspend fun getAnnouncements(@Path("course_id") courseId: String): List } diff --git a/core/src/main/java/com/raccoongang/core/data/model/CourseDetails.kt b/core/src/main/java/com/raccoongang/core/data/model/CourseDetails.kt index 1845f068c..b8de035a6 100644 --- a/core/src/main/java/com/raccoongang/core/data/model/CourseDetails.kt +++ b/core/src/main/java/com/raccoongang/core/data/model/CourseDetails.kt @@ -45,7 +45,9 @@ data class CourseDetails( @SerializedName("start_type") val startType: String?, @SerializedName("overview") - val overview: String? + val overview: String?, + @SerializedName("is_enrolled") + val isEnrolled: Boolean?, ) { fun mapToDomain(): Course { @@ -69,6 +71,7 @@ data class CourseDetails( startType = startType ?: "", pacing = pacing ?: "", overview = overview ?: "", + isEnrolled = isEnrolled ?: false, media = media?.mapToDomain() ?: com.raccoongang.core.domain.model.Media() ) } diff --git a/core/src/main/java/com/raccoongang/core/data/model/CourseStructureModel.kt b/core/src/main/java/com/raccoongang/core/data/model/CourseStructureModel.kt index f86b6eda1..3471ae731 100644 --- a/core/src/main/java/com/raccoongang/core/data/model/CourseStructureModel.kt +++ b/core/src/main/java/com/raccoongang/core/data/model/CourseStructureModel.kt @@ -1,20 +1,79 @@ package com.raccoongang.core.data.model import com.google.gson.annotations.SerializedName +import com.raccoongang.core.data.model.room.BlockDb +import com.raccoongang.core.data.model.room.CourseStructureEntity +import com.raccoongang.core.data.model.room.MediaDb import com.raccoongang.core.domain.model.CourseStructure +import com.raccoongang.core.utils.TimeUtils data class CourseStructureModel( @SerializedName("root") val root: String, @SerializedName("blocks") - val blockData: Map + val blockData: Map, + @SerializedName("id") + var id: String?, + @SerializedName("name") + var name: String?, + @SerializedName("number") + var number: String?, + @SerializedName("org") + var org: String?, + @SerializedName("start") + var start: String?, + @SerializedName("start_display") + var startDisplay: String?, + @SerializedName("start_type") + var startType: String?, + @SerializedName("end") + var end: String?, + @SerializedName("courseware_access") + var coursewareAccess: CoursewareAccess?, + @SerializedName("media") + var media: Media?, + @SerializedName("certificate") + val certificate: Certificate?, + @SerializedName("is_self_paced") + var isSelfPaced: Boolean? ) { fun mapToDomain(): CourseStructure { return CourseStructure( root = root, blockData = blockData.map { - it.key to it.value.mapToDomain() - }.toMap() + it.value.mapToDomain() + }, + id = id ?: "", + name = name ?: "", + number = number ?: "", + org = org ?: "", + start = TimeUtils.iso8601ToDate(start ?: ""), + startDisplay = startDisplay ?: "", + startType = startType ?: "", + end = TimeUtils.iso8601ToDate(end ?: ""), + coursewareAccess = coursewareAccess?.mapToDomain()!!, + media = media?.mapToDomain(), + certificate = certificate?.mapToDomain(), + isSelfPaced = isSelfPaced ?: false + ) + } + + fun mapToRoomEntity(): CourseStructureEntity { + return CourseStructureEntity( + root, + blocks = blockData.map { BlockDb.createFrom(it.value) }, + id = id ?: "", + name = name ?: "", + number = number ?: "", + org = org ?: "", + start = start ?: "", + startDisplay = startDisplay ?: "", + startType = startType ?: "", + end = end ?: "", + coursewareAccess = coursewareAccess?.mapToRoomEntity()!!, + media = MediaDb.createFrom(media), + certificate = certificate?.mapToRoomEntity(), + isSelfPaced = isSelfPaced ?: false ) } } diff --git a/core/src/main/java/com/raccoongang/core/data/model/DashboardCourseList.kt b/core/src/main/java/com/raccoongang/core/data/model/DashboardCourseList.kt new file mode 100644 index 000000000..eb1e64785 --- /dev/null +++ b/core/src/main/java/com/raccoongang/core/data/model/DashboardCourseList.kt @@ -0,0 +1,33 @@ +package com.raccoongang.core.data.model + +import com.google.gson.annotations.SerializedName +import com.raccoongang.core.domain.model.DashboardCourseList + +data class DashboardCourseList( + @SerializedName("next") + val next: String?, + @SerializedName("previous") + val previous: String?, + @SerializedName("count") + val count: Int, + @SerializedName("num_pages") + val numPages: Int, + @SerializedName("current_page") + val currentPage: Int, + @SerializedName("results") + val results: List +) { + + fun mapToDomain(): DashboardCourseList { + return DashboardCourseList( + com.raccoongang.core.domain.model.Pagination( + count, + next ?: "", + numPages, + previous ?: "" + ), + results.map { it.mapToDomain() } + ) + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/raccoongang/core/data/model/EnrolledCourse.kt b/core/src/main/java/com/raccoongang/core/data/model/EnrolledCourse.kt index b3b8f9eaa..550182c2a 100644 --- a/core/src/main/java/com/raccoongang/core/data/model/EnrolledCourse.kt +++ b/core/src/main/java/com/raccoongang/core/data/model/EnrolledCourse.kt @@ -4,7 +4,6 @@ import com.google.gson.annotations.SerializedName import com.raccoongang.core.data.model.room.discovery.EnrolledCourseEntity import com.raccoongang.core.domain.model.EnrolledCourse import com.raccoongang.core.utils.TimeUtils -import java.sql.Time data class EnrolledCourse( @SerializedName("audit_access_expires") @@ -22,7 +21,7 @@ data class EnrolledCourse( ) { fun mapToDomain(): EnrolledCourse { return EnrolledCourse( - auditAccessExpires = TimeUtils.iso8601ToDate(auditAccessExpires?:""), + auditAccessExpires = TimeUtils.iso8601ToDate(auditAccessExpires ?: ""), created = created ?: "", mode = mode ?: "", isActive = isActive ?: false, diff --git a/course/src/main/java/com/raccoongang/course/data/model/BlockDbEntity.kt b/core/src/main/java/com/raccoongang/core/data/model/room/BlockDb.kt similarity index 93% rename from course/src/main/java/com/raccoongang/course/data/model/BlockDbEntity.kt rename to core/src/main/java/com/raccoongang/core/data/model/room/BlockDb.kt index f021a6be6..6286ec4c8 100644 --- a/course/src/main/java/com/raccoongang/course/data/model/BlockDbEntity.kt +++ b/core/src/main/java/com/raccoongang/core/data/model/room/BlockDb.kt @@ -1,17 +1,11 @@ -package com.raccoongang.course.data.model +package com.raccoongang.core.data.model.room import androidx.room.ColumnInfo import androidx.room.Embedded -import androidx.room.Entity -import androidx.room.PrimaryKey import com.raccoongang.core.BlockType import com.raccoongang.core.domain.model.* -@Entity(tableName = "course_blocks_table") -data class BlockDbEntity( - @ColumnInfo("courseId") - val courseId: String, - @PrimaryKey +data class BlockDb( @ColumnInfo("id") val id: String, @ColumnInfo("blockId") @@ -60,12 +54,10 @@ data class BlockDbEntity( companion object { fun createFrom( - block: com.raccoongang.core.data.model.Block, - courseId: String - ): BlockDbEntity { + block: com.raccoongang.core.data.model.Block + ): BlockDb { with(block) { - return BlockDbEntity( - courseId = courseId, + return BlockDb( id = id ?: "", blockId = blockId ?: "", lmsWebUrl = lmsWebUrl ?: "", diff --git a/core/src/main/java/com/raccoongang/core/data/model/room/CourseEntity.kt b/core/src/main/java/com/raccoongang/core/data/model/room/CourseEntity.kt index bd2a6d0a1..821370eb7 100644 --- a/core/src/main/java/com/raccoongang/core/data/model/room/CourseEntity.kt +++ b/core/src/main/java/com/raccoongang/core/data/model/room/CourseEntity.kt @@ -50,7 +50,9 @@ data class CourseEntity( @ColumnInfo("startType") val startType: String, @ColumnInfo("overview") - val overview: String + val overview: String, + @ColumnInfo("isEnrolled") + val isEnrolled: Boolean ) { fun mapToDomain(): Course { @@ -75,6 +77,7 @@ data class CourseEntity( startDisplay = startDisplay, startType = startType, overview = overview, + isEnrolled = isEnrolled ) } @@ -101,7 +104,8 @@ data class CourseEntity( startType = startType ?: "", pacing = pacing ?: "", overview = overview ?: "", - media = MediaDb.createFrom(media) + media = MediaDb.createFrom(media), + isEnrolled = isEnrolled ?: false ) } } diff --git a/core/src/main/java/com/raccoongang/core/data/model/room/CourseStructureEntity.kt b/core/src/main/java/com/raccoongang/core/data/model/room/CourseStructureEntity.kt new file mode 100644 index 000000000..1af4d2e7c --- /dev/null +++ b/core/src/main/java/com/raccoongang/core/data/model/room/CourseStructureEntity.kt @@ -0,0 +1,64 @@ +package com.raccoongang.core.data.model.room + +import androidx.room.ColumnInfo +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.raccoongang.core.data.model.room.discovery.CertificateDb +import com.raccoongang.core.data.model.room.discovery.CoursewareAccessDb +import com.raccoongang.core.domain.model.CourseStructure +import com.raccoongang.core.utils.TimeUtils + +@Entity(tableName = "course_structure_table") +data class CourseStructureEntity( + @ColumnInfo("root") + val root: String, + @PrimaryKey + @ColumnInfo("id") + val id: String, + @ColumnInfo("blocks") + val blocks: List, + @ColumnInfo("name") + val name: String, + @ColumnInfo("number") + val number: String, + @ColumnInfo("org") + val org: String, + @ColumnInfo("start") + val start: String?, + @ColumnInfo("startDisplay") + val startDisplay: String, + @ColumnInfo("startType") + val startType: String, + @ColumnInfo("end") + val end: String?, + @Embedded + val coursewareAccess: CoursewareAccessDb, + @Embedded + val media: MediaDb?, + @Embedded + val certificate: CertificateDb?, + @ColumnInfo("isSelfPaced") + val isSelfPaced: Boolean +) { + + fun mapToDomain(): CourseStructure { + return CourseStructure( + root, + blocks.map { it.mapToDomain() }, + id, + name, + number, + org, + TimeUtils.iso8601ToDate(start ?: ""), + startDisplay, + startType, + TimeUtils.iso8601ToDate(end ?: ""), + coursewareAccess.mapToDomain(), + media?.mapToDomain(), + certificate?.mapToDomain(), + isSelfPaced + ) + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/raccoongang/core/domain/model/Course.kt b/core/src/main/java/com/raccoongang/core/domain/model/Course.kt index 496bfc425..3d41341f3 100644 --- a/core/src/main/java/com/raccoongang/core/domain/model/Course.kt +++ b/core/src/main/java/com/raccoongang/core/domain/model/Course.kt @@ -23,5 +23,6 @@ data class Course( val end: String, val startDisplay: String, val startType: String, - val overview : String + val overview : String, + val isEnrolled: Boolean ) \ No newline at end of file diff --git a/core/src/main/java/com/raccoongang/core/domain/model/CourseStructure.kt b/core/src/main/java/com/raccoongang/core/domain/model/CourseStructure.kt index a75cf2579..e6391dc09 100644 --- a/core/src/main/java/com/raccoongang/core/domain/model/CourseStructure.kt +++ b/core/src/main/java/com/raccoongang/core/domain/model/CourseStructure.kt @@ -1,6 +1,20 @@ package com.raccoongang.core.domain.model -class CourseStructure( +import java.util.* + +data class CourseStructure( val root: String, - val blockData: Map + val blockData: List, + val id: String, + val name: String, + val number: String, + val org: String, + val start: Date?, + val startDisplay: String, + val startType: String, + val end: Date?, + val coursewareAccess: CoursewareAccess, + val media: Media?, + val certificate: Certificate?, + val isSelfPaced: Boolean ) \ No newline at end of file diff --git a/core/src/main/java/com/raccoongang/core/domain/model/DashboardCourseList.kt b/core/src/main/java/com/raccoongang/core/domain/model/DashboardCourseList.kt new file mode 100644 index 000000000..ed3c2c38e --- /dev/null +++ b/core/src/main/java/com/raccoongang/core/domain/model/DashboardCourseList.kt @@ -0,0 +1,6 @@ +package com.raccoongang.core.domain.model + +data class DashboardCourseList( + val pagination: Pagination, + val courses: List +) diff --git a/course/src/main/java/com/raccoongang/course/data/repository/CourseRepository.kt b/course/src/main/java/com/raccoongang/course/data/repository/CourseRepository.kt index a6aacf6c4..9ee3b5766 100644 --- a/course/src/main/java/com/raccoongang/course/data/repository/CourseRepository.kt +++ b/course/src/main/java/com/raccoongang/course/data/repository/CourseRepository.kt @@ -8,7 +8,6 @@ import com.raccoongang.core.data.storage.PreferencesManager import com.raccoongang.core.domain.model.* import com.raccoongang.core.exception.NoCachedDataException import com.raccoongang.core.module.db.DownloadDao -import com.raccoongang.course.data.model.BlockDbEntity import com.raccoongang.course.data.storage.CourseDao import kotlinx.coroutines.flow.map import okhttp3.ResponseBody @@ -19,7 +18,7 @@ class CourseRepository( private val downloadDao: DownloadDao, private val preferencesManager: PreferencesManager, ) { - private val blocksList = mutableListOf() + private var courseStructure: CourseStructure? = null suspend fun getCourseDetail(id: String): Course { val course = api.getCourseDetail(id) @@ -56,47 +55,30 @@ class CourseRepository( preferencesManager.user?.username, courseId ) - val courseStructure = response.mapToDomain() - courseDao.insertCourseBlocks( - *response.blockData.values - .map { - BlockDbEntity.createFrom(it, courseId) - }.toTypedArray() - ) - blocksList.clear() - blocksList.addAll(courseStructure.blockData.values.toList()) + courseDao.insertCourseStructureEntity(response.mapToRoomEntity()) + courseStructure = null + courseStructure = response.mapToDomain() } suspend fun preloadCourseStructureFromCache(courseId: String) { - val response = courseDao.getCourseBlocksById(courseId) - blocksList.clear() - if (!response.isNullOrEmpty()) { - blocksList.addAll(response.map { it.mapToDomain() }) + val cachedCourseStructure = courseDao.getCourseStructureById(courseId) + courseStructure = null + if (cachedCourseStructure != null) { + courseStructure = cachedCourseStructure.mapToDomain() } else { throw NoCachedDataException() } } @Throws(IllegalStateException::class) - fun getCourseStructureFromCache(): List { - if (blocksList.isNotEmpty()) { - return blocksList + fun getCourseStructureFromCache(): CourseStructure { + if (courseStructure != null) { + return courseStructure!! } else { throw IllegalStateException("Course structure is empty") } } - suspend fun getEnrolledCourseById(courseId: String): EnrolledCourse? { - val user = preferencesManager.user - val enrolledCourse = api.getEnrolledCourses( - username = user?.username ?: "" - ) - val course = enrolledCourse.find { - it.course?.id == courseId - } - return course?.mapToDomain() - } - suspend fun getEnrolledCourseFromCacheById(courseId: String): EnrolledCourse? { val course = courseDao.getEnrolledCourseById(courseId) return course?.mapToDomain() @@ -117,8 +99,9 @@ class CourseRepository( return api.markBlocksCompletion(blocksCompletionBody) } - suspend fun getHandouts(url: String) = api.getHandouts(url).mapToDomain() + suspend fun getHandouts(courseId: String) = api.getHandouts(courseId).mapToDomain() - suspend fun getAnnouncements(url: String) = api.getAnnouncements(url).map { it.mapToDomain() } + suspend fun getAnnouncements(courseId: String) = + api.getAnnouncements(courseId).map { it.mapToDomain() } } \ No newline at end of file diff --git a/course/src/main/java/com/raccoongang/course/data/storage/CourseConverter.kt b/course/src/main/java/com/raccoongang/course/data/storage/CourseConverter.kt index c432df2e3..21cac9219 100644 --- a/course/src/main/java/com/raccoongang/course/data/storage/CourseConverter.kt +++ b/course/src/main/java/com/raccoongang/course/data/storage/CourseConverter.kt @@ -2,9 +2,9 @@ package com.raccoongang.course.data.storage import androidx.room.TypeConverter import com.google.gson.Gson +import com.raccoongang.core.data.model.room.BlockDb +import com.raccoongang.core.data.model.room.VideoInfoDb import com.raccoongang.core.extension.genericType -import com.raccoongang.course.data.model.BlockDbEntity -import com.raccoongang.course.data.model.VideoInfoDb class CourseConverter { @@ -34,15 +34,15 @@ class CourseConverter { } @TypeConverter - fun fromListOfBlockDbEntity(value: List): String { + fun fromListOfBlockDbEntity(value: List): String { val json = Gson().toJson(value) return json.toString() } @TypeConverter - fun toListOfBlockDbEntity(value: String): List { - val type = genericType>() + fun toListOfBlockDbEntity(value: String): List { + val type = genericType>() return Gson().fromJson(value, type) } -} \ No newline at end of file +} diff --git a/course/src/main/java/com/raccoongang/course/data/storage/CourseDao.kt b/course/src/main/java/com/raccoongang/course/data/storage/CourseDao.kt index 03a35a014..cbb9a7fbd 100644 --- a/course/src/main/java/com/raccoongang/course/data/storage/CourseDao.kt +++ b/course/src/main/java/com/raccoongang/course/data/storage/CourseDao.kt @@ -5,8 +5,8 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import com.raccoongang.core.data.model.room.CourseEntity +import com.raccoongang.core.data.model.room.CourseStructureEntity import com.raccoongang.core.data.model.room.discovery.EnrolledCourseEntity -import com.raccoongang.course.data.model.BlockDbEntity @Dao interface CourseDao { @@ -17,15 +17,12 @@ interface CourseDao { @Query("SELECT * FROM course_enrolled_table WHERE id=:id") suspend fun getEnrolledCourseById(id: String): EnrolledCourseEntity? - @Query("SELECT * FROM course_blocks_table WHERE courseId=:id") - suspend fun getCourseBlocksById(id: String): List? - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertCourseBlocks(vararg BlockDbEntityEntity: BlockDbEntity) + @Query("SELECT * FROM course_structure_table WHERE id=:id") + suspend fun getCourseStructureById(id: String): CourseStructureEntity? @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertCourseEntity(vararg courseEntity: CourseEntity) - @Query("DELETE FROM course_blocks_table") - suspend fun clearAllCourseBlocks() -} \ No newline at end of file + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertCourseStructureEntity(vararg courseStructureEntity: CourseStructureEntity) +} diff --git a/course/src/main/java/com/raccoongang/course/domain/interactor/CourseInteractor.kt b/course/src/main/java/com/raccoongang/course/domain/interactor/CourseInteractor.kt index 6e607cee9..b901fc78b 100644 --- a/course/src/main/java/com/raccoongang/course/domain/interactor/CourseInteractor.kt +++ b/course/src/main/java/com/raccoongang/course/domain/interactor/CourseInteractor.kt @@ -2,7 +2,7 @@ package com.raccoongang.course.domain.interactor import com.raccoongang.core.BlockType import com.raccoongang.core.domain.model.Block -import com.raccoongang.core.module.db.DownloadModel +import com.raccoongang.core.domain.model.CourseStructure import com.raccoongang.course.data.repository.CourseRepository class CourseInteractor( @@ -27,8 +27,9 @@ class CourseInteractor( fun getCourseStructureFromCache() = repository.getCourseStructureFromCache() @Throws(IllegalStateException::class) - fun getCourseStructureForVideos(): List { - val blocks = repository.getCourseStructureFromCache() + fun getCourseStructureForVideos(): CourseStructure { + val courseStructure = repository.getCourseStructureFromCache() + val blocks = courseStructure.blockData val videoBlocks = blocks.filter { it.type == BlockType.VIDEO } val resultBlocks = ArrayList() videoBlocks.forEach { videoBlock -> @@ -62,19 +63,17 @@ class CourseInteractor( } } - return resultBlocks.toList() + return courseStructure.copy(blockData = resultBlocks.toList()) } - suspend fun getEnrolledCourseById(courseId: String) = repository.getEnrolledCourseById(courseId) - suspend fun getEnrolledCourseFromCacheById(courseId: String) = repository.getEnrolledCourseFromCacheById(courseId) suspend fun getCourseStatus(courseId: String) = repository.getCourseStatus(courseId) - suspend fun getHandouts(url: String) = repository.getHandouts(url) + suspend fun getHandouts(courseId: String) = repository.getHandouts(courseId) - suspend fun getAnnouncements(url: String) = repository.getAnnouncements(url) + suspend fun getAnnouncements(courseId: String) = repository.getAnnouncements(courseId) suspend fun removeDownloadModel(id: String) = repository.removeDownloadModel(id) diff --git a/course/src/main/java/com/raccoongang/course/presentation/CourseRouter.kt b/course/src/main/java/com/raccoongang/course/presentation/CourseRouter.kt index 6519b2d66..b0c61b932 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/CourseRouter.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/CourseRouter.kt @@ -1,7 +1,6 @@ package com.raccoongang.course.presentation import androidx.fragment.app.FragmentManager -import com.raccoongang.core.domain.model.Certificate import com.raccoongang.core.domain.model.CoursewareAccess import com.raccoongang.core.presentation.course.CourseViewMode import com.raccoongang.course.presentation.handouts.HandoutsType @@ -12,12 +11,16 @@ interface CourseRouter { fun navigateToCourseOutline( fm: FragmentManager, courseId: String, + courseTitle: String + ) + + fun navigateToNoAccess( + fm: FragmentManager, title: String, - image: String, - certificate: Certificate, coursewareAccess: CoursewareAccess, auditAccessExpires: Date? ) + fun navigateToCourseUnits( fm: FragmentManager, courseId: String, diff --git a/course/src/main/java/com/raccoongang/course/presentation/container/CourseContainerFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/container/CourseContainerFragment.kt index 62be4cf56..1ae1a62b9 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/container/CourseContainerFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/container/CourseContainerFragment.kt @@ -7,15 +7,15 @@ import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.snackbar.Snackbar -import com.raccoongang.core.domain.model.Certificate -import com.raccoongang.core.extension.parcelable import com.raccoongang.core.presentation.global.viewBinding import com.raccoongang.course.R import com.raccoongang.course.databinding.FragmentCourseContainerBinding +import com.raccoongang.course.presentation.CourseRouter import com.raccoongang.course.presentation.handouts.HandoutsFragment import com.raccoongang.course.presentation.outline.CourseOutlineFragment import com.raccoongang.course.presentation.videos.CourseVideosFragment import com.raccoongang.discussion.presentation.topics.DiscussionTopicsFragment +import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf @@ -25,19 +25,17 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { private val viewModel by viewModel { parametersOf(requireArguments().getString(ARG_COURSE_ID, "")) } + private val router by inject() + private var adapter: CourseNavigationFragmentAdapter? = null private var courseTitle = "" - private var courseImage = "" - private var courseCertificate = Certificate("") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) with(requireArguments()) { - courseImage = getString(ARG_IMAGE, "") courseTitle = getString(ARG_TITLE, "") - courseCertificate = parcelable(ARG_CERTIFICATE)!! } viewModel.preloadCourseStructure() } @@ -66,11 +64,20 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { } private fun observe() { - viewModel.dataReady.observe(viewLifecycleOwner) { ready -> - if (ready) { - binding.viewPager.isVisible = true - binding.bottomNavView.isVisible = true - initViewPager() + viewModel.dataReady.observe(viewLifecycleOwner) { coursewareAccess -> + if (coursewareAccess != null) { + if (coursewareAccess.hasAccess) { + binding.viewPager.isVisible = true + binding.bottomNavView.isVisible = true + initViewPager() + } else { + router.navigateToNoAccess( + requireActivity().supportFragmentManager, + courseTitle, + coursewareAccess, + null + ) + } } } viewModel.errorMessage.observe(viewLifecycleOwner) { @@ -90,8 +97,8 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { binding.viewPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL binding.viewPager.offscreenPageLimit = 4 adapter = CourseNavigationFragmentAdapter(this).apply { - addFragment(CourseOutlineFragment.newInstance(viewModel.courseId,courseTitle, courseImage, courseCertificate)) - addFragment(CourseVideosFragment.newInstance(viewModel.courseId,courseTitle, courseImage, courseCertificate)) + addFragment(CourseOutlineFragment.newInstance(viewModel.courseId, courseTitle)) + addFragment(CourseVideosFragment.newInstance(viewModel.courseId, courseTitle)) addFragment(DiscussionTopicsFragment.newInstance(viewModel.courseId)) addFragment(HandoutsFragment.newInstance(viewModel.courseId)) } @@ -99,27 +106,21 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { binding.viewPager.isUserInputEnabled = false } - fun updateCourseStructure(withSwipeRefresh: Boolean){ + fun updateCourseStructure(withSwipeRefresh: Boolean) { viewModel.updateData(withSwipeRefresh) } companion object { private const val ARG_COURSE_ID = "courseId" private const val ARG_TITLE = "title" - private const val ARG_IMAGE = "image" - private const val ARG_CERTIFICATE = "certificate" fun newInstance( courseId: String, - title: String, - image: String, - certificate: Certificate + courseTitle: String ): CourseContainerFragment { val fragment = CourseContainerFragment() fragment.arguments = bundleOf( ARG_COURSE_ID to courseId, - ARG_TITLE to title, - ARG_IMAGE to image, - ARG_CERTIFICATE to certificate + ARG_TITLE to courseTitle ) return fragment } diff --git a/course/src/main/java/com/raccoongang/course/presentation/container/CourseContainerViewModel.kt b/course/src/main/java/com/raccoongang/course/presentation/container/CourseContainerViewModel.kt index 27e032a77..ebf3c24a6 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/container/CourseContainerViewModel.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/container/CourseContainerViewModel.kt @@ -7,6 +7,7 @@ import com.raccoongang.core.BaseViewModel import com.raccoongang.core.system.connection.NetworkConnection import com.raccoongang.core.R import com.raccoongang.core.SingleEventLiveData +import com.raccoongang.core.domain.model.CoursewareAccess import com.raccoongang.core.exception.NoCachedDataException import com.raccoongang.core.extension.isInternetError import com.raccoongang.core.system.ResourceManager @@ -23,8 +24,8 @@ class CourseContainerViewModel( private val networkConnection: NetworkConnection ) : BaseViewModel() { - private val _dataReady = MutableLiveData() - val dataReady: LiveData + private val _dataReady = MutableLiveData() + val dataReady: LiveData get() = _dataReady private val _errorMessage = SingleEventLiveData() @@ -48,7 +49,8 @@ class CourseContainerViewModel( } else { interactor.preloadCourseStructureFromCache(courseId) } - _dataReady.value = true + val courseStructure = interactor.getCourseStructureFromCache() + _dataReady.value = courseStructure.coursewareAccess } catch (e: Exception) { if (e.isInternetError() || e is NoCachedDataException) { _errorMessage.value = diff --git a/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt index 8ec9db7ef..c2f161634 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt @@ -39,7 +39,6 @@ import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import com.raccoongang.core.BuildConfig import com.raccoongang.core.UIMessage -import com.raccoongang.core.domain.model.Certificate import com.raccoongang.core.domain.model.Course import com.raccoongang.core.domain.model.Media import com.raccoongang.core.ui.* @@ -57,7 +56,7 @@ import java.nio.charset.StandardCharsets import java.util.* import com.raccoongang.course.R as courseR -class CourseDetailFragment : Fragment() { +class CourseDetailsFragment : Fragment() { private val viewModel by viewModel { parametersOf(requireArguments().getString(ARG_COURSE_ID, "")) @@ -98,15 +97,11 @@ class CourseDetailFragment : Fragment() { onButtonClick = { val currentState = uiState if (currentState is CourseDetailsUIState.CourseData) { - if (currentState.enrolledCourse != null) { + if (currentState.course.isEnrolled) { router.navigateToCourseOutline( requireActivity().supportFragmentManager, currentState.course.courseId, - currentState.enrolledCourse.course.name, - currentState.enrolledCourse.course.courseImage, - currentState.enrolledCourse.certificate ?: Certificate(""), - currentState.enrolledCourse.course.coursewareAccess, - currentState.enrolledCourse.auditAccessExpires + currentState.course.name ) } else { viewModel.enrollInACourse(currentState.course.courseId) @@ -119,8 +114,8 @@ class CourseDetailFragment : Fragment() { companion object { private const val ARG_COURSE_ID = "courseId" - fun newInstance(courseId: String): CourseDetailFragment { - val fragment = CourseDetailFragment() + fun newInstance(courseId: String): CourseDetailsFragment { + val fragment = CourseDetailsFragment() fragment.arguments = bundleOf( ARG_COURSE_ID to courseId ) @@ -236,7 +231,6 @@ internal fun CourseDetailsScreen( CourseDetailNativeContentLandscape( windowSize = windowSize, course = uiState.course, - uiState.enrolledCourse != null, onButtonClick = { onButtonClick() } @@ -245,7 +239,6 @@ internal fun CourseDetailsScreen( CourseDetailNativeContent( windowSize = windowSize, course = uiState.course, - uiState.enrolledCourse != null, onButtonClick = { onButtonClick() } @@ -308,7 +301,6 @@ internal fun CourseDetailsScreen( private fun CourseDetailNativeContent( windowSize: WindowSize, course: Course, - isEnrolled: Boolean, onButtonClick: () -> Unit, ) { val buttonWidth by remember(key1 = windowSize) { @@ -320,15 +312,6 @@ private fun CourseDetailNativeContent( ) } - val imageHeight by remember(key1 = windowSize) { - mutableStateOf( - windowSize.windowSizeValue( - expanded = 260.dp, - compact = 200.dp - ) - ) - } - val contentHorizontalPadding by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( @@ -338,7 +321,7 @@ private fun CourseDetailNativeContent( ) } - val buttonText = if (isEnrolled) { + val buttonText = if (course.isEnrolled) { stringResource(id = R.string.course_view_course) } else { stringResource(id = R.string.course_enroll_now) @@ -397,7 +380,6 @@ private fun CourseDetailNativeContent( private fun CourseDetailNativeContentLandscape( windowSize: WindowSize, course: Course, - isEnrolled: Boolean, onButtonClick: () -> Unit, ) { val buttonWidth by remember(key1 = windowSize) { @@ -409,7 +391,7 @@ private fun CourseDetailNativeContentLandscape( ) } - val buttonText = if (isEnrolled) { + val buttonText = if (course.isEnrolled) { stringResource(id = R.string.course_view_course) } else { stringResource(id = R.string.course_enroll_now) @@ -572,7 +554,7 @@ private fun CourseDetailNativeContentPreview() { NewEdxTheme { CourseDetailsScreen( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), - uiState = CourseDetailsUIState.CourseData(mockCourse, null), + uiState = CourseDetailsUIState.CourseData(mockCourse), uiMessage = null, hasInternetConnection = false, htmlBody = "Preview text", @@ -590,7 +572,7 @@ private fun CourseDetailNativeContentTabletPreview() { NewEdxTheme { CourseDetailsScreen( windowSize = WindowSize(WindowType.Medium, WindowType.Medium), - uiState = CourseDetailsUIState.CourseData(mockCourse, null), + uiState = CourseDetailsUIState.CourseData(mockCourse), uiMessage = null, hasInternetConnection = false, htmlBody = "Preview text", @@ -621,5 +603,6 @@ private val mockCourse = Course( end = "end", startDisplay = "startDisplay", startType = "startType", - overview = "" -) \ No newline at end of file + overview = "", + isEnrolled = false +) diff --git a/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsUIState.kt b/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsUIState.kt index 4cfb6d06b..1a2148f53 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsUIState.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsUIState.kt @@ -1,10 +1,8 @@ package com.raccoongang.course.presentation.detail import com.raccoongang.core.domain.model.Course -import com.raccoongang.core.domain.model.EnrolledCourse - sealed class CourseDetailsUIState { - data class CourseData(val course: Course, val enrolledCourse: EnrolledCourse?) : CourseDetailsUIState() + data class CourseData(val course: Course) : CourseDetailsUIState() object Loading : CourseDetailsUIState() } \ No newline at end of file diff --git a/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsViewModel.kt b/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsViewModel.kt index c8ee96b5b..ede39fe73 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsViewModel.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsViewModel.kt @@ -8,17 +8,13 @@ import com.raccoongang.core.R import com.raccoongang.core.SingleEventLiveData import com.raccoongang.core.UIMessage import com.raccoongang.core.domain.model.Course -import com.raccoongang.core.domain.model.EnrolledCourse import com.raccoongang.core.extension.isInternetError import com.raccoongang.core.system.ResourceManager import com.raccoongang.core.system.connection.NetworkConnection import com.raccoongang.core.system.notifier.CourseDashboardUpdate import com.raccoongang.core.system.notifier.CourseNotifier import com.raccoongang.course.domain.interactor.CourseInteractor -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch -import kotlinx.coroutines.supervisorScope class CourseDetailsViewModel( private val courseId: String, @@ -48,28 +44,12 @@ class CourseDetailsViewModel( _uiState.value = CourseDetailsUIState.Loading viewModelScope.launch { try { - supervisorScope { - val courseJob = async { - if (networkConnection.isOnline()) { - interactor.getCourseDetails(courseId) - } else { - interactor.getCourseDetailsFromCache(courseId) - } - } - val enrolledCourse = async { - if (networkConnection.isOnline()) { - interactor.getEnrolledCourseById(courseId) - } else { - interactor.getEnrolledCourseFromCacheById(courseId) - } - } - val data = awaitAll(courseJob, enrolledCourse) - course = data[0] as Course - _uiState.value = CourseDetailsUIState.CourseData( - course = course!!, - data[1] as EnrolledCourse? - ) + course = if (networkConnection.isOnline()) { + interactor.getCourseDetails(courseId) + } else { + interactor.getCourseDetailsFromCache(courseId) } + _uiState.value = CourseDetailsUIState.CourseData(course = course!!) } catch (e: Exception) { if (e.isInternetError()) { _uiMessage.value = @@ -86,10 +66,10 @@ class CourseDetailsViewModel( viewModelScope.launch { try { interactor.enrollInACourse(id) - val enrolledCourse = interactor.getEnrolledCourseById(id) + val course = interactor.getCourseDetails(id) val courseData = _uiState.value if (courseData is CourseDetailsUIState.CourseData) { - _uiState.value = courseData.copy(enrolledCourse = enrolledCourse) + _uiState.value = courseData.copy(course = course) notifier.send(CourseDashboardUpdate()) } } catch (e: Exception) { diff --git a/course/src/main/java/com/raccoongang/course/presentation/handouts/HandoutsViewModel.kt b/course/src/main/java/com/raccoongang/course/presentation/handouts/HandoutsViewModel.kt index 5ee62ef84..592f18952 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/handouts/HandoutsViewModel.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/handouts/HandoutsViewModel.kt @@ -16,8 +16,6 @@ class HandoutsViewModel( private val interactor: CourseInteractor ) : BaseViewModel() { - private var course: EnrolledCourse? = null - private val _htmlContent = MutableLiveData() val htmlContent: LiveData get() = _htmlContent @@ -29,14 +27,11 @@ class HandoutsViewModel( private fun getEnrolledCourse() { viewModelScope.launch { try { - if (course == null) { - course = interactor.getEnrolledCourseFromCacheById(courseId) - } if (HandoutsType.valueOf(handoutsType) == HandoutsType.Handouts) { - val handouts = interactor.getHandouts(course!!.course.courseHandouts) + val handouts = interactor.getHandouts(courseId) _htmlContent.value = handoutsToHtml(handouts) } else { - val announcements = interactor.getAnnouncements(course!!.course.courseUpdates) + val announcements = interactor.getAnnouncements(courseId) _htmlContent.value = announcementsToHtml(announcements) } } catch (e: Exception) { diff --git a/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt index b4bef6484..4aa2a633a 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt @@ -37,7 +37,6 @@ import com.raccoongang.core.BlockType import com.raccoongang.core.R import com.raccoongang.core.UIMessage import com.raccoongang.core.domain.model.* -import com.raccoongang.core.extension.parcelable import com.raccoongang.core.presentation.course.CourseViewMode import com.raccoongang.core.ui.* import com.raccoongang.core.ui.theme.NewEdxTheme @@ -64,11 +63,8 @@ class CourseOutlineFragment : Fragment() { super.onCreate(savedInstanceState) lifecycle.addObserver(viewModel) with(requireArguments()) { - viewModel.courseImage = getString(ARG_IMAGE, "") viewModel.courseTitle = getString(ARG_TITLE, "") - viewModel.courseCertificate = parcelable(ARG_CERTIFICATE)!! } - viewModel.getCourseData() } override fun onCreateView( @@ -88,9 +84,7 @@ class CourseOutlineFragment : Fragment() { CourseOutlineScreen( windowSize = windowSize, uiState = uiState!!, - courseImage = viewModel.courseImage, courseTitle = viewModel.courseTitle, - courseCertificate = viewModel.courseCertificate, uiMessage = uiMessage, refreshing = refreshing, onSwipeRefresh = { @@ -146,20 +140,14 @@ class CourseOutlineFragment : Fragment() { companion object { private const val ARG_COURSE_ID = "courseId" private const val ARG_TITLE = "title" - private const val ARG_IMAGE = "image" - private const val ARG_CERTIFICATE = "certificate" fun newInstance( courseId: String, - title: String, - image: String, - certificate: Certificate + title: String ): CourseOutlineFragment { val fragment = CourseOutlineFragment() fragment.arguments = bundleOf( ARG_COURSE_ID to courseId, - ARG_TITLE to title, - ARG_IMAGE to image, - ARG_CERTIFICATE to certificate + ARG_TITLE to title ) return fragment } @@ -173,8 +161,6 @@ internal fun CourseOutlineScreen( windowSize: WindowSize, uiState: CourseOutlineUIState, courseTitle: String, - courseImage: String, - courseCertificate: Certificate, uiMessage: UIMessage?, refreshing: Boolean, hasInternetConnection: Boolean, @@ -291,14 +277,15 @@ internal fun CourseOutlineScreen( modifier = Modifier .aspectRatio(1.86f) .padding(6.dp), - courseImage = courseImage, - courseCertificate = courseCertificate + courseImage = uiState.courseStructure.media?.image?.large + ?: "", + courseCertificate = uiState.courseStructure.certificate ) LazyColumn( modifier = Modifier.fillMaxWidth(), contentPadding = listPadding ) { - items(uiState.blocks) { block -> + items(uiState.courseStructure.blockData) { block -> if (block.type == BlockType.CHAPTER) { Text( modifier = Modifier.padding( @@ -406,15 +393,11 @@ private fun CourseOutlineScreenPreview() { CourseOutlineScreen( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), uiState = CourseOutlineUIState.CourseData( - listOf( - mockSequentialBlock, mockSequentialBlock - ), + mockCourseStructure, mapOf(), mockChapterBlock ), courseTitle = "", - courseImage = "", - courseCertificate = Certificate(""), uiMessage = null, refreshing = false, hasInternetConnection = true, @@ -436,15 +419,11 @@ private fun CourseOutlineScreenTabletPreview() { CourseOutlineScreen( windowSize = WindowSize(WindowType.Medium, WindowType.Medium), uiState = CourseOutlineUIState.CourseData( - listOf( - mockSequentialBlock, mockSequentialBlock - ), + mockCourseStructure, mapOf(), mockChapterBlock ), courseTitle = "", - courseImage = "", - courseCertificate = Certificate(""), uiMessage = null, refreshing = false, hasInternetConnection = true, @@ -467,42 +446,6 @@ private fun ResumeCoursePreview() { } } -private val mockCourse = EnrolledCourse( - auditAccessExpires = Date(), - created = "created", - certificate = Certificate(""), - mode = "mode", - isActive = true, - 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", @@ -533,3 +476,27 @@ private val mockSequentialBlock = Block( descendants = emptyList(), completion = 0.0 ) + +private val mockCourseStructure = CourseStructure( + root = "", + blockData = listOf(mockSequentialBlock, mockSequentialBlock), + id = "id", + name = "Course name", + number = "", + org = "Org", + start = Date(), + startDisplay = "", + startType = "", + end = Date(), + coursewareAccess = CoursewareAccess( + true, + "", + "", + "", + "", + "" + ), + media = null, + certificate = null, + isSelfPaced = false +) diff --git a/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineUIState.kt b/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineUIState.kt index f91077956..bcf9a78b9 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineUIState.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineUIState.kt @@ -1,11 +1,12 @@ package com.raccoongang.course.presentation.outline import com.raccoongang.core.domain.model.Block +import com.raccoongang.core.domain.model.CourseStructure import com.raccoongang.core.module.db.DownloadedState sealed class CourseOutlineUIState { data class CourseData( - val blocks: List, + val courseStructure: CourseStructure, val downloadedState: Map, val resumeBlock: Block? ) : CourseOutlineUIState() diff --git a/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineViewModel.kt b/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineViewModel.kt index 2976571ba..633062089 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineViewModel.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineViewModel.kt @@ -48,8 +48,6 @@ class CourseOutlineViewModel( get() = _isUpdating var courseTitle = "" - var courseImage = "" - var courseCertificate = Certificate("") val hasInternetConnection: Boolean get() = networkConnection.isOnline() @@ -71,7 +69,7 @@ class CourseOutlineViewModel( if (_uiState.value is CourseOutlineUIState.CourseData) { val state = _uiState.value as CourseOutlineUIState.CourseData _uiState.value = CourseOutlineUIState.CourseData( - blocks = state.blocks, + courseStructure = state.courseStructure, downloadedState = it.toMap(), resumeBlock = state.resumeBlock ) @@ -80,6 +78,10 @@ class CourseOutlineViewModel( } } + init { + getCourseData() + } + fun setIsUpdating() { _isUpdating.value = true } @@ -109,7 +111,8 @@ class CourseOutlineViewModel( private fun getCourseDataInternal() { viewModelScope.launch { - val blocks = interactor.getCourseStructureFromCache() + var courseStructure = interactor.getCourseStructureFromCache() + val blocks = courseStructure.blockData try { val courseStatus = if (networkConnection.isOnline()) { @@ -118,11 +121,11 @@ class CourseOutlineViewModel( CourseComponentStatus("") } setBlocks(blocks) - val list = sortBlocks(blocks) + courseStructure = courseStructure.copy(blockData = sortBlocks(blocks)) initDownloadModelsStatus() _uiState.value = CourseOutlineUIState.CourseData( - blocks = list, + courseStructure = courseStructure, downloadedState = getDownloadModelsStatus(), resumeBlock = blocks.firstOrNull { it.id == courseStatus.lastVisitedBlockId } ) diff --git a/course/src/main/java/com/raccoongang/course/presentation/section/CourseSectionViewModel.kt b/course/src/main/java/com/raccoongang/course/presentation/section/CourseSectionViewModel.kt index 31dcc5868..660771018 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/section/CourseSectionViewModel.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/section/CourseSectionViewModel.kt @@ -57,10 +57,11 @@ class CourseSectionViewModel( _uiState.value = CourseSectionUIState.Loading viewModelScope.launch { try { - val blocks = when (mode) { + val courseStructure = when (mode) { CourseViewMode.FULL -> interactor.getCourseStructureFromCache() CourseViewMode.VIDEOS -> interactor.getCourseStructureForVideos() } + val blocks = courseStructure.blockData setBlocks(blocks) val newList = getDescendantBlocks(blocks, blockId) initDownloadModelsStatus() diff --git a/course/src/main/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerViewModel.kt b/course/src/main/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerViewModel.kt index 9aa274b39..afa83fe7a 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerViewModel.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerViewModel.kt @@ -44,10 +44,11 @@ class CourseUnitContainerViewModel( fun loadBlocks(mode: CourseViewMode) { try { - val blocks = when (mode) { + val courseStructure = when (mode) { CourseViewMode.FULL -> interactor.getCourseStructureFromCache() CourseViewMode.VIDEOS -> interactor.getCourseStructureForVideos() } + val blocks = courseStructure.blockData this.blocks.clear() this.blocks.addAll(blocks) } catch (e: Exception) { diff --git a/course/src/main/java/com/raccoongang/course/presentation/units/CourseUnitsViewModel.kt b/course/src/main/java/com/raccoongang/course/presentation/units/CourseUnitsViewModel.kt index 097576798..b17db9456 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/units/CourseUnitsViewModel.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/units/CourseUnitsViewModel.kt @@ -61,10 +61,11 @@ class CourseUnitsViewModel( _uiState.value = CourseUnitsUIState.Loading viewModelScope.launch { try { - val blocks = when (mode) { + val courseStructure = when (mode) { CourseViewMode.FULL -> interactor.getCourseStructureFromCache() CourseViewMode.VIDEOS -> interactor.getCourseStructureForVideos() } + val blocks = courseStructure.blockData val newList = getDescendantBlocks(blocks, blockId) _uiState.value = CourseUnitsUIState.Blocks(newList.map { block -> val downloadingBlock = getDownloadModels().first().find { it.id == block.id } diff --git a/course/src/main/java/com/raccoongang/course/presentation/videos/CourseVideoViewModel.kt b/course/src/main/java/com/raccoongang/course/presentation/videos/CourseVideoViewModel.kt index b329e4976..88347cae3 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/videos/CourseVideoViewModel.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/videos/CourseVideoViewModel.kt @@ -9,7 +9,6 @@ import com.raccoongang.core.SingleEventLiveData import com.raccoongang.core.UIMessage import com.raccoongang.core.data.storage.PreferencesManager import com.raccoongang.core.domain.model.Block -import com.raccoongang.core.domain.model.Certificate import com.raccoongang.core.module.DownloadWorkerController import com.raccoongang.core.module.db.DownloadDao import com.raccoongang.core.module.download.BaseDownloadViewModel @@ -32,15 +31,11 @@ class CourseVideoViewModel( workerController: DownloadWorkerController ) : BaseDownloadViewModel(downloadDao, preferencesManager, workerController) { - private val _uiState = MutableLiveData( - CourseVideosUIState.CourseData(emptyList(), emptyMap()) - ) + private val _uiState = MutableLiveData() val uiState: LiveData get() = _uiState var courseTitle = "" - var courseImage = "" - var courseCertificate = Certificate("") private val _isUpdating = MutableLiveData() val isUpdating: LiveData @@ -70,7 +65,7 @@ class CourseVideoViewModel( if (_uiState.value is CourseVideosUIState.CourseData) { val state = _uiState.value as CourseVideosUIState.CourseData _uiState.value = CourseVideosUIState.CourseData( - blocks = state.blocks, + courseStructure = state.courseStructure, downloadedState = it.toMap() ) } @@ -78,6 +73,10 @@ class CourseVideoViewModel( } } + init { + getVideos() + } + override fun saveDownloadModels(folder: String, id: String) { if (preferencesManager.videoSettings.wifiDownloadOnly) { if (networkConnection.isWifiConnected()) { @@ -102,16 +101,18 @@ class CourseVideoViewModel( fun getVideos() { viewModelScope.launch { - val blocks = interactor.getCourseStructureForVideos() + var courseStructure = interactor.getCourseStructureForVideos() + val blocks = courseStructure.blockData if (blocks.isEmpty()) { _uiState.value = CourseVideosUIState.Empty( message = resourceManager.getString(R.string.course_does_not_include_videos) ) } else { - setBlocks(blocks) - val list = sortBlocks(blocks) + setBlocks(courseStructure.blockData) + courseStructure = courseStructure.copy(blockData = sortBlocks(blocks)) initDownloadModelsStatus() - _uiState.value = CourseVideosUIState.CourseData(list, getDownloadModelsStatus()) + _uiState.value = + CourseVideosUIState.CourseData(courseStructure, getDownloadModelsStatus()) } } } diff --git a/course/src/main/java/com/raccoongang/course/presentation/videos/CourseVideosFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/videos/CourseVideosFragment.kt index 602e7a2fa..c2419d028 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/videos/CourseVideosFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/videos/CourseVideosFragment.kt @@ -31,10 +31,7 @@ import androidx.fragment.app.Fragment import com.raccoongang.core.BlockType import com.raccoongang.core.R import com.raccoongang.core.UIMessage -import com.raccoongang.core.domain.model.Block -import com.raccoongang.core.domain.model.BlockCounts -import com.raccoongang.core.domain.model.Certificate -import com.raccoongang.core.extension.parcelable +import com.raccoongang.core.domain.model.* import com.raccoongang.core.presentation.course.CourseViewMode import com.raccoongang.core.ui.* import com.raccoongang.core.ui.theme.NewEdxTheme @@ -49,6 +46,7 @@ import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import java.io.File +import java.util.* class CourseVideosFragment : Fragment() { @@ -61,11 +59,8 @@ class CourseVideosFragment : Fragment() { super.onCreate(savedInstanceState) lifecycle.addObserver(viewModel) with(requireArguments()) { - viewModel.courseImage = getString(ARG_IMAGE, "") viewModel.courseTitle = getString(ARG_TITLE, "") - viewModel.courseCertificate = parcelable(ARG_CERTIFICATE)!! } - viewModel.getVideos() } override fun onCreateView( @@ -78,17 +73,15 @@ class CourseVideosFragment : Fragment() { NewEdxTheme { val windowSize = rememberWindowSize() - val uiState by viewModel.uiState.observeAsState() + val uiState by viewModel.uiState.observeAsState(CourseVideosUIState.Empty("")) val uiMessage by viewModel.uiMessage.observeAsState() val isUpdating by viewModel.isUpdating.observeAsState(false) CourseVideosScreen( windowSize = windowSize, - uiState = uiState!!, + uiState = uiState, uiMessage = uiMessage, - courseImage = viewModel.courseImage, courseTitle = viewModel.courseTitle, - courseCertificate = viewModel.courseCertificate, hasInternetConnection = viewModel.hasInternetConnection, isUpdating = isUpdating, onSwipeRefresh = { @@ -133,20 +126,14 @@ class CourseVideosFragment : Fragment() { companion object { private const val ARG_COURSE_ID = "courseId" private const val ARG_TITLE = "title" - private const val ARG_IMAGE = "image" - private const val ARG_CERTIFICATE = "certificate" fun newInstance( courseId: String, - title: String, - image: String, - certificate: Certificate + title: String ): CourseVideosFragment { val fragment = CourseVideosFragment() fragment.arguments = bundleOf( ARG_COURSE_ID to courseId, - ARG_TITLE to title, - ARG_IMAGE to image, - ARG_CERTIFICATE to certificate + ARG_TITLE to title ) return fragment } @@ -163,8 +150,6 @@ private fun CourseVideosScreen( hasInternetConnection: Boolean, onSwipeRefresh: () -> Unit, courseTitle: String, - courseImage: String, - courseCertificate: Certificate, onItemClick: (Block) -> Unit, onReloadClick: () -> Unit, onBackClick: () -> Unit, @@ -281,14 +266,15 @@ private fun CourseVideosScreen( modifier = Modifier .aspectRatio(1.86f) .padding(6.dp), - courseImage = courseImage, - courseCertificate = courseCertificate + courseImage = uiState.courseStructure.media?.image?.large + ?: "", + courseCertificate = uiState.courseStructure.certificate ) LazyColumn( modifier = Modifier.fillMaxWidth(), contentPadding = listPadding ) { - items(uiState.blocks) { block -> + items(uiState.courseStructure.blockData) { block -> if (block.type == BlockType.CHAPTER) { Text( modifier = Modifier.padding( @@ -350,13 +336,9 @@ private fun CourseVideosScreenPreview() { windowSize = WindowSize(WindowType.Compact, WindowType.Compact), uiMessage = null, uiState = CourseVideosUIState.CourseData( - listOf( - mockSequentialBlock, mockSequentialBlock - ), + mockCourseStructure, emptyMap() ), - courseCertificate = Certificate(""), - courseImage = "", courseTitle = "Course", onItemClick = { }, onBackClick = {}, @@ -380,8 +362,6 @@ private fun CourseVideosScreenEmptyPreview() { uiState = CourseVideosUIState.Empty( "This course does not include any videos." ), - courseCertificate = Certificate(""), - courseImage = "", courseTitle = "Course", onItemClick = { }, onBackClick = {}, @@ -403,13 +383,9 @@ private fun CourseVideosScreenTabletPreview() { windowSize = WindowSize(WindowType.Medium, WindowType.Medium), uiMessage = null, uiState = CourseVideosUIState.CourseData( - listOf( - mockSequentialBlock, mockSequentialBlock - ), + mockCourseStructure, emptyMap() ), - courseCertificate = Certificate(""), - courseImage = "", courseTitle = "Course", onItemClick = { }, onBackClick = {}, @@ -453,4 +429,28 @@ private val mockSequentialBlock = Block( blockCounts = BlockCounts(1), descendants = emptyList(), completion = 0.0 +) + +private val mockCourseStructure = CourseStructure( + root = "", + blockData = listOf(mockSequentialBlock, mockChapterBlock), + id = "id", + name = "Course name", + number = "", + org = "Org", + start = Date(), + startDisplay = "", + startType = "", + end = Date(), + coursewareAccess = CoursewareAccess( + true, + "", + "", + "", + "", + "" + ), + media = null, + certificate = null, + isSelfPaced = false ) \ No newline at end of file diff --git a/course/src/main/java/com/raccoongang/course/presentation/videos/CourseVideosUIState.kt b/course/src/main/java/com/raccoongang/course/presentation/videos/CourseVideosUIState.kt index 964e173f3..3ad5ec643 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/videos/CourseVideosUIState.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/videos/CourseVideosUIState.kt @@ -1,11 +1,11 @@ package com.raccoongang.course.presentation.videos -import com.raccoongang.core.domain.model.Block +import com.raccoongang.core.domain.model.CourseStructure import com.raccoongang.core.module.db.DownloadedState sealed class CourseVideosUIState { data class CourseData( - val blocks: List, + val courseStructure: CourseStructure, val downloadedState: Map, ) : CourseVideosUIState() diff --git a/dashboard/src/main/java/com/raccoongang/dashboard/data/repository/DashboardRepository.kt b/dashboard/src/main/java/com/raccoongang/dashboard/data/repository/DashboardRepository.kt index 0432f9713..572f37cc2 100644 --- a/dashboard/src/main/java/com/raccoongang/dashboard/data/repository/DashboardRepository.kt +++ b/dashboard/src/main/java/com/raccoongang/dashboard/data/repository/DashboardRepository.kt @@ -2,6 +2,7 @@ package com.raccoongang.dashboard.data.repository import com.raccoongang.core.data.api.CourseApi import com.raccoongang.core.data.storage.PreferencesManager +import com.raccoongang.core.domain.model.DashboardCourseList import com.raccoongang.core.domain.model.EnrolledCourse import com.raccoongang.dashboard.data.DashboardDao @@ -11,16 +12,15 @@ class DashboardRepository( private val preferencesManager: PreferencesManager ) { - suspend fun getEnrolledCourses(): List { + suspend fun getEnrolledCourses(page: Int): DashboardCourseList { val user = preferencesManager.user val result = api.getEnrolledCourses( - username = user?.username ?: "" + username = user?.username ?: "", + page = page ) - dao.clearCachedData() - dao.insertEnrolledCourseEntity(*result.map { it.mapToRoomEntity() }.toTypedArray()) - return result.map { - it.mapToDomain() - } + if (page == 1) dao.clearCachedData() + dao.insertEnrolledCourseEntity(*result.results.map { it.mapToRoomEntity() }.toTypedArray()) + return result.mapToDomain() } suspend fun getEnrolledCoursesFromCache(): List { diff --git a/dashboard/src/main/java/com/raccoongang/dashboard/domain/interactor/DashboardInteractor.kt b/dashboard/src/main/java/com/raccoongang/dashboard/domain/interactor/DashboardInteractor.kt index ea5c97476..2226b9690 100644 --- a/dashboard/src/main/java/com/raccoongang/dashboard/domain/interactor/DashboardInteractor.kt +++ b/dashboard/src/main/java/com/raccoongang/dashboard/domain/interactor/DashboardInteractor.kt @@ -1,14 +1,14 @@ package com.raccoongang.dashboard.domain.interactor -import com.raccoongang.core.domain.model.EnrolledCourse +import com.raccoongang.core.domain.model.DashboardCourseList import com.raccoongang.dashboard.data.repository.DashboardRepository class DashboardInteractor( private val repository: DashboardRepository ) { - suspend fun getEnrolledCourses(): List { - return repository.getEnrolledCourses() + suspend fun getEnrolledCourses(page: Int): DashboardCourseList { + return repository.getEnrolledCourses(page) } suspend fun getEnrolledCoursesFromCache() = repository.getEnrolledCoursesFromCache() diff --git a/dashboard/src/main/java/com/raccoongang/dashboard/presentation/DashboardFragment.kt b/dashboard/src/main/java/com/raccoongang/dashboard/presentation/DashboardFragment.kt index 93ba74688..aba6dda9b 100644 --- a/dashboard/src/main/java/com/raccoongang/dashboard/presentation/DashboardFragment.kt +++ b/dashboard/src/main/java/com/raccoongang/dashboard/presentation/DashboardFragment.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* 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.* import androidx.compose.material.icons.Icons @@ -25,7 +26,6 @@ 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.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.res.painterResource @@ -75,11 +75,13 @@ class DashboardFragment : Fragment() { 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) MyCoursesScreen( windowSize = windowSize, uiState!!, uiMessage, + canLoadMore = canLoadMore, refreshing = refreshing, hasInternetConnection = viewModel.hasInternetConnection, onReloadClick = { @@ -89,15 +91,14 @@ class DashboardFragment : Fragment() { router.navigateToCourseOutline( requireParentFragment().parentFragmentManager, it.course.id, - it.course.name, - it.course.courseImage, - it.certificate ?: Certificate(""), - it.course.coursewareAccess, - it.auditAccessExpires + it.course.name ) }, onSwipeRefresh = { viewModel.updateCourses() + }, + paginationCallback = { + viewModel.fetchMore() } ) } @@ -111,20 +112,25 @@ internal fun MyCoursesScreen( windowSize: WindowSize, state: DashboardUIState, uiMessage: UIMessage?, + canLoadMore: Boolean, refreshing: Boolean, hasInternetConnection: Boolean, onReloadClick: () -> Unit, onSwipeRefresh: () -> Unit, + paginationCallback: () -> Unit, onItemClick: (EnrolledCourse) -> Unit, ) { val scaffoldState = rememberScaffoldState() - val configuration = LocalConfiguration.current 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, @@ -210,6 +216,7 @@ internal fun MyCoursesScreen( modifier = Modifier .fillMaxHeight() .then(contentWidth), + state = scrollState, contentPadding = contentPaddings, content = { item() { @@ -234,7 +241,22 @@ internal fun MyCoursesScreen( }) Divider() } + item { + if (canLoadMore) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + } }) + if (scrollState.shouldLoadMore(firstVisibleIndex, 4)) { + paginationCallback() + } } } is DashboardUIState.Empty -> { @@ -467,7 +489,9 @@ private fun MyCoursesScreenDay() { onItemClick = {}, onReloadClick = {}, hasInternetConnection = true, - refreshing = false + refreshing = false, + canLoadMore = false, + paginationCallback = {} ) } } @@ -494,7 +518,9 @@ private fun MyCoursesScreenTabletPreview() { onItemClick = {}, onReloadClick = {}, hasInternetConnection = true, - refreshing = false + refreshing = false, + canLoadMore = false, + paginationCallback = {} ) } } diff --git a/dashboard/src/main/java/com/raccoongang/dashboard/presentation/DashboardRouter.kt b/dashboard/src/main/java/com/raccoongang/dashboard/presentation/DashboardRouter.kt index 9da404c27..e06048ffa 100644 --- a/dashboard/src/main/java/com/raccoongang/dashboard/presentation/DashboardRouter.kt +++ b/dashboard/src/main/java/com/raccoongang/dashboard/presentation/DashboardRouter.kt @@ -1,20 +1,13 @@ package com.raccoongang.dashboard.presentation import androidx.fragment.app.FragmentManager -import com.raccoongang.core.domain.model.Certificate -import com.raccoongang.core.domain.model.CoursewareAccess -import java.util.* interface DashboardRouter { fun navigateToCourseOutline( fm: FragmentManager, courseId: String, - title: String, - image: String, - certificate: Certificate, - coursewareAccess: CoursewareAccess, - auditAccessExpires: Date? + courseTitle: String ) } \ No newline at end of file diff --git a/dashboard/src/main/java/com/raccoongang/dashboard/presentation/DashboardViewModel.kt b/dashboard/src/main/java/com/raccoongang/dashboard/presentation/DashboardViewModel.kt index 4e330ede4..d108bea79 100644 --- a/dashboard/src/main/java/com/raccoongang/dashboard/presentation/DashboardViewModel.kt +++ b/dashboard/src/main/java/com/raccoongang/dashboard/presentation/DashboardViewModel.kt @@ -26,6 +26,8 @@ class DashboardViewModel( ) : BaseViewModel() { private val coursesList = mutableListOf() + private var page = 1 + private var isLoading = false private val _uiState = MutableLiveData(DashboardUIState.Loading) val uiState: LiveData get() = _uiState @@ -41,6 +43,10 @@ class DashboardViewModel( val hasInternetConnection: Boolean get() = networkConnection.isOnline() + private val _canLoadMore = MutableLiveData() + val canLoadMore: LiveData + get() = _canLoadMore + override fun onCreate(owner: LifecycleOwner) { super.onCreate(owner) viewModelScope.launch { @@ -63,12 +69,21 @@ class DashboardViewModel( } fun updateCourses() { - _updating.value = true viewModelScope.launch { try { - val response = interactor.getEnrolledCourses() + _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.toList()) + coursesList.addAll(response.courses) if (coursesList.isEmpty()) { _uiState.value = DashboardUIState.Empty } else { @@ -85,18 +100,34 @@ class DashboardViewModel( } } _updating.value = false + isLoading = false } } private fun internalLoadingCourses() { viewModelScope.launch { try { - val response = if (networkConnection.isOnline()) { - interactor.getEnrolledCourses() + 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 { - interactor.getEnrolledCoursesFromCache() + val cachedList = interactor.getEnrolledCoursesFromCache() + _canLoadMore.value = false + page = -1 + coursesList.addAll(cachedList) } - coursesList.addAll(response.toList()) if (coursesList.isEmpty()) { _uiState.value = DashboardUIState.Empty } else { @@ -112,6 +143,14 @@ class DashboardViewModel( UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) } } + _updating.value = false + isLoading = false + } + } + + fun fetchMore() { + if (!isLoading && page != -1) { + internalLoadingCourses() } } diff --git a/discovery/src/main/java/com/raccoongang/discovery/presentation/DiscoveryFragment.kt b/discovery/src/main/java/com/raccoongang/discovery/presentation/DiscoveryFragment.kt index 261491876..2490fa055 100644 --- a/discovery/src/main/java/com/raccoongang/discovery/presentation/DiscoveryFragment.kt +++ b/discovery/src/main/java/com/raccoongang/discovery/presentation/DiscoveryFragment.kt @@ -381,5 +381,6 @@ private val mockCourse = Course( end = "end", startDisplay = "startDisplay", startType = "startType", - overview = "" + overview = "", + isEnrolled = false ) \ No newline at end of file diff --git a/discovery/src/main/java/com/raccoongang/discovery/presentation/search/CourseSearchFragment.kt b/discovery/src/main/java/com/raccoongang/discovery/presentation/search/CourseSearchFragment.kt index ce2c5897b..5cdece24e 100644 --- a/discovery/src/main/java/com/raccoongang/discovery/presentation/search/CourseSearchFragment.kt +++ b/discovery/src/main/java/com/raccoongang/discovery/presentation/search/CourseSearchFragment.kt @@ -386,5 +386,6 @@ private val mockCourse = Course( end = "end", startDisplay = "startDisplay", startType = "startType", - overview = "" + overview = "", + isEnrolled = false ) \ No newline at end of file diff --git a/discussion/src/main/java/com/raccoongang/discussion/data/api/DiscussionApi.kt b/discussion/src/main/java/com/raccoongang/discussion/data/api/DiscussionApi.kt index ecf208071..d0756b8ee 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/data/api/DiscussionApi.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/data/api/DiscussionApi.kt @@ -20,6 +20,7 @@ interface DiscussionApi { @Query("following") following: Boolean?, @Query("topic_id") topicId: String?, @Query("order_by") orderBy: String, + @Query("view") view: String?, @Query("page") page: Int = 1, @Query("requested_fields") requestedFields: List = listOf("profile_image") ): ThreadsResponse @@ -96,10 +97,9 @@ interface DiscussionApi { @Query("requested_fields") requestedFields: List = listOf("profile_image") ): CommentsResponse - @POST("/api/discussion/v1/comments/") + @POST("/mobile_api_extensions/discussion/v1/comments/") suspend fun createComment( - @Body commentBody: CommentBody, - @Query("requested_fields") requestedFields: List = listOf("profile_image") + @Body commentBody: CommentBody ) : CommentResult @POST("/api/discussion/v1/threads/") diff --git a/discussion/src/main/java/com/raccoongang/discussion/data/model/response/CommentsResponse.kt b/discussion/src/main/java/com/raccoongang/discussion/data/model/response/CommentsResponse.kt index 2497f9237..9e483d4ec 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/data/model/response/CommentsResponse.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/data/model/response/CommentsResponse.kt @@ -2,6 +2,7 @@ package com.raccoongang.discussion.data.model.response import com.google.gson.annotations.SerializedName import com.raccoongang.core.data.model.Pagination +import com.raccoongang.core.data.model.ProfileImage import com.raccoongang.core.extension.TextConverter import com.raccoongang.discussion.domain.model.CommentsData import com.raccoongang.discussion.domain.model.DiscussionComment @@ -63,6 +64,8 @@ data class CommentResult( val children: List, @SerializedName("abuse_flagged_any_user") val abuseFlaggedAnyUser: String?, + @SerializedName("profile_image") + val profileImage: ProfileImage?, @SerializedName("users") val users: Map? ) { @@ -89,6 +92,7 @@ data class CommentResult( endorsedAt ?: "", childCount, children, + profileImage?.mapToDomain(), users?.entries?.associate { it.key to it.value.mapToDomain() } ) } diff --git a/discussion/src/main/java/com/raccoongang/discussion/data/repository/DiscussionRepository.kt b/discussion/src/main/java/com/raccoongang/discussion/data/repository/DiscussionRepository.kt index 435d25438..c57af4537 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/data/repository/DiscussionRepository.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/data/repository/DiscussionRepository.kt @@ -33,9 +33,10 @@ class DiscussionRepository(private val api: DiscussionApi) { following: Boolean?, topicId: String?, orderBy: String, + view: String?, page: Int ): ThreadsData { - return api.getCourseThreads(courseId, following, topicId, orderBy, page).mapToDomain() + return api.getCourseThreads(courseId, following, topicId, orderBy, view, page).mapToDomain() } suspend fun searchThread( diff --git a/discussion/src/main/java/com/raccoongang/discussion/domain/interactor/DiscussionInteractor.kt b/discussion/src/main/java/com/raccoongang/discussion/domain/interactor/DiscussionInteractor.kt index 6d45593ea..1d08d49a9 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/domain/interactor/DiscussionInteractor.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/domain/interactor/DiscussionInteractor.kt @@ -10,19 +10,26 @@ class DiscussionInteractor( fun getCachedTopics(courseId: String) = repository.getCachedTopics(courseId) - suspend fun getAllThreads(courseId: String, orderBy: String, page: Int) = - repository.getCourseThreads(courseId, null, null, orderBy, page) + suspend fun getAllThreads(courseId: String, orderBy: String, view: String? = null, page: Int) = + repository.getCourseThreads(courseId, null, null, orderBy, view, page) suspend fun getFollowingThreads( courseId: String, following: Boolean, orderBy: String, + view: String? = null, page: Int ) = - repository.getCourseThreads(courseId, following, null, orderBy, page) + repository.getCourseThreads(courseId, following, null, orderBy, view, page) - suspend fun getThreads(courseId: String, topicId: String, orderBy: String, page: Int) = - repository.getCourseThreads(courseId, null, topicId, orderBy, page) + suspend fun getThreads( + courseId: String, + topicId: String, + orderBy: String, + view: String? = null, + page: Int + ) = + repository.getCourseThreads(courseId, null, topicId, orderBy, view, page) suspend fun searchThread(courseId: String, query: String, page: Int) = repository.searchThread(courseId, query, page) diff --git a/discussion/src/main/java/com/raccoongang/discussion/domain/model/DiscussionComment.kt b/discussion/src/main/java/com/raccoongang/discussion/domain/model/DiscussionComment.kt index 2fb957958..fe4b4ffef 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/domain/model/DiscussionComment.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/domain/model/DiscussionComment.kt @@ -1,6 +1,7 @@ package com.raccoongang.discussion.domain.model import android.os.Parcelable +import com.raccoongang.core.domain.model.ProfileImage import com.raccoongang.core.extension.LinkedImageText import kotlinx.parcelize.Parcelize @@ -27,5 +28,6 @@ data class DiscussionComment( val endorsedAt: String, val childCount: Int, val children: List, + val profileImage: ProfileImage?, val users: Map? ) : Parcelable \ No newline at end of file diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsFragment.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsFragment.kt index 9acd60635..6fd44d0a9 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsFragment.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsFragment.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.unit.dp import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import com.raccoongang.core.UIMessage +import com.raccoongang.core.domain.model.ProfileImage import com.raccoongang.core.extension.TextConverter import com.raccoongang.core.extension.parcelable import com.raccoongang.core.ui.* @@ -537,5 +538,6 @@ private val mockComment = DiscussionComment( "", 21, emptyList(), - emptyMap() + profileImage = ProfileImage("", "", "", "", false), + mapOf() ) \ No newline at end of file diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsViewModel.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsViewModel.kt index fa53644b5..89a26bae1 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsViewModel.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsViewModel.kt @@ -13,7 +13,6 @@ import com.raccoongang.core.extension.isInternetError import com.raccoongang.core.system.ResourceManager import com.raccoongang.discussion.domain.interactor.DiscussionInteractor import com.raccoongang.discussion.domain.model.DiscussionComment -import com.raccoongang.discussion.domain.model.DiscussionProfile import com.raccoongang.discussion.domain.model.DiscussionType import com.raccoongang.discussion.system.notifier.DiscussionCommentAdded import com.raccoongang.discussion.system.notifier.DiscussionCommentDataChanged @@ -269,15 +268,7 @@ class DiscussionCommentsViewModel( fun createComment(rawBody: String) { viewModelScope.launch { try { - var response = interactor.createComment(thread.id, rawBody, null) - response = response.copy( - users = mapOf( - Pair( - preferencesManager.profile?.username ?: "", - DiscussionProfile(preferencesManager.profile?.profileImage) - ) - ) - ) + val response = interactor.createComment(thread.id, rawBody, null) thread = thread.copy(commentCount = thread.commentCount + 1) sendThreadUpdated() if (page == -1) { diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/responses/DiscussionResponsesFragment.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/responses/DiscussionResponsesFragment.kt index c20821c6c..1705e7d8b 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/responses/DiscussionResponsesFragment.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/responses/DiscussionResponsesFragment.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.unit.dp import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import com.raccoongang.core.UIMessage +import com.raccoongang.core.domain.model.ProfileImage import com.raccoongang.core.extension.TextConverter import com.raccoongang.core.extension.parcelable import com.raccoongang.core.ui.* @@ -520,7 +521,8 @@ private val mockComment = DiscussionComment( "", 21, emptyList(), - emptyMap() + ProfileImage("", "", "", "", false), + mapOf() ) diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/responses/DiscussionResponsesViewModel.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/responses/DiscussionResponsesViewModel.kt index 04584d065..8b548730b 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/responses/DiscussionResponsesViewModel.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/responses/DiscussionResponsesViewModel.kt @@ -12,7 +12,6 @@ import com.raccoongang.core.extension.isInternetError import com.raccoongang.core.system.ResourceManager import com.raccoongang.discussion.domain.interactor.DiscussionInteractor import com.raccoongang.discussion.domain.model.DiscussionComment -import com.raccoongang.discussion.domain.model.DiscussionProfile import com.raccoongang.discussion.system.notifier.DiscussionCommentDataChanged import com.raccoongang.discussion.system.notifier.DiscussionNotifier import kotlinx.coroutines.launch @@ -158,15 +157,7 @@ class DiscussionResponsesViewModel( fun createComment(rawBody: String) { viewModelScope.launch { try { - var response = interactor.createComment(comment.threadId, rawBody, comment.id) - response = response.copy( - users = mapOf( - Pair( - preferencesManager.profile?.username ?: "", - DiscussionProfile(preferencesManager.profile?.profileImage) - ) - ) - ) + val response = interactor.createComment(comment.threadId, rawBody, comment.id) comment = comment.copy(childCount = comment.childCount + 1) sendUpdatedComment() if (page == -1) { diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsFragment.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsFragment.kt index 3e80f5355..6351295a0 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsFragment.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsFragment.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.* import androidx.compose.material.pullrefresh.PullRefreshIndicator @@ -79,6 +80,7 @@ class DiscussionThreadsFragment : Fragment() { val uiState by viewModel.uiState.observeAsState(DiscussionThreadsUIState.Loading) val uiMessage by viewModel.uiMessage.observeAsState() + val canLoadMore by viewModel.canLoadMore.observeAsState(false) val refreshing by viewModel.isUpdating.observeAsState(false) DiscussionThreadsScreen( @@ -86,6 +88,7 @@ class DiscussionThreadsFragment : Fragment() { title = requireArguments().getString(ARG_TITLE, ""), uiState = uiState, uiMessage = uiMessage, + canLoadMore = canLoadMore, viewType = viewType, refreshing = refreshing, onSwipeRefresh = { @@ -111,6 +114,9 @@ class DiscussionThreadsFragment : Fragment() { viewModel.courseId ) }, + paginationCallback = { + viewModel.fetchMore() + }, onBackClick = { requireActivity().supportFragmentManager.popBackStack() } @@ -153,6 +159,7 @@ private fun DiscussionThreadsScreen( title: String, uiState: DiscussionThreadsUIState, uiMessage: UIMessage?, + canLoadMore: Boolean, viewType: FragmentViewType, refreshing: Boolean, onSwipeRefresh: () -> Unit, @@ -160,6 +167,7 @@ private fun DiscussionThreadsScreen( updatedFilter: (String) -> Unit, onItemClick: (com.raccoongang.discussion.domain.model.Thread) -> Unit, onCreatePostClick: () -> Unit, + paginationCallback: () -> Unit, onBackClick: () -> Unit ) { @@ -171,6 +179,10 @@ private fun DiscussionThreadsScreen( val pullRefreshState = rememberPullRefreshState(refreshing = refreshing, onRefresh = { onSwipeRefresh() }) val coroutine = rememberCoroutineScope() + val scrollState = rememberLazyListState() + val firstVisibleIndex = remember { + mutableStateOf(scrollState.firstVisibleItemIndex) + } val context = LocalContext.current var sortType by rememberSaveable { mutableStateOf( @@ -230,8 +242,13 @@ private fun DiscussionThreadsScreen( val listPadding by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( - expanded = PaddingValues(vertical = 24.dp), - compact = PaddingValues(24.dp) + expanded = PaddingValues(top = 24.dp, bottom = 80.dp), + compact = PaddingValues( + start = 24.dp, + end = 24.dp, + top = 24.dp, + bottom = 80.dp + ) ) ) } @@ -418,7 +435,8 @@ private fun DiscussionThreadsScreen( Divider() LazyColumn( modifier = Modifier.fillMaxSize(), - contentPadding = listPadding + contentPadding = listPadding, + state = scrollState ) { item { Text( @@ -434,6 +452,25 @@ private fun DiscussionThreadsScreen( }) Divider() } + item { + if (canLoadMore) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + } + if (scrollState.shouldLoadMore( + firstVisibleIndex, + 4 + ) + ) { + paginationCallback() + } } } NewEdxButton( @@ -501,7 +538,9 @@ private fun DiscussionThreadsScreenPreview() { onCreatePostClick = {}, viewType = FragmentViewType.FULL_CONTENT, onSwipeRefresh = {}, - refreshing = false + paginationCallback = {}, + refreshing = false, + canLoadMore = false ) } } @@ -523,7 +562,9 @@ private fun DiscussionThreadsScreenTabletPreview() { onCreatePostClick = {}, viewType = FragmentViewType.FULL_CONTENT, onSwipeRefresh = {}, - refreshing = false + paginationCallback = {}, + refreshing = false, + canLoadMore = false, ) } } diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsViewModel.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsViewModel.kt index 72369b444..086d5613e 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsViewModel.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsViewModel.kt @@ -11,7 +11,6 @@ import com.raccoongang.core.UIMessage import com.raccoongang.core.extension.isInternetError import com.raccoongang.core.system.ResourceManager import com.raccoongang.discussion.domain.interactor.DiscussionInteractor -import com.raccoongang.discussion.domain.model.DiscussionType import com.raccoongang.discussion.presentation.topics.DiscussionTopicsFragment import com.raccoongang.discussion.system.notifier.DiscussionNotifier import com.raccoongang.discussion.system.notifier.DiscussionThreadAdded @@ -38,12 +37,17 @@ class DiscussionThreadsViewModel( val isUpdating: LiveData get() = _isUpdating + private val _canLoadMore = MutableLiveData() + val canLoadMore: LiveData + get() = _canLoadMore private val threadsList = mutableListOf() private var nextPage = 1 + private var isLoading = false var topicId = "" private var lastOrderBy = "" + private var filterType: String? = null override fun onCreate(owner: LifecycleOwner) { super.onCreate(owner) @@ -73,10 +77,23 @@ class DiscussionThreadsViewModel( fun updateThread(orderBy: String) { _isUpdating.value = true + threadsList.clear() + nextPage = 1 internalLoadThreads(orderBy) } + fun fetchMore() { + if (!isLoading && nextPage != -1) { + isLoading = true + internalLoadThreads(lastOrderBy) + } + } + private fun internalLoadThreads(orderBy: String) { + if (lastOrderBy != orderBy) { + threadsList.clear() + nextPage = 1 + } lastOrderBy = orderBy when (threadType) { DiscussionTopicsFragment.ALL_POSTS -> { @@ -94,32 +111,45 @@ class DiscussionThreadsViewModel( } } - fun filterThreads(filter: String) { - when (filter) { - FilterType.ALL_POSTS.value -> { - _uiState.value = DiscussionThreadsUIState.Threads(threadsList.toList()) + fun filterThreads(filter: String?) { + if (filterType != filter || (filter != FilterType.ALL_POSTS.value && filterType.isNullOrEmpty())) { + threadsList.clear() + nextPage = 1 + } + filterType = if (filter == FilterType.ALL_POSTS.value) { + null + } else { + filter + } + when (threadType) { + DiscussionTopicsFragment.ALL_POSTS -> { + getAllThreads(lastOrderBy) } - FilterType.UNREAD.value -> { - _uiState.value = DiscussionThreadsUIState.Threads(threadsList.filter { !it.read }) + DiscussionTopicsFragment.FOLLOWING_POSTS -> { + getFollowingThreads(lastOrderBy) } - FilterType.UNANSWERED.value -> { - _uiState.value = DiscussionThreadsUIState.Threads(threadsList.filter { - it.type == DiscussionType.QUESTION && !it.hasEndorsed - }) + DiscussionTopicsFragment.TOPIC -> { + getThreads( + topicId, + lastOrderBy + ) } } } private fun getThreads(topicId: String, orderBy: String) { - nextPage = 1 - threadsList.clear() viewModelScope.launch { try { - while (nextPage > 0) { - val response = interactor.getThreads(courseId, topicId, orderBy, nextPage) - if (response.pagination.next.isNotEmpty()) nextPage++ else nextPage = -1 - threadsList.addAll(response.results) + val response = + interactor.getThreads(courseId, topicId, orderBy, filterType, nextPage) + if (response.pagination.next.isNotEmpty()) { + _canLoadMore.value = true + nextPage++ + } else { + _canLoadMore.value = false + nextPage = -1 } + threadsList.addAll(response.results) _uiState.value = DiscussionThreadsUIState.Threads(threadsList.toList()) } catch (e: Exception) { if (e.isInternetError()) { @@ -131,19 +161,22 @@ class DiscussionThreadsViewModel( } } _isUpdating.value = false + isLoading = false } } private fun getAllThreads(orderBy: String) { - nextPage = 1 - threadsList.clear() viewModelScope.launch { try { - while (nextPage > 0) { - val response = interactor.getAllThreads(courseId, orderBy, nextPage) - if (response.pagination.next.isNotEmpty()) nextPage++ else nextPage = -1 - threadsList.addAll(response.results) + val response = interactor.getAllThreads(courseId, orderBy, filterType, nextPage) + if (response.pagination.next.isNotEmpty()) { + _canLoadMore.value = true + nextPage++ + } else { + _canLoadMore.value = false + nextPage = -1 } + threadsList.addAll(response.results) _uiState.value = DiscussionThreadsUIState.Threads(threadsList.toList()) } catch (e: Exception) { if (e.isInternetError()) { @@ -155,19 +188,23 @@ class DiscussionThreadsViewModel( } } _isUpdating.value = false + isLoading = false } } private fun getFollowingThreads(orderBy: String) { - nextPage = 1 - threadsList.clear() viewModelScope.launch { try { - while (nextPage > 0) { - val response = interactor.getFollowingThreads(courseId, true, orderBy, nextPage) - if (response.pagination.next.isNotEmpty()) nextPage++ else nextPage = -1 - threadsList.addAll(response.results) + val response = + interactor.getFollowingThreads(courseId, true, orderBy, page = nextPage) + if (response.pagination.next.isNotEmpty()) { + _canLoadMore.value = true + nextPage++ + } else { + _canLoadMore.value = false + nextPage = -1 } + threadsList.addAll(response.results) _uiState.value = DiscussionThreadsUIState.Threads(threadsList.toList()) } catch (e: Exception) { if (e.isInternetError()) { @@ -179,6 +216,7 @@ class DiscussionThreadsViewModel( } } _isUpdating.value = false + isLoading = false } } } \ No newline at end of file diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/ui/DiscussionUI.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/ui/DiscussionUI.kt index 2261e7e8d..add2cd76e 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/ui/DiscussionUI.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/ui/DiscussionUI.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import coil.compose.rememberAsyncImagePainter +import com.raccoongang.core.domain.model.ProfileImage import com.raccoongang.core.extension.TextConverter import com.raccoongang.core.ui.AutoSizeText import com.raccoongang.core.ui.HyperlinkImageText @@ -166,7 +167,8 @@ fun CommentItem( onClick: (String, String, Boolean) -> Unit, onAddCommentClick: () -> Unit = {}, ) { - val profileImageUrl = comment.users?.get(comment.author)?.image?.imageUrlFull ?: "" + val profileImageUrl = comment.profileImage?.imageUrlFull + ?: comment.users?.get(comment.author)?.image?.imageUrlFull ?: "" val reportText = if (comment.abuseFlagged) { stringResource(id = R.string.discussion_unreport) @@ -310,7 +312,8 @@ fun CommentMainItem( comment: DiscussionComment, onClick: (String, String, Boolean) -> Unit, ) { - val profileImageUrl = comment.users?.get(comment.author)?.image?.imageUrlFull ?: "" + val profileImageUrl = comment.profileImage?.imageUrlFull + ?: comment.users?.get(comment.author)?.image?.imageUrlFull ?: "" val reportText = if (comment.abuseFlagged) { stringResource(id = R.string.discussion_unreport) @@ -659,7 +662,8 @@ private val mockComment = DiscussionComment( "", 21, emptyList(), - emptyMap() + ProfileImage("", "", "", "", false), + mapOf() ) private val mockThread = com.raccoongang.discussion.domain.model.Thread( From 667ad51f94c17fc0c8fb4e2e3b399ae3c3c62756 Mon Sep 17 00:00:00 2001 From: volodymyr-chekyrta <127732735+volodymyr-chekyrta@users.noreply.github.com> Date: Tue, 4 Apr 2023 14:22:51 +0300 Subject: [PATCH 09/20] Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b5a8baca5..939583e08 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,15 @@ Modern vision of the mobile application for the Open EdX platform from Raccoon G 6. Click the **Run** button. +## API plugin +This project uses custom APIs to improve performance and reduce the number of requests to the server. + +You can find the plugin with the API and installation guide [here](https://github.com/raccoongang/mobile-api-extensions). + ## Roadmap Please feel welcome to develop any of the suggested features below and submit a pull request. -- Migrate to the new APIs +- ✅ ~~Migrate to the new APIs~~ - Recent searches - Migrate to the Olive and JWT token - UnAuth User mode From 8591e8a52c23c2f359cc9914d093a23f75257482 Mon Sep 17 00:00:00 2001 From: Serhii <128455389+hryh27@users.noreply.github.com> Date: Wed, 5 Apr 2023 12:35:16 +0300 Subject: [PATCH 10/20] video and progress bar (#9) --- .../restore/RestorePasswordFragment.kt | 2 +- .../presentation/signin/SignInFragment.kt | 2 +- .../presentation/signup/SignUpFragment.kt | 4 +- .../raccoongang/core/domain/model/Block.kt | 10 ++-- .../detail/CourseDetailsFragment.kt | 4 +- .../presentation/handouts/WebViewFragment.kt | 2 +- .../outline/CourseOutlineFragment.kt | 2 +- .../section/CourseSectionFragment.kt | 7 ++- .../course/presentation/ui/CourseUI.kt | 5 +- .../container/CourseUnitContainerFragment.kt | 52 +++++++++---------- .../unit/html/HtmlUnitFragment.kt | 2 +- .../presentation/units/CourseUnitsFragment.kt | 7 ++- .../videos/CourseVideosFragment.kt | 10 +++- .../videos/CourseVideosUIState.kt | 1 + .../presentation/DashboardFragment.kt | 4 +- .../presentation/DiscoveryFragment.kt | 4 +- .../search/CourseSearchFragment.kt | 4 +- .../comments/DiscussionCommentsFragment.kt | 4 +- .../responses/DiscussionResponsesFragment.kt | 4 +- .../search/DiscussionSearchThreadFragment.kt | 4 +- .../threads/DiscussionAddThreadFragment.kt | 2 +- .../threads/DiscussionThreadsFragment.kt | 4 +- .../topics/DiscussionTopicsFragment.kt | 2 +- .../presentation/profile/ProfileFragment.kt | 2 +- 24 files changed, 81 insertions(+), 63 deletions(-) diff --git a/auth/src/main/java/com/raccoongang/auth/presentation/restore/RestorePasswordFragment.kt b/auth/src/main/java/com/raccoongang/auth/presentation/restore/RestorePasswordFragment.kt index 93fe5e5b0..e716393fa 100644 --- a/auth/src/main/java/com/raccoongang/auth/presentation/restore/RestorePasswordFragment.kt +++ b/auth/src/main/java/com/raccoongang/auth/presentation/restore/RestorePasswordFragment.kt @@ -224,7 +224,7 @@ private fun RestorePasswordScreen( .height(42.dp), contentAlignment = Alignment.Center ) { - CircularProgressIndicator(color = MaterialTheme.colors.primary) + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } else { NewEdxButton( diff --git a/auth/src/main/java/com/raccoongang/auth/presentation/signin/SignInFragment.kt b/auth/src/main/java/com/raccoongang/auth/presentation/signin/SignInFragment.kt index 494becbd8..9f53771ac 100644 --- a/auth/src/main/java/com/raccoongang/auth/presentation/signin/SignInFragment.kt +++ b/auth/src/main/java/com/raccoongang/auth/presentation/signin/SignInFragment.kt @@ -255,7 +255,7 @@ private fun AuthForm( } if (isLoading) { - CircularProgressIndicator(color = MaterialTheme.colors.primary) + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } else { NewEdxButton( width = buttonWidth, diff --git a/auth/src/main/java/com/raccoongang/auth/presentation/signup/SignUpFragment.kt b/auth/src/main/java/com/raccoongang/auth/presentation/signup/SignUpFragment.kt index 7a1b99790..80ef362ab 100644 --- a/auth/src/main/java/com/raccoongang/auth/presentation/signup/SignUpFragment.kt +++ b/auth/src/main/java/com/raccoongang/auth/presentation/signup/SignUpFragment.kt @@ -290,7 +290,7 @@ internal fun RegistrationScreen( Modifier .fillMaxSize(), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } is SignUpUIState.Fields -> { @@ -380,7 +380,7 @@ internal fun RegistrationScreen( .height(42.dp), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } else { NewEdxButton( diff --git a/core/src/main/java/com/raccoongang/core/domain/model/Block.kt b/core/src/main/java/com/raccoongang/core/domain/model/Block.kt index 6f18eecd6..a36ef6d4b 100644 --- a/core/src/main/java/com/raccoongang/core/domain/model/Block.kt +++ b/core/src/main/java/com/raccoongang/core/domain/model/Block.kt @@ -70,11 +70,11 @@ data class EncodedVideos( var mobileLow: VideoInfo? ) { val hasDownloadableVideo: Boolean - get() = (youtube == null && (!hls?.url.isNullOrEmpty() || - !fallback?.url.isNullOrEmpty() || - !desktopMp4?.url.isNullOrEmpty() || - !mobileHigh?.url.isNullOrEmpty() || - !mobileLow?.url.isNullOrEmpty())) + get() = isPreferredVideoInfo(hls) || + isPreferredVideoInfo(fallback) || + isPreferredVideoInfo(desktopMp4) || + isPreferredVideoInfo(mobileHigh) || + isPreferredVideoInfo(mobileLow) fun getPreferredVideoInfoForDownloading(preferredVideoQuality: VideoQuality): VideoInfo? { var preferredVideoInfo = when (preferredVideoQuality) { diff --git a/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt index c2f161634..76a5311d8 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt @@ -222,7 +222,7 @@ internal fun CourseDetailsScreen( .padding(it), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } is CourseDetailsUIState.CourseData -> { @@ -255,7 +255,7 @@ internal fun CourseDetailsScreen( .padding(top = 20.dp), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } Surface( diff --git a/course/src/main/java/com/raccoongang/course/presentation/handouts/WebViewFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/handouts/WebViewFragment.kt index 18b1df27f..510703ee6 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/handouts/WebViewFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/handouts/WebViewFragment.kt @@ -179,7 +179,7 @@ private fun WebContentScreen( .zIndex(1f), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } } diff --git a/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt index 4aa2a633a..ff3ba2617 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt @@ -265,7 +265,7 @@ internal fun CourseOutlineScreen( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } is CourseOutlineUIState.CourseData -> { diff --git a/course/src/main/java/com/raccoongang/course/presentation/section/CourseSectionFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/section/CourseSectionFragment.kt index 0bd6aa3d6..e555deba0 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/section/CourseSectionFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/section/CourseSectionFragment.kt @@ -221,7 +221,7 @@ private fun CourseSectionScreen( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } is CourseSectionUIState.Blocks -> { @@ -315,7 +315,10 @@ private fun CourseSubsectionItem( } else if (downloadedState != null) { Box(contentAlignment = Alignment.Center) { if (downloadedState == DownloadedState.DOWNLOADING || downloadedState == DownloadedState.WAITING) { - CircularProgressIndicator(modifier = Modifier.size(34.dp)) + CircularProgressIndicator( + modifier = Modifier.size(34.dp), + color = MaterialTheme.appColors.primary + ) } IconButton(modifier = iconModifier, onClick = { onDownloadClick(block) }) { diff --git a/course/src/main/java/com/raccoongang/course/presentation/ui/CourseUI.kt b/course/src/main/java/com/raccoongang/course/presentation/ui/CourseUI.kt index 7dfce2dd3..bb7e4d027 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/ui/CourseUI.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/ui/CourseUI.kt @@ -167,7 +167,10 @@ fun CourseSectionCard( } else if (downloadedState != null) { Box(contentAlignment = Alignment.Center) { if (downloadedState == DownloadedState.DOWNLOADING || downloadedState == DownloadedState.WAITING) { - CircularProgressIndicator(modifier = Modifier.size(34.dp)) + CircularProgressIndicator( + modifier = Modifier.size(34.dp), + color = MaterialTheme.appColors.primary + ) } IconButton(modifier = iconModifier, onClick = { onDownloadClick(block) }) { diff --git a/course/src/main/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerFragment.kt index fca1f3e21..fa7482736 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerFragment.kt @@ -177,32 +177,25 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta } BlockType.VIDEO -> { val encodedVideos = block.studentViewData!!.encodedVideos!! - if (encodedVideos.youtube != null) { - YoutubeVideoUnitFragment.newInstance( - block.id, - viewModel.courseId, - encodedVideos.youtube?.url!!, - block.displayName - ) - } else { - with(encodedVideos) { - var isDownloaded = false - val videoUrl = if (viewModel.getDownloadModelById(block.id) != null) { - isDownloaded = true - viewModel.getDownloadModelById(block.id)!!.path - } else if (fallback != null) { - fallback!!.url - } else if (hls != null) { - hls!!.url - } else if (desktopMp4 != null) { - desktopMp4!!.url - } else if (mobileHigh != null) { - mobileHigh!!.url - } else if (mobileLow != null) { - mobileLow!!.url - } else { - "" - } + with(encodedVideos) { + var isDownloaded = false + val videoUrl = if (viewModel.getDownloadModelById(block.id) != null) { + isDownloaded = true + viewModel.getDownloadModelById(block.id)!!.path + } else if (fallback != null) { + fallback!!.url + } else if (hls != null) { + hls!!.url + } else if (desktopMp4 != null) { + desktopMp4!!.url + } else if (mobileHigh != null) { + mobileHigh!!.url + } else if (mobileLow != null) { + mobileLow!!.url + } else { + "" + } + if (videoUrl.isNotEmpty()) { VideoUnitFragment.newInstance( block.id, viewModel.courseId, @@ -210,6 +203,13 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta block.displayName, isDownloaded ) + } else { + YoutubeVideoUnitFragment.newInstance( + block.id, + viewModel.courseId, + encodedVideos.youtube?.url!!, + block.displayName + ) } } } diff --git a/course/src/main/java/com/raccoongang/course/presentation/unit/html/HtmlUnitFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/unit/html/HtmlUnitFragment.kt index 7537af832..9400a908c 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/unit/html/HtmlUnitFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/unit/html/HtmlUnitFragment.kt @@ -102,7 +102,7 @@ class HtmlUnitFragment : Fragment() { .zIndex(1f), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } } diff --git a/course/src/main/java/com/raccoongang/course/presentation/units/CourseUnitsFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/units/CourseUnitsFragment.kt index a3f928dce..e25a8f8b7 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/units/CourseUnitsFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/units/CourseUnitsFragment.kt @@ -223,7 +223,7 @@ private fun CourseUnitsScreen( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } is CourseUnitsUIState.Blocks -> { @@ -317,7 +317,10 @@ private fun CourseUnitItem( } } else if (block.isDownloadable) { Box(contentAlignment = Alignment.Center) { - CircularProgressIndicator(modifier = Modifier.size(34.dp)) + CircularProgressIndicator( + modifier = Modifier.size(34.dp), + color = MaterialTheme.appColors.primary + ) IconButton(modifier = iconModifier, onClick = { onDownloadClick(block) }) { Icon( diff --git a/course/src/main/java/com/raccoongang/course/presentation/videos/CourseVideosFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/videos/CourseVideosFragment.kt index c2419d028..5ad441a0c 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/videos/CourseVideosFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/videos/CourseVideosFragment.kt @@ -73,7 +73,7 @@ class CourseVideosFragment : Fragment() { NewEdxTheme { val windowSize = rememberWindowSize() - val uiState by viewModel.uiState.observeAsState(CourseVideosUIState.Empty("")) + val uiState by viewModel.uiState.observeAsState(CourseVideosUIState.Loading) val uiMessage by viewModel.uiMessage.observeAsState() val isUpdating by viewModel.isUpdating.observeAsState(false) @@ -261,6 +261,14 @@ private fun CourseVideosScreen( ) } } + is CourseVideosUIState.Loading -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = MaterialTheme.appColors.primary) + } + } is CourseVideosUIState.CourseData -> { CourseImageHeader( modifier = Modifier diff --git a/course/src/main/java/com/raccoongang/course/presentation/videos/CourseVideosUIState.kt b/course/src/main/java/com/raccoongang/course/presentation/videos/CourseVideosUIState.kt index 3ad5ec643..882eba6ce 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/videos/CourseVideosUIState.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/videos/CourseVideosUIState.kt @@ -10,4 +10,5 @@ sealed class CourseVideosUIState { ) : CourseVideosUIState() data class Empty(val message: String) : CourseVideosUIState() + object Loading : CourseVideosUIState() } \ No newline at end of file diff --git a/dashboard/src/main/java/com/raccoongang/dashboard/presentation/DashboardFragment.kt b/dashboard/src/main/java/com/raccoongang/dashboard/presentation/DashboardFragment.kt index aba6dda9b..ef26afe64 100644 --- a/dashboard/src/main/java/com/raccoongang/dashboard/presentation/DashboardFragment.kt +++ b/dashboard/src/main/java/com/raccoongang/dashboard/presentation/DashboardFragment.kt @@ -204,7 +204,7 @@ internal fun MyCoursesScreen( Modifier .fillMaxSize(), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } is DashboardUIState.Courses -> { @@ -249,7 +249,7 @@ internal fun MyCoursesScreen( .padding(vertical = 16.dp), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } } diff --git a/discovery/src/main/java/com/raccoongang/discovery/presentation/DiscoveryFragment.kt b/discovery/src/main/java/com/raccoongang/discovery/presentation/DiscoveryFragment.kt index 2490fa055..9688008ae 100644 --- a/discovery/src/main/java/com/raccoongang/discovery/presentation/DiscoveryFragment.kt +++ b/discovery/src/main/java/com/raccoongang/discovery/presentation/DiscoveryFragment.kt @@ -199,7 +199,7 @@ internal fun DiscoveryScreen( Modifier .fillMaxSize(), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } is DiscoveryUIState.Courses -> { @@ -247,7 +247,7 @@ internal fun DiscoveryScreen( .padding(vertical = 16.dp), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } } diff --git a/discovery/src/main/java/com/raccoongang/discovery/presentation/search/CourseSearchFragment.kt b/discovery/src/main/java/com/raccoongang/discovery/presentation/search/CourseSearchFragment.kt index 5cdece24e..acb970bca 100644 --- a/discovery/src/main/java/com/raccoongang/discovery/presentation/search/CourseSearchFragment.kt +++ b/discovery/src/main/java/com/raccoongang/discovery/presentation/search/CourseSearchFragment.kt @@ -281,7 +281,7 @@ private fun CourseSearchScreen( .padding(vertical = 25.dp), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } } @@ -303,7 +303,7 @@ private fun CourseSearchScreen( .padding(vertical = 16.dp), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } } diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsFragment.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsFragment.kt index 6fd44d0a9..a6bb4921c 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsFragment.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsFragment.kt @@ -326,7 +326,7 @@ private fun DiscussionCommentsScreen( modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } } @@ -406,7 +406,7 @@ private fun DiscussionCommentsScreen( Modifier .fillMaxSize(), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } } diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/responses/DiscussionResponsesFragment.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/responses/DiscussionResponsesFragment.kt index 1705e7d8b..de88ce103 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/responses/DiscussionResponsesFragment.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/responses/DiscussionResponsesFragment.kt @@ -341,7 +341,7 @@ private fun DiscussionResponsesScreen( modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } } @@ -422,7 +422,7 @@ private fun DiscussionResponsesScreen( Modifier .fillMaxSize(), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } } diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/search/DiscussionSearchThreadFragment.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/search/DiscussionSearchThreadFragment.kt index 7c72b86d2..e72c006a7 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/search/DiscussionSearchThreadFragment.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/search/DiscussionSearchThreadFragment.kt @@ -298,7 +298,7 @@ private fun DiscussionSearchThreadScreen( .padding(vertical = 25.dp), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } } @@ -315,7 +315,7 @@ private fun DiscussionSearchThreadScreen( .padding(vertical = 16.dp), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } } diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionAddThreadFragment.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionAddThreadFragment.kt index 9eac99c70..958ba16f8 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionAddThreadFragment.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionAddThreadFragment.kt @@ -376,7 +376,7 @@ private fun DiscussionAddThreadScreen( } Spacer(Modifier.height(44.dp)) if (isLoading) { - CircularProgressIndicator(color = MaterialTheme.colors.primary) + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } else { NewEdxButton( width = buttonWidth, diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsFragment.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsFragment.kt index 6351295a0..eab2ea97c 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsFragment.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsFragment.kt @@ -460,7 +460,7 @@ private fun DiscussionThreadsScreen( .padding(vertical = 16.dp), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } } @@ -502,7 +502,7 @@ private fun DiscussionThreadsScreen( Modifier .fillMaxSize(), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } } diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/topics/DiscussionTopicsFragment.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/topics/DiscussionTopicsFragment.kt index 699a62ef2..4af2b3ac2 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/topics/DiscussionTopicsFragment.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/topics/DiscussionTopicsFragment.kt @@ -316,7 +316,7 @@ private fun DiscussionTopicsScreen( Modifier .fillMaxSize(), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } } diff --git a/profile/src/main/java/com/raccoongang/profile/presentation/profile/ProfileFragment.kt b/profile/src/main/java/com/raccoongang/profile/presentation/profile/ProfileFragment.kt index 360b26ba3..0c31ccc46 100644 --- a/profile/src/main/java/com/raccoongang/profile/presentation/profile/ProfileFragment.kt +++ b/profile/src/main/java/com/raccoongang/profile/presentation/profile/ProfileFragment.kt @@ -227,7 +227,7 @@ private fun ProfileScreen( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } is ProfileUIState.Data -> { From 328712e8febdb5b377bf3a32f7927ba5572e4249 Mon Sep 17 00:00:00 2001 From: Serhii <128455389+hryh27@users.noreply.github.com> Date: Thu, 6 Apr 2023 11:57:17 +0300 Subject: [PATCH 11/20] open video in details screen (#10) --- .../detail/CourseDetailsFragment.kt | 69 ++++++++++++++----- 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt index 76a5311d8..49e8fe7df 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PlayCircle import androidx.compose.material.icons.outlined.Report import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState @@ -22,10 +23,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.shadow -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.* import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -303,6 +302,7 @@ private fun CourseDetailNativeContent( course: Course, onButtonClick: () -> Unit, ) { + val uriHandler = LocalUriHandler.current val buttonWidth by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( @@ -328,13 +328,29 @@ private fun CourseDetailNativeContent( } Column { - CourseImageHeader( - modifier = Modifier - .aspectRatio(1.86f) - .padding(6.dp), - courseImage = course.media.image?.large, - courseCertificate = null - ) + Box(contentAlignment = Alignment.Center) { + CourseImageHeader( + modifier = Modifier + .aspectRatio(1.86f) + .padding(6.dp), + courseImage = course.media.image?.large, + courseCertificate = null + ) + if (!course.media.courseVideo?.uri.isNullOrEmpty()) { + IconButton( + onClick = { + uriHandler.openUri(course.media.courseVideo?.uri!!) + } + ) { + Icon( + modifier = Modifier.size(64.dp), + imageVector = Icons.Filled.PlayCircle, + contentDescription = null, + tint = Color.LightGray + ) + } + } + } Spacer(Modifier.height(16.dp)) Column( Modifier @@ -382,6 +398,7 @@ private fun CourseDetailNativeContentLandscape( course: Course, onButtonClick: () -> Unit, ) { + val uriHandler = LocalUriHandler.current val buttonWidth by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( @@ -440,13 +457,29 @@ private fun CourseDetailNativeContentLandscape( } } Spacer(Modifier.width(24.dp)) - CourseImageHeader( - modifier = Modifier - .width(263.dp) - .height(200.dp), - courseImage = course.media.image?.large, - courseCertificate = null - ) + Box(contentAlignment = Alignment.Center) { + CourseImageHeader( + modifier = Modifier + .width(263.dp) + .height(200.dp), + courseImage = course.media.image?.large, + courseCertificate = null + ) + if (!course.media.courseVideo?.uri.isNullOrEmpty()) { + IconButton( + onClick = { + uriHandler.openUri(course.media.courseVideo?.uri!!) + } + ) { + Icon( + modifier = Modifier.size(64.dp), + imageVector = Icons.Filled.PlayCircle, + contentDescription = null, + tint = Color.LightGray + ) + } + } + } } } From 37fa986b7cabe92940f2aa2ad35c428a910c787b Mon Sep 17 00:00:00 2001 From: Serhii <128455389+hryh27@users.noreply.github.com> Date: Fri, 7 Apr 2023 11:55:19 +0300 Subject: [PATCH 12/20] Feature/resume block (#11) * resume block * version for tablets --- .../com/raccoongang/core/ui/ComposeCommon.kt | 28 ++++ .../outline/CourseOutlineFragment.kt | 146 +++++++++++++----- .../outline/CourseOutlineViewModel.kt | 22 ++- .../presentation/units/CourseUnitsFragment.kt | 19 +-- course/src/main/res/values-uk/strings.xml | 2 + course/src/main/res/values/strings.xml | 2 + 6 files changed, 170 insertions(+), 49 deletions(-) diff --git a/core/src/main/java/com/raccoongang/core/ui/ComposeCommon.kt b/core/src/main/java/com/raccoongang/core/ui/ComposeCommon.kt index 6b534c86d..a058d7b2b 100644 --- a/core/src/main/java/com/raccoongang/core/ui/ComposeCommon.kt +++ b/core/src/main/java/com/raccoongang/core/ui/ComposeCommon.kt @@ -749,6 +749,34 @@ fun TextIcon( } } +@Composable +fun TextIcon( + text: String, + painter: Painter, + color: Color, + textStyle: TextStyle = MaterialTheme.appTypography.bodySmall, + onClick: (() -> Unit)? = null, +) { + val modifier = if (onClick == null) { + Modifier + } else { + Modifier.noRippleClickable { onClick.invoke() } + } + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text(text = text, color = color, style = textStyle) + Icon( + modifier = Modifier.size((textStyle.fontSize.value + 4).dp), + painter = painter, + contentDescription = null, + tint = color + ) + } +} + @Composable fun OfflineModeDialog( modifier: Modifier, diff --git a/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt index ff3ba2617..96e87b957 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt @@ -5,13 +5,10 @@ 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.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState @@ -22,8 +19,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Devices @@ -46,6 +43,7 @@ import com.raccoongang.course.presentation.CourseRouter import com.raccoongang.course.presentation.container.CourseContainerFragment import com.raccoongang.course.presentation.ui.CourseImageHeader import com.raccoongang.course.presentation.ui.CourseSectionCard +import com.raccoongang.course.presentation.units.CourseUnitsFragment import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf @@ -105,13 +103,24 @@ class CourseOutlineFragment : Fragment() { ) }, onResumeClick = { blockId -> - router.navigateToCourseContainer( - requireActivity().supportFragmentManager, - blockId = blockId, - courseId = viewModel.courseId, - courseName = viewModel.courseTitle, - mode = CourseViewMode.FULL - ) + viewModel.resumeSectionBlock?.let { sequential -> + router.navigateToCourseSubsections( + requireActivity().supportFragmentManager, + viewModel.courseId, + sequential.id, + sequential.displayName, + CourseViewMode.FULL + ) + viewModel.resumeVerticalBlock?.let { vertical -> + router.navigateToCourseUnits( + requireActivity().supportFragmentManager, + viewModel.courseId, + vertical.id, + vertical.displayName, + CourseViewMode.FULL + ) + } + } }, onBackClick = { requireActivity().supportFragmentManager.popBackStack() @@ -285,6 +294,22 @@ internal fun CourseOutlineScreen( modifier = Modifier.fillMaxWidth(), contentPadding = listPadding ) { + if (uiState.resumeBlock != null) { + item { + Spacer(Modifier.height(28.dp)) + if (windowSize.isTablet) { + ResumeCourseTablet( + block = uiState.resumeBlock, + onResumeClick = onResumeClick + ) + } else { + ResumeCourse( + block = uiState.resumeBlock, + onResumeClick = onResumeClick + ) + } + } + } items(uiState.courseStructure.blockData) { block -> if (block.type == BlockType.CHAPTER) { Text( @@ -342,43 +367,88 @@ internal fun CourseOutlineScreen( private fun ResumeCourse( block: Block, onResumeClick: (String) -> Unit, +) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = stringResource(id = com.raccoongang.course.R.string.course_continue_with), + style = MaterialTheme.appTypography.labelMedium, + color = MaterialTheme.appColors.textPrimaryVariant + ) + Spacer(Modifier.height(6.dp)) + IconText( + text = block.displayName, + painter = painterResource(id = CourseUnitsFragment.getUnitBlockIcon(block)), + color = MaterialTheme.appColors.textPrimary, + textStyle = MaterialTheme.appTypography.titleMedium + ) + Spacer(Modifier.height(24.dp)) + NewEdxButton( + text = stringResource(id = com.raccoongang.course.R.string.course_continue), + onClick = { + onResumeClick(block.id) + }, + content = { + TextIcon( + text = stringResource(id = com.raccoongang.course.R.string.course_continue), + painter = painterResource(id = R.drawable.core_ic_forward), + color = MaterialTheme.appColors.buttonText, + textStyle = MaterialTheme.appTypography.labelLarge + ) + } + ) + } +} + + +@Composable +private fun ResumeCourseTablet( + block: Block, + onResumeClick: (String) -> Unit, ) { Row( - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.appColors.secondaryVariant) - .padding(horizontal = 20.dp, vertical = 10.dp), - verticalAlignment = Alignment.CenterVertically + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween ) { - Column(modifier = Modifier.weight(1f)) { + Column { Text( - text = stringResource(id = com.raccoongang.course.R.string.course_resume_unit_title), - style = MaterialTheme.appTypography.bodyLarge, - fontWeight = FontWeight.Medium, - color = MaterialTheme.appColors.textPrimary - ) - Text( - text = block.displayName, - style = MaterialTheme.appTypography.bodyLarge, - color = MaterialTheme.appColors.textPrimary + text = stringResource(id = com.raccoongang.course.R.string.course_continue_with), + style = MaterialTheme.appTypography.labelMedium, + color = MaterialTheme.appColors.textPrimaryVariant ) + Spacer(Modifier.height(6.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + modifier = Modifier.size((MaterialTheme.appTypography.titleMedium.fontSize.value + 4).dp), + painter = painterResource(id = CourseUnitsFragment.getUnitBlockIcon(block)), + contentDescription = null, + tint = MaterialTheme.appColors.textPrimary + ) + Text( + text = block.displayName, + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.titleMedium, + overflow = TextOverflow.Ellipsis, + maxLines = 4 + ) + } } - NewEdxOutlinedButton( - modifier = Modifier, - borderColor = MaterialTheme.appColors.textFieldBorder, - textColor = MaterialTheme.appColors.textPrimary, - text = stringResource(id = com.raccoongang.course.R.string.course_resume_unit_btn), + NewEdxButton( + width = Modifier.width(194.dp), + text = stringResource(id = com.raccoongang.course.R.string.course_continue), onClick = { onResumeClick(block.id) }, content = { - Text(text = stringResource(id = com.raccoongang.course.R.string.course_resume_unit_btn).uppercase()) - Icon( - imageVector = Icons.Filled.ChevronRight, - contentDescription = null, - modifier = Modifier - .size(20.dp) - .offset(x = 4.dp) + TextIcon( + text = stringResource(id = com.raccoongang.course.R.string.course_continue), + painter = painterResource(id = R.drawable.core_ic_forward), + color = MaterialTheme.appColors.buttonText, + textStyle = MaterialTheme.appTypography.labelLarge ) } ) diff --git a/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineViewModel.kt b/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineViewModel.kt index 633062089..1f8f8a0d3 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineViewModel.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineViewModel.kt @@ -10,7 +10,6 @@ import com.raccoongang.core.SingleEventLiveData import com.raccoongang.core.UIMessage import com.raccoongang.core.data.storage.PreferencesManager import com.raccoongang.core.domain.model.Block -import com.raccoongang.core.domain.model.Certificate import com.raccoongang.core.domain.model.CourseComponentStatus import com.raccoongang.core.extension.isInternetError import com.raccoongang.core.module.DownloadWorkerController @@ -49,6 +48,11 @@ class CourseOutlineViewModel( var courseTitle = "" + var resumeSectionBlock: Block? = null + private set + var resumeVerticalBlock: Block? = null + private set + val hasInternetConnection: Boolean get() = networkConnection.isOnline() @@ -127,7 +131,7 @@ class CourseOutlineViewModel( _uiState.value = CourseOutlineUIState.CourseData( courseStructure = courseStructure, downloadedState = getDownloadModelsStatus(), - resumeBlock = blocks.firstOrNull { it.id == courseStatus.lastVisitedBlockId } + resumeBlock = getResumeBlock(blocks, courseStatus.lastVisitedBlockId) ) } catch (e: Exception) { if (e.isInternetError()) { @@ -161,4 +165,18 @@ class CourseOutlineViewModel( return resultBlocks.toList() } + private fun getResumeBlock( + blocks: List, + continueBlockId: String + ): Block? { + val resumeBlock = blocks.firstOrNull { it.id == continueBlockId } + resumeVerticalBlock = blocks.find { + it.descendants.contains(resumeBlock?.id) && it.type == BlockType.VERTICAL + } + resumeSectionBlock = blocks.find { + it.descendants.contains(resumeVerticalBlock?.id) && it.type == BlockType.SEQUENTIAL + } + return resumeVerticalBlock + } + } \ No newline at end of file diff --git a/course/src/main/java/com/raccoongang/course/presentation/units/CourseUnitsFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/units/CourseUnitsFragment.kt index e25a8f8b7..e1410c8c5 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/units/CourseUnitsFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/units/CourseUnitsFragment.kt @@ -47,6 +47,7 @@ import com.raccoongang.core.ui.theme.appTypography import com.raccoongang.course.R import com.raccoongang.course.presentation.CourseRouter import com.raccoongang.course.presentation.ui.CardArrow +import com.raccoongang.course.presentation.units.CourseUnitsFragment.Companion.getUnitBlockIcon import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import java.io.File @@ -139,6 +140,15 @@ class CourseUnitsFragment : Fragment() { ) return fragment } + + fun getUnitBlockIcon(block: Block): Int { + return when (block.type) { + BlockType.VIDEO -> R.drawable.ic_course_video + BlockType.PROBLEM -> R.drawable.ic_course_pen + BlockType.DISCUSSION -> R.drawable.ic_course_discussion + else -> R.drawable.ic_course_block + } + } } } @@ -339,15 +349,6 @@ private fun CourseUnitItem( } } -private fun getUnitBlockIcon(block: Block): Int { - return when (block.type) { - BlockType.VIDEO -> R.drawable.ic_course_video - BlockType.PROBLEM -> R.drawable.ic_course_pen - BlockType.DISCUSSION -> R.drawable.ic_course_discussion - else -> R.drawable.ic_course_block - } -} - @Preview(uiMode = UI_MODE_NIGHT_NO) @Preview(uiMode = UI_MODE_NIGHT_YES) diff --git a/course/src/main/res/values-uk/strings.xml b/course/src/main/res/values-uk/strings.xml index ab9ca719a..039a7e37b 100644 --- a/course/src/main/res/values-uk/strings.xml +++ b/course/src/main/res/values-uk/strings.xml @@ -43,5 +43,7 @@ Ця інтерактивна компонента ще не доступна Досліджуйте інші частини цього курсу або перегляньте це на веб-сайті. Відкрити в браузері + Остання активність: + Продовжити \ No newline at end of file diff --git a/course/src/main/res/values/strings.xml b/course/src/main/res/values/strings.xml index 2ae076a58..304022056 100644 --- a/course/src/main/res/values/strings.xml +++ b/course/src/main/res/values/strings.xml @@ -43,5 +43,7 @@ This interactive component isn’t yet available Explore other parts of this course or view this on web. Open in browser + Continue with: + Continue \ No newline at end of file From 2ffb24b79c53415960929c0fe0a8b7ef19dd9880 Mon Sep 17 00:00:00 2001 From: Serhii <128455389+hryh27@users.noreply.github.com> Date: Fri, 7 Apr 2023 11:59:03 +0300 Subject: [PATCH 13/20] handouts fix (#13) * handouts fix * create new discussion post fix --- .../raccoongang/core/extension/StringExt.kt | 17 +++ .../com/raccoongang/core/utils/EmailUtil.kt | 2 +- .../detail/CourseDetailsFragment.kt | 10 ++ .../presentation/handouts/HandoutsFragment.kt | 124 +++++++++--------- .../handouts/HandoutsViewModel.kt | 2 +- .../presentation/handouts/WebViewFragment.kt | 25 +++- .../unit/html/HtmlUnitFragment.kt | 10 ++ .../res/drawable/course_ic_announcements.xml | 31 +++++ .../main/res/drawable/course_ic_handouts.xml | 45 +++++++ .../threads/DiscussionThreadsViewModel.kt | 2 +- .../presentation/edit/EditProfileFragment.kt | 11 +- 11 files changed, 209 insertions(+), 70 deletions(-) create mode 100644 course/src/main/res/drawable/course_ic_announcements.xml create mode 100644 course/src/main/res/drawable/course_ic_handouts.xml diff --git a/core/src/main/java/com/raccoongang/core/extension/StringExt.kt b/core/src/main/java/com/raccoongang/core/extension/StringExt.kt index ae8a61a27..6cd69752d 100644 --- a/core/src/main/java/com/raccoongang/core/extension/StringExt.kt +++ b/core/src/main/java/com/raccoongang/core/extension/StringExt.kt @@ -11,3 +11,20 @@ fun String.isEmailValid(): Boolean { } fun String.isLinkValid() = Patterns.WEB_URL.matcher(this).matches() + +fun String.replaceLinkTags(isDarkTheme: Boolean): String { + val linkColor = if (isDarkTheme) "879FF5" else "0000EE" + var text = ("" + + "" + + "" + this) + "" + var str: String + while (text.indexOf("\u0082") > 0) { + if (text.indexOf("\u0082") > 0 && text.indexOf("\u0083") > 0) { + str = text.substring(text.indexOf("\u0082") + 1, text.indexOf("\u0083")) + text = text.replace(("\u0082" + str + "\u0083").toRegex(), "$str") + } + } + return text +} diff --git a/core/src/main/java/com/raccoongang/core/utils/EmailUtil.kt b/core/src/main/java/com/raccoongang/core/utils/EmailUtil.kt index c43d6fb2d..e11360e80 100644 --- a/core/src/main/java/com/raccoongang/core/utils/EmailUtil.kt +++ b/core/src/main/java/com/raccoongang/core/utils/EmailUtil.kt @@ -30,7 +30,7 @@ object EmailUtil { sendEmailIntent(context, to, subject, body.toString()) } - private fun sendEmailIntent( + fun sendEmailIntent( context: Context?, to: String, subject: String, diff --git a/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt index 49e8fe7df..07b4ae183 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt @@ -40,11 +40,13 @@ import com.raccoongang.core.BuildConfig import com.raccoongang.core.UIMessage import com.raccoongang.core.domain.model.Course import com.raccoongang.core.domain.model.Media +import com.raccoongang.core.extension.isEmailValid import com.raccoongang.core.ui.* import com.raccoongang.core.ui.theme.NewEdxTheme import com.raccoongang.core.ui.theme.appColors import com.raccoongang.core.ui.theme.appShapes import com.raccoongang.core.ui.theme.appTypography +import com.raccoongang.core.utils.EmailUtil import com.raccoongang.course.R import com.raccoongang.course.presentation.CourseRouter import com.raccoongang.course.presentation.ui.CourseImageHeader @@ -558,6 +560,14 @@ private fun CourseDescription( ) { context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(clickUrl))) true + } else if (clickUrl.startsWith("mailto:")) { + val email = clickUrl.replace("mailto:", "") + if (email.isEmailValid()) { + EmailUtil.sendEmailIntent(context, email, "", "") + true + } else { + false + } } else { false } diff --git a/course/src/main/java/com/raccoongang/course/presentation/handouts/HandoutsFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/handouts/HandoutsFragment.kt index 2fffd7250..4cb2e798d 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/handouts/HandoutsFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/handouts/HandoutsFragment.kt @@ -8,17 +8,16 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Campaign -import androidx.compose.material.icons.filled.Description import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy +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 @@ -34,6 +33,7 @@ import com.raccoongang.core.ui.theme.NewEdxTheme import com.raccoongang.core.ui.theme.appColors import com.raccoongang.core.ui.theme.appTypography import com.raccoongang.course.presentation.CourseRouter +import com.raccoongang.course.presentation.ui.CardArrow import org.koin.android.ext.android.inject import com.raccoongang.course.R as courseR @@ -112,10 +112,11 @@ private fun HandoutsScreen( ) } - Box(modifier = Modifier - .fillMaxWidth() - .padding(it) - .statusBarsInset(), + Box( + modifier = Modifier + .fillMaxWidth() + .padding(it) + .statusBarsInset(), contentAlignment = Alignment.TopCenter ) { Column(screenWidth) { @@ -146,64 +147,23 @@ private fun HandoutsScreen( ) { LazyColumn( Modifier.fillMaxSize(), - contentPadding = PaddingValues(vertical = 10.dp) + contentPadding = PaddingValues(vertical = 10.dp, horizontal = 24.dp) ) { item { - Row( - Modifier - .fillMaxWidth() - .clickable { onHandoutsClick() } - .padding(vertical = 16.dp, horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Filled.Description, - contentDescription = null - ) - Spacer(modifier = Modifier.width(16.dp)) - Column( - verticalArrangement = Arrangement.Center - ) { - Text( - text = stringResource(id = courseR.string.course_handouts), - style = MaterialTheme.appTypography.titleLarge, - color = MaterialTheme.appColors.textPrimary - ) - Text( - text = stringResource(id = courseR.string.course_find_important_info), - style = MaterialTheme.appTypography.bodySmall, - color = MaterialTheme.appColors.textPrimary - ) - } - } - Divider() + HandoutsItem( + title = stringResource(id = courseR.string.course_handouts), + description = stringResource(id = courseR.string.course_find_important_info), + painter = painterResource(id = courseR.drawable.course_ic_handouts), + onClick = onHandoutsClick + ) } item { - Row( - Modifier - .fillMaxWidth() - .clickable { onAnnouncementsClick() } - .padding(vertical = 16.dp, horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon(imageVector = Icons.Filled.Campaign, contentDescription = null) - Spacer(modifier = Modifier.width(16.dp)) - Column( - verticalArrangement = Arrangement.Center - ) { - Text( - text = stringResource(id = courseR.string.course_announcements), - style = MaterialTheme.appTypography.titleLarge, - color = MaterialTheme.appColors.textPrimary - ) - Text( - text = stringResource(id = courseR.string.course_latest_news), - style = MaterialTheme.appTypography.bodySmall, - color = MaterialTheme.appColors.textPrimary - ) - } - } - Divider() + HandoutsItem( + title = stringResource(id = courseR.string.course_announcements), + description = stringResource(id = courseR.string.course_latest_news), + painter = painterResource(id = courseR.drawable.course_ic_announcements), + onClick = onAnnouncementsClick + ) } } } @@ -212,6 +172,48 @@ private fun HandoutsScreen( } } +@Composable +private fun HandoutsItem( + title: String, + description: String, + painter: Painter, + onClick: () -> Unit +) { + Row( + Modifier + .fillMaxWidth() + .clickable { onClick() } + .padding(vertical = 16.dp, horizontal = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painter = painter, + contentDescription = null, + tint = MaterialTheme.appColors.textPrimary + ) + Spacer(modifier = Modifier.width(12.dp)) + Column( + verticalArrangement = Arrangement.Center + ) { + Text( + text = title, + style = MaterialTheme.appTypography.titleSmall, + color = MaterialTheme.appColors.textPrimary + ) + Text( + text = description, + style = MaterialTheme.appTypography.labelSmall, + color = MaterialTheme.appColors.textFieldHint + ) + } + } + CardArrow(degrees = 0f) + } + Divider() +} + @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable diff --git a/course/src/main/java/com/raccoongang/course/presentation/handouts/HandoutsViewModel.kt b/course/src/main/java/com/raccoongang/course/presentation/handouts/HandoutsViewModel.kt index 592f18952..9a9d8f2c9 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/handouts/HandoutsViewModel.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/handouts/HandoutsViewModel.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.raccoongang.core.BaseViewModel import com.raccoongang.core.domain.model.AnnouncementModel -import com.raccoongang.core.domain.model.EnrolledCourse import com.raccoongang.core.domain.model.HandoutsModel import com.raccoongang.course.domain.interactor.CourseInteractor import kotlinx.coroutines.launch @@ -58,6 +57,7 @@ class HandoutsViewModel( append("") for (model in announcements) { append("
") + append("
") append(model.date) append("
") append("
") diff --git a/course/src/main/java/com/raccoongang/course/presentation/handouts/WebViewFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/handouts/WebViewFragment.kt index 510703ee6..c2e4d5538 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/handouts/WebViewFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/handouts/WebViewFragment.kt @@ -11,6 +11,7 @@ import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* @@ -33,10 +34,13 @@ import androidx.compose.ui.zIndex import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import com.raccoongang.core.BuildConfig +import com.raccoongang.core.extension.isEmailValid +import com.raccoongang.core.extension.replaceLinkTags import com.raccoongang.core.ui.* import com.raccoongang.core.ui.theme.NewEdxTheme import com.raccoongang.core.ui.theme.appColors import com.raccoongang.core.ui.theme.appTypography +import com.raccoongang.core.utils.EmailUtil import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import java.nio.charset.StandardCharsets @@ -192,6 +196,7 @@ private fun WebContentScreen( @SuppressLint("SetJavaScriptEnabled") private fun HandoutsContent(body: String, onWebPageLoaded: () -> Unit) { val context = LocalContext.current + val isDarkTheme = isSystemInDarkTheme() AndroidView(modifier = Modifier, factory = { WebView(context).apply { webViewClient = object : WebViewClient() { @@ -211,6 +216,14 @@ private fun HandoutsContent(body: String, onWebPageLoaded: () -> Unit) { ) { context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(clickUrl))) true + } else if (clickUrl.startsWith("mailto:")) { + val email = clickUrl.replace("mailto:", "") + if (email.isEmailValid()) { + EmailUtil.sendEmailIntent(context, email, "", "") + true + } else { + false + } } else { false } @@ -227,12 +240,20 @@ private fun HandoutsContent(body: String, onWebPageLoaded: () -> Unit) { isVerticalScrollBarEnabled = false isHorizontalScrollBarEnabled = false loadDataWithBaseURL( - BuildConfig.BASE_URL, body, "text/html", StandardCharsets.UTF_8.name(), null + BuildConfig.BASE_URL, + body.replaceLinkTags(isDarkTheme), + "text/html", + StandardCharsets.UTF_8.name(), + null ) } }, update = { it.loadDataWithBaseURL( - BuildConfig.BASE_URL, body, "text/html", StandardCharsets.UTF_8.name(), null + BuildConfig.BASE_URL, + body.replaceLinkTags(isDarkTheme), + "text/html", + StandardCharsets.UTF_8.name(), + null ) }) } diff --git a/course/src/main/java/com/raccoongang/course/presentation/unit/html/HtmlUnitFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/unit/html/HtmlUnitFragment.kt index 9400a908c..ac8f4011e 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/unit/html/HtmlUnitFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/unit/html/HtmlUnitFragment.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.zIndex import androidx.core.os.bundleOf import androidx.fragment.app.Fragment +import com.raccoongang.core.extension.isEmailValid import com.raccoongang.core.system.AppCookieManager import com.raccoongang.core.system.connection.NetworkConnection import com.raccoongang.core.ui.WindowSize @@ -32,6 +33,7 @@ import com.raccoongang.core.ui.theme.NewEdxTheme import com.raccoongang.core.ui.theme.appColors import com.raccoongang.core.ui.theme.appShapes import com.raccoongang.core.ui.windowSizeValue +import com.raccoongang.core.utils.EmailUtil import com.raccoongang.course.presentation.ui.ConnectionErrorView import kotlinx.coroutines.launch import org.koin.android.ext.android.inject @@ -174,6 +176,14 @@ private fun HTMLContentView( ) { context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(clickUrl))) true + } else if (clickUrl.startsWith("mailto:")) { + val email = clickUrl.replace("mailto:", "") + if (email.isEmailValid()) { + EmailUtil.sendEmailIntent(context, email, "", "") + true + } else { + false + } } else { false } diff --git a/course/src/main/res/drawable/course_ic_announcements.xml b/course/src/main/res/drawable/course_ic_announcements.xml new file mode 100644 index 000000000..99dcb4bcf --- /dev/null +++ b/course/src/main/res/drawable/course_ic_announcements.xml @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/course/src/main/res/drawable/course_ic_handouts.xml b/course/src/main/res/drawable/course_ic_handouts.xml new file mode 100644 index 000000000..3aa877674 --- /dev/null +++ b/course/src/main/res/drawable/course_ic_handouts.xml @@ -0,0 +1,45 @@ + + + + + + + + + + diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsViewModel.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsViewModel.kt index 086d5613e..49addb963 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsViewModel.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsViewModel.kt @@ -55,7 +55,7 @@ class DiscussionThreadsViewModel( notifier.notifier.collect { if (it is DiscussionThreadAdded) { if (lastOrderBy.isNotEmpty()) { - getThreadByType(lastOrderBy) + updateThread(lastOrderBy) } } else if (it is DiscussionThreadDataChanged) { val index = threadsList.indexOfFirst { thread -> diff --git a/profile/src/main/java/com/raccoongang/profile/presentation/edit/EditProfileFragment.kt b/profile/src/main/java/com/raccoongang/profile/presentation/edit/EditProfileFragment.kt index 30fa34734..93ad7bdc5 100644 --- a/profile/src/main/java/com/raccoongang/profile/presentation/edit/EditProfileFragment.kt +++ b/profile/src/main/java/com/raccoongang/profile/presentation/edit/EditProfileFragment.kt @@ -293,7 +293,7 @@ private fun EditProfileScreen( var expandedList by rememberSaveable { mutableStateOf(emptyList()) } - var openDialog by rememberSaveable { + var openWarningMessageDialog by rememberSaveable { mutableStateOf(false) } @@ -539,6 +539,9 @@ private fun EditProfileScreen( .noRippleClickable { isOpenChangeImageDialogState = true + if (!uiState.account.isOlderThanMinAge()) { + openWarningMessageDialog = true + } } ) Icon( @@ -570,7 +573,7 @@ private fun EditProfileScreen( mapFields[ACCOUNT_PRIVACY] = privacy onLimitedProfileChange(!uiState.isLimited) } else { - openDialog = true + openWarningMessageDialog = true } }, text = stringResource(if (uiState.isLimited) profileR.string.profile_switch_to_full else profileR.string.profile_switch_to_limited), @@ -629,11 +632,11 @@ private fun EditProfileScreen( }) Spacer(Modifier.height(52.dp)) } - if (openDialog) { + if (openWarningMessageDialog) { LimitedProfileDialog( modifier = popUpModifier ) { - openDialog = false + openWarningMessageDialog = false } } } From 55c490fa420e2da56273e209c84c540aae1dc529 Mon Sep 17 00:00:00 2001 From: Serhii <128455389+hryh27@users.noreply.github.com> Date: Fri, 7 Apr 2023 12:02:31 +0300 Subject: [PATCH 14/20] subtitles for videos (#12) --- .../com/raccoongang/newedx/di/AppModule.kt | 3 + .../com/raccoongang/newedx/di/ScreenModule.kt | 2 +- core/libs/subtitleConvert-1.0.2.jar | Bin 0 -> 49359 bytes .../core/module/TranscriptManager.kt | 128 ++++++++++++++++++ .../module/download/AbstractDownloader.kt | 79 +++++++++++ .../core/module/download/FileDownloader.kt | 73 +--------- .../raccoongang/core/ui/ComposeExtensions.kt | 18 +++ .../com/raccoongang/core/utils/FileUtil.kt | 21 +++ .../com/raccoongang/core/utils/IOUtils.kt | 21 +++ .../course/presentation/ui/CourseUI.kt | 57 +++++++- .../container/CourseUnitContainerFragment.kt | 3 + .../unit/video/VideoUnitFragment.kt | 56 +++++++- .../unit/video/VideoUnitViewModel.kt | 49 ++++++- .../video/YoutubeVideoFullScreenFragment.kt | 2 +- .../unit/video/YoutubeVideoUnitFragment.kt | 39 +++++- .../fragment_video_unit.xml | 13 ++ .../fragment_youtube_video_unit.xml | 13 ++ .../main/res/layout/fragment_video_unit.xml | 13 ++ .../layout/fragment_youtube_video_unit.xml | 17 ++- course/src/main/res/values-uk/strings.xml | 2 +- course/src/main/res/values/strings.xml | 2 +- 21 files changed, 516 insertions(+), 95 deletions(-) create mode 100644 core/libs/subtitleConvert-1.0.2.jar create mode 100644 core/src/main/java/com/raccoongang/core/module/TranscriptManager.kt create mode 100644 core/src/main/java/com/raccoongang/core/module/download/AbstractDownloader.kt create mode 100644 core/src/main/java/com/raccoongang/core/utils/FileUtil.kt create mode 100644 core/src/main/java/com/raccoongang/core/utils/IOUtils.kt diff --git a/app/src/main/java/com/raccoongang/newedx/di/AppModule.kt b/app/src/main/java/com/raccoongang/newedx/di/AppModule.kt index 0df1ed077..7dc0a89e5 100644 --- a/app/src/main/java/com/raccoongang/newedx/di/AppModule.kt +++ b/app/src/main/java/com/raccoongang/newedx/di/AppModule.kt @@ -7,6 +7,7 @@ import com.google.gson.GsonBuilder import com.raccoongang.auth.presentation.AuthRouter import com.raccoongang.core.data.storage.PreferencesManager import com.raccoongang.core.module.DownloadWorkerController +import com.raccoongang.core.module.TranscriptManager import com.raccoongang.core.module.download.FileDownloader import com.raccoongang.core.system.AppCookieManager import com.raccoongang.core.system.ResourceManager @@ -99,4 +100,6 @@ val appModule = module { single { DownloadWorkerController(get(), get(), get()) } + + single { TranscriptManager(get()) } } \ No newline at end of file diff --git a/app/src/main/java/com/raccoongang/newedx/di/ScreenModule.kt b/app/src/main/java/com/raccoongang/newedx/di/ScreenModule.kt index 506e5a522..b31891643 100644 --- a/app/src/main/java/com/raccoongang/newedx/di/ScreenModule.kt +++ b/app/src/main/java/com/raccoongang/newedx/di/ScreenModule.kt @@ -84,7 +84,7 @@ val screenModule = module { viewModel { (courseId: String) -> CourseUnitContainerViewModel(get(), get(), courseId) } viewModel { (courseId: String) -> CourseVideoViewModel(courseId, get(), get(), get(), get(), get(), get(), get()) } viewModel { (courseId: String) -> VideoViewModel(courseId, get(), get(), get()) } - viewModel { (courseId: String) -> VideoUnitViewModel(courseId, get(), get(), get(), get()) } + viewModel { (courseId: String) -> VideoUnitViewModel(courseId, get(), get(), get(), get(), get()) } viewModel { (courseId:String, handoutsType: String) -> HandoutsViewModel(courseId, handoutsType, get()) } viewModel { CourseSearchViewModel(get(), get()) } diff --git a/core/libs/subtitleConvert-1.0.2.jar b/core/libs/subtitleConvert-1.0.2.jar new file mode 100644 index 0000000000000000000000000000000000000000..0f4d3afa92728397988abefb6478c6fe1ae21313 GIT binary patch literal 49359 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 + val buffer = ByteArray(4 * 1024) + var read: Int + while (input!!.read(buffer).also { read = it } != -1) { + output.write(buffer, 0, read) + } + output.flush() + } + true + } else { + false + } + } catch (e: Exception) { + e.printStackTrace() + false + } finally { + input?.close() + } + } + + + suspend fun cancelDownloading() { + isCanceled = true + withContext(Dispatchers.IO) { + input?.close() + } + currentDownloadingFilePath?.let { + val file = File(it) + if (file.exists()) { + file.delete() + } + } + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/raccoongang/core/module/download/FileDownloader.kt b/core/src/main/java/com/raccoongang/core/module/download/FileDownloader.kt index 17d197f31..d77fe3fa0 100644 --- a/core/src/main/java/com/raccoongang/core/module/download/FileDownloader.kt +++ b/core/src/main/java/com/raccoongang/core/module/download/FileDownloader.kt @@ -1,25 +1,19 @@ package com.raccoongang.core.module.download import android.util.Log -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Response import okhttp3.ResponseBody -import retrofit2.Retrofit import retrofit2.http.GET import retrofit2.http.Streaming import retrofit2.http.Url -import java.io.File -import java.io.FileOutputStream -import java.io.InputStream -class FileDownloader : ProgressListener { +class FileDownloader : AbstractDownloader(), ProgressListener { private var firstUpdate = true - private val client: OkHttpClient = OkHttpClient.Builder() + override val client: OkHttpClient = OkHttpClient.Builder() .addNetworkInterceptor(Interceptor { chain: Interceptor.Chain -> val originalResponse: Response = chain.proceed(chain.request()) originalResponse.newBuilder() @@ -27,71 +21,8 @@ class FileDownloader : ProgressListener { .build() }) .build() - - private val downloadApi = Retrofit.Builder() - .baseUrl(com.raccoongang.core.BuildConfig.BASE_URL) - .client(client) - .build() - .create(DownloadApi::class.java) - - private var currentDownloadingFilePath: String? = null - var progressListener: CurrentProgress? = null - var isCanceled = false - - private var input: InputStream? = null - - suspend fun download( - url: String, - path: String - ): Boolean { - isCanceled = false - return try { - val response = downloadApi.downloadFile(url).body() - if (response != null) { - val file = File(path) - if (file.exists()) { - file.delete() - } - file.createNewFile() - input = response.byteStream() - currentDownloadingFilePath = path - val fos = FileOutputStream(file) - fos.use { output -> - val buffer = ByteArray(4 * 1024) - var read: Int - while (input!!.read(buffer).also { read = it } != -1) { - output.write(buffer, 0, read) - } - output.flush() - } - true - } else { - false - } - } catch (e: Exception) { - e.printStackTrace() - false - } finally { - input?.close() - } - } - - - suspend fun cancelDownloading() { - isCanceled = true - withContext(Dispatchers.IO) { - input?.close() - } - currentDownloadingFilePath?.let { - val file = File(it) - if (file.exists()) { - file.delete() - } - } - } - override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { if (done) { Log.d("DownloadProgress", "done") diff --git a/core/src/main/java/com/raccoongang/core/ui/ComposeExtensions.kt b/core/src/main/java/com/raccoongang/core/ui/ComposeExtensions.kt index 9926a09f2..be94fea6c 100644 --- a/core/src/main/java/com/raccoongang/core/ui/ComposeExtensions.kt +++ b/core/src/main/java/com/raccoongang/core/ui/ComposeExtensions.kt @@ -1,5 +1,6 @@ package com.raccoongang.core.ui +import androidx.compose.foundation.MutatePriority import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.padding @@ -13,6 +14,9 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode import com.raccoongang.core.presentation.global.InsetHolder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.launch inline val isPreview: Boolean @ReadOnlyComposable @@ -76,3 +80,17 @@ fun rememberSaveableMap(init: () -> MutableMap): MutableMa } } +fun LazyListState.disableScrolling(scope: CoroutineScope) { + scope.launch { + scroll(scrollPriority = MutatePriority.PreventUserInput) { + awaitCancellation() + } + } +} + +fun LazyListState.reEnableScrolling(scope: CoroutineScope) { + scope.launch { + scroll(scrollPriority = MutatePriority.PreventUserInput) {} + } +} + diff --git a/core/src/main/java/com/raccoongang/core/utils/FileUtil.kt b/core/src/main/java/com/raccoongang/core/utils/FileUtil.kt new file mode 100644 index 000000000..7f8b83dad --- /dev/null +++ b/core/src/main/java/com/raccoongang/core/utils/FileUtil.kt @@ -0,0 +1,21 @@ +package com.raccoongang.core.utils + +import android.content.Context +import java.io.File + +object FileUtil { + + fun getExternalAppDir(context: Context): File { + val dir = context.externalCacheDir.toString() + File.separator + + context.getString(com.raccoongang.core.R.string.app_name).replace(Regex("\\s"), "_") + val file = File(dir) + file.mkdirs() + return file + } + + +} + +enum class Directories { + VIDEOS, SUBTITLES +} diff --git a/core/src/main/java/com/raccoongang/core/utils/IOUtils.kt b/core/src/main/java/com/raccoongang/core/utils/IOUtils.kt new file mode 100644 index 000000000..33e660b79 --- /dev/null +++ b/core/src/main/java/com/raccoongang/core/utils/IOUtils.kt @@ -0,0 +1,21 @@ +package com.raccoongang.core.utils + +import okio.buffer +import okio.sink +import okio.source +import java.io.File +import java.io.InputStream +import java.io.OutputStream +import java.nio.charset.Charset + +object IOUtils { + + fun toString(file: File, charset: Charset): String { + return file.source().buffer().readString(charset) + } + + fun copy(input: InputStream, out: OutputStream) { + out.sink().buffer().writeAll(input.source()) + } + +} diff --git a/course/src/main/java/com/raccoongang/course/presentation/ui/CourseUI.kt b/course/src/main/java/com/raccoongang/course/presentation/ui/CourseUI.kt index bb7e4d027..6c141aab8 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/ui/CourseUI.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/ui/CourseUI.kt @@ -5,13 +5,13 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -40,7 +40,10 @@ import com.raccoongang.core.ui.theme.appColors import com.raccoongang.core.ui.theme.appShapes import com.raccoongang.core.ui.theme.appTypography import com.raccoongang.course.R +import org.jsoup.Jsoup +import subtitleFile.TimedTextObject import java.util.* +import com.raccoongang.course.R as courseR @Composable fun CourseImageHeader( @@ -419,6 +422,52 @@ fun ConnectionErrorView( } } +@Composable +fun VideoSubtitles( + listState: LazyListState, + timedTextObject: TimedTextObject?, + currentIndex: Int +) { + timedTextObject?.let { + LaunchedEffect(key1 = currentIndex) { + if (currentIndex > 1) { + listState.animateScrollToItem(currentIndex - 1) + } + } + val scaffoldState = rememberScaffoldState() + val subtitles = timedTextObject.captions.values.toList() + Scaffold(scaffoldState = scaffoldState) { + Column(Modifier.padding(it)) { + Text( + text = stringResource(id = courseR.string.course_subtitles), + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.titleMedium + ) + Spacer(Modifier.height(24.dp)) + LazyColumn( + state = listState, + userScrollEnabled = false + ) { + itemsIndexed(subtitles) { index, item -> + val textColor = + if (currentIndex == index) { + MaterialTheme.appColors.textPrimary + } else { + MaterialTheme.appColors.textFieldBorder + } + Text( + modifier = Modifier.fillMaxWidth(), + text = Jsoup.parse(item.content).text(), + color = textColor, + style = MaterialTheme.appTypography.bodyMedium + ) + Spacer(Modifier.height(16.dp)) + } + } + } + } + } +} @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) diff --git a/course/src/main/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerFragment.kt index fa7482736..38601e60d 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerFragment.kt @@ -177,6 +177,7 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta } BlockType.VIDEO -> { val encodedVideos = block.studentViewData!!.encodedVideos!! + val transcripts = block.studentViewData!!.transcripts with(encodedVideos) { var isDownloaded = false val videoUrl = if (viewModel.getDownloadModelById(block.id) != null) { @@ -200,6 +201,7 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta block.id, viewModel.courseId, videoUrl, + transcripts?.en, block.displayName, isDownloaded ) @@ -208,6 +210,7 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta block.id, viewModel.courseId, encodedVideos.youtube?.url!!, + transcripts?.en, block.displayName ) } diff --git a/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitFragment.kt index 112675da5..831a19db7 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitFragment.kt @@ -2,12 +2,18 @@ package com.raccoongang.course.presentation.unit.video import android.graphics.Point import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.view.OrientationEventListener import android.view.View import android.widget.FrameLayout import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier import androidx.core.os.bundleOf import androidx.core.view.isVisible @@ -26,6 +32,7 @@ import com.raccoongang.course.databinding.FragmentVideoUnitBinding import com.raccoongang.course.presentation.CourseRouter import com.raccoongang.course.presentation.ui.ConnectionErrorView import com.raccoongang.course.presentation.ui.VideoRotateView +import com.raccoongang.course.presentation.ui.VideoSubtitles import com.raccoongang.course.presentation.ui.VideoTitle import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel @@ -46,13 +53,31 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { private var orientationListener: OrientationEventListener? = null private var blockId = "" + private val handler = Handler(Looper.getMainLooper()) + private var videoTimeRunnable: Runnable = object : Runnable { + override fun run() { + exoPlayer?.let { + if (it.isPlaying) { + viewModel.setCurrentVideoTime(it.currentPosition) + } + } + handler.postDelayed(this, 200) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) windowSize = computeWindowSizeClasses() lifecycle.addObserver(viewModel) - viewModel.videoUrl = requireArguments().getString(ARG_VIDEO_URL, "") - viewModel.isDownloaded = requireArguments().getBoolean(ARG_DOWNLOADED) - blockId = requireArguments().getString(ARG_BLOCK_ID, "") + handler.post(videoTimeRunnable) + requireArguments().apply { + viewModel.videoUrl = getString(ARG_VIDEO_URL, "") + viewModel.transcriptUrl = getString(ARG_TRANSCRIPT_URL, "") + viewModel.isDownloaded = getBoolean(ARG_DOWNLOADED) + blockId = getString(ARG_BLOCK_ID, "") + } + viewModel.downloadSubtitles() orientationListener = object : OrientationEventListener(requireActivity()) { override fun onOrientationChanged(orientation: Int) { if (windowSize?.isTablet != true) { @@ -61,7 +86,7 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { router.navigateToFullScreenVideo( requireActivity().supportFragmentManager, viewModel.videoUrl, - exoPlayer?.currentPosition ?: viewModel.currentVideoTime, + exoPlayer?.currentPosition ?: viewModel.getCurrentVideoTime(), blockId, viewModel.courseId ) @@ -107,6 +132,19 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { } } + binding.subtitles.setContent { + NewEdxTheme { + val state = rememberLazyListState() + val currentIndex by viewModel.currentIndex.collectAsState(0) + val transcriptObject by viewModel.transcriptObject.observeAsState() + VideoSubtitles( + listState = state, + timedTextObject = transcriptObject, + currentIndex = currentIndex + ) + } + } + binding.connectionError.isVisible = !viewModel.hasInternetConnection && !viewModel.isDownloaded val display = requireActivity().windowManager.defaultDisplay @@ -148,7 +186,7 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { playerView.setShowNextButton(false) playerView.setShowPreviousButton(false) val mediaItem = MediaItem.fromUri(viewModel.videoUrl) - exoPlayer?.setMediaItem(mediaItem, viewModel.currentVideoTime) + exoPlayer?.setMediaItem(mediaItem, viewModel.getCurrentVideoTime()) exoPlayer?.prepare() exoPlayer?.playWhenReady = !(viewModel.isVideoPaused.value ?: false) @@ -193,9 +231,15 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { super.onDestroyView() } + override fun onDestroy() { + handler.removeCallbacks(videoTimeRunnable) + super.onDestroy() + } + companion object { private const val ARG_BLOCK_ID = "blockId" private const val ARG_VIDEO_URL = "videoUrl" + private const val ARG_TRANSCRIPT_URL = "transcriptUrl" private const val ARG_COURSE_ID = "courseId" private const val ARG_TITLE = "title" private const val ARG_DOWNLOADED = "isDownloaded" @@ -203,6 +247,7 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { blockId: String, courseId: String, videoUrl: String, + transcriptUrl: String?, title: String, isDownloaded: Boolean ): VideoUnitFragment { @@ -211,6 +256,7 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { ARG_BLOCK_ID to blockId, ARG_COURSE_ID to courseId, ARG_VIDEO_URL to videoUrl, + ARG_TRANSCRIPT_URL to transcriptUrl, ARG_TITLE to title, ARG_DOWNLOADED to isDownloaded ) diff --git a/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitViewModel.kt b/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitViewModel.kt index ed98a2304..13692b54b 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitViewModel.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitViewModel.kt @@ -6,24 +6,33 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.raccoongang.core.BaseViewModel import com.raccoongang.core.data.storage.PreferencesManager +import com.raccoongang.core.module.TranscriptManager import com.raccoongang.core.system.connection.NetworkConnection import com.raccoongang.core.system.notifier.CourseNotifier import com.raccoongang.core.system.notifier.CoursePauseVideo import com.raccoongang.core.system.notifier.CourseVideoPositionChanged import com.raccoongang.course.data.repository.CourseRepository import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import subtitleFile.TimedTextObject class VideoUnitViewModel( val courseId: String, private val courseRepository: CourseRepository, private val preferencesManager: PreferencesManager, private val notifier: CourseNotifier, - private val networkConnection: NetworkConnection + private val networkConnection: NetworkConnection, + private val transcriptManager: TranscriptManager ) : BaseViewModel() { var videoUrl = "" - var currentVideoTime = 0L + var transcriptUrl = "" + + private val _currentVideoTime = MutableLiveData(0) + val currentVideoTime: LiveData + get() = _currentVideoTime var fullscreenHandled = false @@ -41,6 +50,15 @@ class VideoUnitViewModel( val isVideoPaused: LiveData get() = _isVideoPaused + private val _currentIndex = MutableStateFlow(0) + val currentIndex = _currentIndex.asStateFlow() + + private val _transcriptObject = MutableLiveData() + val transcriptObject: LiveData + get() = _transcriptObject + + private var timeList: List? = emptyList() + val hasInternetConnection: Boolean get() = networkConnection.isOnline() @@ -57,7 +75,7 @@ class VideoUnitViewModel( notifier.notifier.collect { if (it is CourseVideoPositionChanged && videoUrl == it.videoUrl) { _isUpdated.value = false - currentVideoTime = it.videoTime + _currentVideoTime.value = it.videoTime _isUpdated.value = true } else if (it is CoursePauseVideo) { _isVideoPaused.value = true @@ -66,6 +84,16 @@ class VideoUnitViewModel( } } + fun downloadSubtitles() { + viewModelScope.launch { + transcriptManager.downloadTranscriptsForVideo(transcriptUrl)?.let { result -> + _transcriptObject.value = result + timeList = result.captions.values.toList() + .map { it.start.mseconds.toLong() } + } + } + } + fun markBlockCompleted(blockId: String) { viewModelScope.launch { try { @@ -78,4 +106,19 @@ class VideoUnitViewModel( } } } + + fun setCurrentVideoTime(value: Long) { + _currentVideoTime.value = value + timeList?.let { + val index = it.indexOfLast { subtitleTime -> + subtitleTime < value + } + if (index != currentIndex.value) { + _currentIndex.value = index + } + } + } + + fun getCurrentVideoTime() = currentVideoTime.value ?: 0 + } \ No newline at end of file diff --git a/course/src/main/java/com/raccoongang/course/presentation/unit/video/YoutubeVideoFullScreenFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/unit/video/YoutubeVideoFullScreenFragment.kt index a71b5258e..e73ac53f2 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/unit/video/YoutubeVideoFullScreenFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/unit/video/YoutubeVideoFullScreenFragment.kt @@ -90,7 +90,7 @@ class YoutubeVideoFullScreenFragment : Fragment(R.layout.fragment_youtube_video_ override fun onCurrentSecond(youTubePlayer: YouTubePlayer, second: Float) { super.onCurrentSecond(youTubePlayer, second) - viewModel.currentVideoTime = second.toLong() + viewModel.currentVideoTime = (second * 1000f).toLong() } override fun onReady(youTubePlayer: YouTubePlayer) { diff --git a/course/src/main/java/com/raccoongang/course/presentation/unit/video/YoutubeVideoUnitFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/unit/video/YoutubeVideoUnitFragment.kt index 84f611d39..48289a82d 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/unit/video/YoutubeVideoUnitFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/unit/video/YoutubeVideoUnitFragment.kt @@ -5,7 +5,11 @@ import android.view.OrientationEventListener import android.view.View import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier import androidx.core.os.bundleOf import androidx.core.view.isVisible @@ -25,6 +29,7 @@ import com.raccoongang.course.databinding.FragmentYoutubeVideoUnitBinding import com.raccoongang.course.presentation.CourseRouter import com.raccoongang.course.presentation.ui.ConnectionErrorView import com.raccoongang.course.presentation.ui.VideoRotateView +import com.raccoongang.course.presentation.ui.VideoSubtitles import com.raccoongang.course.presentation.ui.VideoTitle import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel @@ -48,8 +53,12 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) super.onCreate(savedInstanceState) windowSize = computeWindowSizeClasses() lifecycle.addObserver(viewModel) - viewModel.videoUrl = requireArguments().getString(ARG_VIDEO_URL, "") - blockId = requireArguments().getString(ARG_BLOCK_ID, "") + requireArguments().apply { + viewModel.videoUrl = getString(ARG_VIDEO_URL, "") + viewModel.transcriptUrl = getString(ARG_TRANSCRIPT_URL, "") + blockId = getString(ARG_BLOCK_ID, "") + } + viewModel.downloadSubtitles() orientationListener = object : OrientationEventListener(requireActivity()) { override fun onOrientationChanged(orientation: Int) { if (windowSize?.isTablet != true) { @@ -58,7 +67,7 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) router.navigateToFullScreenYoutubeVideo( requireActivity().supportFragmentManager, viewModel.videoUrl, - viewModel.currentVideoTime, + viewModel.getCurrentVideoTime(), blockId, viewModel.courseId ) @@ -103,6 +112,19 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) } } + binding.subtitles.setContent { + NewEdxTheme { + val state = rememberLazyListState() + val currentIndex by viewModel.currentIndex.collectAsState(0) + val transcriptObject by viewModel.transcriptObject.observeAsState() + VideoSubtitles( + listState = state, + timedTextObject = transcriptObject, + currentIndex = currentIndex + ) + } + } + binding.connectionError.isVisible = !viewModel.hasInternetConnection lifecycle.addObserver(binding.youtubePlayerView) @@ -125,7 +147,7 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) override fun onCurrentSecond(youTubePlayer: YouTubePlayer, second: Float) { super.onCurrentSecond(youTubePlayer, second) - viewModel.currentVideoTime = second.toLong() + viewModel.setCurrentVideoTime((second * 1000f).toLong()) } override fun onReady(youTubePlayer: YouTubePlayer) { @@ -140,7 +162,7 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) router.navigateToFullScreenYoutubeVideo( requireActivity().supportFragmentManager, viewModel.videoUrl, - viewModel.currentVideoTime, + viewModel.getCurrentVideoTime(), blockId, viewModel.courseId ) @@ -148,7 +170,7 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) binding.youtubePlayerView.setCustomPlayerUi(defPlayerUiController.rootView) val videoId = viewModel.videoUrl.split("watch?v=")[1] - youTubePlayer.loadVideo(videoId, viewModel.currentVideoTime.toFloat()) + youTubePlayer.loadVideo(videoId, viewModel.getCurrentVideoTime().toFloat()) if (viewModel.isVideoPaused.value == true) { youTubePlayer.pause() } @@ -177,19 +199,24 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) } companion object { + private const val ARG_VIDEO_URL = "videoUrl" + private const val ARG_TRANSCRIPT_URL = "transcriptUrl" private const val ARG_BLOCK_ID = "blockId" private const val ARG_COURSE_ID = "courseId" private const val ARG_TITLE = "blockTitle" + fun newInstance( blockId: String, courseId: String, videoUrl: String, + transcriptUrl: String?, blockTitle: String ): YoutubeVideoUnitFragment { val fragment = YoutubeVideoUnitFragment() fragment.arguments = bundleOf( ARG_VIDEO_URL to videoUrl, + ARG_TRANSCRIPT_URL to transcriptUrl, ARG_BLOCK_ID to blockId, ARG_COURSE_ID to courseId, ARG_TITLE to blockTitle diff --git a/course/src/main/res/layout-w600dp-h480dp/fragment_video_unit.xml b/course/src/main/res/layout-w600dp-h480dp/fragment_video_unit.xml index 2d8c6c2ff..f39691bb9 100644 --- a/course/src/main/res/layout-w600dp-h480dp/fragment_video_unit.xml +++ b/course/src/main/res/layout-w600dp-h480dp/fragment_video_unit.xml @@ -44,6 +44,19 @@ + + + + + + + + + android:layout_height="match_parent" + android:visibility="gone" /> \ No newline at end of file diff --git a/course/src/main/res/values-uk/strings.xml b/course/src/main/res/values-uk/strings.xml index 039a7e37b..0741f8b96 100644 --- a/course/src/main/res/values-uk/strings.xml +++ b/course/src/main/res/values-uk/strings.xml @@ -43,7 +43,7 @@ Ця інтерактивна компонента ще не доступна Досліджуйте інші частини цього курсу або перегляньте це на веб-сайті. Відкрити в браузері + Субтитри Остання активність: Продовжити - \ No newline at end of file diff --git a/course/src/main/res/values/strings.xml b/course/src/main/res/values/strings.xml index 304022056..c42b409cb 100644 --- a/course/src/main/res/values/strings.xml +++ b/course/src/main/res/values/strings.xml @@ -43,7 +43,7 @@ This interactive component isn’t yet available Explore other parts of this course or view this on web. Open in browser + Subtitles Continue with: Continue - \ No newline at end of file From 7805d02a53545a540a726448d8acd6975411b81c Mon Sep 17 00:00:00 2001 From: Serhii <128455389+hryh27@users.noreply.github.com> Date: Tue, 11 Apr 2023 13:43:31 +0300 Subject: [PATCH 15/20] change subtitles language (#14) * change subtitles language --- .../com/raccoongang/newedx/di/ScreenModule.kt | 2 + .../com/raccoongang/core/data/model/Block.kt | 13 +-- .../core/data/model/room/BlockDb.kt | 21 +--- .../raccoongang/core/domain/model/Block.kt | 8 +- .../raccoongang/core/extension/BundleExt.kt | 20 +++- .../core/module/TranscriptManager.kt | 8 +- .../dialog/SelectBottomDialogFragment.kt | 105 ++++++++++++++++++ .../dialog/SelectDialogViewModel.kt | 22 ++++ .../core/system/notifier/CourseNotifier.kt | 1 + .../notifier/CourseSubtitleLanguageChanged.kt | 5 + .../com/raccoongang/core/utils/LocaleUtils.kt | 11 +- core/src/main/res/values/values.xml | 10 ++ .../course/data/storage/CourseConverter.kt | 12 ++ .../detail/CourseDetailsFragment.kt | 10 +- .../course/presentation/ui/CourseUI.kt | 29 ++++- .../container/CourseUnitContainerFragment.kt | 4 +- .../unit/video/VideoUnitFragment.kt | 26 ++++- .../unit/video/VideoUnitViewModel.kt | 24 +++- .../unit/video/YoutubeVideoUnitFragment.kt | 26 ++++- course/src/main/res/drawable/course_ic_cc.xml | 31 ++++++ .../src/main/res/drawable/course_ic_play.xml | 12 ++ .../fragment_video_unit.xml | 2 +- .../fragment_youtube_video_unit.xml | 2 +- .../comments/DiscussionCommentsFragment.kt | 10 +- .../comments/DiscussionCommentsUIState.kt | 3 +- .../comments/DiscussionCommentsViewModel.kt | 42 +++++-- 26 files changed, 373 insertions(+), 86 deletions(-) create mode 100644 core/src/main/java/com/raccoongang/core/presentation/dialog/SelectBottomDialogFragment.kt create mode 100644 core/src/main/java/com/raccoongang/core/presentation/dialog/SelectDialogViewModel.kt create mode 100644 core/src/main/java/com/raccoongang/core/system/notifier/CourseSubtitleLanguageChanged.kt create mode 100644 core/src/main/res/values/values.xml create mode 100644 course/src/main/res/drawable/course_ic_cc.xml create mode 100644 course/src/main/res/drawable/course_ic_play.xml diff --git a/app/src/main/java/com/raccoongang/newedx/di/ScreenModule.kt b/app/src/main/java/com/raccoongang/newedx/di/ScreenModule.kt index b31891643..924880a76 100644 --- a/app/src/main/java/com/raccoongang/newedx/di/ScreenModule.kt +++ b/app/src/main/java/com/raccoongang/newedx/di/ScreenModule.kt @@ -7,6 +7,7 @@ import com.raccoongang.auth.presentation.signin.SignInViewModel import com.raccoongang.auth.presentation.signup.SignUpViewModel import com.raccoongang.core.Validator import com.raccoongang.core.domain.model.Account +import com.raccoongang.core.presentation.dialog.SelectDialogViewModel import com.raccoongang.course.data.repository.CourseRepository import com.raccoongang.course.domain.interactor.CourseInteractor import com.raccoongang.course.presentation.container.CourseContainerViewModel @@ -87,6 +88,7 @@ val screenModule = module { viewModel { (courseId: String) -> VideoUnitViewModel(courseId, get(), get(), get(), get(), get()) } viewModel { (courseId:String, handoutsType: String) -> HandoutsViewModel(courseId, handoutsType, get()) } viewModel { CourseSearchViewModel(get(), get()) } + viewModel { SelectDialogViewModel(get()) } single { DiscussionRepository(get()) } factory { DiscussionInteractor(get()) } diff --git a/core/src/main/java/com/raccoongang/core/data/model/Block.kt b/core/src/main/java/com/raccoongang/core/data/model/Block.kt index 06a2c29d4..fac04a00b 100644 --- a/core/src/main/java/com/raccoongang/core/data/model/Block.kt +++ b/core/src/main/java/com/raccoongang/core/data/model/Block.kt @@ -57,7 +57,7 @@ data class StudentViewData( @SerializedName("duration") var duration: Any?, @SerializedName("transcripts") - var transcripts: Transcripts?, + var transcripts: HashMap?, @SerializedName("encoded_videos") var encodedVideos: EncodedVideos?, @SerializedName("all_sources") @@ -69,22 +69,13 @@ data class StudentViewData( return com.raccoongang.core.domain.model.StudentViewData( onlyOnWeb = onlyOnWeb ?: false, duration = duration ?: "", - transcripts = transcripts?.mapToDomain(), + transcripts = transcripts, encodedVideos = encodedVideos?.mapToDomain(), topicId = topicId ?: "" ) } } -data class Transcripts( - @SerializedName("en") - var en: String? -) { - fun mapToDomain(): com.raccoongang.core.domain.model.Transcripts { - return com.raccoongang.core.domain.model.Transcripts(en = en ?: "") - } -} - data class EncodedVideos( @SerializedName("youtube") var videoInfo: VideoInfo?, diff --git a/core/src/main/java/com/raccoongang/core/data/model/room/BlockDb.kt b/core/src/main/java/com/raccoongang/core/data/model/room/BlockDb.kt index 6286ec4c8..00aa62d93 100644 --- a/core/src/main/java/com/raccoongang/core/data/model/room/BlockDb.kt +++ b/core/src/main/java/com/raccoongang/core/data/model/room/BlockDb.kt @@ -85,7 +85,7 @@ data class StudentViewDataDb( @ColumnInfo("topicId") val topicId: String, @Embedded - val transcripts: TranscriptsDb?, + val transcripts: HashMap?, @Embedded val encodedVideos: EncodedVideosDb? ) { @@ -93,7 +93,7 @@ data class StudentViewDataDb( return StudentViewData( onlyOnWeb, duration, - transcripts?.mapToDomain(), + transcripts, encodedVideos?.mapToDomain(), topicId ) @@ -105,7 +105,7 @@ data class StudentViewDataDb( return StudentViewDataDb( onlyOnWeb = studentViewData?.onlyOnWeb ?: false, duration = studentViewData?.duration.toString(), - transcripts = TranscriptsDb.createFrom(studentViewData?.transcripts), + transcripts = studentViewData?.transcripts, encodedVideos = EncodedVideosDb.createFrom(studentViewData?.encodedVideos), topicId = studentViewData?.topicId ?: "" ) @@ -114,19 +114,6 @@ data class StudentViewDataDb( } } -data class TranscriptsDb( - @ColumnInfo("en") - val en: String -) { - fun mapToDomain() = Transcripts(en) - - companion object { - fun createFrom(transcripts: com.raccoongang.core.data.model.Transcripts?): TranscriptsDb { - return TranscriptsDb(transcripts?.en ?: "") - } - } -} - data class EncodedVideosDb( @ColumnInfo("youtube") val youtube: VideoInfoDb?, @@ -197,4 +184,4 @@ data class BlockCountsDb( return BlockCountsDb(blocksCounts?.video ?: 0) } } -} \ No newline at end of file +} diff --git a/core/src/main/java/com/raccoongang/core/domain/model/Block.kt b/core/src/main/java/com/raccoongang/core/domain/model/Block.kt index a36ef6d4b..501947f67 100644 --- a/core/src/main/java/com/raccoongang/core/domain/model/Block.kt +++ b/core/src/main/java/com/raccoongang/core/domain/model/Block.kt @@ -52,15 +52,11 @@ data class Block( data class StudentViewData( val onlyOnWeb: Boolean, val duration: Any, - val transcripts: Transcripts?, + val transcripts: HashMap?, val encodedVideos: EncodedVideos?, val topicId: String ) -data class Transcripts( - val en: String -) - data class EncodedVideos( val youtube: VideoInfo?, var hls: VideoInfo?, @@ -134,4 +130,4 @@ data class VideoInfo( data class BlockCounts( val video: Int -) \ No newline at end of file +) diff --git a/core/src/main/java/com/raccoongang/core/extension/BundleExt.kt b/core/src/main/java/com/raccoongang/core/extension/BundleExt.kt index 28c5aefc3..186c72c72 100644 --- a/core/src/main/java/com/raccoongang/core/extension/BundleExt.kt +++ b/core/src/main/java/com/raccoongang/core/extension/BundleExt.kt @@ -1,8 +1,11 @@ +@file:Suppress("NOTHING_TO_INLINE") + package com.raccoongang.core.extension import android.os.Build.VERSION.SDK_INT import android.os.Bundle import android.os.Parcelable +import com.google.gson.Gson import java.io.Serializable inline fun Bundle.parcelable(key: String): T? = when { @@ -13,4 +16,19 @@ inline fun Bundle.parcelable(key: String): T? = when { inline fun Bundle.serializable(key: String): T? = when { SDK_INT >= 33 -> getSerializable(key, T::class.java) else -> @Suppress("DEPRECATION") getSerializable(key) as? T -} \ No newline at end of file +} + +inline fun Bundle.parcelableArrayList(key: String): ArrayList? = when { + SDK_INT >= 33 -> getParcelableArrayList(key, T::class.java) + else -> @Suppress("DEPRECATION") getParcelableArrayList(key) +} + +inline fun objectToString(value: T): String = Gson().toJson(value) + +inline fun stringToObject(value: String): T? { + return try { + Gson().fromJson(value, genericType()) + } catch (e: Exception) { + null + } +} diff --git a/core/src/main/java/com/raccoongang/core/module/TranscriptManager.kt b/core/src/main/java/com/raccoongang/core/module/TranscriptManager.kt index 25272e07c..1e6d0d0e6 100644 --- a/core/src/main/java/com/raccoongang/core/module/TranscriptManager.kt +++ b/core/src/main/java/com/raccoongang/core/module/TranscriptManager.kt @@ -2,10 +2,7 @@ package com.raccoongang.core.module import android.content.Context import com.raccoongang.core.module.download.AbstractDownloader -import com.raccoongang.core.utils.Directories -import com.raccoongang.core.utils.FileUtil -import com.raccoongang.core.utils.IOUtils -import com.raccoongang.core.utils.Sha1Util +import com.raccoongang.core.utils.* import okhttp3.OkHttpClient import subtitleFile.FormatSRT import subtitleFile.TimedTextObject @@ -14,6 +11,7 @@ import java.io.FileInputStream import java.io.IOException import java.io.InputStream import java.nio.charset.Charset +import java.util.concurrent.TimeUnit class TranscriptManager( val context: Context @@ -30,7 +28,7 @@ class TranscriptManager( val transcriptDir = getTranscriptDir() ?: return false val hash = Sha1Util.SHA1(url) val file = File(transcriptDir, hash) - return file.exists() + return file.exists() && System.currentTimeMillis() - file.lastModified() < TimeUnit.HOURS.toMillis(12) } fun get(url: String): String? { diff --git a/core/src/main/java/com/raccoongang/core/presentation/dialog/SelectBottomDialogFragment.kt b/core/src/main/java/com/raccoongang/core/presentation/dialog/SelectBottomDialogFragment.kt new file mode 100644 index 000000000..7de379ad9 --- /dev/null +++ b/core/src/main/java/com/raccoongang/core/presentation/dialog/SelectBottomDialogFragment.kt @@ -0,0 +1,105 @@ +package com.raccoongang.core.presentation.dialog + +import android.content.res.Configuration +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.DialogFragment +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.raccoongang.core.R +import com.raccoongang.core.domain.model.RegistrationField +import com.raccoongang.core.extension.parcelableArrayList +import com.raccoongang.core.ui.SheetContent +import com.raccoongang.core.ui.px +import com.raccoongang.core.ui.rememberWindowSize +import com.raccoongang.core.ui.theme.BottomSheetShape +import com.raccoongang.core.ui.theme.NewEdxTheme +import com.raccoongang.core.ui.theme.appColors +import com.raccoongang.core.ui.windowSizeValue +import org.koin.androidx.viewmodel.ext.android.viewModel + +class SelectBottomDialogFragment() : BottomSheetDialogFragment() { + + private val viewModel by viewModel() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + viewModel.values = requireArguments().parcelableArrayList(ARG_LIST_VALUES)!! + setStyle(DialogFragment.STYLE_NORMAL, R.style.BottomSheetDialog) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = ComposeView(requireContext()).apply { + if (dialog != null && dialog!!.window != null) { + dialog!!.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + (dialog as? BottomSheetDialog)?.behavior?.apply { + state = BottomSheetBehavior.STATE_EXPANDED + } + } + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + NewEdxTheme { + val windowSize = rememberWindowSize() + val configuration = LocalConfiguration.current + val listState = rememberLazyListState() + val bottomSheetWeight by remember(key1 = windowSize) { + mutableStateOf( + windowSize.windowSizeValue( + expanded = if (configuration.orientation == Configuration.ORIENTATION_PORTRAIT) 0.8f else 0.6f, + compact = 1f + ) + ) + } + + Surface( + shape = BottomSheetShape( + width = configuration.screenWidthDp.px, + height = configuration.screenHeightDp.px, + weight = bottomSheetWeight + ), + color = MaterialTheme.appColors.background + ) { + SheetContent( + expandedList = viewModel.values, + onItemClick = { item -> + viewModel.sendCourseEventChanged(item.value) + dismiss() + }, + listState = listState + ) + } + } + } + } + + companion object { + private const val ARG_LIST_VALUES = "argListValues" + + fun newInstance( + values: List + ): SelectBottomDialogFragment { + val dialog = SelectBottomDialogFragment() + dialog.arguments = Bundle().apply { + putParcelableArrayList(ARG_LIST_VALUES, ArrayList(values)) + } + return dialog + } + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/raccoongang/core/presentation/dialog/SelectDialogViewModel.kt b/core/src/main/java/com/raccoongang/core/presentation/dialog/SelectDialogViewModel.kt new file mode 100644 index 000000000..07c2050f5 --- /dev/null +++ b/core/src/main/java/com/raccoongang/core/presentation/dialog/SelectDialogViewModel.kt @@ -0,0 +1,22 @@ +package com.raccoongang.core.presentation.dialog + +import androidx.lifecycle.viewModelScope +import com.raccoongang.core.BaseViewModel +import com.raccoongang.core.domain.model.RegistrationField +import com.raccoongang.core.system.notifier.CourseNotifier +import com.raccoongang.core.system.notifier.CourseSubtitleLanguageChanged +import kotlinx.coroutines.launch + +class SelectDialogViewModel( + private val notifier: CourseNotifier +) : BaseViewModel() { + + var values = mutableListOf() + + fun sendCourseEventChanged(value: String) { + viewModelScope.launch { + notifier.send(CourseSubtitleLanguageChanged(value)) + } + } + +} diff --git a/core/src/main/java/com/raccoongang/core/system/notifier/CourseNotifier.kt b/core/src/main/java/com/raccoongang/core/system/notifier/CourseNotifier.kt index 196bf379e..a2a002672 100644 --- a/core/src/main/java/com/raccoongang/core/system/notifier/CourseNotifier.kt +++ b/core/src/main/java/com/raccoongang/core/system/notifier/CourseNotifier.kt @@ -14,5 +14,6 @@ class CourseNotifier { suspend fun send(event: CourseStructureUpdated) = channel.emit(event) suspend fun send(event: CourseDashboardUpdate) = channel.emit(event) suspend fun send(event: CoursePauseVideo) = channel.emit(event) + suspend fun send(event: CourseSubtitleLanguageChanged) = channel.emit(event) } \ No newline at end of file diff --git a/core/src/main/java/com/raccoongang/core/system/notifier/CourseSubtitleLanguageChanged.kt b/core/src/main/java/com/raccoongang/core/system/notifier/CourseSubtitleLanguageChanged.kt new file mode 100644 index 000000000..43a3bc243 --- /dev/null +++ b/core/src/main/java/com/raccoongang/core/system/notifier/CourseSubtitleLanguageChanged.kt @@ -0,0 +1,5 @@ +package com.raccoongang.core.system.notifier + +data class CourseSubtitleLanguageChanged( + val value: String +) : CourseEvent diff --git a/core/src/main/java/com/raccoongang/core/utils/LocaleUtils.kt b/core/src/main/java/com/raccoongang/core/utils/LocaleUtils.kt index f4474fddd..c60517c55 100644 --- a/core/src/main/java/com/raccoongang/core/utils/LocaleUtils.kt +++ b/core/src/main/java/com/raccoongang/core/utils/LocaleUtils.kt @@ -3,7 +3,6 @@ package com.raccoongang.core.utils import com.raccoongang.core.AppDataConstants.USER_MAX_YEAR import com.raccoongang.core.AppDataConstants.defaultLocale import com.raccoongang.core.domain.model.RegistrationField -import com.raccoongang.core.utils.LocaleUtils.getLanguages import java.util.* object LocaleUtils { @@ -28,6 +27,10 @@ object LocaleUtils { fun getLanguages() = getAvailableLanguages() + fun getLanguages(languages: List) = getAvailableLanguages().filter { + languages.contains(it.value) + } + fun getCountryByCountryCode(code: String): String? { val countryISO = Locale.getISOCountries().firstOrNull { it == code } return countryISO?.let { @@ -60,8 +63,8 @@ object LocaleUtils { .sortedBy { it.name } .toList() -} + fun getDisplayLanguage(languageCode: String): String { + return Locale(languageCode, "").getDisplayLanguage(defaultLocale) + } -fun main() { - println(getLanguages()) } diff --git a/core/src/main/res/values/values.xml b/core/src/main/res/values/values.xml new file mode 100644 index 000000000..1c985aac9 --- /dev/null +++ b/core/src/main/res/values/values.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/course/src/main/java/com/raccoongang/course/data/storage/CourseConverter.kt b/course/src/main/java/com/raccoongang/course/data/storage/CourseConverter.kt index 21cac9219..504742ee2 100644 --- a/course/src/main/java/com/raccoongang/course/data/storage/CourseConverter.kt +++ b/course/src/main/java/com/raccoongang/course/data/storage/CourseConverter.kt @@ -45,4 +45,16 @@ class CourseConverter { return Gson().fromJson(value, type) } + @TypeConverter + fun fromStringToMap(value: String?): Map { + val mapType = genericType>() + return Gson().fromJson(value, mapType) + } + + @TypeConverter + fun fromMapToString(map: Map): String { + val gson = Gson() + return gson.toJson(map) + } + } diff --git a/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt index 07b4ae183..9780acc05 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/detail/CourseDetailsFragment.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.PlayCircle import androidx.compose.material.icons.outlined.Report import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState @@ -25,6 +24,7 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.* +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 @@ -345,8 +345,8 @@ private fun CourseDetailNativeContent( } ) { Icon( - modifier = Modifier.size(64.dp), - imageVector = Icons.Filled.PlayCircle, + modifier = Modifier.size(40.dp), + painter = painterResource(courseR.drawable.course_ic_play), contentDescription = null, tint = Color.LightGray ) @@ -474,8 +474,8 @@ private fun CourseDetailNativeContentLandscape( } ) { Icon( - modifier = Modifier.size(64.dp), - imageVector = Icons.Filled.PlayCircle, + modifier = Modifier.size(40.dp), + painter = painterResource(courseR.drawable.course_ic_play), contentDescription = null, tint = Color.LightGray ) diff --git a/course/src/main/java/com/raccoongang/course/presentation/ui/CourseUI.kt b/course/src/main/java/com/raccoongang/course/presentation/ui/CourseUI.kt index 6c141aab8..06581b056 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/ui/CourseUI.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/ui/CourseUI.kt @@ -426,7 +426,9 @@ fun ConnectionErrorView( fun VideoSubtitles( listState: LazyListState, timedTextObject: TimedTextObject?, - currentIndex: Int + subtitleLanguage: String, + currentIndex: Int, + onSettingsClick: () -> Unit ) { timedTextObject?.let { LaunchedEffect(key1 = currentIndex) { @@ -438,11 +440,26 @@ fun VideoSubtitles( val subtitles = timedTextObject.captions.values.toList() Scaffold(scaffoldState = scaffoldState) { Column(Modifier.padding(it)) { - Text( - text = stringResource(id = courseR.string.course_subtitles), - color = MaterialTheme.appColors.textPrimary, - style = MaterialTheme.appTypography.titleMedium - ) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(id = courseR.string.course_subtitles), + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.titleMedium + ) + IconText( + modifier = Modifier.noRippleClickable { + onSettingsClick() + }, + text = subtitleLanguage, + painter = painterResource(id = courseR.drawable.course_ic_cc), + color = MaterialTheme.appColors.textAccent, + textStyle = MaterialTheme.appTypography.labelLarge + ) + } Spacer(Modifier.height(24.dp)) LazyColumn( state = listState, diff --git a/course/src/main/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerFragment.kt index 38601e60d..130b5d419 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerFragment.kt @@ -201,7 +201,7 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta block.id, viewModel.courseId, videoUrl, - transcripts?.en, + transcripts?.toMap() ?: emptyMap(), block.displayName, isDownloaded ) @@ -210,7 +210,7 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta block.id, viewModel.courseId, encodedVideos.youtube?.url!!, - transcripts?.en, + transcripts?.toMap() ?: emptyMap(), block.displayName ) } diff --git a/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitFragment.kt index 831a19db7..badd6fd2b 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitFragment.kt @@ -23,10 +23,14 @@ import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.Player import com.raccoongang.core.extension.computeWindowSizeClasses import com.raccoongang.core.extension.dpToPixel +import com.raccoongang.core.extension.objectToString +import com.raccoongang.core.extension.stringToObject +import com.raccoongang.core.presentation.dialog.SelectBottomDialogFragment import com.raccoongang.core.presentation.global.viewBinding import com.raccoongang.core.ui.WindowSize import com.raccoongang.core.ui.theme.NewEdxTheme import com.raccoongang.core.ui.theme.appColors +import com.raccoongang.core.utils.LocaleUtils import com.raccoongang.course.R import com.raccoongang.course.databinding.FragmentVideoUnitBinding import com.raccoongang.course.presentation.CourseRouter @@ -73,7 +77,10 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { handler.post(videoTimeRunnable) requireArguments().apply { viewModel.videoUrl = getString(ARG_VIDEO_URL, "") - viewModel.transcriptUrl = getString(ARG_TRANSCRIPT_URL, "") + viewModel.transcripts = + stringToObject>( + getString(ARG_TRANSCRIPT_URL, "") + ) ?: emptyMap() viewModel.isDownloaded = getBoolean(ARG_DOWNLOADED) blockId = getString(ARG_BLOCK_ID, "") } @@ -140,7 +147,18 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { VideoSubtitles( listState = state, timedTextObject = transcriptObject, - currentIndex = currentIndex + subtitleLanguage = LocaleUtils.getDisplayLanguage(viewModel.transcriptLanguage), + currentIndex = currentIndex, + onSettingsClick = { + exoPlayer?.pause() + val dialog = SelectBottomDialogFragment.newInstance( + LocaleUtils.getLanguages(viewModel.transcripts.keys.toList()) + ) + dialog.show( + requireActivity().supportFragmentManager, + SelectBottomDialogFragment::class.simpleName + ) + } ) } } @@ -247,7 +265,7 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { blockId: String, courseId: String, videoUrl: String, - transcriptUrl: String?, + transcriptsUrl: Map, title: String, isDownloaded: Boolean ): VideoUnitFragment { @@ -256,7 +274,7 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { ARG_BLOCK_ID to blockId, ARG_COURSE_ID to courseId, ARG_VIDEO_URL to videoUrl, - ARG_TRANSCRIPT_URL to transcriptUrl, + ARG_TRANSCRIPT_URL to objectToString(transcriptsUrl), ARG_TITLE to title, ARG_DOWNLOADED to isDownloaded ) diff --git a/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitViewModel.kt b/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitViewModel.kt index 13692b54b..bed0bbf0a 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitViewModel.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitViewModel.kt @@ -4,12 +4,14 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import com.raccoongang.core.AppDataConstants import com.raccoongang.core.BaseViewModel import com.raccoongang.core.data.storage.PreferencesManager import com.raccoongang.core.module.TranscriptManager import com.raccoongang.core.system.connection.NetworkConnection import com.raccoongang.core.system.notifier.CourseNotifier import com.raccoongang.core.system.notifier.CoursePauseVideo +import com.raccoongang.core.system.notifier.CourseSubtitleLanguageChanged import com.raccoongang.core.system.notifier.CourseVideoPositionChanged import com.raccoongang.course.data.repository.CourseRepository import kotlinx.coroutines.delay @@ -28,7 +30,9 @@ class VideoUnitViewModel( ) : BaseViewModel() { var videoUrl = "" - var transcriptUrl = "" + var transcripts = emptyMap() + var transcriptLanguage = AppDataConstants.defaultLocale.language ?: "en" + private set private val _currentVideoTime = MutableLiveData(0) val currentVideoTime: LiveData @@ -79,6 +83,10 @@ class VideoUnitViewModel( _isUpdated.value = true } else if (it is CoursePauseVideo) { _isVideoPaused.value = true + } else if (it is CourseSubtitleLanguageChanged) { + transcriptLanguage = it.value + _transcriptObject.value = null + downloadSubtitles() } } } @@ -86,7 +94,7 @@ class VideoUnitViewModel( fun downloadSubtitles() { viewModelScope.launch { - transcriptManager.downloadTranscriptsForVideo(transcriptUrl)?.let { result -> + transcriptManager.downloadTranscriptsForVideo(getTranscriptUrl())?.let { result -> _transcriptObject.value = result timeList = result.captions.values.toList() .map { it.start.mseconds.toLong() } @@ -94,6 +102,18 @@ class VideoUnitViewModel( } } + private fun getTranscriptUrl(): String { + val defaultTranscripts = transcripts[transcriptLanguage] + if (!defaultTranscripts.isNullOrEmpty()) { + return defaultTranscripts + } + if (transcripts.values.isNotEmpty()) { + return transcripts.values.toList().first() + } + return "" + } + + fun markBlockCompleted(blockId: String) { viewModelScope.launch { try { diff --git a/course/src/main/java/com/raccoongang/course/presentation/unit/video/YoutubeVideoUnitFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/unit/video/YoutubeVideoUnitFragment.kt index 48289a82d..21ca700a6 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/unit/video/YoutubeVideoUnitFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/unit/video/YoutubeVideoUnitFragment.kt @@ -20,10 +20,14 @@ import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.Abs import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.options.IFramePlayerOptions import com.pierfrancescosoffritti.androidyoutubeplayer.core.ui.DefaultPlayerUiController import com.raccoongang.core.extension.computeWindowSizeClasses +import com.raccoongang.core.extension.objectToString +import com.raccoongang.core.extension.stringToObject +import com.raccoongang.core.presentation.dialog.SelectBottomDialogFragment import com.raccoongang.core.presentation.global.viewBinding import com.raccoongang.core.ui.WindowSize import com.raccoongang.core.ui.theme.NewEdxTheme import com.raccoongang.core.ui.theme.appColors +import com.raccoongang.core.utils.LocaleUtils import com.raccoongang.course.R import com.raccoongang.course.databinding.FragmentYoutubeVideoUnitBinding import com.raccoongang.course.presentation.CourseRouter @@ -55,7 +59,9 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) lifecycle.addObserver(viewModel) requireArguments().apply { viewModel.videoUrl = getString(ARG_VIDEO_URL, "") - viewModel.transcriptUrl = getString(ARG_TRANSCRIPT_URL, "") + viewModel.transcripts = stringToObject>( + getString(ARG_TRANSCRIPT_URL, "") + ) ?: emptyMap() blockId = getString(ARG_BLOCK_ID, "") } viewModel.downloadSubtitles() @@ -120,7 +126,19 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) VideoSubtitles( listState = state, timedTextObject = transcriptObject, - currentIndex = currentIndex + subtitleLanguage = LocaleUtils.getDisplayLanguage(viewModel.transcriptLanguage), + currentIndex = currentIndex, + onSettingsClick = { + _youTubePlayer?.pause() + val dialog = + SelectBottomDialogFragment.newInstance( + LocaleUtils.getLanguages(viewModel.transcripts.keys.toList()) + ) + dialog.show( + requireActivity().supportFragmentManager, + SelectBottomDialogFragment::class.simpleName + ) + } ) } } @@ -210,13 +228,13 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) blockId: String, courseId: String, videoUrl: String, - transcriptUrl: String?, + transcriptsUrl: Map, blockTitle: String ): YoutubeVideoUnitFragment { val fragment = YoutubeVideoUnitFragment() fragment.arguments = bundleOf( ARG_VIDEO_URL to videoUrl, - ARG_TRANSCRIPT_URL to transcriptUrl, + ARG_TRANSCRIPT_URL to objectToString(transcriptsUrl), ARG_BLOCK_ID to blockId, ARG_COURSE_ID to courseId, ARG_TITLE to blockTitle diff --git a/course/src/main/res/drawable/course_ic_cc.xml b/course/src/main/res/drawable/course_ic_cc.xml new file mode 100644 index 000000000..3f77d8be6 --- /dev/null +++ b/course/src/main/res/drawable/course_ic_cc.xml @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/course/src/main/res/drawable/course_ic_play.xml b/course/src/main/res/drawable/course_ic_play.xml new file mode 100644 index 000000000..8ab184579 --- /dev/null +++ b/course/src/main/res/drawable/course_ic_play.xml @@ -0,0 +1,12 @@ + + + diff --git a/course/src/main/res/layout-w600dp-h480dp/fragment_video_unit.xml b/course/src/main/res/layout-w600dp-h480dp/fragment_video_unit.xml index f39691bb9..0ab81bdab 100644 --- a/course/src/main/res/layout-w600dp-h480dp/fragment_video_unit.xml +++ b/course/src/main/res/layout-w600dp-h480dp/fragment_video_unit.xml @@ -46,7 +46,7 @@ + val commentsData: List, + val count: Int ) : DiscussionCommentsUIState() object Loading : DiscussionCommentsUIState() diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsViewModel.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsViewModel.kt index 89a26bae1..bf0d59d5f 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsViewModel.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsViewModel.kt @@ -32,6 +32,7 @@ class DiscussionCommentsViewModel( var thread: com.raccoongang.discussion.domain.model.Thread private set + private var commentCount = 0 private val _uiState = MutableLiveData() val uiState: LiveData @@ -64,8 +65,11 @@ class DiscussionCommentsViewModel( if (it is DiscussionCommentAdded) { if (page == -1) { comments.add(it.comment) - _uiState.value = - DiscussionCommentsUIState.Success(thread, comments.toList()) + _uiState.value = DiscussionCommentsUIState.Success( + thread, + comments.toList(), + commentCount + ) _scrollToBottom.value = true } else { _uiMessage.value = @@ -79,8 +83,11 @@ class DiscussionCommentsViewModel( } if (index >= 0) { comments[index] = it.discussionComment - _uiState.value = - DiscussionCommentsUIState.Success(thread, comments.toList()) + _uiState.value = DiscussionCommentsUIState.Success( + thread, + comments.toList(), + commentCount + ) } } } @@ -114,8 +121,10 @@ class DiscussionCommentsViewModel( _canLoadMore.value = false page = -1 } + commentCount = response.pagination.count comments.addAll(response.results) - _uiState.value = DiscussionCommentsUIState.Success(thread, comments.toList()) + _uiState.value = + DiscussionCommentsUIState.Success(thread, comments.toList(), commentCount) if (markReadIfSuccessful) { markRead() @@ -139,7 +148,10 @@ class DiscussionCommentsViewModel( viewModelScope.launch { try { val response = interactor.setThreadRead(thread.id) - thread = thread.copy(read = response.read, unreadCommentCount = response.unreadCommentCount) + thread = thread.copy( + read = response.read, + unreadCommentCount = response.unreadCommentCount + ) sendThreadUpdated() } catch (e: Exception) { e.printStackTrace() @@ -170,7 +182,8 @@ class DiscussionCommentsViewModel( try { val response = interactor.setThreadVoted(thread.id, vote) thread = thread.copy(voted = response.voted, voteCount = response.voteCount) - _uiState.value = DiscussionCommentsUIState.Success(thread, comments.toList()) + _uiState.value = + DiscussionCommentsUIState.Success(thread, comments.toList(), commentCount) sendThreadUpdated() } catch (e: Exception) { if (e.isInternetError()) { @@ -189,7 +202,8 @@ class DiscussionCommentsViewModel( try { val response = interactor.setThreadFollowed(thread.id, followed) thread = thread.copy(following = response.following) - _uiState.value = DiscussionCommentsUIState.Success(thread, comments.toList()) + _uiState.value = + DiscussionCommentsUIState.Success(thread, comments.toList(), commentCount) sendThreadUpdated() } catch (e: Exception) { if (e.isInternetError()) { @@ -208,7 +222,8 @@ class DiscussionCommentsViewModel( try { val response = interactor.setThreadFlagged(thread.id, reported) thread = thread.copy(abuseFlagged = response.abuseFlagged) - _uiState.value = DiscussionCommentsUIState.Success(thread, comments.toList()) + _uiState.value = + DiscussionCommentsUIState.Success(thread, comments.toList(), commentCount) sendThreadUpdated() } catch (e: Exception) { if (e.isInternetError()) { @@ -231,7 +246,8 @@ class DiscussionCommentsViewModel( } comments[index] = comments[index].copy(voted = response.voted, voteCount = response.voteCount) - _uiState.value = DiscussionCommentsUIState.Success(thread, comments.toList()) + _uiState.value = + DiscussionCommentsUIState.Success(thread, comments.toList(), commentCount) } catch (e: Exception) { if (e.isInternetError()) { _uiMessage.value = @@ -252,7 +268,8 @@ class DiscussionCommentsViewModel( it.id == response.id } comments[index] = comments[index].copy(abuseFlagged = response.abuseFlagged) - _uiState.value = DiscussionCommentsUIState.Success(thread, comments.toList()) + _uiState.value = + DiscussionCommentsUIState.Success(thread, comments.toList(), commentCount) } catch (e: Exception) { if (e.isInternetError()) { _uiMessage.value = @@ -277,7 +294,8 @@ class DiscussionCommentsViewModel( _uiMessage.value = UIMessage.ToastMessage(resourceManager.getString(com.raccoongang.discussion.R.string.discussion_comment_added)) } - _uiState.value = DiscussionCommentsUIState.Success(thread, comments.toList()) + _uiState.value = + DiscussionCommentsUIState.Success(thread, comments.toList(), commentCount) } catch (e: Exception) { if (e.isInternetError()) { _uiMessage.value = From 20c56ca70e2641c5312c3e401d5e3bd851b6bc12 Mon Sep 17 00:00:00 2001 From: Serhii <128455389+hryh27@users.noreply.github.com> Date: Tue, 11 Apr 2023 20:46:52 +0300 Subject: [PATCH 16/20] change subtitles language (#15) * change subtitles language * changed transcript cache life limit --- .../core/module/TranscriptManager.kt | 2 +- .../course/presentation/ui/CourseUI.kt | 21 +++++++++-------- .../unit/video/VideoUnitFragment.kt | 1 + .../unit/video/VideoUnitViewModel.kt | 3 ++- .../unit/video/YoutubeVideoUnitFragment.kt | 1 + .../src/main/res/drawable/course_ic_play.xml | 23 ++++++++++++------- 6 files changed, 32 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/com/raccoongang/core/module/TranscriptManager.kt b/core/src/main/java/com/raccoongang/core/module/TranscriptManager.kt index 1e6d0d0e6..8b9621782 100644 --- a/core/src/main/java/com/raccoongang/core/module/TranscriptManager.kt +++ b/core/src/main/java/com/raccoongang/core/module/TranscriptManager.kt @@ -28,7 +28,7 @@ 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(12) + return file.exists() && System.currentTimeMillis() - file.lastModified() < TimeUnit.HOURS.toMillis(5) } fun get(url: String): String? { diff --git a/course/src/main/java/com/raccoongang/course/presentation/ui/CourseUI.kt b/course/src/main/java/com/raccoongang/course/presentation/ui/CourseUI.kt index 06581b056..184c87c38 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/ui/CourseUI.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/ui/CourseUI.kt @@ -427,6 +427,7 @@ fun VideoSubtitles( listState: LazyListState, timedTextObject: TimedTextObject?, subtitleLanguage: String, + showSubtitleLanguage: Boolean, currentIndex: Int, onSettingsClick: () -> Unit ) { @@ -450,15 +451,17 @@ fun VideoSubtitles( color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.titleMedium ) - IconText( - modifier = Modifier.noRippleClickable { - onSettingsClick() - }, - text = subtitleLanguage, - painter = painterResource(id = courseR.drawable.course_ic_cc), - color = MaterialTheme.appColors.textAccent, - textStyle = MaterialTheme.appTypography.labelLarge - ) + if (showSubtitleLanguage) { + IconText( + modifier = Modifier.noRippleClickable { + onSettingsClick() + }, + text = subtitleLanguage, + painter = painterResource(id = courseR.drawable.course_ic_cc), + color = MaterialTheme.appColors.textAccent, + textStyle = MaterialTheme.appTypography.labelLarge + ) + } } Spacer(Modifier.height(24.dp)) LazyColumn( diff --git a/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitFragment.kt index badd6fd2b..36e5227e8 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitFragment.kt @@ -148,6 +148,7 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { listState = state, timedTextObject = transcriptObject, subtitleLanguage = LocaleUtils.getDisplayLanguage(viewModel.transcriptLanguage), + showSubtitleLanguage = viewModel.transcripts.size > 1, currentIndex = currentIndex, onSettingsClick = { exoPlayer?.pause() diff --git a/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitViewModel.kt b/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitViewModel.kt index bed0bbf0a..52b068210 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitViewModel.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/unit/video/VideoUnitViewModel.kt @@ -108,7 +108,8 @@ class VideoUnitViewModel( return defaultTranscripts } if (transcripts.values.isNotEmpty()) { - return transcripts.values.toList().first() + transcriptLanguage = transcripts.keys.toList().first() + return transcripts[transcriptLanguage]?:"" } return "" } diff --git a/course/src/main/java/com/raccoongang/course/presentation/unit/video/YoutubeVideoUnitFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/unit/video/YoutubeVideoUnitFragment.kt index 21ca700a6..ecf8a272f 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/unit/video/YoutubeVideoUnitFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/unit/video/YoutubeVideoUnitFragment.kt @@ -127,6 +127,7 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) listState = state, timedTextObject = transcriptObject, subtitleLanguage = LocaleUtils.getDisplayLanguage(viewModel.transcriptLanguage), + showSubtitleLanguage = viewModel.transcripts.size > 1, currentIndex = currentIndex, onSettingsClick = { _youTubePlayer?.pause() diff --git a/course/src/main/res/drawable/course_ic_play.xml b/course/src/main/res/drawable/course_ic_play.xml index 8ab184579..a172ae772 100644 --- a/course/src/main/res/drawable/course_ic_play.xml +++ b/course/src/main/res/drawable/course_ic_play.xml @@ -1,12 +1,19 @@ + android:width="48dp" + android:height="48dp" + android:viewportWidth="48" + android:viewportHeight="48"> + android:fillAlpha="0.9" + android:fillType="evenOdd"/> + + + + From 59c4c46eeb819fa0f5104a4bb03344a915140d5a Mon Sep 17 00:00:00 2001 From: Serhii <128455389+hryh27@users.noreply.github.com> Date: Wed, 12 Apr 2023 12:20:05 +0300 Subject: [PATCH 17/20] fix issues (#16) --- .../outline/CourseOutlineFragment.kt | 24 ++++++++++++++----- .../comments/DiscussionCommentsFragment.kt | 2 ++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt index 96e87b957..76dbcae45 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt @@ -377,12 +377,24 @@ private fun ResumeCourse( color = MaterialTheme.appColors.textPrimaryVariant ) Spacer(Modifier.height(6.dp)) - IconText( - text = block.displayName, - painter = painterResource(id = CourseUnitsFragment.getUnitBlockIcon(block)), - color = MaterialTheme.appColors.textPrimary, - textStyle = MaterialTheme.appTypography.titleMedium - ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(id = CourseUnitsFragment.getUnitBlockIcon(block)), + contentDescription = null, + tint = MaterialTheme.appColors.textPrimary + ) + Text( + text = block.displayName, + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.titleMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } Spacer(Modifier.height(24.dp)) NewEdxButton( text = stringResource(id = com.raccoongang.course.R.string.course_continue), diff --git a/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsFragment.kt b/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsFragment.kt index 91c9fc4fc..c22a1fd6a 100644 --- a/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsFragment.kt +++ b/discussion/src/main/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsFragment.kt @@ -106,6 +106,8 @@ class DiscussionCommentsFragment : Fragment() { ACTION_UPVOTE_COMMENT -> viewModel.setCommentUpvoted(id, bool) ACTION_UPVOTE_THREAD -> viewModel.setThreadUpvoted(bool) ACTION_FOLLOW_THREAD -> viewModel.setThreadFollowed(bool) + ACTION_REPORT_COMMENT -> viewModel.setCommentReported(id, bool) + ACTION_REPORT_THREAD -> viewModel.setThreadReported(bool) } } else { when (action) { From 7e2e135cc2494c64fb0ce22a347273d0851e726f Mon Sep 17 00:00:00 2001 From: Serhii <128455389+hryh27@users.noreply.github.com> Date: Wed, 12 Apr 2023 15:11:59 +0300 Subject: [PATCH 18/20] Fix/UI issues (#17) * fix issues --- .../course/presentation/outline/CourseOutlineFragment.kt | 4 ++-- .../course/presentation/outline/CourseOutlineViewModel.kt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt index 76dbcae45..63f276291 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt @@ -423,7 +423,7 @@ private fun ResumeCourseTablet( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { - Column { + Column(Modifier.weight(1f)) { Text( text = stringResource(id = com.raccoongang.course.R.string.course_continue_with), style = MaterialTheme.appTypography.labelMedium, @@ -431,7 +431,7 @@ private fun ResumeCourseTablet( ) Spacer(Modifier.height(6.dp)) Row( - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = Alignment.Top, horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Icon( diff --git a/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineViewModel.kt b/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineViewModel.kt index 1f8f8a0d3..16c2736f2 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineViewModel.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineViewModel.kt @@ -115,10 +115,10 @@ class CourseOutlineViewModel( private fun getCourseDataInternal() { viewModelScope.launch { - var courseStructure = interactor.getCourseStructureFromCache() - val blocks = courseStructure.blockData - try { + var courseStructure = interactor.getCourseStructureFromCache() + val blocks = courseStructure.blockData + val courseStatus = if (networkConnection.isOnline()) { interactor.getCourseStatus(courseId) } else { From 9fe15bd5b89455b98bd509e39b4ad9e9d15a266e Mon Sep 17 00:00:00 2001 From: Volodymyr Chekyrta Date: Wed, 12 Apr 2023 17:05:33 +0300 Subject: [PATCH 19/20] Add padding for ResumeCourseTablet --- .../course/presentation/outline/CourseOutlineFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt b/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt index 63f276291..10abb8254 100644 --- a/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt +++ b/course/src/main/java/com/raccoongang/course/presentation/outline/CourseOutlineFragment.kt @@ -423,7 +423,7 @@ private fun ResumeCourseTablet( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { - Column(Modifier.weight(1f)) { + Column(Modifier.weight(1f).padding(end = 35.dp)) { Text( text = stringResource(id = com.raccoongang.course.R.string.course_continue_with), style = MaterialTheme.appTypography.labelMedium, From e1e1eec58ae320938456c949fb8ac4bfea47bd9a Mon Sep 17 00:00:00 2001 From: Serhii <128455389+hryh27@users.noreply.github.com> Date: Wed, 12 Apr 2023 17:06:25 +0300 Subject: [PATCH 20/20] fix tests (#18) --- .../container/CourseContainerViewModelTest.kt | 32 +++- .../detail/CourseDetailsViewModelTest.kt | 13 -- .../handouts/HandoutsViewModelTest.kt | 87 ++++------ .../outline/CourseOutlineViewModelTest.kt | 157 +++++++++++------- .../section/CourseSectionViewModelTest.kt | 35 +++- .../CourseUnitContainerViewModelTest.kt | 99 +++++++---- .../unit/video/VideoUnitViewModelTest.kt | 31 +++- .../units/CourseUnitsViewModelTest.kt | 21 ++- .../videos/CourseVideoViewModelTest.kt | 59 +++++-- .../presentation/DashboardViewModelTest.kt | 83 +++++++-- .../search/CourseSearchViewModelTest.kt | 3 +- .../DiscussionCommentsViewModelTest.kt | 71 +++++++- .../DiscussionResponsesViewModelTest.kt | 9 +- .../DiscussionSearchThreadViewModelTest.kt | 5 +- .../DiscussionAddThreadViewModelTest.kt | 7 +- .../threads/DiscussionThreadsViewModelTest.kt | 110 ++++++++---- .../edit/EditProfileViewModelTest.kt | 44 +++-- .../profile/ProfileViewModelTest.kt | 61 ++++++- 18 files changed, 647 insertions(+), 280 deletions(-) diff --git a/course/src/test/java/com/raccoongang/course/presentation/container/CourseContainerViewModelTest.kt b/course/src/test/java/com/raccoongang/course/presentation/container/CourseContainerViewModelTest.kt index d4de361dc..0265d4750 100644 --- a/course/src/test/java/com/raccoongang/course/presentation/container/CourseContainerViewModelTest.kt +++ b/course/src/test/java/com/raccoongang/course/presentation/container/CourseContainerViewModelTest.kt @@ -3,6 +3,8 @@ package com.raccoongang.course.presentation.container import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.raccoongang.core.system.connection.NetworkConnection import com.raccoongang.core.R +import com.raccoongang.core.domain.model.CourseStructure +import com.raccoongang.core.domain.model.CoursewareAccess import com.raccoongang.core.system.ResourceManager import com.raccoongang.core.system.notifier.CourseNotifier import com.raccoongang.core.system.notifier.CourseStructureUpdated @@ -35,6 +37,30 @@ class CourseContainerViewModelTest { private val noInternet = "Slow or no internet connection" private val somethingWrong = "Something went wrong" + private val courseStructure = CourseStructure( + root = "", + blockData = listOf(), + id = "id", + name = "Course name", + number = "", + org = "Org", + start = null, + startDisplay = "", + startType = "", + end = null, + coursewareAccess = CoursewareAccess( + true, + "", + "", + "", + "", + "" + ), + media = null, + certificate = null, + isSelfPaced = false + ) + @Before fun setUp() { Dispatchers.setMain(dispatcher) @@ -84,6 +110,7 @@ class CourseContainerViewModelTest { val viewModel = CourseContainerViewModel("", interactor,resourceManager, notifier, networkConnection) every { networkConnection.isOnline() } returns true coEvery { interactor.preloadCourseStructure(any()) } returns Unit + every { interactor.getCourseStructureFromCache() } returns courseStructure viewModel.preloadCourseStructure() advanceUntilIdle() @@ -91,7 +118,7 @@ class CourseContainerViewModelTest { assert(viewModel.errorMessage.value == null) assert(viewModel.showProgress.value == false) - assert(viewModel.dataReady.value == true) + assert(viewModel.dataReady.value != null) } @Test @@ -99,6 +126,7 @@ class CourseContainerViewModelTest { val viewModel = CourseContainerViewModel("", interactor,resourceManager, notifier, networkConnection) every { networkConnection.isOnline() } returns false coEvery { interactor.preloadCourseStructureFromCache(any()) } returns Unit + every { interactor.getCourseStructureFromCache() } returns courseStructure viewModel.preloadCourseStructure() advanceUntilIdle() @@ -107,7 +135,7 @@ class CourseContainerViewModelTest { assert(viewModel.errorMessage.value == null) assert(viewModel.showProgress.value == false) - assert(viewModel.dataReady.value == true) + assert(viewModel.dataReady.value != null) } @Test diff --git a/course/src/test/java/com/raccoongang/course/presentation/detail/CourseDetailsViewModelTest.kt b/course/src/test/java/com/raccoongang/course/presentation/detail/CourseDetailsViewModelTest.kt index e82e75008..d69e22cbc 100644 --- a/course/src/test/java/com/raccoongang/course/presentation/detail/CourseDetailsViewModelTest.kt +++ b/course/src/test/java/com/raccoongang/course/presentation/detail/CourseDetailsViewModelTest.kt @@ -55,11 +55,9 @@ class CourseDetailsViewModelTest { CourseDetailsViewModel("", networkConnection, interactor, resourceManager, notifier) every { networkConnection.isOnline() } returns true coEvery { interactor.getCourseDetails(any()) } throws UnknownHostException() - coEvery { interactor.getEnrolledCourseById(any()) } throws UnknownHostException() advanceUntilIdle() coVerify(exactly = 1) { interactor.getCourseDetails(any()) } - coVerify(exactly = 1) { interactor.getEnrolledCourseById(any()) } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage @@ -73,11 +71,9 @@ class CourseDetailsViewModelTest { CourseDetailsViewModel("", networkConnection, interactor, resourceManager, notifier) every { networkConnection.isOnline() } returns true coEvery { interactor.getCourseDetails(any()) } throws Exception() - coEvery { interactor.getEnrolledCourseById(any()) } throws Exception() advanceUntilIdle() coVerify(exactly = 1) { interactor.getCourseDetails(any()) } - coVerify(exactly = 1) { interactor.getEnrolledCourseById(any()) } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage @@ -91,12 +87,10 @@ class CourseDetailsViewModelTest { CourseDetailsViewModel("", networkConnection, interactor, resourceManager, notifier) every { networkConnection.isOnline() } returns true coEvery { interactor.getCourseDetails(any()) } returns mockk() - coEvery { interactor.getEnrolledCourseById(any()) } returns mockk() advanceUntilIdle() coVerify(exactly = 1) { interactor.getCourseDetails(any()) } - coVerify(exactly = 1) { interactor.getEnrolledCourseById(any()) } assert(viewModel.uiMessage.value == null) assert(viewModel.uiState.value is CourseDetailsUIState.CourseData) @@ -108,14 +102,11 @@ class CourseDetailsViewModelTest { CourseDetailsViewModel("", networkConnection, interactor, resourceManager, notifier) every { networkConnection.isOnline() } returns false coEvery { interactor.getCourseDetailsFromCache(any()) } returns mockk() - coEvery { interactor.getEnrolledCourseFromCacheById(any()) } returns mockk() advanceUntilIdle() coVerify(exactly = 0) { interactor.getCourseDetails(any()) } - coVerify(exactly = 0) { interactor.getEnrolledCourseById(any()) } coVerify(exactly = 1) { interactor.getCourseDetailsFromCache(any()) } - coVerify(exactly = 1) { interactor.getEnrolledCourseFromCacheById(any()) } assert(viewModel.uiMessage.value == null) assert(viewModel.uiState.value is CourseDetailsUIState.CourseData) @@ -126,7 +117,6 @@ class CourseDetailsViewModelTest { val viewModel = CourseDetailsViewModel("", networkConnection, interactor, resourceManager, notifier) coEvery { interactor.enrollInACourse(any()) } throws UnknownHostException() - coEvery { interactor.getEnrolledCourseById(any()) } returns mockk() coEvery { notifier.send(CourseDashboardUpdate()) } returns Unit every { networkConnection.isOnline() } returns true coEvery { interactor.getCourseDetails(any()) } returns mockk() @@ -147,7 +137,6 @@ class CourseDetailsViewModelTest { val viewModel = CourseDetailsViewModel("", networkConnection, interactor, resourceManager, notifier) coEvery { interactor.enrollInACourse(any()) } throws Exception() - coEvery { interactor.getEnrolledCourseById(any()) } returns mockk() coEvery { notifier.send(CourseDashboardUpdate()) } returns Unit every { networkConnection.isOnline() } returns true coEvery { interactor.getCourseDetails(any()) } returns mockk() @@ -168,7 +157,6 @@ class CourseDetailsViewModelTest { val viewModel = CourseDetailsViewModel("", networkConnection, interactor, resourceManager, notifier) coEvery { interactor.enrollInACourse(any()) } returns Unit - coEvery { interactor.getEnrolledCourseById(any()) } returns mockk() coEvery { notifier.send(CourseDashboardUpdate()) } returns Unit every { networkConnection.isOnline() } returns true coEvery { interactor.getCourseDetails(any()) } returns mockk() @@ -179,7 +167,6 @@ class CourseDetailsViewModelTest { advanceUntilIdle() coVerify(exactly = 1) { interactor.enrollInACourse(any()) } - coVerify(exactly = 2) { interactor.getEnrolledCourseById(any()) } assert(viewModel.uiMessage.value == null) assert(viewModel.uiState.value is CourseDetailsUIState.CourseData) diff --git a/course/src/test/java/com/raccoongang/course/presentation/handouts/HandoutsViewModelTest.kt b/course/src/test/java/com/raccoongang/course/presentation/handouts/HandoutsViewModelTest.kt index 088f0aa16..cbd662459 100644 --- a/course/src/test/java/com/raccoongang/course/presentation/handouts/HandoutsViewModelTest.kt +++ b/course/src/test/java/com/raccoongang/course/presentation/handouts/HandoutsViewModelTest.kt @@ -28,44 +28,10 @@ class HandoutsViewModelTest { private val interactor = mockk() - //region mockEnrolledCourse - - private val mockCourseEnrolled = EnrolledCourse( - auditAccessExpires = Date(), - created = "created", - certificate = Certificate(""), - mode = "mode", - isActive = true, - course = EnrolledCourseData( - id = "id", - name = "name", - number = "", - org = "Org", - start = Date(), - startDisplay = "", - startType = "", - end = null, - dynamicUpgradeDeadline = "", - subscriptionId = "", - coursewareAccess = CoursewareAccess( - true, - "", - "", - "", - "", - "" - ), - media = null, - courseImage = "", - courseAbout = "", - courseSharingUtmParameters = CourseSharingUtmParameters("", ""), - courseUpdates = "", - courseHandouts = "", - discussionUrl = "", - videoOutline = "", - isSelfPaced = false - ) - ) + //region mockHandoutsModel + + private val handoutsModel = HandoutsModel("") + //endregion @Before @@ -80,36 +46,29 @@ class HandoutsViewModelTest { @Test fun `getEnrolledCourse no internet connection exception`() = runTest { - val viewModel = HandoutsViewModel("","Handouts", interactor) - coEvery { interactor.getEnrolledCourseFromCacheById(any()) } throws UnknownHostException() - coEvery { interactor.getAnnouncements(any()) } returns mockk() + val viewModel = HandoutsViewModel("", "Handouts", interactor) + coEvery { interactor.getHandouts(any()) } throws UnknownHostException() advanceUntilIdle() - coVerify(exactly = 1) { interactor.getEnrolledCourseFromCacheById(any()) } assert(viewModel.htmlContent.value == null) } @Test fun `getEnrolledCourse unknown exception`() = runTest { - val viewModel = HandoutsViewModel("","Handouts", interactor) - coEvery { interactor.getEnrolledCourseFromCacheById(any()) } throws Exception() - coEvery { interactor.getAnnouncements(any()) } returns mockk() - + val viewModel = HandoutsViewModel("", "Handouts", interactor) + coEvery { interactor.getHandouts(any()) } throws Exception() advanceUntilIdle() - coVerify(exactly = 1) { interactor.getEnrolledCourseFromCacheById(any()) } assert(viewModel.htmlContent.value == null) } @Test fun `getEnrolledCourse handouts success`() = runTest { - val viewModel = HandoutsViewModel("",HandoutsType.Handouts.name, interactor) - coEvery { interactor.getEnrolledCourseFromCacheById(any()) } returns mockCourseEnrolled + val viewModel = HandoutsViewModel("", HandoutsType.Handouts.name, interactor) coEvery { interactor.getHandouts(any()) } returns HandoutsModel("hello") advanceUntilIdle() - coVerify(exactly = 1) { interactor.getEnrolledCourseFromCacheById(any()) } coVerify(exactly = 1) { interactor.getHandouts(any()) } coVerify(exactly = 0) { interactor.getAnnouncements(any()) } @@ -118,12 +77,15 @@ class HandoutsViewModelTest { @Test fun `getEnrolledCourse announcements success`() = runTest { - val viewModel = HandoutsViewModel("",HandoutsType.Announcements.name, interactor) - coEvery { interactor.getEnrolledCourseFromCacheById(any()) } returns mockCourseEnrolled - coEvery { interactor.getAnnouncements(any()) } returns listOf(AnnouncementModel("date","content")) + val viewModel = HandoutsViewModel("", HandoutsType.Announcements.name, interactor) + coEvery { interactor.getAnnouncements(any()) } returns listOf( + AnnouncementModel( + "date", + "content" + ) + ) advanceUntilIdle() - coVerify(exactly = 1) { interactor.getEnrolledCourseFromCacheById(any()) } coVerify(exactly = 0) { interactor.getHandouts(any()) } coVerify(exactly = 1) { interactor.getAnnouncements(any()) } @@ -132,12 +94,19 @@ class HandoutsViewModelTest { @Test fun `injectDarkMode test`() = runTest { - val viewModel = HandoutsViewModel("",HandoutsType.Announcements.name, interactor) - coEvery { interactor.getEnrolledCourseFromCacheById(any()) } returns mockCourseEnrolled - coEvery { interactor.getAnnouncements(any()) } returns listOf(AnnouncementModel("date","content")) - viewModel.injectDarkMode(viewModel.htmlContent.value.toString(), ULong.MAX_VALUE, ULong.MAX_VALUE) + val viewModel = HandoutsViewModel("", HandoutsType.Announcements.name, interactor) + coEvery { interactor.getAnnouncements(any()) } returns listOf( + AnnouncementModel( + "date", + "content" + ) + ) + viewModel.injectDarkMode( + viewModel.htmlContent.value.toString(), + ULong.MAX_VALUE, + ULong.MAX_VALUE + ) advanceUntilIdle() - coVerify(exactly = 1) { interactor.getEnrolledCourseFromCacheById(any()) } coVerify(exactly = 0) { interactor.getHandouts(any()) } coVerify(exactly = 1) { interactor.getAnnouncements(any()) } diff --git a/course/src/test/java/com/raccoongang/course/presentation/outline/CourseOutlineViewModelTest.kt b/course/src/test/java/com/raccoongang/course/presentation/outline/CourseOutlineViewModelTest.kt index 23b058e08..59e1c7688 100644 --- a/course/src/test/java/com/raccoongang/course/presentation/outline/CourseOutlineViewModelTest.kt +++ b/course/src/test/java/com/raccoongang/course/presentation/outline/CourseOutlineViewModelTest.kt @@ -8,9 +8,7 @@ import com.raccoongang.core.BlockType import com.raccoongang.core.R import com.raccoongang.core.UIMessage import com.raccoongang.core.data.storage.PreferencesManager -import com.raccoongang.core.domain.model.Block -import com.raccoongang.core.domain.model.BlockCounts -import com.raccoongang.core.domain.model.CourseComponentStatus +import com.raccoongang.core.domain.model.* import com.raccoongang.core.module.DownloadWorkerController import com.raccoongang.core.module.db.* import com.raccoongang.core.system.ResourceManager @@ -31,6 +29,7 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule import java.net.UnknownHostException +import java.util.* @OptIn(ExperimentalCoroutinesApi::class) class CourseOutlineViewModelTest { @@ -100,6 +99,30 @@ class CourseOutlineViewModelTest { ) ) + val courseStructure = CourseStructure( + root = "", + blockData = blocks, + id = "id", + name = "Course name", + number = "", + org = "Org", + start = Date(), + startDisplay = "", + startType = "", + end = Date(), + coursewareAccess = CoursewareAccess( + true, + "", + "", + "", + "", + "" + ), + media = null, + certificate = null, + isSelfPaced = false + ) + private val downloadModel = DownloadModel( "id", "title", @@ -126,6 +149,10 @@ class CourseOutlineViewModelTest { @Test fun `getCourseDataInternal no internet connection exception`() = runTest { + every { interactor.getCourseStructureFromCache() } returns courseStructure + every { networkConnection.isOnline() } returns true + coEvery { interactor.getCourseStatus(any()) } throws UnknownHostException() + val viewModel = CourseOutlineViewModel( "", interactor, @@ -136,11 +163,7 @@ class CourseOutlineViewModelTest { downloadDao, workerController ) - every { interactor.getCourseStructureFromCache() } returns emptyList() - every { networkConnection.isOnline() } returns true - coEvery { interactor.getCourseStatus(any()) } throws UnknownHostException() - viewModel.getCourseData() advanceUntilIdle() verify(exactly = 1) { interactor.getCourseStructureFromCache() } @@ -154,6 +177,9 @@ class CourseOutlineViewModelTest { @Test fun `getCourseDataInternal unknown exception`() = runTest { + every { interactor.getCourseStructureFromCache() } returns courseStructure + every { networkConnection.isOnline() } returns true + coEvery { interactor.getCourseStatus(any()) } throws Exception() val viewModel = CourseOutlineViewModel( "", interactor, @@ -164,11 +190,7 @@ class CourseOutlineViewModelTest { downloadDao, workerController ) - every { interactor.getCourseStructureFromCache() } returns emptyList() - every { networkConnection.isOnline() } returns true - coEvery { interactor.getCourseStatus(any()) } throws Exception() - viewModel.getCourseData() advanceUntilIdle() verify(exactly = 1) { interactor.getCourseStructureFromCache() } @@ -182,17 +204,7 @@ class CourseOutlineViewModelTest { @Test fun `getCourseDataInternal success with internet connection`() = runTest { - val viewModel = CourseOutlineViewModel( - "", - interactor, - resourceManager, - notifier, - networkConnection, - preferencesManager, - downloadDao, - workerController - ) - every { interactor.getCourseStructureFromCache() } returns blocks + every { interactor.getCourseStructureFromCache() } returns courseStructure every { networkConnection.isOnline() } returns true coEvery { downloadDao.readAllData() } returns flow { emit( @@ -205,7 +217,17 @@ class CourseOutlineViewModelTest { } coEvery { interactor.getCourseStatus(any()) } returns CourseComponentStatus("id") - viewModel.getCourseData() + val viewModel = CourseOutlineViewModel( + "", + interactor, + resourceManager, + notifier, + networkConnection, + preferencesManager, + downloadDao, + workerController + ) + advanceUntilIdle() verify(exactly = 1) { interactor.getCourseStructureFromCache() } @@ -218,17 +240,7 @@ class CourseOutlineViewModelTest { @Test fun `getCourseDataInternal success without internet connection`() = runTest { - val viewModel = CourseOutlineViewModel( - "", - interactor, - resourceManager, - notifier, - networkConnection, - preferencesManager, - downloadDao, - workerController - ) - every { interactor.getCourseStructureFromCache() } returns blocks + every { interactor.getCourseStructureFromCache() } returns courseStructure every { networkConnection.isOnline() } returns false coEvery { downloadDao.readAllData() } returns flow { emit( @@ -241,7 +253,17 @@ class CourseOutlineViewModelTest { } coEvery { interactor.getCourseStatus(any()) } returns CourseComponentStatus("id") - viewModel.getCourseData() + val viewModel = CourseOutlineViewModel( + "", + interactor, + resourceManager, + notifier, + networkConnection, + preferencesManager, + downloadDao, + workerController + ) + advanceUntilIdle() verify(exactly = 1) { interactor.getCourseStructureFromCache() } @@ -254,17 +276,7 @@ class CourseOutlineViewModelTest { @Test fun `updateCourseData success with internet connection`() = runTest { - val viewModel = CourseOutlineViewModel( - "", - interactor, - resourceManager, - notifier, - networkConnection, - preferencesManager, - downloadDao, - workerController - ) - every { interactor.getCourseStructureFromCache() } returns blocks + every { interactor.getCourseStructureFromCache() } returns courseStructure every { networkConnection.isOnline() } returns true coEvery { downloadDao.readAllData() } returns flow { emit( @@ -277,11 +289,22 @@ class CourseOutlineViewModelTest { } coEvery { interactor.getCourseStatus(any()) } returns CourseComponentStatus("id") + val viewModel = CourseOutlineViewModel( + "", + interactor, + resourceManager, + notifier, + networkConnection, + preferencesManager, + downloadDao, + workerController + ) + viewModel.updateCourseData(false) advanceUntilIdle() - coVerify(exactly = 1) { interactor.getCourseStructureFromCache() } - coVerify(exactly = 1) { interactor.getCourseStatus(any()) } + coVerify(exactly = 2) { interactor.getCourseStructureFromCache() } + coVerify(exactly = 2) { interactor.getCourseStatus(any()) } assert(viewModel.uiMessage.value == null) assert(viewModel.isUpdating.value == false) @@ -301,7 +324,7 @@ class CourseOutlineViewModelTest { workerController ) coEvery { notifier.notifier } returns flow { emit(CourseStructureUpdated("", false)) } - every { interactor.getCourseStructureFromCache() } returns blocks + every { interactor.getCourseStructureFromCache() } returns courseStructure every { downloadDao.readAllData() } returns flow { repeat(5) { delay(10000) @@ -319,12 +342,20 @@ class CourseOutlineViewModelTest { viewModel.setIsUpdating() advanceUntilIdle() - coVerify(exactly = 1) { interactor.getCourseStructureFromCache() } + coVerify(exactly = 2) { interactor.getCourseStructureFromCache() } coVerify(exactly = 1) { interactor.getCourseStatus(any()) } } @Test fun `saveDownloadModels test`() = runTest { + every { preferencesManager.videoSettings.wifiDownloadOnly } returns false + every { interactor.getCourseStructureFromCache() } returns courseStructure + every { networkConnection.isWifiConnected() } returns true + every { networkConnection.isOnline() } returns true + coEvery { workerController.saveModels(*anyVararg()) } returns Unit + coEvery { interactor.getCourseStatus(any()) } returns CourseComponentStatus("id") + coEvery { downloadDao.readAllData() } returns flow { emit(emptyList()) } + val viewModel = CourseOutlineViewModel( "", interactor, @@ -335,9 +366,6 @@ class CourseOutlineViewModelTest { downloadDao, workerController ) - every { preferencesManager.videoSettings.wifiDownloadOnly } returns false - every { networkConnection.isWifiConnected() } returns true - coEvery { workerController.saveModels(*anyVararg()) } returns Unit viewModel.saveDownloadModels("", "") advanceUntilIdle() @@ -347,6 +375,15 @@ class CourseOutlineViewModelTest { @Test fun `saveDownloadModels only wifi download, with connection`() = runTest { + every { interactor.getCourseStructureFromCache() } returns courseStructure + coEvery { interactor.getCourseStatus(any()) } returns CourseComponentStatus("id") + every { preferencesManager.videoSettings.wifiDownloadOnly } returns true + every { networkConnection.isWifiConnected() } returns true + every { networkConnection.isOnline() } returns true + coEvery { downloadDao.readAllData() } returns mockk() + coEvery { workerController.saveModels(*anyVararg()) } returns Unit + coEvery { downloadDao.readAllData() } returns flow { emit(emptyList()) } + val viewModel = CourseOutlineViewModel( "", interactor, @@ -357,9 +394,6 @@ class CourseOutlineViewModelTest { downloadDao, workerController ) - every { preferencesManager.videoSettings.wifiDownloadOnly } returns true - every { networkConnection.isWifiConnected() } returns true - coEvery { workerController.saveModels(*anyVararg()) } returns Unit viewModel.saveDownloadModels("", "") advanceUntilIdle() @@ -369,6 +403,13 @@ class CourseOutlineViewModelTest { @Test fun `saveDownloadModels only wifi download, without connection`() = runTest { + every { interactor.getCourseStructureFromCache() } returns courseStructure + every { preferencesManager.videoSettings.wifiDownloadOnly } returns true + every { networkConnection.isWifiConnected() } returns false + every { networkConnection.isOnline() } returns false + coEvery { workerController.saveModels(*anyVararg()) } returns Unit + coEvery { downloadDao.readAllData() } returns flow { emit(emptyList()) } + val viewModel = CourseOutlineViewModel( "", interactor, @@ -379,10 +420,6 @@ class CourseOutlineViewModelTest { downloadDao, workerController ) - every { preferencesManager.videoSettings.wifiDownloadOnly } returns true - every { networkConnection.isWifiConnected() } returns false - every { networkConnection.isOnline() } returns false - coEvery { workerController.saveModels(*anyVararg()) } returns Unit viewModel.saveDownloadModels("", "") diff --git a/course/src/test/java/com/raccoongang/course/presentation/section/CourseSectionViewModelTest.kt b/course/src/test/java/com/raccoongang/course/presentation/section/CourseSectionViewModelTest.kt index b0b04edc5..64c3df5c1 100644 --- a/course/src/test/java/com/raccoongang/course/presentation/section/CourseSectionViewModelTest.kt +++ b/course/src/test/java/com/raccoongang/course/presentation/section/CourseSectionViewModelTest.kt @@ -10,6 +10,8 @@ import com.raccoongang.core.UIMessage import com.raccoongang.core.data.storage.PreferencesManager import com.raccoongang.core.domain.model.Block import com.raccoongang.core.domain.model.BlockCounts +import com.raccoongang.core.domain.model.CourseStructure +import com.raccoongang.core.domain.model.CoursewareAccess import com.raccoongang.core.module.DownloadWorkerController import com.raccoongang.core.module.db.* import com.raccoongang.core.presentation.course.CourseViewMode @@ -32,6 +34,7 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule import java.net.UnknownHostException +import java.util.* @OptIn(ExperimentalCoroutinesApi::class) class CourseSectionViewModelTest { @@ -101,6 +104,30 @@ class CourseSectionViewModelTest { ) ) + private val courseStructure = CourseStructure( + root = "", + blockData = blocks, + id = "id", + name = "Course name", + number = "", + org = "Org", + start = Date(), + startDisplay = "", + startType = "", + end = Date(), + coursewareAccess = CoursewareAccess( + true, + "", + "", + "", + "", + "" + ), + media = null, + certificate = null, + isSelfPaced = false + ) + private val downloadModel = DownloadModel( "id", "title", @@ -192,8 +219,8 @@ class CourseSectionViewModelTest { coEvery { downloadDao.readAllData() } returns flow { emit(listOf(DownloadModelEntity.createFrom(downloadModel))) } - coEvery { interactor.getCourseStructureFromCache() } returns emptyList() - coEvery { interactor.getCourseStructureForVideos() } returns blocks + coEvery { interactor.getCourseStructureFromCache() } returns courseStructure + coEvery { interactor.getCourseStructureForVideos() } returns courseStructure viewModel.getBlocks("id", CourseViewMode.VIDEOS) advanceUntilIdle() @@ -289,8 +316,8 @@ class CourseSectionViewModelTest { emit(emptyList()) } } - coEvery { interactor.getCourseStructureFromCache() } returns emptyList() - coEvery { interactor.getCourseStructureForVideos() } returns blocks + coEvery { interactor.getCourseStructureFromCache() } returns courseStructure + coEvery { interactor.getCourseStructureForVideos() } returns courseStructure val mockLifeCycleOwner: LifecycleOwner = mockk() val lifecycleRegistry = LifecycleRegistry(mockLifeCycleOwner) diff --git a/course/src/test/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerViewModelTest.kt b/course/src/test/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerViewModelTest.kt index 4c589b037..af3f4413b 100644 --- a/course/src/test/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerViewModelTest.kt +++ b/course/src/test/java/com/raccoongang/course/presentation/unit/container/CourseUnitContainerViewModelTest.kt @@ -4,7 +4,10 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.raccoongang.core.BlockType import com.raccoongang.core.domain.model.Block import com.raccoongang.core.domain.model.BlockCounts +import com.raccoongang.core.domain.model.CourseStructure +import com.raccoongang.core.domain.model.CoursewareAccess import com.raccoongang.core.presentation.course.CourseViewMode +import com.raccoongang.core.system.notifier.CourseNotifier import com.raccoongang.course.domain.interactor.CourseInteractor import io.mockk.every import io.mockk.mockk @@ -18,6 +21,7 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule import java.net.UnknownHostException +import java.util.* @OptIn(ExperimentalCoroutinesApi::class) class CourseUnitContainerViewModelTest { @@ -28,6 +32,7 @@ class CourseUnitContainerViewModelTest { private val dispatcher = StandardTestDispatcher() private val interactor = mockk() + private val notifier = mockk() private val blocks = listOf( Block( @@ -93,6 +98,30 @@ class CourseUnitContainerViewModelTest { ) + private val courseStructure = CourseStructure( + root = "", + blockData = blocks, + id = "id", + name = "Course name", + number = "", + org = "Org", + start = Date(), + startDisplay = "", + startType = "", + end = Date(), + coursewareAccess = CoursewareAccess( + true, + "", + "", + "", + "", + "" + ), + media = null, + certificate = null, + isSelfPaced = false + ) + @Before fun setUp() { Dispatchers.setMain(dispatcher) @@ -105,7 +134,7 @@ class CourseUnitContainerViewModelTest { @Test fun `getBlocks no internet connection exception`() = runTest { - val viewModel = CourseUnitContainerViewModel(interactor, "") + val viewModel = CourseUnitContainerViewModel(interactor, notifier, "") every { interactor.getCourseStructureFromCache() } throws UnknownHostException() every { interactor.getCourseStructureForVideos() } throws UnknownHostException() @@ -118,7 +147,7 @@ class CourseUnitContainerViewModelTest { @Test fun `getBlocks unknown exception`() = runTest { - val viewModel = CourseUnitContainerViewModel(interactor, "") + val viewModel = CourseUnitContainerViewModel(interactor, notifier, "") every { interactor.getCourseStructureFromCache() } throws UnknownHostException() every { interactor.getCourseStructureForVideos() } throws UnknownHostException() @@ -131,10 +160,10 @@ class CourseUnitContainerViewModelTest { @Test fun `getBlocks unknown success`() = runTest { - val viewModel = CourseUnitContainerViewModel(interactor, "") + val viewModel = CourseUnitContainerViewModel(interactor, notifier, "") - every { interactor.getCourseStructureFromCache() } returns blocks - every { interactor.getCourseStructureForVideos() } returns blocks + every { interactor.getCourseStructureFromCache() } returns courseStructure + every { interactor.getCourseStructureForVideos() } returns courseStructure viewModel.loadBlocks(CourseViewMode.VIDEOS) @@ -146,9 +175,9 @@ class CourseUnitContainerViewModelTest { @Test fun `setupCurrentIndex`() = runTest { - val viewModel = CourseUnitContainerViewModel(interactor, "") - every { interactor.getCourseStructureFromCache() } returns blocks - every { interactor.getCourseStructureForVideos() } returns blocks + val viewModel = CourseUnitContainerViewModel(interactor, notifier, "") + every { interactor.getCourseStructureFromCache() } returns courseStructure + every { interactor.getCourseStructureForVideos() } returns courseStructure viewModel.loadBlocks(CourseViewMode.VIDEOS) viewModel.setupCurrentIndex("id") @@ -160,9 +189,9 @@ class CourseUnitContainerViewModelTest { @Test fun `prevVertical`() = runTest { - val viewModel = CourseUnitContainerViewModel(interactor, "") - every { interactor.getCourseStructureFromCache() } returns blocks - every { interactor.getCourseStructureForVideos() } returns blocks + val viewModel = CourseUnitContainerViewModel(interactor, notifier, "") + every { interactor.getCourseStructureFromCache() } returns courseStructure + every { interactor.getCourseStructureForVideos() } returns courseStructure viewModel.loadBlocks(CourseViewMode.VIDEOS) viewModel.setupCurrentIndex("id2") @@ -177,9 +206,9 @@ class CourseUnitContainerViewModelTest { @Test fun `nextVertical null`() = runTest { - val viewModel = CourseUnitContainerViewModel(interactor, "") - every { interactor.getCourseStructureFromCache() } returns blocks - every { interactor.getCourseStructureForVideos() } returns blocks + val viewModel = CourseUnitContainerViewModel(interactor, notifier, "") + every { interactor.getCourseStructureFromCache() } returns courseStructure + every { interactor.getCourseStructureForVideos() } returns courseStructure viewModel.loadBlocks(CourseViewMode.VIDEOS) viewModel.setupCurrentIndex("id2") @@ -194,9 +223,9 @@ class CourseUnitContainerViewModelTest { @Test fun `getCurrentBlock test`() = runTest { - val viewModel = CourseUnitContainerViewModel(interactor, "") - every { interactor.getCourseStructureFromCache() } returns blocks - every { interactor.getCourseStructureForVideos() } returns blocks + val viewModel = CourseUnitContainerViewModel(interactor, notifier, "") + every { interactor.getCourseStructureFromCache() } returns courseStructure + every { interactor.getCourseStructureForVideos() } returns courseStructure viewModel.loadBlocks(CourseViewMode.VIDEOS) viewModel.setupCurrentIndex("id") @@ -210,9 +239,9 @@ class CourseUnitContainerViewModelTest { @Test fun `moveToPrevBlock null`() = runTest { - val viewModel = CourseUnitContainerViewModel(interactor, "") - every { interactor.getCourseStructureFromCache() } returns blocks - every { interactor.getCourseStructureForVideos() } returns blocks + val viewModel = CourseUnitContainerViewModel(interactor, notifier, "") + every { interactor.getCourseStructureFromCache() } returns courseStructure + every { interactor.getCourseStructureForVideos() } returns courseStructure viewModel.loadBlocks(CourseViewMode.VIDEOS) viewModel.setupCurrentIndex("id") @@ -226,9 +255,9 @@ class CourseUnitContainerViewModelTest { @Test fun `moveToPrevBlock not null`() = runTest { - val viewModel = CourseUnitContainerViewModel(interactor, "") - every { interactor.getCourseStructureFromCache() } returns blocks - every { interactor.getCourseStructureForVideos() } returns blocks + val viewModel = CourseUnitContainerViewModel(interactor, notifier, "") + every { interactor.getCourseStructureFromCache() } returns courseStructure + every { interactor.getCourseStructureForVideos() } returns courseStructure viewModel.loadBlocks(CourseViewMode.VIDEOS) viewModel.setupCurrentIndex("id3") @@ -242,9 +271,9 @@ class CourseUnitContainerViewModelTest { @Test fun `moveToNextBlock null`() = runTest { - val viewModel = CourseUnitContainerViewModel(interactor, "") - every { interactor.getCourseStructureFromCache() } returns blocks - every { interactor.getCourseStructureForVideos() } returns blocks + val viewModel = CourseUnitContainerViewModel(interactor, notifier, "") + every { interactor.getCourseStructureFromCache() } returns courseStructure + every { interactor.getCourseStructureForVideos() } returns courseStructure viewModel.loadBlocks(CourseViewMode.VIDEOS) viewModel.setupCurrentIndex("id3") @@ -258,9 +287,9 @@ class CourseUnitContainerViewModelTest { @Test fun `moveToNextBlock not null`() = runTest { - val viewModel = CourseUnitContainerViewModel(interactor, "") - every { interactor.getCourseStructureFromCache() } returns blocks - every { interactor.getCourseStructureForVideos() } returns blocks + val viewModel = CourseUnitContainerViewModel(interactor, notifier, "") + every { interactor.getCourseStructureFromCache() } returns courseStructure + every { interactor.getCourseStructureForVideos() } returns courseStructure viewModel.loadBlocks(CourseViewMode.VIDEOS) viewModel.setupCurrentIndex("id") @@ -274,9 +303,9 @@ class CourseUnitContainerViewModelTest { @Test fun `currentIndex equals 0`() = runTest { - val viewModel = CourseUnitContainerViewModel(interactor, "") - every { interactor.getCourseStructureFromCache() } returns blocks - every { interactor.getCourseStructureForVideos() } returns blocks + val viewModel = CourseUnitContainerViewModel(interactor, notifier, "") + every { interactor.getCourseStructureFromCache() } returns courseStructure + every { interactor.getCourseStructureForVideos() } returns courseStructure viewModel.loadBlocks(CourseViewMode.VIDEOS) viewModel.setupCurrentIndex("id") @@ -292,9 +321,9 @@ class CourseUnitContainerViewModelTest { @Test fun `currentIndex isLastIndex`() = runTest { - val viewModel = CourseUnitContainerViewModel(interactor, "") - every { interactor.getCourseStructureFromCache() } returns blocks - every { interactor.getCourseStructureForVideos() } returns blocks + val viewModel = CourseUnitContainerViewModel(interactor, notifier, "") + every { interactor.getCourseStructureFromCache() } returns courseStructure + every { interactor.getCourseStructureForVideos() } returns courseStructure viewModel.loadBlocks(CourseViewMode.VIDEOS) viewModel.setupCurrentIndex("id3") diff --git a/course/src/test/java/com/raccoongang/course/presentation/unit/video/VideoUnitViewModelTest.kt b/course/src/test/java/com/raccoongang/course/presentation/unit/video/VideoUnitViewModelTest.kt index 80cf72840..db9c48f77 100644 --- a/course/src/test/java/com/raccoongang/course/presentation/unit/video/VideoUnitViewModelTest.kt +++ b/course/src/test/java/com/raccoongang/course/presentation/unit/video/VideoUnitViewModelTest.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import com.raccoongang.core.data.storage.PreferencesManager +import com.raccoongang.core.module.TranscriptManager import com.raccoongang.core.system.connection.NetworkConnection import com.raccoongang.core.system.notifier.CourseNotifier import com.raccoongang.core.system.notifier.CourseVideoPositionChanged @@ -34,6 +35,7 @@ class VideoUnitViewModelTest { private val notifier = mockk() private val preferencesManager = mockk() private val networkConnection = mockk() + private val transcriptManager = mockk() @Before @@ -48,7 +50,14 @@ class VideoUnitViewModelTest { @Test fun `markBlockCompleted exception`() = runTest { - val viewModel = VideoUnitViewModel("", courseRepository, preferencesManager, notifier, networkConnection) + val viewModel = VideoUnitViewModel( + "", + courseRepository, + preferencesManager, + notifier, + networkConnection, + transcriptManager + ) coEvery { courseRepository.markBlocksCompletion( any(), @@ -68,7 +77,14 @@ class VideoUnitViewModelTest { @Test fun `markBlockCompleted success`() = runTest { - val viewModel = VideoUnitViewModel("", courseRepository, preferencesManager, notifier, networkConnection) + val viewModel = VideoUnitViewModel( + "", + courseRepository, + preferencesManager, + notifier, + networkConnection, + transcriptManager + ) coEvery { courseRepository.markBlocksCompletion( any(), @@ -88,7 +104,14 @@ class VideoUnitViewModelTest { @Test fun `CourseVideoPositionChanged notifier test`() = runTest { - val viewModel = VideoUnitViewModel("", courseRepository, preferencesManager, notifier, networkConnection) + val viewModel = VideoUnitViewModel( + "", + courseRepository, + preferencesManager, + notifier, + networkConnection, + transcriptManager + ) coEvery { notifier.notifier } returns flow { emit(CourseVideoPositionChanged("", 10)) } val mockLifeCycleOwner: LifecycleOwner = mockk() val lifecycleRegistry = LifecycleRegistry(mockLifeCycleOwner) @@ -97,7 +120,7 @@ class VideoUnitViewModelTest { advanceUntilIdle() - assert(viewModel.currentVideoTime == 10L) + assert(viewModel.currentVideoTime.value == 10L) assert(viewModel.isUpdated.value == true) } diff --git a/course/src/test/java/com/raccoongang/course/presentation/units/CourseUnitsViewModelTest.kt b/course/src/test/java/com/raccoongang/course/presentation/units/CourseUnitsViewModelTest.kt index 2c1e267e6..0cacb2162 100644 --- a/course/src/test/java/com/raccoongang/course/presentation/units/CourseUnitsViewModelTest.kt +++ b/course/src/test/java/com/raccoongang/course/presentation/units/CourseUnitsViewModelTest.kt @@ -111,6 +111,15 @@ class CourseUnitsViewModelTest { ) ) + private val courseStructure = CourseStructure( + "", blocks, "", "", "", "", + null, "", "", null, + CoursewareAccess(false, "", "", "", "", ""), + null, + null, + false + ) + private val downloadModel = DownloadModel( "id", "title", @@ -189,8 +198,8 @@ class CourseUnitsViewModelTest { networkConnection, workerController ) - every { interactor.getCourseStructureFromCache() } returns blocks - every { interactor.getCourseStructureForVideos() } returns blocks + every { interactor.getCourseStructureFromCache() } returns courseStructure + every { interactor.getCourseStructureForVideos() } returns courseStructure coEvery { interactor.getDownloadModels() } returns flow { emit(listOf(downloadModel)) } @@ -213,8 +222,8 @@ class CourseUnitsViewModelTest { networkConnection, workerController ) - every { interactor.getCourseStructureFromCache() } returns blocks - every { interactor.getCourseStructureForVideos() } returns blocks + every { interactor.getCourseStructureFromCache() } returns courseStructure + every { interactor.getCourseStructureForVideos() } returns courseStructure coEvery { interactor.getDownloadModels() } returns flow { emit(listOf(downloadModel)) } @@ -237,8 +246,8 @@ class CourseUnitsViewModelTest { networkConnection, workerController ) - every { interactor.getCourseStructureFromCache() } returns blocks - every { interactor.getCourseStructureForVideos() } returns blocks + every { interactor.getCourseStructureFromCache() } returns courseStructure + every { interactor.getCourseStructureForVideos() } returns courseStructure coEvery { interactor.getDownloadModels() } returns flow { repeat(5) { delay(10000) diff --git a/course/src/test/java/com/raccoongang/course/presentation/videos/CourseVideoViewModelTest.kt b/course/src/test/java/com/raccoongang/course/presentation/videos/CourseVideoViewModelTest.kt index 4f25e69d7..579cf1e76 100644 --- a/course/src/test/java/com/raccoongang/course/presentation/videos/CourseVideoViewModelTest.kt +++ b/course/src/test/java/com/raccoongang/course/presentation/videos/CourseVideoViewModelTest.kt @@ -8,6 +8,8 @@ import com.raccoongang.core.BlockType import com.raccoongang.core.data.storage.PreferencesManager import com.raccoongang.core.domain.model.Block import com.raccoongang.core.domain.model.BlockCounts +import com.raccoongang.core.domain.model.CourseStructure +import com.raccoongang.core.domain.model.CoursewareAccess import com.raccoongang.core.module.DownloadWorkerController import com.raccoongang.core.module.db.DownloadDao import com.raccoongang.core.system.ResourceManager @@ -27,6 +29,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule +import java.util.* @OptIn(ExperimentalCoroutinesApi::class) class CourseVideoViewModelTest { @@ -45,7 +48,6 @@ class CourseVideoViewModelTest { private val cantDownload = "You can download content only from Wi-fi" - private val blocks = listOf( Block( id = "id", @@ -94,6 +96,30 @@ class CourseVideoViewModelTest { ) ) + private val courseStructure = CourseStructure( + root = "", + blockData = blocks, + id = "id", + name = "Course name", + number = "", + org = "Org", + start = Date(), + startDisplay = "", + startType = "", + end = Date(), + coursewareAccess = CoursewareAccess( + true, + "", + "", + "", + "", + "" + ), + media = null, + certificate = null, + isSelfPaced = false + ) + @Before fun setUp() { @@ -109,6 +135,9 @@ class CourseVideoViewModelTest { @Test fun `getVideos empty list`() = runTest { + every { interactor.getCourseStructureForVideos() } returns courseStructure.copy(blockData = emptyList()) + every { downloadDao.readAllData() } returns flow { emit(emptyList()) } + val viewModel = CourseVideoViewModel( "", interactor, @@ -120,17 +149,18 @@ class CourseVideoViewModelTest { workerController ) - every { interactor.getCourseStructureForVideos() } returns emptyList() viewModel.getVideos() advanceUntilIdle() - coVerify(exactly = 1) { interactor.getCourseStructureForVideos() } + coVerify(exactly = 2) { interactor.getCourseStructureForVideos() } assert(viewModel.uiState.value is CourseVideosUIState.Empty) } @Test fun `getVideos success`() = runTest { + every { interactor.getCourseStructureForVideos() } returns courseStructure + every { downloadDao.readAllData() } returns flow { emit(emptyList()) } val viewModel = CourseVideoViewModel( "", interactor, @@ -142,17 +172,25 @@ class CourseVideoViewModelTest { workerController ) - every { interactor.getCourseStructureForVideos() } returns blocks + viewModel.getVideos() advanceUntilIdle() - coVerify(exactly = 1) { interactor.getCourseStructureForVideos() } + coVerify(exactly = 2) { interactor.getCourseStructureForVideos() } assert(viewModel.uiState.value is CourseVideosUIState.CourseData) } @Test fun `updateVideos success`() = runTest { + every { interactor.getCourseStructureForVideos() } returns courseStructure + coEvery { notifier.notifier } returns flow { emit(CourseStructureUpdated("", false)) } + every { downloadDao.readAllData() } returns flow { + repeat(5) { + delay(10000) + emit(emptyList()) + } + } val viewModel = CourseVideoViewModel( "", interactor, @@ -164,15 +202,6 @@ class CourseVideoViewModelTest { workerController ) - every { interactor.getCourseStructureForVideos() } returns blocks - coEvery { notifier.notifier } returns flow { emit(CourseStructureUpdated("", false)) } - every { downloadDao.readAllData() } returns flow { - repeat(5) { - delay(10000) - emit(emptyList()) - } - } - val mockLifeCycleOwner: LifecycleOwner = mockk() val lifecycleRegistry = LifecycleRegistry(mockLifeCycleOwner) lifecycleRegistry.addObserver(viewModel) @@ -180,7 +209,7 @@ class CourseVideoViewModelTest { advanceUntilIdle() - coVerify(exactly = 1) { interactor.getCourseStructureForVideos() } + coVerify(exactly = 2) { interactor.getCourseStructureForVideos() } assert(viewModel.uiState.value is CourseVideosUIState.CourseData) assert(viewModel.isUpdating.value == false) diff --git a/dashboard/src/test/java/com/raccoongang/dashboard/presentation/DashboardViewModelTest.kt b/dashboard/src/test/java/com/raccoongang/dashboard/presentation/DashboardViewModelTest.kt index ed761ac12..fc1d94966 100644 --- a/dashboard/src/test/java/com/raccoongang/dashboard/presentation/DashboardViewModelTest.kt +++ b/dashboard/src/test/java/com/raccoongang/dashboard/presentation/DashboardViewModelTest.kt @@ -7,6 +7,8 @@ import androidx.lifecycle.LifecycleRegistry import com.raccoongang.core.system.connection.NetworkConnection import com.raccoongang.core.R import com.raccoongang.core.UIMessage +import com.raccoongang.core.domain.model.DashboardCourseList +import com.raccoongang.core.domain.model.Pagination import com.raccoongang.core.system.ResourceManager import com.raccoongang.core.system.notifier.CourseDashboardUpdate import com.raccoongang.core.system.notifier.CourseNotifier @@ -33,7 +35,6 @@ class DashboardViewModelTest { @get:Rule val testInstantTaskExecutorRule: TestRule = InstantTaskExecutorRule() - private val dispatcher = StandardTestDispatcher() private val resourceManager = mockk() @@ -44,6 +45,11 @@ class DashboardViewModelTest { private val noInternet = "Slow or no internet connection" private val somethingWrong = "Something went wrong" + private val dashboardCourseList = DashboardCourseList( + Pagination(10, "", 3, ""), + listOf(mockk()) + ) + @Before fun setUp() { Dispatchers.setMain(dispatcher) @@ -60,11 +66,11 @@ class DashboardViewModelTest { fun `getCourses no internet connection`() = runTest { val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier) every { networkConnection.isOnline() } returns true - coEvery { interactor.getEnrolledCourses() } throws UnknownHostException() + coEvery { interactor.getEnrolledCourses(any()) } throws UnknownHostException() advanceUntilIdle() - coVerify(exactly = 1) { interactor.getEnrolledCourses() } + coVerify(exactly = 1) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage @@ -76,11 +82,11 @@ class DashboardViewModelTest { fun `getCourses unknown error`() = runTest { val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier) every { networkConnection.isOnline() } returns true - coEvery { interactor.getEnrolledCourses() } throws Exception() + coEvery { interactor.getEnrolledCourses(any()) } throws Exception() advanceUntilIdle() - coVerify(exactly = 1) { interactor.getEnrolledCourses() } + coVerify(exactly = 1) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage @@ -92,11 +98,35 @@ class DashboardViewModelTest { fun `getCourses from network`() = runTest { val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier) every { networkConnection.isOnline() } returns true - coEvery { interactor.getEnrolledCourses() } returns emptyList() + coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList + coEvery { interactor.getEnrolledCoursesFromCache() } returns listOf(mockk()) + + advanceUntilIdle() + + coVerify(exactly = 1) { interactor.getEnrolledCourses(any()) } + coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } + + assert(viewModel.uiMessage.value == null) + assert(viewModel.uiState.value is DashboardUIState.Courses) + } + + @Test + fun `getCourses from network with next page`() = runTest { + val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier) + every { networkConnection.isOnline() } returns true + coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList.copy( + Pagination( + 10, + "2", + 2, + "" + ) + ) + coEvery { interactor.getEnrolledCoursesFromCache() } returns listOf(mockk()) advanceUntilIdle() - coVerify(exactly = 1) { interactor.getEnrolledCourses() } + coVerify(exactly = 1) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } assert(viewModel.uiMessage.value == null) @@ -106,13 +136,13 @@ class DashboardViewModelTest { @Test fun `getCourses from cache`() = runTest { every { networkConnection.isOnline() } returns false - coEvery { interactor.getEnrolledCoursesFromCache() } returns emptyList() + coEvery { interactor.getEnrolledCoursesFromCache() } returns listOf(mockk()) val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier) advanceUntilIdle() - coVerify(exactly = 0) { interactor.getEnrolledCourses() } + coVerify(exactly = 0) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 1) { interactor.getEnrolledCoursesFromCache() } assert(viewModel.uiMessage.value == null) @@ -122,14 +152,14 @@ class DashboardViewModelTest { @Test fun `updateCourses no internet error`() = runTest { every { networkConnection.isOnline() } returns true - coEvery { interactor.getEnrolledCourses() } returns emptyList() + coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier) - coEvery { interactor.getEnrolledCourses() } throws UnknownHostException() + coEvery { interactor.getEnrolledCourses(any()) } throws UnknownHostException() viewModel.updateCourses() advanceUntilIdle() - coVerify(exactly = 2) { interactor.getEnrolledCourses() } + coVerify(exactly = 2) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage @@ -141,15 +171,15 @@ class DashboardViewModelTest { @Test fun `updateCourses unknown exception`() = runTest { every { networkConnection.isOnline() } returns true - coEvery { interactor.getEnrolledCourses() } returns emptyList() + coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier) - coEvery { interactor.getEnrolledCourses() } throws Exception() + coEvery { interactor.getEnrolledCourses(any()) } throws Exception() viewModel.updateCourses() advanceUntilIdle() - coVerify(exactly = 2) { interactor.getEnrolledCourses() } + coVerify(exactly = 2) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage @@ -161,13 +191,30 @@ class DashboardViewModelTest { @Test fun `updateCourses success`() = runTest { every { networkConnection.isOnline() } returns true - coEvery { interactor.getEnrolledCourses() } returns emptyList() + coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList + val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier) + + viewModel.updateCourses() + advanceUntilIdle() + + coVerify(exactly = 2) { interactor.getEnrolledCourses(any()) } + coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } + + assert(viewModel.uiMessage.value == null) + assert(viewModel.updating.value == false) + assert(viewModel.uiState.value is DashboardUIState.Courses) + } + + @Test + fun `updateCourses success with next page`() = runTest { + every { networkConnection.isOnline() } returns true + coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList.copy(Pagination(10,"2",2,"")) val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier) viewModel.updateCourses() advanceUntilIdle() - coVerify(exactly = 2) { interactor.getEnrolledCourses() } + coVerify(exactly = 2) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } assert(viewModel.uiMessage.value == null) @@ -188,7 +235,7 @@ class DashboardViewModelTest { advanceUntilIdle() - coVerify(exactly = 1) { interactor.getEnrolledCourses() } + coVerify(exactly = 1) { interactor.getEnrolledCourses(any()) } } diff --git a/discovery/src/test/java/com/raccoongang/discovery/presentation/search/CourseSearchViewModelTest.kt b/discovery/src/test/java/com/raccoongang/discovery/presentation/search/CourseSearchViewModelTest.kt index 347575db6..916ca1a2d 100644 --- a/discovery/src/test/java/com/raccoongang/discovery/presentation/search/CourseSearchViewModelTest.kt +++ b/discovery/src/test/java/com/raccoongang/discovery/presentation/search/CourseSearchViewModelTest.kt @@ -60,7 +60,8 @@ class CourseSearchViewModelTest { end = "end", startDisplay = "startDisplay", startType = "startType", - overview = "" + overview = "", + false ) //endregion diff --git a/discussion/src/test/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsViewModelTest.kt b/discussion/src/test/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsViewModelTest.kt index b806b0947..dad00d8da 100644 --- a/discussion/src/test/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsViewModelTest.kt +++ b/discussion/src/test/java/com/raccoongang/discussion/presentation/comments/DiscussionCommentsViewModelTest.kt @@ -73,7 +73,10 @@ class DiscussionCommentsViewModelTest { 4, false, false, - mapOf() + mapOf(), + 0, + false, + false ) //endregion @@ -102,7 +105,8 @@ class DiscussionCommentsViewModelTest { "", 21, emptyList(), - emptyMap() + null, + mapOf() ) //endregion @@ -129,6 +133,7 @@ class DiscussionCommentsViewModelTest { @Test fun `getThreadComments no internet connection exception`() = runTest { coEvery { interactor.getThreadComments(any(), any()) } throws UnknownHostException() + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" val viewModel = DiscussionCommentsViewModel( interactor, @@ -152,6 +157,7 @@ class DiscussionCommentsViewModelTest { @Test fun `getThreadComments unknown exception`() = runTest { coEvery { interactor.getThreadComments(any(), any()) } throws Exception() + every { resourceManager.getString(eq(DiscussionType.QUESTION.resId)) } returns "" val viewModel = DiscussionCommentsViewModel( interactor, @@ -181,6 +187,8 @@ class DiscussionCommentsViewModelTest { Pagination(10, "2", 4, "1") ) coEvery { interactor.setThreadRead(any()) } returns mockThread + every { resourceManager.getString(eq(DiscussionType.QUESTION.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -209,6 +217,8 @@ class DiscussionCommentsViewModelTest { Pagination(10, "", 4, "1") ) coEvery { interactor.setThreadRead(any()) } returns mockThread + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -241,6 +251,8 @@ class DiscussionCommentsViewModelTest { Pagination(10, "", 4, "1") ) coEvery { interactor.setThreadRead(any()) } returns mockThread + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -275,6 +287,8 @@ class DiscussionCommentsViewModelTest { Pagination(10, "", 4, "1") ) coEvery { interactor.setThreadRead(any()) } throws UnknownHostException() + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -309,6 +323,8 @@ class DiscussionCommentsViewModelTest { comments, Pagination(10, "2", 4, "1") ) + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -341,6 +357,8 @@ class DiscussionCommentsViewModelTest { comments, Pagination(10, "", 4, "1") ) + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -369,6 +387,8 @@ class DiscussionCommentsViewModelTest { comments, Pagination(10, "", 4, "1") ) + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -396,6 +416,8 @@ class DiscussionCommentsViewModelTest { comments, Pagination(10, "", 4, "1") ) + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -422,6 +444,8 @@ class DiscussionCommentsViewModelTest { comments, Pagination(10, "", 4, "1") ) + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -450,6 +474,8 @@ class DiscussionCommentsViewModelTest { comments, Pagination(10, "", 4, "1") ) + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -478,6 +504,8 @@ class DiscussionCommentsViewModelTest { comments, Pagination(10, "", 4, "1") ) + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -505,6 +533,8 @@ class DiscussionCommentsViewModelTest { comments, Pagination(10, "", 4, "1") ) + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -532,6 +562,8 @@ class DiscussionCommentsViewModelTest { comments, Pagination(10, "", 4, "1") ) + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -559,6 +591,8 @@ class DiscussionCommentsViewModelTest { comments, Pagination(10, "", 4, "1") ) + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -585,6 +619,8 @@ class DiscussionCommentsViewModelTest { comments, Pagination(10, "", 4, "1") ) + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -612,6 +648,8 @@ class DiscussionCommentsViewModelTest { comments, Pagination(10, "", 4, "1") ) + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -639,6 +677,8 @@ class DiscussionCommentsViewModelTest { comments, Pagination(10, "", 4, "1") ) + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -666,6 +706,8 @@ class DiscussionCommentsViewModelTest { comments, Pagination(10, "", 4, "1") ) + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -693,6 +735,8 @@ class DiscussionCommentsViewModelTest { comments, Pagination(10, "", 4, "1") ) + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -721,6 +765,8 @@ class DiscussionCommentsViewModelTest { comments, Pagination(10, "", 4, "1") ) + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -747,6 +793,8 @@ class DiscussionCommentsViewModelTest { comments, Pagination(10, "", 4, "1") ) + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -781,6 +829,8 @@ class DiscussionCommentsViewModelTest { comments, Pagination(10, "2", 4, "1") ) + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -815,6 +865,8 @@ class DiscussionCommentsViewModelTest { comments, Pagination(10, "2", 4, "1") ) + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -847,6 +899,8 @@ class DiscussionCommentsViewModelTest { comments, Pagination(10, "2", 4, "1") ) + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -873,6 +927,8 @@ class DiscussionCommentsViewModelTest { comments, Pagination(10, "2", 4, "1") ) + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -900,6 +956,8 @@ class DiscussionCommentsViewModelTest { Pagination(10, "2", 4, "1") ) coEvery { interactor.setThreadRead(any()) } returns mockThread + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -909,14 +967,11 @@ class DiscussionCommentsViewModelTest { mockThread ) coEvery { interactor.createComment(any(), any(), any()) } returns mockComment - every { preferencesManager.profile?.username } returns "" - every { preferencesManager.profile?.profileImage } returns mockk() viewModel.createComment("") advanceUntilIdle() coVerify(exactly = 1) { interactor.createComment(any(), any(), any()) } - verify(exactly = 2) { preferencesManager.profile } assert(viewModel.uiMessage.value != null) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) @@ -928,6 +983,8 @@ class DiscussionCommentsViewModelTest { comments, Pagination(10, "2", 4, "1") ) + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -950,6 +1007,8 @@ class DiscussionCommentsViewModelTest { comments, Pagination(10, "2", 4, "1") ) + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, @@ -973,6 +1032,8 @@ class DiscussionCommentsViewModelTest { comments, Pagination(10, "", 4, "1") ) + every { resourceManager.getString(eq(mockThread.type.resId)) } returns "" + val viewModel = DiscussionCommentsViewModel( interactor, diff --git a/discussion/src/test/java/com/raccoongang/discussion/presentation/responses/DiscussionResponsesViewModelTest.kt b/discussion/src/test/java/com/raccoongang/discussion/presentation/responses/DiscussionResponsesViewModelTest.kt index 6ce6e6773..d586e431d 100644 --- a/discussion/src/test/java/com/raccoongang/discussion/presentation/responses/DiscussionResponsesViewModelTest.kt +++ b/discussion/src/test/java/com/raccoongang/discussion/presentation/responses/DiscussionResponsesViewModelTest.kt @@ -69,7 +69,10 @@ class DiscussionResponsesViewModelTest { 4, false, false, - mapOf() + mapOf(), + 0, + false, + false ) //endregion @@ -98,6 +101,7 @@ class DiscussionResponsesViewModelTest { "", 21, emptyList(), + null, emptyMap() ) @@ -516,14 +520,11 @@ class DiscussionResponsesViewModelTest { mockComment.copy(id = "0") ) coEvery { interactor.createComment(any(), any(), any()) } returns mockComment - every { preferencesManager.profile?.username } returns "" - every { preferencesManager.profile?.profileImage } returns mockk() viewModel.createComment("") advanceUntilIdle() coVerify(exactly = 1) { interactor.createComment(any(), any(), any()) } - verify(exactly = 2) { preferencesManager.profile } assert(viewModel.uiMessage.value != null) diff --git a/discussion/src/test/java/com/raccoongang/discussion/presentation/search/DiscussionSearchThreadViewModelTest.kt b/discussion/src/test/java/com/raccoongang/discussion/presentation/search/DiscussionSearchThreadViewModelTest.kt index cba85846f..fb2753180 100644 --- a/discussion/src/test/java/com/raccoongang/discussion/presentation/search/DiscussionSearchThreadViewModelTest.kt +++ b/discussion/src/test/java/com/raccoongang/discussion/presentation/search/DiscussionSearchThreadViewModelTest.kt @@ -77,7 +77,10 @@ class DiscussionSearchThreadViewModelTest { 4, false, false, - mapOf() + mapOf(), + 0, + false, + false ) //endregion diff --git a/discussion/src/test/java/com/raccoongang/discussion/presentation/threads/DiscussionAddThreadViewModelTest.kt b/discussion/src/test/java/com/raccoongang/discussion/presentation/threads/DiscussionAddThreadViewModelTest.kt index 972346a5e..f0559e59c 100644 --- a/discussion/src/test/java/com/raccoongang/discussion/presentation/threads/DiscussionAddThreadViewModelTest.kt +++ b/discussion/src/test/java/com/raccoongang/discussion/presentation/threads/DiscussionAddThreadViewModelTest.kt @@ -73,7 +73,10 @@ class DiscussionAddThreadViewModelTest { 4, false, false, - mapOf() + mapOf(), + 0, + false, + false ) //endregion @@ -102,6 +105,7 @@ class DiscussionAddThreadViewModelTest { "", 21, emptyList(), + null, emptyMap() ) @@ -127,6 +131,7 @@ class DiscussionAddThreadViewModelTest { "", 21, emptyList(), + null, mapOf("" to DiscussionProfile(ProfileImage("", "", "", "", false))) ) diff --git a/discussion/src/test/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsViewModelTest.kt b/discussion/src/test/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsViewModelTest.kt index f377fbf6f..14887621d 100644 --- a/discussion/src/test/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsViewModelTest.kt +++ b/discussion/src/test/java/com/raccoongang/discussion/presentation/threads/DiscussionThreadsViewModelTest.kt @@ -76,7 +76,10 @@ class DiscussionThreadsViewModelTest { 4, false, false, - mapOf() + mapOf(), + 0, + false, + false ) //endregion @@ -109,11 +112,18 @@ class DiscussionThreadsViewModelTest { "", DiscussionTopicsFragment.ALL_POSTS ) - coEvery { interactor.getAllThreads(any(), any(), any()) } throws UnknownHostException() + coEvery { + interactor.getAllThreads( + any(), + any(), + any(), + any() + ) + } throws UnknownHostException() viewModel.getThreadByType("") advanceUntilIdle() - coVerify(exactly = 1) { interactor.getAllThreads(any(), any(), any()) } + coVerify(exactly = 1) { interactor.getAllThreads(any(), any(), any(), any()) } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage assertEquals(noInternet, message?.message) @@ -130,11 +140,11 @@ class DiscussionThreadsViewModelTest { "", DiscussionTopicsFragment.ALL_POSTS ) - coEvery { interactor.getAllThreads(any(), any(), any()) } throws Exception() + coEvery { interactor.getAllThreads(any(), any(), any(), any()) } throws Exception() viewModel.getThreadByType("") advanceUntilIdle() - coVerify(exactly = 1) { interactor.getAllThreads(any(), any(), any()) } + coVerify(exactly = 1) { interactor.getAllThreads(any(), any(), any(), any()) } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage assertEquals(somethingWrong, message?.message) @@ -151,12 +161,12 @@ class DiscussionThreadsViewModelTest { "", DiscussionTopicsFragment.ALL_POSTS ) - coEvery { interactor.getAllThreads("", "", range(1, 2)) } returns ThreadsData( + coEvery { interactor.getAllThreads("", "", null, range(1, 2)) } returns ThreadsData( threads, "", Pagination(10, "2", 4, "1") ) - coEvery { interactor.getAllThreads("", "", eq(3)) } returns ThreadsData( + coEvery { interactor.getAllThreads("", "", null, eq(3)) } returns ThreadsData( threads, "", Pagination(10, "", 4, "1") @@ -164,7 +174,7 @@ class DiscussionThreadsViewModelTest { viewModel.getThreadByType("") advanceUntilIdle() - coVerify(exactly = 3) { interactor.getAllThreads(any(), any(), any()) } + coVerify(exactly = 1) { interactor.getAllThreads(any(), any(), any(), any()) } assert(viewModel.uiMessage.value == null) assert(viewModel.isUpdating.value == false) @@ -185,13 +195,14 @@ class DiscussionThreadsViewModelTest { any(), any(), any(), + any(), any() ) } throws UnknownHostException() viewModel.getThreadByType("") advanceUntilIdle() - coVerify(exactly = 1) { interactor.getFollowingThreads(any(), any(), any(), any()) } + coVerify(exactly = 1) { interactor.getFollowingThreads(any(), any(), any(), any(), any()) } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage assertEquals(noInternet, message?.message) @@ -208,11 +219,19 @@ class DiscussionThreadsViewModelTest { "", DiscussionTopicsFragment.FOLLOWING_POSTS ) - coEvery { interactor.getFollowingThreads(any(), any(), any(), any()) } throws Exception() + coEvery { + interactor.getFollowingThreads( + any(), + any(), + any(), + any(), + any() + ) + } throws Exception() viewModel.getThreadByType("") advanceUntilIdle() - coVerify(exactly = 1) { interactor.getFollowingThreads(any(), any(), any(), any()) } + coVerify(exactly = 1) { interactor.getFollowingThreads(any(), any(), any(), any(), any()) } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage assertEquals(somethingWrong, message?.message) @@ -229,12 +248,20 @@ class DiscussionThreadsViewModelTest { "", DiscussionTopicsFragment.FOLLOWING_POSTS ) - coEvery { interactor.getFollowingThreads("", any(), "", range(1, 2)) } returns ThreadsData( + coEvery { + interactor.getFollowingThreads( + "", + any(), + "", + null, + range(1, 2) + ) + } returns ThreadsData( threads, "", Pagination(10, "2", 4, "1") ) - coEvery { interactor.getFollowingThreads("", any(), "", eq(3)) } returns ThreadsData( + coEvery { interactor.getFollowingThreads("", any(), "", null, eq(3)) } returns ThreadsData( threads, "", Pagination(10, "", 4, "1") @@ -242,7 +269,7 @@ class DiscussionThreadsViewModelTest { viewModel.getThreadByType("") advanceUntilIdle() - coVerify(exactly = 3) { interactor.getFollowingThreads(any(), any(), any(), any()) } + coVerify(exactly = 1) { interactor.getFollowingThreads(any(), any(), any(), any(), any()) } assert(viewModel.uiMessage.value == null) assert(viewModel.isUpdating.value == false) @@ -258,11 +285,19 @@ class DiscussionThreadsViewModelTest { "", DiscussionTopicsFragment.TOPIC ) - coEvery { interactor.getThreads(any(), any(), any(), any()) } throws UnknownHostException() + coEvery { + interactor.getThreads( + any(), + any(), + any(), + any(), + any() + ) + } throws UnknownHostException() viewModel.getThreadByType("") advanceUntilIdle() - coVerify(exactly = 1) { interactor.getThreads(any(), any(), any(), any()) } + coVerify(exactly = 1) { interactor.getThreads(any(), any(), any(), any(), any()) } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage assertEquals(noInternet, message?.message) @@ -279,11 +314,11 @@ class DiscussionThreadsViewModelTest { "", DiscussionTopicsFragment.TOPIC ) - coEvery { interactor.getThreads(any(), any(), any(), any()) } throws Exception() + coEvery { interactor.getThreads(any(), any(), any(), any(), any()) } throws Exception() viewModel.getThreadByType("") advanceUntilIdle() - coVerify(exactly = 1) { interactor.getThreads(any(), any(), any(), any()) } + coVerify(exactly = 1) { interactor.getThreads(any(), any(), any(), any(), any()) } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage assertEquals(somethingWrong, message?.message) @@ -300,12 +335,12 @@ class DiscussionThreadsViewModelTest { "", DiscussionTopicsFragment.TOPIC ) - coEvery { interactor.getThreads("", any(), "", range(1, 2)) } returns ThreadsData( + coEvery { interactor.getThreads("", any(), "", null, range(1, 2)) } returns ThreadsData( threads, "", Pagination(10, "2", 4, "1") ) - coEvery { interactor.getThreads("", any(), "", eq(3)) } returns ThreadsData( + coEvery { interactor.getThreads("", any(), "", null, eq(3)) } returns ThreadsData( threads, "", Pagination(10, "", 4, "1") @@ -313,7 +348,7 @@ class DiscussionThreadsViewModelTest { viewModel.getThreadByType("") advanceUntilIdle() - coVerify(exactly = 3) { interactor.getThreads(any(), any(), any(), any()) } + coVerify(exactly = 1) { interactor.getThreads(any(), any(), any(), any(), any()) } assert(viewModel.uiMessage.value == null) assert(viewModel.isUpdating.value == false) @@ -329,6 +364,11 @@ class DiscussionThreadsViewModelTest { "", DiscussionTopicsFragment.TOPIC ) + coEvery { interactor.getThreads(any(),any(), any(), any(), any()) } returns ThreadsData( + threads, + "", + pagination = Pagination(10,"", 2,"") + ) viewModel.filterThreads(FilterType.ALL_POSTS.value) advanceUntilIdle() assert(viewModel.uiState.value is DiscussionThreadsUIState.Threads) @@ -343,6 +383,11 @@ class DiscussionThreadsViewModelTest { "", DiscussionTopicsFragment.TOPIC ) + coEvery { interactor.getThreads(any(),any(), any(), any(), any()) } returns ThreadsData( + threads, + "", + pagination = Pagination(10,"", 2,"") + ) viewModel.filterThreads(FilterType.UNREAD.value) advanceUntilIdle() assert(viewModel.uiState.value is DiscussionThreadsUIState.Threads) @@ -357,6 +402,11 @@ class DiscussionThreadsViewModelTest { "", DiscussionTopicsFragment.TOPIC ) + coEvery { interactor.getThreads(any(),any(), any(), any(), any()) } returns ThreadsData( + threads, + "", + pagination = Pagination(10,"", 2,"") + ) viewModel.filterThreads(FilterType.UNANSWERED.value) advanceUntilIdle() assert(viewModel.uiState.value is DiscussionThreadsUIState.Threads) @@ -371,12 +421,12 @@ class DiscussionThreadsViewModelTest { "", DiscussionTopicsFragment.TOPIC ) - coEvery { interactor.getThreads("", any(), "", range(1, 2)) } returns ThreadsData( + coEvery { interactor.getThreads("", any(), "", null, range(1, 2)) } returns ThreadsData( threads, "", Pagination(10, "2", 4, "1") ) - coEvery { interactor.getThreads("", any(), "", eq(3)) } returns ThreadsData( + coEvery { interactor.getThreads("", any(), "", null, eq(3)) } returns ThreadsData( threads, "", Pagination(10, "", 4, "1") @@ -384,7 +434,7 @@ class DiscussionThreadsViewModelTest { viewModel.updateThread("") advanceUntilIdle() - coVerify(exactly = 3) { interactor.getThreads(any(), any(), any(), any()) } + coVerify(exactly = 1) { interactor.getThreads(any(), any(), any(), any(), any()) } assert(viewModel.uiMessage.value == null) assert(viewModel.isUpdating.value == false) @@ -400,12 +450,12 @@ class DiscussionThreadsViewModelTest { "", DiscussionTopicsFragment.TOPIC ) - coEvery { interactor.getThreads("", any(), any(), range(1, 2)) } returns ThreadsData( + coEvery { interactor.getThreads("", any(), any(), null, range(1, 2)) } returns ThreadsData( threads, "", Pagination(10, "2", 4, "1") ) - coEvery { interactor.getThreads("", any(), any(), eq(3)) } returns ThreadsData( + coEvery { interactor.getThreads("", any(), any(), null, eq(3)) } returns ThreadsData( threads, "", Pagination(10, "", 4, "1") @@ -425,7 +475,7 @@ class DiscussionThreadsViewModelTest { viewModel.updateThread("date") advanceUntilIdle() - coVerify(exactly = 6) { interactor.getThreads(any(), any(), any(), any()) } + coVerify(exactly = 2) { interactor.getThreads(any(), any(), any(), any(), any()) } } @Test @@ -437,12 +487,12 @@ class DiscussionThreadsViewModelTest { "", DiscussionTopicsFragment.TOPIC ) - coEvery { interactor.getThreads("", any(), any(), range(1, 2)) } returns ThreadsData( + coEvery { interactor.getThreads("", any(), any(), null,range(1, 2)) } returns ThreadsData( threads, "", Pagination(10, "2", 4, "1") ) - coEvery { interactor.getThreads("", any(), any(), eq(3)) } returns ThreadsData( + coEvery { interactor.getThreads("", any(), any(), null,eq(3)) } returns ThreadsData( threads, "", Pagination(10, "", 4, "1") @@ -462,7 +512,7 @@ class DiscussionThreadsViewModelTest { viewModel.updateThread("date") advanceUntilIdle() - coVerify(exactly = 3) { interactor.getThreads(any(), any(), any(), any()) } + coVerify(exactly = 1) { interactor.getThreads(any(), any(), any(), any(), any()) } } diff --git a/profile/src/test/java/com/raccoongang/profile/presentation/edit/EditProfileViewModelTest.kt b/profile/src/test/java/com/raccoongang/profile/presentation/edit/EditProfileViewModelTest.kt index b687b798b..41ec01e2e 100644 --- a/profile/src/test/java/com/raccoongang/profile/presentation/edit/EditProfileViewModelTest.kt +++ b/profile/src/test/java/com/raccoongang/profile/presentation/edit/EditProfileViewModelTest.kt @@ -4,13 +4,12 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.raccoongang.core.R import com.raccoongang.core.UIMessage import com.raccoongang.core.domain.model.Account +import com.raccoongang.core.domain.model.ProfileImage import com.raccoongang.core.system.ResourceManager import com.raccoongang.profile.domain.interactor.ProfileInteractor +import com.raccoongang.profile.system.notifier.AccountUpdated import com.raccoongang.profile.system.notifier.ProfileNotifier -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every -import io.mockk.mockk +import io.mockk.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.* @@ -35,7 +34,24 @@ class EditProfileViewModelTest { private val interactor = mockk() private val notifier = mockk() - private val account = mockk() + private val account = Account( + username = "thom84", + bio = "He as compliment unreserved projecting. Between had observe pretend delight for believe. Do newspaper questions consulted sweetness do. Our sportsman his unwilling fulfilled departure law.", + requiresParentalConsent = true, + name = "Thomas", + country = "Ukraine", + isActive = true, + profileImage = ProfileImage("", "", "", "", false), + yearOfBirth = 2000, + levelOfEducation = "Bachelor", + goals = "130", + languageProficiencies = emptyList(), + gender = "male", + mailingAddress = "", + "", + null, + accountPrivacy = Account.Privacy.ALL_USERS + ) private val file = mockk() private val noInternet = "Slow or no internet connection" @@ -57,7 +73,6 @@ class EditProfileViewModelTest { fun `updateAccount no internet connection`() = runTest { val viewModel = EditProfileViewModel(interactor, resourceManager, notifier, account) coEvery { interactor.updateAccount(any()) } throws UnknownHostException() - every { account.copy() } returns account viewModel.updateAccount(emptyMap()) advanceUntilIdle() @@ -72,7 +87,7 @@ class EditProfileViewModelTest { fun `updateAccount unknown exception`() = runTest { val viewModel = EditProfileViewModel(interactor, resourceManager, notifier, account) coEvery { interactor.updateAccount(any()) } throws Exception() - every { account.copy() } returns account + viewModel.updateAccount(emptyMap()) advanceUntilIdle() @@ -87,8 +102,7 @@ class EditProfileViewModelTest { fun `updateAccount success`() = runTest { val viewModel = EditProfileViewModel(interactor, resourceManager, notifier, account) coEvery { interactor.updateAccount(any()) } returns account - coEvery { notifier.send(any()) } returns Unit - every { account.copy() } returns account + coEvery { notifier.send(any()) } returns Unit viewModel.updateAccount(emptyMap()) advanceUntilIdle() @@ -103,8 +117,8 @@ class EditProfileViewModelTest { val viewModel = EditProfileViewModel(interactor, resourceManager, notifier, account) coEvery { interactor.setProfileImage(any(), any()) } throws UnknownHostException() coEvery { interactor.updateAccount(any()) } returns account - coEvery { notifier.send(any()) } returns Unit - every { account.copy() } returns account + coEvery { notifier.send(AccountUpdated()) } returns Unit + viewModel.updateAccountAndImage(emptyMap(), file, "") advanceUntilIdle() @@ -122,8 +136,8 @@ class EditProfileViewModelTest { val viewModel = EditProfileViewModel(interactor, resourceManager, notifier, account) coEvery { interactor.setProfileImage(any(), any()) } throws Exception() coEvery { interactor.updateAccount(any()) } returns account - coEvery { notifier.send(any()) } returns Unit - every { account.copy() } returns account + coEvery { notifier.send(AccountUpdated()) } returns Unit + viewModel.updateAccountAndImage(emptyMap(), file, "") advanceUntilIdle() @@ -141,8 +155,8 @@ class EditProfileViewModelTest { val viewModel = EditProfileViewModel(interactor, resourceManager, notifier, account) coEvery { interactor.setProfileImage(any(), any()) } returns Unit coEvery { interactor.updateAccount(any()) } returns account - coEvery { notifier.send(any()) } returns Unit - every { account.copy() } returns account + coEvery { notifier.send(any()) } returns Unit + viewModel.updateAccountAndImage(emptyMap(), file, "") advanceUntilIdle() diff --git a/profile/src/test/java/com/raccoongang/profile/presentation/profile/ProfileViewModelTest.kt b/profile/src/test/java/com/raccoongang/profile/presentation/profile/ProfileViewModelTest.kt index 33fe425e1..ae7a671fe 100644 --- a/profile/src/test/java/com/raccoongang/profile/presentation/profile/ProfileViewModelTest.kt +++ b/profile/src/test/java/com/raccoongang/profile/presentation/profile/ProfileViewModelTest.kt @@ -8,6 +8,7 @@ import com.raccoongang.core.R import com.raccoongang.core.UIMessage import com.raccoongang.core.data.storage.PreferencesManager import com.raccoongang.core.domain.model.Account +import com.raccoongang.core.module.DownloadWorkerController import com.raccoongang.core.system.AppCookieManager import com.raccoongang.core.system.ResourceManager import com.raccoongang.profile.domain.interactor.ProfileInteractor @@ -43,6 +44,7 @@ class ProfileViewModelTest { private val interactor = mockk() private val notifier = mockk() private val cookieManager = mockk() + private val workerController = mockk() private val account = mockk() @@ -64,7 +66,15 @@ class ProfileViewModelTest { @Test fun `getAccount no internetConnection`() = runTest { val viewModel = - ProfileViewModel(interactor, preferencesManager, resourceManager, notifier, dispatcher, cookieManager) + ProfileViewModel( + interactor, + preferencesManager, + resourceManager, + notifier, + dispatcher, + cookieManager, + workerController + ) coEvery { interactor.getAccount() } throws UnknownHostException() advanceUntilIdle() @@ -78,7 +88,15 @@ class ProfileViewModelTest { @Test fun `getAccount unknown exception`() = runTest { val viewModel = - ProfileViewModel(interactor, preferencesManager, resourceManager, notifier, dispatcher, cookieManager) + ProfileViewModel( + interactor, + preferencesManager, + resourceManager, + notifier, + dispatcher, + cookieManager, + workerController + ) coEvery { interactor.getAccount() } throws Exception() advanceUntilIdle() @@ -92,7 +110,15 @@ class ProfileViewModelTest { @Test fun `getAccount success`() = runTest { val viewModel = - ProfileViewModel(interactor, preferencesManager, resourceManager, notifier, dispatcher, cookieManager) + ProfileViewModel( + interactor, + preferencesManager, + resourceManager, + notifier, + dispatcher, + cookieManager, + workerController + ) coEvery { interactor.getAccount() } returns account every { preferencesManager.profile = any() } returns Unit advanceUntilIdle() @@ -106,8 +132,17 @@ class ProfileViewModelTest { @Test fun `logout no internet connection`() = runTest { val viewModel = - ProfileViewModel(interactor, preferencesManager, resourceManager, notifier, dispatcher, cookieManager) + ProfileViewModel( + interactor, + preferencesManager, + resourceManager, + notifier, + dispatcher, + cookieManager, + workerController + ) coEvery { interactor.logout() } throws UnknownHostException() + coEvery { workerController.cancelWork() } returns Unit viewModel.logout() advanceUntilIdle() @@ -122,8 +157,17 @@ class ProfileViewModelTest { @Test fun `logout unknown exception`() = runTest { val viewModel = - ProfileViewModel(interactor, preferencesManager, resourceManager, notifier, dispatcher, cookieManager) + ProfileViewModel( + interactor, + preferencesManager, + resourceManager, + notifier, + dispatcher, + cookieManager, + workerController + ) coEvery { interactor.logout() } throws Exception() + coEvery { workerController.cancelWork() } returns Unit viewModel.logout() advanceUntilIdle() @@ -142,11 +186,13 @@ class ProfileViewModelTest { resourceManager, notifier, dispatcherIO, - cookieManager + cookieManager, + workerController ) coEvery { interactor.getAccount() } returns mockk() every { preferencesManager.profile = any() } returns Unit coEvery { interactor.logout() } returns Unit + coEvery { workerController.cancelWork() } returns Unit every { cookieManager.clearWebViewCookie() } returns Unit viewModel.logout() advanceUntilIdle() @@ -165,7 +211,8 @@ class ProfileViewModelTest { resourceManager, notifier, dispatcherIO, - cookieManager + cookieManager, + workerController ) every { notifier.notifier } returns flow { emit(AccountUpdated()) } val mockLifeCycleOwner: LifecycleOwner = mockk()