From ffac374281b0540603b2ed1c16ba04e3e6e40779 Mon Sep 17 00:00:00 2001 From: Martin Felber Date: Fri, 22 Nov 2024 11:01:58 +0100 Subject: [PATCH 1/8] Created package for ui --- .../www1/artemis/native_app/android/ui/MainActivity.kt | 6 +++--- .../native_app/feature/dashboard/DashboardScreenshots.kt | 2 ++ .../native_app/feature/dashboard/dashboard_module.kt | 1 + .../feature/dashboard/{ => ui}/CourseOverviewViewModel.kt | 3 +-- .../feature/dashboard/{ => ui}/CoursesOverview.kt | 6 +++--- .../native_app/feature/dashboard/DashboardE2eTest.kt | 6 +++++- 6 files changed, 15 insertions(+), 9 deletions(-) rename feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/{ => ui}/CourseOverviewViewModel.kt (98%) rename feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/{ => ui}/CoursesOverview.kt (99%) diff --git a/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/ui/MainActivity.kt b/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/ui/MainActivity.kt index e7903bcba..5582d5430 100644 --- a/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/ui/MainActivity.kt +++ b/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/ui/MainActivity.kt @@ -40,9 +40,9 @@ import de.tum.informatics.www1.artemis.native_app.feature.courseregistration.cou import de.tum.informatics.www1.artemis.native_app.feature.courseregistration.navigateToCourseRegistration import de.tum.informatics.www1.artemis.native_app.feature.courseview.ui.course_overview.course import de.tum.informatics.www1.artemis.native_app.feature.courseview.ui.course_overview.navigateToCourse -import de.tum.informatics.www1.artemis.native_app.feature.dashboard.DASHBOARD_DESTINATION -import de.tum.informatics.www1.artemis.native_app.feature.dashboard.dashboard -import de.tum.informatics.www1.artemis.native_app.feature.dashboard.navigateToDashboard +import de.tum.informatics.www1.artemis.native_app.feature.dashboard.ui.DASHBOARD_DESTINATION +import de.tum.informatics.www1.artemis.native_app.feature.dashboard.ui.dashboard +import de.tum.informatics.www1.artemis.native_app.feature.dashboard.ui.navigateToDashboard import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.ExerciseViewDestination import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.ExerciseViewMode import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.exercise diff --git a/feature/dashboard/src/debug/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/DashboardScreenshots.kt b/feature/dashboard/src/debug/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/DashboardScreenshots.kt index f5d28492f..8120ab925 100644 --- a/feature/dashboard/src/debug/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/DashboardScreenshots.kt +++ b/feature/dashboard/src/debug/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/DashboardScreenshots.kt @@ -21,6 +21,8 @@ import de.tum.informatics.www1.artemis.native_app.core.ui.LocalCourseImageProvid import de.tum.informatics.www1.artemis.native_app.core.ui.PlayStoreScreenshots import de.tum.informatics.www1.artemis.native_app.core.ui.ScreenshotFrame import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.DashboardService +import de.tum.informatics.www1.artemis.native_app.feature.dashboard.ui.CourseOverviewViewModel +import de.tum.informatics.www1.artemis.native_app.feature.dashboard.ui.CoursesOverview private const val IMAGE_MARS = "mars" private const val IMAGE_SATURN_5 = "saturn5" diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/dashboard_module.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/dashboard_module.kt index 1a229c804..373552f06 100644 --- a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/dashboard_module.kt +++ b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/dashboard_module.kt @@ -4,6 +4,7 @@ import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.Beta import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.DashboardService import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.impl.BetaHintServiceImpl import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.impl.DashboardServiceImpl +import de.tum.informatics.www1.artemis.native_app.feature.dashboard.ui.CourseOverviewViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/CourseOverviewViewModel.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CourseOverviewViewModel.kt similarity index 98% rename from feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/CourseOverviewViewModel.kt rename to feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CourseOverviewViewModel.kt index 64997b67b..9d0948f6e 100644 --- a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/CourseOverviewViewModel.kt +++ b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CourseOverviewViewModel.kt @@ -1,4 +1,4 @@ -package de.tum.informatics.www1.artemis.native_app.feature.dashboard +package de.tum.informatics.www1.artemis.native_app.feature.dashboard.ui import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -15,7 +15,6 @@ import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.Dash import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/CoursesOverview.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt similarity index 99% rename from feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/CoursesOverview.kt rename to feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt index dc93016f4..6ed027de4 100644 --- a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/CoursesOverview.kt +++ b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt @@ -1,4 +1,4 @@ -package de.tum.informatics.www1.artemis.native_app.feature.dashboard +package de.tum.informatics.www1.artemis.native_app.feature.dashboard.ui import androidx.compose.foundation.Canvas import androidx.compose.foundation.border @@ -9,12 +9,10 @@ import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.shape.RoundedCornerShape @@ -73,6 +71,8 @@ import de.tum.informatics.www1.artemis.native_app.core.ui.common.course.CourseEx import de.tum.informatics.www1.artemis.native_app.core.ui.common.course.CourseItemGrid import de.tum.informatics.www1.artemis.native_app.core.ui.common.course.ExpandedCourseItemHeader import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.CoursePointsDecimalFormat +import de.tum.informatics.www1.artemis.native_app.feature.dashboard.BuildConfig +import de.tum.informatics.www1.artemis.native_app.feature.dashboard.R import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.BetaHintService import kotlinx.coroutines.launch import org.koin.androidx.compose.getViewModel diff --git a/feature/dashboard/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/DashboardE2eTest.kt b/feature/dashboard/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/DashboardE2eTest.kt index 487d3d919..faad06077 100644 --- a/feature/dashboard/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/DashboardE2eTest.kt +++ b/feature/dashboard/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/DashboardE2eTest.kt @@ -10,12 +10,16 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performScrollToKey import androidx.test.platform.app.InstrumentationRegistry +import de.tum.informatics.www1.artemis.native_app.core.common.test.DefaultTestTimeoutMillis import de.tum.informatics.www1.artemis.native_app.core.common.test.EndToEndTest import de.tum.informatics.www1.artemis.native_app.core.test.BaseComposeTest import de.tum.informatics.www1.artemis.native_app.core.test.coreTestModules -import de.tum.informatics.www1.artemis.native_app.core.common.test.DefaultTestTimeoutMillis import de.tum.informatics.www1.artemis.native_app.core.test.test_setup.DefaultTimeoutMillis import de.tum.informatics.www1.artemis.native_app.core.test.test_setup.course_creation.createCourse +import de.tum.informatics.www1.artemis.native_app.feature.dashboard.ui.CourseOverviewViewModel +import de.tum.informatics.www1.artemis.native_app.feature.dashboard.ui.CoursesOverview +import de.tum.informatics.www1.artemis.native_app.feature.dashboard.ui.TEST_TAG_COURSE_LIST +import de.tum.informatics.www1.artemis.native_app.feature.dashboard.ui.testTagForCourse import de.tum.informatics.www1.artemis.native_app.feature.login.loginModule import de.tum.informatics.www1.artemis.native_app.feature.login.test.getAdminAccessToken import de.tum.informatics.www1.artemis.native_app.feature.login.test.performTestLogin From 162da1847771756600a3acee5772df70e0101cf1 Mon Sep 17 00:00:00 2001 From: Martin Felber Date: Fri, 22 Nov 2024 11:48:43 +0100 Subject: [PATCH 2/8] Add basics for survey hint --- .../feature/dashboard/dashboard_module.kt | 3 + .../dashboard/service/SurveyHintService.kt | 10 ++ .../service/impl/SurveyHintServiceImpl.kt | 42 +++++++++ .../feature/dashboard/ui/CoursesOverview.kt | 11 ++- .../feature/dashboard/ui/SurveyDialog.kt | 94 +++++++++++++++++++ 5 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/service/SurveyHintService.kt create mode 100644 feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/service/impl/SurveyHintServiceImpl.kt create mode 100644 feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/SurveyDialog.kt diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/dashboard_module.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/dashboard_module.kt index 373552f06..82e9061d2 100644 --- a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/dashboard_module.kt +++ b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/dashboard_module.kt @@ -2,8 +2,10 @@ package de.tum.informatics.www1.artemis.native_app.feature.dashboard import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.BetaHintService import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.DashboardService +import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.SurveyHintService import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.impl.BetaHintServiceImpl import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.impl.DashboardServiceImpl +import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.impl.SurveyHintServiceImpl import de.tum.informatics.www1.artemis.native_app.feature.dashboard.ui.CourseOverviewViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module @@ -12,4 +14,5 @@ val dashboardModule = module { viewModel { CourseOverviewViewModel(get(), get(), get(), get()) } single { DashboardServiceImpl(get()) } single { BetaHintServiceImpl(get()) } + single { SurveyHintServiceImpl(get()) } } \ No newline at end of file diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/service/SurveyHintService.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/service/SurveyHintService.kt new file mode 100644 index 000000000..ef8f2ebb4 --- /dev/null +++ b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/service/SurveyHintService.kt @@ -0,0 +1,10 @@ +package de.tum.informatics.www1.artemis.native_app.feature.dashboard.service + +import kotlinx.coroutines.flow.Flow + +interface SurveyHintService { + + val shouldShowSurveyHint: Flow + + suspend fun dismissSurveyHintPermanently() +} \ No newline at end of file diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/service/impl/SurveyHintServiceImpl.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/service/impl/SurveyHintServiceImpl.kt new file mode 100644 index 000000000..518f68134 --- /dev/null +++ b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/service/impl/SurveyHintServiceImpl.kt @@ -0,0 +1,42 @@ +package de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.impl + +import android.content.Context +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.preferencesDataStore +import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.SurveyHintService +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import java.time.LocalDate + + +// TODO: set the correct dates +private val SURVEY_START_DATE = LocalDate.of(2024, 1, 1) +private val SURVEY_END_DATE = LocalDate.of(2022, 12, 31) + +class SurveyHintServiceImpl( + private val context: Context, +) : SurveyHintService { + + private companion object { + private const val DATA_STORE_KEY = "survey_hint_store" + + private val KEY_SHOW_SURVEY = booleanPreferencesKey("showSurvey1") // Change this to 2 for the second survey + } + + private val Context.storage by preferencesDataStore(DATA_STORE_KEY) + + override val shouldShowSurveyHint: Flow = context.storage.data + .map { it[KEY_SHOW_SURVEY] ?: isSurveyActive() } + + override suspend fun dismissSurveyHintPermanently() { + context.storage.edit { data -> + data[KEY_SHOW_SURVEY] = false + } + } + + private fun isSurveyActive(): Boolean { + val currentDate = LocalDate.now() + return currentDate.isAfter(SURVEY_START_DATE) && currentDate.isBefore(SURVEY_END_DATE) + } +} \ No newline at end of file diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt index 6ed027de4..596ff0f4b 100644 --- a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt +++ b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt @@ -74,6 +74,7 @@ import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.CoursePointsD import de.tum.informatics.www1.artemis.native_app.feature.dashboard.BuildConfig import de.tum.informatics.www1.artemis.native_app.feature.dashboard.R import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.BetaHintService +import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.SurveyHintService import kotlinx.coroutines.launch import org.koin.androidx.compose.getViewModel import org.koin.compose.koinInject @@ -116,7 +117,8 @@ internal fun CoursesOverview( onClickRegisterForCourse: () -> Unit, onViewCourse: (courseId: Long) -> Unit, isBeta: Boolean = BuildConfig.isBeta, - betaHintService: BetaHintService = koinInject() + betaHintService: BetaHintService = koinInject(), + surveyHintService: SurveyHintService = koinInject() ) { val coursesDataState by viewModel.dashboard.collectAsState() @@ -139,6 +141,13 @@ internal fun CoursesOverview( if (shouldDisplayBetaDialog) displayBetaDialog = true } + val shouldDisplaySurveyHint by surveyHintService.shouldShowSurveyHint.collectAsState(initial = false) + var displaySurveyHint by rememberSaveable { mutableStateOf(false) } + + LaunchedEffect(shouldDisplaySurveyHint) { + if (shouldDisplaySurveyHint) displaySurveyHint = true + } + Scaffold( modifier = modifier.then(Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)), topBar = { diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/SurveyDialog.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/SurveyDialog.kt new file mode 100644 index 000000000..abae031f3 --- /dev/null +++ b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/SurveyDialog.kt @@ -0,0 +1,94 @@ +package de.tum.informatics.www1.artemis.native_app.feature.dashboard.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import de.tum.informatics.www1.artemis.native_app.feature.dashboard.R + +private const val SURVEY_URL = "https://example.com/survey" + +@Composable +fun SurveyDialog( + modifier: Modifier = Modifier, + dismiss: (dismissPermanently: Boolean) -> Unit +) { + val uriHandler = LocalUriHandler.current + var isDismissPersistentlyChecked by remember { mutableStateOf(false) } + + AlertDialog( + onDismissRequest = { dismiss(false) }, + title = { Text(text = "Help needed!") }, + text = { + Column( + modifier = Modifier, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text(text = "Do you have 5-10 minutes to take a quick survey and help us improve the app?") + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable( + role = Role.Checkbox, + onClick = { isDismissPersistentlyChecked = !isDismissPersistentlyChecked } + ), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + modifier = Modifier, + checked = isDismissPersistentlyChecked, + onCheckedChange = { isDismissPersistentlyChecked = it } + ) + + Text(text = stringResource(id = R.string.dashboard_dialog_beta_do_not_show_again)) + } + } + }, + confirmButton = { + Button( + onClick = { + uriHandler.openUri(SURVEY_URL) + + dismiss(true) + } + ) { + Text(text = "Participate") + } + }, + dismissButton = { + TextButton( + onClick = { dismiss(isDismissPersistentlyChecked) } + ) { + Text(text = "Not now") + } + } + ) +} + + + +@Preview +@Composable +fun SurveyDialogPreview() { + SurveyDialog(dismiss = {}) +} \ No newline at end of file From f4a994793013a766b16ef9091b64fbca7180f6e2 Mon Sep 17 00:00:00 2001 From: Martin Felber Date: Fri, 22 Nov 2024 11:51:28 +0100 Subject: [PATCH 3/8] Fix BetaHint display only for beta builds --- .../dashboard/service/impl/BetaHintServiceImpl.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/service/impl/BetaHintServiceImpl.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/service/impl/BetaHintServiceImpl.kt index f812625c2..87935d6c9 100644 --- a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/service/impl/BetaHintServiceImpl.kt +++ b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/service/impl/BetaHintServiceImpl.kt @@ -1,11 +1,8 @@ package de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.impl import android.content.Context -import androidx.datastore.core.DataMigration -import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.preferencesDataStore import de.tum.informatics.www1.artemis.native_app.feature.dashboard.BuildConfig import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.BetaHintService @@ -22,7 +19,11 @@ class BetaHintServiceImpl(private val context: Context) : BetaHintService { private val Context.storage by preferencesDataStore(DATA_STORE_KEY) - override val shouldShowBetaHint: Flow = context.storage.data.map { it[KEY_DISMISSED] ?: false }.map { !it } + private val isBetaHintDismissed: Flow = context.storage.data.map { it[KEY_DISMISSED] ?: false } + + override val shouldShowBetaHint: Flow = isBetaHintDismissed.map { dismissed -> + BuildConfig.isBeta && !dismissed + } override suspend fun dismissBetaHintPermanently() { context.storage.edit { data -> From 18fde4cb8c8b2fc308b17aabe4bdb43841bdb5e3 Mon Sep 17 00:00:00 2001 From: Martin Felber Date: Fri, 22 Nov 2024 11:53:05 +0100 Subject: [PATCH 4/8] Extract betaDialog and courseList --- .../feature/dashboard/ui/BetaHintDialog.kt | 68 +++++ .../feature/dashboard/ui/CourseList.kt | 256 ++++++++++++++++ .../feature/dashboard/ui/CoursesOverview.kt | 285 ------------------ 3 files changed, 324 insertions(+), 285 deletions(-) create mode 100644 feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/BetaHintDialog.kt create mode 100644 feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CourseList.kt diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/BetaHintDialog.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/BetaHintDialog.kt new file mode 100644 index 000000000..42da444bb --- /dev/null +++ b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/BetaHintDialog.kt @@ -0,0 +1,68 @@ +package de.tum.informatics.www1.artemis.native_app.feature.dashboard.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import de.tum.informatics.www1.artemis.native_app.feature.dashboard.R + +@Composable +fun BetaHintDialog( + dismiss: (dismissPermanently: Boolean) -> Unit +) { + var isDismissPersistentlyChecked by remember { mutableStateOf(false) } + + AlertDialog( + onDismissRequest = { dismiss(false) }, + title = { Text(text = stringResource(id = R.string.dashboard_dialog_beta_title)) }, + text = { + Column( + modifier = Modifier, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text(text = stringResource(id = R.string.dashboard_dialog_beta_message)) + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable( + role = Role.Checkbox, + onClick = { isDismissPersistentlyChecked = !isDismissPersistentlyChecked } + ), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + modifier = Modifier, + checked = isDismissPersistentlyChecked, + onCheckedChange = { isDismissPersistentlyChecked = it } + ) + + Text(text = stringResource(id = R.string.dashboard_dialog_beta_do_not_show_again)) + } + } + }, + confirmButton = { + TextButton( + onClick = { dismiss(isDismissPersistentlyChecked) } + ) { + Text(text = stringResource(id = R.string.dashboard_dialog_beta_positive)) + } + } + ) +} \ No newline at end of file diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CourseList.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CourseList.kt new file mode 100644 index 000000000..4314375fd --- /dev/null +++ b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CourseList.kt @@ -0,0 +1,256 @@ +package de.tum.informatics.www1.artemis.native_app.feature.dashboard.ui + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Divider +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.min +import androidx.compose.ui.unit.sp +import de.tum.informatics.www1.artemis.native_app.core.model.Course +import de.tum.informatics.www1.artemis.native_app.core.model.CourseWithScore +import de.tum.informatics.www1.artemis.native_app.core.ui.common.course.CompactCourseHeaderViewMode +import de.tum.informatics.www1.artemis.native_app.core.ui.common.course.CompactCourseItemHeader +import de.tum.informatics.www1.artemis.native_app.core.ui.common.course.CourseExerciseAndLectureCount +import de.tum.informatics.www1.artemis.native_app.core.ui.common.course.CourseItemGrid +import de.tum.informatics.www1.artemis.native_app.core.ui.common.course.ExpandedCourseItemHeader +import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.CoursePointsDecimalFormat +import de.tum.informatics.www1.artemis.native_app.feature.dashboard.R +import java.text.DecimalFormat + +/** + * Displays a lazy list of all the courses supplied. + */ +@Composable +fun CourseList( + modifier: Modifier, + courses: List, + serverUrl: String, + authorizationToken: String, + onClickOnCourse: (Course) -> Unit +) { + CourseItemGrid( + modifier = modifier, + courses = courses, + ) { dashboardCourse, courseItemModifier, isCompact -> + CourseItem( + modifier = courseItemModifier.testTag(testTagForCourse(dashboardCourse.course.id!!)), + courseWithScore = dashboardCourse, + serverUrl = serverUrl, + authorizationToken = authorizationToken, + onClick = { onClickOnCourse(dashboardCourse.course) }, + isCompact = isCompact + ) + } +} + +/** + * Displays course icon, title and description in a Material Design Card. + */ +@Composable +fun CourseItem( + modifier: Modifier, + isCompact: Boolean, + courseWithScore: CourseWithScore, + serverUrl: String, + authorizationToken: String, + onClick: () -> Unit +) { + val currentPoints = courseWithScore.totalScores.studentScores.absoluteScore + val maxPoints = courseWithScore.totalScores.maxPoints + + val currentPointsFormatted = remember(currentPoints) { + CoursePointsDecimalFormat.format(currentPoints) + } + val maxPointsFormatted = remember(maxPoints) { + CoursePointsDecimalFormat.format(maxPoints) + } + + val progress = if (maxPoints == 0f) 0f else currentPoints / maxPoints + + val progressPercentFormatted = remember(progress) { + DecimalFormat.getPercentInstance().format(progress) + } + + if (isCompact) { + CompactCourseItemHeader( + modifier = modifier, + course = courseWithScore.course, + serverUrl = serverUrl, + authorizationToken = authorizationToken, + onClick = onClick, + compactCourseHeaderViewMode = CompactCourseHeaderViewMode.EXERCISE_AND_LECTURE_COUNT, + content = { + Divider() + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp, horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + LinearProgressIndicator( + modifier = Modifier.weight(1f), + progress = progress, + trackColor = MaterialTheme.colorScheme.onPrimary + ) + + CourseProgressText( + modifier = Modifier, + currentPointsFormatted = currentPointsFormatted, + maxPointsFormatted = maxPointsFormatted, + progressPercentFormatted = progressPercentFormatted + ) + } + } + ) + } else { + ExpandedCourseItemHeader( + modifier = modifier, + course = courseWithScore.course, + serverUrl = serverUrl, + authorizationToken = authorizationToken, + onClick = onClick, + content = { + Box( + modifier = Modifier + .weight(1f) + .aspectRatio(1f) + .align(Alignment.CenterHorizontally) + ) { + CircularCourseProgress( + modifier = Modifier + .fillMaxSize(0.8f) + .align(Alignment.Center), + progress = progress, + currentPointsFormatted = currentPointsFormatted, + maxPointsFormatted = maxPointsFormatted, + progressPercentFormatted = progressPercentFormatted + ) + } + + CourseExerciseAndLectureCount( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(vertical = 8.dp), + exerciseCount = courseWithScore.course.exercises.size, + lectureCount = courseWithScore.course.lectures.size, + textStyle = MaterialTheme.typography.bodyLarge.copy(textAlign = TextAlign.Center), + alignment = Alignment.CenterHorizontally + ) + }, + rightHeaderContent = { } + ) + } +} + + +@Composable +private fun CircularCourseProgress( + modifier: Modifier, + progress: Float, + currentPointsFormatted: String, + maxPointsFormatted: String, + progressPercentFormatted: String +) { + BoxWithConstraints(modifier = modifier) { + val progressBarWidthDp = min(24.dp, maxWidth * 0.1f) + val progressBarWidth = with(LocalDensity.current) { progressBarWidthDp.toPx() } + + Canvas( + modifier = Modifier + .fillMaxSize() + .padding(progressBarWidthDp) + ) { + drawArc( + color = Color.Green, + startAngle = 180f, + sweepAngle = 360f * progress, + useCenter = false, + style = Stroke(width = progressBarWidth) + ) + + drawArc( + color = Color.Red, + startAngle = 180f + 360f * progress, + sweepAngle = 360f * (1f - progress), + useCenter = false, + style = Stroke(width = progressBarWidth) + ) + } + + val (percentFontSize, ptsFontSize) = with(LocalDensity.current) { + val availableSpace = maxHeight - progressBarWidthDp * 2 + (availableSpace * 0.2f).toSp() to (availableSpace * 0.1f).toSp() + } + + Column( + modifier = Modifier.align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource( + id = R.string.course_overview_course_progress_percentage, + progressPercentFormatted + ), + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + fontSize = percentFontSize, + fontWeight = FontWeight.Normal + ) + + Text( + text = stringResource( + id = R.string.course_overview_course_progress_pts, + currentPointsFormatted, + maxPointsFormatted + ), + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + fontSize = ptsFontSize, + fontWeight = FontWeight.Bold + ) + } + } +} + +@Composable +private fun CourseProgressText( + modifier: Modifier, + currentPointsFormatted: String, + maxPointsFormatted: String, + progressPercentFormatted: String +) { + Text( + modifier = modifier, + text = stringResource( + id = R.string.course_overview_course_progress, + currentPointsFormatted, + maxPointsFormatted, + progressPercentFormatted + ), + fontSize = 14.sp + ) +} \ No newline at end of file diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt index 596ff0f4b..109fa3cad 100644 --- a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt +++ b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt @@ -1,15 +1,11 @@ package de.tum.informatics.www1.artemis.native_app.feature.dashboard.ui -import androidx.compose.foundation.Canvas import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -20,17 +16,12 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Settings -import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button -import androidx.compose.material3.Checkbox -import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState @@ -39,38 +30,22 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.min -import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptionsBuilder import androidx.navigation.compose.composable -import de.tum.informatics.www1.artemis.native_app.core.model.Course -import de.tum.informatics.www1.artemis.native_app.core.model.CourseWithScore import de.tum.informatics.www1.artemis.native_app.core.model.Dashboard import de.tum.informatics.www1.artemis.native_app.core.ui.common.BasicDataStateUi -import de.tum.informatics.www1.artemis.native_app.core.ui.common.course.CompactCourseHeaderViewMode -import de.tum.informatics.www1.artemis.native_app.core.ui.common.course.CompactCourseItemHeader -import de.tum.informatics.www1.artemis.native_app.core.ui.common.course.CourseExerciseAndLectureCount -import de.tum.informatics.www1.artemis.native_app.core.ui.common.course.CourseItemGrid -import de.tum.informatics.www1.artemis.native_app.core.ui.common.course.ExpandedCourseItemHeader -import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.CoursePointsDecimalFormat import de.tum.informatics.www1.artemis.native_app.feature.dashboard.BuildConfig import de.tum.informatics.www1.artemis.native_app.feature.dashboard.R import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.BetaHintService @@ -78,7 +53,6 @@ import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.Surv import kotlinx.coroutines.launch import org.koin.androidx.compose.getViewModel import org.koin.compose.koinInject -import java.text.DecimalFormat const val DASHBOARD_DESTINATION = "dashboard" internal const val TEST_TAG_COURSE_LIST = "TEST_TAG_COURSE_LIST" @@ -247,266 +221,7 @@ internal fun CoursesOverview( } } -@Composable -private fun BetaHintDialog( - dismiss: (dismissPermanently: Boolean) -> Unit -) { - var isDismissPersistentlyChecked by remember { mutableStateOf(false) } - - AlertDialog( - onDismissRequest = { dismiss(false) }, - title = { Text(text = stringResource(id = R.string.dashboard_dialog_beta_title)) }, - text = { - Column( - modifier = Modifier, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text(text = stringResource(id = R.string.dashboard_dialog_beta_message)) - - Row( - modifier = Modifier - .fillMaxWidth() - .clickable( - role = Role.Checkbox, - onClick = { isDismissPersistentlyChecked = !isDismissPersistentlyChecked } - ), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox( - modifier = Modifier, - checked = isDismissPersistentlyChecked, - onCheckedChange = { isDismissPersistentlyChecked = it } - ) - - Text(text = stringResource(id = R.string.dashboard_dialog_beta_do_not_show_again)) - } - } - }, - confirmButton = { - TextButton( - onClick = { dismiss(isDismissPersistentlyChecked) } - ) { - Text(text = stringResource(id = R.string.dashboard_dialog_beta_positive)) - } - } - ) -} - -/** - * Displays a lazy list of all the courses supplied. - */ -@Composable -private fun CourseList( - modifier: Modifier, - courses: List, - serverUrl: String, - authorizationToken: String, - onClickOnCourse: (Course) -> Unit -) { - CourseItemGrid( - modifier = modifier, - courses = courses, - ) { dashboardCourse, courseItemModifier, isCompact -> - CourseItem( - modifier = courseItemModifier.testTag(testTagForCourse(dashboardCourse.course.id!!)), - courseWithScore = dashboardCourse, - serverUrl = serverUrl, - authorizationToken = authorizationToken, - onClick = { onClickOnCourse(dashboardCourse.course) }, - isCompact = isCompact - ) - } -} - -/** - * Displays course icon, title and description in a Material Design Card. - */ -@Composable -fun CourseItem( - modifier: Modifier, - isCompact: Boolean, - courseWithScore: CourseWithScore, - serverUrl: String, - authorizationToken: String, - onClick: () -> Unit -) { - val currentPoints = courseWithScore.totalScores.studentScores.absoluteScore - val maxPoints = courseWithScore.totalScores.maxPoints - - val currentPointsFormatted = remember(currentPoints) { - CoursePointsDecimalFormat.format(currentPoints) - } - val maxPointsFormatted = remember(maxPoints) { - CoursePointsDecimalFormat.format(maxPoints) - } - - val progress = if (maxPoints == 0f) 0f else currentPoints / maxPoints - - val progressPercentFormatted = remember(progress) { - DecimalFormat.getPercentInstance().format(progress) - } - - if (isCompact) { - CompactCourseItemHeader( - modifier = modifier, - course = courseWithScore.course, - serverUrl = serverUrl, - authorizationToken = authorizationToken, - onClick = onClick, - compactCourseHeaderViewMode = CompactCourseHeaderViewMode.EXERCISE_AND_LECTURE_COUNT, - content = { - Divider() - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp, horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - LinearProgressIndicator( - modifier = Modifier.weight(1f), - progress = progress, - trackColor = MaterialTheme.colorScheme.onPrimary - ) - - CourseProgressText( - modifier = Modifier, - currentPointsFormatted = currentPointsFormatted, - maxPointsFormatted = maxPointsFormatted, - progressPercentFormatted = progressPercentFormatted - ) - } - } - ) - } else { - ExpandedCourseItemHeader( - modifier = modifier, - course = courseWithScore.course, - serverUrl = serverUrl, - authorizationToken = authorizationToken, - onClick = onClick, - content = { - Box( - modifier = Modifier - .weight(1f) - .aspectRatio(1f) - .align(Alignment.CenterHorizontally) - ) { - CircularCourseProgress( - modifier = Modifier - .fillMaxSize(0.8f) - .align(Alignment.Center), - progress = progress, - currentPointsFormatted = currentPointsFormatted, - maxPointsFormatted = maxPointsFormatted, - progressPercentFormatted = progressPercentFormatted - ) - } - - CourseExerciseAndLectureCount( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(vertical = 8.dp), - exerciseCount = courseWithScore.course.exercises.size, - lectureCount = courseWithScore.course.lectures.size, - textStyle = MaterialTheme.typography.bodyLarge.copy(textAlign = TextAlign.Center), - alignment = Alignment.CenterHorizontally - ) - }, - rightHeaderContent = { } - ) - } -} - -@Composable -private fun CircularCourseProgress( - modifier: Modifier, - progress: Float, - currentPointsFormatted: String, - maxPointsFormatted: String, - progressPercentFormatted: String -) { - BoxWithConstraints(modifier = modifier) { - val progressBarWidthDp = min(24.dp, maxWidth * 0.1f) - val progressBarWidth = with(LocalDensity.current) { progressBarWidthDp.toPx() } - - Canvas( - modifier = Modifier - .fillMaxSize() - .padding(progressBarWidthDp) - ) { - drawArc( - color = Color.Green, - startAngle = 180f, - sweepAngle = 360f * progress, - useCenter = false, - style = Stroke(width = progressBarWidth) - ) - - drawArc( - color = Color.Red, - startAngle = 180f + 360f * progress, - sweepAngle = 360f * (1f - progress), - useCenter = false, - style = Stroke(width = progressBarWidth) - ) - } - val (percentFontSize, ptsFontSize) = with(LocalDensity.current) { - val availableSpace = maxHeight - progressBarWidthDp * 2 - (availableSpace * 0.2f).toSp() to (availableSpace * 0.1f).toSp() - } - - Column( - modifier = Modifier.align(Alignment.Center), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = stringResource( - id = R.string.course_overview_course_progress_percentage, - progressPercentFormatted - ), - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, - fontSize = percentFontSize, - fontWeight = FontWeight.Normal - ) - - Text( - text = stringResource( - id = R.string.course_overview_course_progress_pts, - currentPointsFormatted, - maxPointsFormatted - ), - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, - fontSize = ptsFontSize, - fontWeight = FontWeight.Bold - ) - } - } -} - -@Composable -private fun CourseProgressText( - modifier: Modifier, - currentPointsFormatted: String, - maxPointsFormatted: String, - progressPercentFormatted: String -) { - Text( - modifier = modifier, - text = stringResource( - id = R.string.course_overview_course_progress, - currentPointsFormatted, - maxPointsFormatted, - progressPercentFormatted - ), - fontSize = 14.sp - ) -} @Composable private fun DashboardEmpty(modifier: Modifier, onClickSignup: () -> Unit) { From 68340650355374fba5cc0bb4da490a434018c201 Mon Sep 17 00:00:00 2001 From: Martin Felber Date: Fri, 22 Nov 2024 14:18:50 +0100 Subject: [PATCH 5/8] Implement SurveyHint --- .../service/impl/SurveyHintServiceImpl.kt | 14 +- .../feature/dashboard/ui/BetaHintDialog.kt | 47 +++-- .../feature/dashboard/ui/CoursesOverview.kt | 67 ++++--- .../feature/dashboard/ui/SurveyDialog.kt | 94 --------- .../feature/dashboard/ui/SurveyHint.kt | 184 ++++++++++++++++++ 5 files changed, 255 insertions(+), 151 deletions(-) delete mode 100644 feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/SurveyDialog.kt create mode 100644 feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/SurveyHint.kt diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/service/impl/SurveyHintServiceImpl.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/service/impl/SurveyHintServiceImpl.kt index 518f68134..23d2102d6 100644 --- a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/service/impl/SurveyHintServiceImpl.kt +++ b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/service/impl/SurveyHintServiceImpl.kt @@ -12,7 +12,7 @@ import java.time.LocalDate // TODO: set the correct dates private val SURVEY_START_DATE = LocalDate.of(2024, 1, 1) -private val SURVEY_END_DATE = LocalDate.of(2022, 12, 31) +private val SURVEY_END_DATE = LocalDate.of(2024, 12, 31) class SurveyHintServiceImpl( private val context: Context, @@ -21,7 +21,7 @@ class SurveyHintServiceImpl( private companion object { private const val DATA_STORE_KEY = "survey_hint_store" - private val KEY_SHOW_SURVEY = booleanPreferencesKey("showSurvey1") // Change this to 2 for the second survey + private val KEY_SHOW_SURVEY = booleanPreferencesKey("showSurvey1") // Change this to "showSurvey2" for the second survey } private val Context.storage by preferencesDataStore(DATA_STORE_KEY) @@ -29,14 +29,14 @@ class SurveyHintServiceImpl( override val shouldShowSurveyHint: Flow = context.storage.data .map { it[KEY_SHOW_SURVEY] ?: isSurveyActive() } + private fun isSurveyActive(): Boolean { + val currentDate = LocalDate.now() + return currentDate.isAfter(SURVEY_START_DATE) && currentDate.isBefore(SURVEY_END_DATE) + } + override suspend fun dismissSurveyHintPermanently() { context.storage.edit { data -> data[KEY_SHOW_SURVEY] = false } } - - private fun isSurveyActive(): Boolean { - val currentDate = LocalDate.now() - return currentDate.isAfter(SURVEY_START_DATE) && currentDate.isBefore(SURVEY_END_DATE) - } } \ No newline at end of file diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/BetaHintDialog.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/BetaHintDialog.kt index 42da444bb..af47ab7b7 100644 --- a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/BetaHintDialog.kt +++ b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/BetaHintDialog.kt @@ -37,24 +37,10 @@ fun BetaHintDialog( ) { Text(text = stringResource(id = R.string.dashboard_dialog_beta_message)) - Row( - modifier = Modifier - .fillMaxWidth() - .clickable( - role = Role.Checkbox, - onClick = { isDismissPersistentlyChecked = !isDismissPersistentlyChecked } - ), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox( - modifier = Modifier, - checked = isDismissPersistentlyChecked, - onCheckedChange = { isDismissPersistentlyChecked = it } - ) - - Text(text = stringResource(id = R.string.dashboard_dialog_beta_do_not_show_again)) - } + DoNotShowAgainCheckBox( + isChecked = isDismissPersistentlyChecked, + onCheckedChange = { isDismissPersistentlyChecked = it } + ) } }, confirmButton = { @@ -65,4 +51,29 @@ fun BetaHintDialog( } } ) +} + +@Composable +fun DoNotShowAgainCheckBox( + isChecked: Boolean, + onCheckedChange: (Boolean) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable( + role = Role.Checkbox, + onClick = { onCheckedChange(!isChecked) } + ), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + modifier = Modifier, + checked = isChecked, + onCheckedChange = onCheckedChange + ) + + Text(text = stringResource(id = R.string.dashboard_dialog_beta_do_not_show_again)) + } } \ No newline at end of file diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt index 109fa3cad..6c4f99d5c 100644 --- a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt +++ b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt @@ -115,13 +115,6 @@ internal fun CoursesOverview( if (shouldDisplayBetaDialog) displayBetaDialog = true } - val shouldDisplaySurveyHint by surveyHintService.shouldShowSurveyHint.collectAsState(initial = false) - var displaySurveyHint by rememberSaveable { mutableStateOf(false) } - - LaunchedEffect(shouldDisplaySurveyHint) { - if (shouldDisplaySurveyHint) displaySurveyHint = true - } - Scaffold( modifier = modifier.then(Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)), topBar = { @@ -173,33 +166,43 @@ internal fun CoursesOverview( ) } ) { padding -> - BasicDataStateUi( + Column( modifier = Modifier - .fillMaxSize() .padding(top = padding.calculateTopPadding()) - .consumeWindowInsets(WindowInsets.systemBars), - dataState = coursesDataState, - loadingText = stringResource(id = R.string.courses_loading_loading), - failureText = stringResource(id = R.string.courses_loading_failure), - retryButtonText = stringResource(id = R.string.courses_loading_try_again), - onClickRetry = viewModel::requestReloadDashboard - ) { dashboard: Dashboard -> - if (dashboard.courses.isEmpty()) { - DashboardEmpty( - modifier = Modifier.fillMaxSize(), - onClickSignup = onClickRegisterForCourse - ) - } else { - CourseList( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 8.dp) - .testTag(TEST_TAG_COURSE_LIST), - courses = dashboard.courses, - serverUrl = serverUrl, - authorizationToken = authToken, - onClickOnCourse = { course -> onViewCourse(course.id ?: 0L) } - ) + .consumeWindowInsets(WindowInsets.systemBars) + ) { + SurveyHint( + modifier = Modifier + .padding(8.dp) + .fillMaxWidth(), + surveyHintService = surveyHintService + ) + + BasicDataStateUi( + modifier = Modifier.fillMaxSize(), + dataState = coursesDataState, + loadingText = stringResource(id = R.string.courses_loading_loading), + failureText = stringResource(id = R.string.courses_loading_failure), + retryButtonText = stringResource(id = R.string.courses_loading_try_again), + onClickRetry = viewModel::requestReloadDashboard + ) { dashboard: Dashboard -> + if (dashboard.courses.isEmpty()) { + DashboardEmpty( + modifier = Modifier.fillMaxSize(), + onClickSignup = onClickRegisterForCourse + ) + } else { + CourseList( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 8.dp) + .testTag(TEST_TAG_COURSE_LIST), + courses = dashboard.courses, + serverUrl = serverUrl, + authorizationToken = authToken, + onClickOnCourse = { course -> onViewCourse(course.id ?: 0L) } + ) + } } } } diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/SurveyDialog.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/SurveyDialog.kt deleted file mode 100644 index abae031f3..000000000 --- a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/SurveyDialog.kt +++ /dev/null @@ -1,94 +0,0 @@ -package de.tum.informatics.www1.artemis.native_app.feature.dashboard.ui - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.Checkbox -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import de.tum.informatics.www1.artemis.native_app.feature.dashboard.R - -private const val SURVEY_URL = "https://example.com/survey" - -@Composable -fun SurveyDialog( - modifier: Modifier = Modifier, - dismiss: (dismissPermanently: Boolean) -> Unit -) { - val uriHandler = LocalUriHandler.current - var isDismissPersistentlyChecked by remember { mutableStateOf(false) } - - AlertDialog( - onDismissRequest = { dismiss(false) }, - title = { Text(text = "Help needed!") }, - text = { - Column( - modifier = Modifier, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text(text = "Do you have 5-10 minutes to take a quick survey and help us improve the app?") - - Row( - modifier = Modifier - .fillMaxWidth() - .clickable( - role = Role.Checkbox, - onClick = { isDismissPersistentlyChecked = !isDismissPersistentlyChecked } - ), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox( - modifier = Modifier, - checked = isDismissPersistentlyChecked, - onCheckedChange = { isDismissPersistentlyChecked = it } - ) - - Text(text = stringResource(id = R.string.dashboard_dialog_beta_do_not_show_again)) - } - } - }, - confirmButton = { - Button( - onClick = { - uriHandler.openUri(SURVEY_URL) - - dismiss(true) - } - ) { - Text(text = "Participate") - } - }, - dismissButton = { - TextButton( - onClick = { dismiss(isDismissPersistentlyChecked) } - ) { - Text(text = "Not now") - } - } - ) -} - - - -@Preview -@Composable -fun SurveyDialogPreview() { - SurveyDialog(dismiss = {}) -} \ No newline at end of file diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/SurveyHint.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/SurveyHint.kt new file mode 100644 index 000000000..faf02ac10 --- /dev/null +++ b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/SurveyHint.kt @@ -0,0 +1,184 @@ +package de.tum.informatics.www1.artemis.native_app.feature.dashboard.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.OpenInNew +import androidx.compose.material.icons.filled.ChevronRight +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.outlined.Feedback +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.SurveyHintService +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +private const val SURVEY_URL = "https://example.com/survey" // TODO + +@Composable +fun SurveyHint( + modifier: Modifier = Modifier, + surveyHintService: SurveyHintService +) { + val shouldDisplaySurveyHint by surveyHintService.shouldShowSurveyHint.collectAsState(initial = false) + var displaySurveyHint by rememberSaveable { mutableStateOf(false) } + var showSurveyDialog by rememberSaveable { mutableStateOf(false) } + + LaunchedEffect(shouldDisplaySurveyHint) { + if (shouldDisplaySurveyHint) displaySurveyHint = true + } + + AnimatedVisibility(displaySurveyHint) { + SurveyHintImpl( + modifier = modifier, + onClick = { showSurveyDialog = true } + ) + } + + val uriHandler = LocalUriHandler.current + val scope = rememberCoroutineScope() + if (showSurveyDialog) { + SurveyDialog( + onClose = { participate -> + if (participate) { + scope.launch { + uriHandler.openUri(SURVEY_URL) + surveyHintService.dismissSurveyHintPermanently() + + delay(2000) // Wait for the survey to open before hiding the hint + displaySurveyHint = false + } + } + + showSurveyDialog = false + } + ) + } +} + +@Composable +private fun SurveyHintImpl( + modifier: Modifier, + onClick: () -> Unit +) { + Card( + modifier = modifier, + onClick = onClick + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = "Survey available", + ) + + Spacer(modifier = Modifier.weight(1f)) + + Icon( + imageVector = Icons.Default.ChevronRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + } + } +} + + +@Composable +private fun SurveyDialog( + modifier: Modifier = Modifier, + onClose: (participate: Boolean) -> Unit +) { + + AlertDialog( + onDismissRequest = { onClose(false) }, + title = { Text(text = "We need your help!") }, + text = { + Column( + modifier = Modifier, + verticalArrangement = Arrangement.spacedBy(32.dp) + ) { + Text(text = "Do you have 5-10 minutes to take a short survey and help us improve the app?") + + Icon( + modifier = Modifier + .size(72.dp) + .align(Alignment.CenterHorizontally) + , + imageVector = Icons.Outlined.Feedback, + contentDescription = null, + ) + + Text("Thank you for your consideration!") + } + }, + confirmButton = { + Button( + onClick = { onClose(true) } + ) { + Text(text = "Participate") + + Icon( + imageVector = Icons.AutoMirrored.Filled.OpenInNew, + contentDescription = null, + modifier = Modifier.padding(start = 4.dp) + ) + } + }, + dismissButton = { + TextButton( + onClick = { onClose(false) } + ) { + Text(text = "Not now") + } + } + ) +} + +@Preview +@Composable +fun SurveyHintPreview() { + SurveyHintImpl( + modifier = Modifier, + onClick = {} + ) +} + +@Preview +@Composable +fun SurveyDialogPreview() { + SurveyDialog(onClose = {}) +} \ No newline at end of file From 8e19f24e3d9be7bc465a5da658a506b6bfe47bbc Mon Sep 17 00:00:00 2001 From: Martin Felber Date: Fri, 22 Nov 2024 14:23:09 +0100 Subject: [PATCH 6/8] Use string res --- .../native_app/feature/dashboard/ui/SurveyHint.kt | 14 ++++++++------ .../src/main/res/values/survey_hint_strings.xml | 9 +++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 feature/dashboard/src/main/res/values/survey_hint_strings.xml diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/SurveyHint.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/SurveyHint.kt index faf02ac10..63f78a141 100644 --- a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/SurveyHint.kt +++ b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/SurveyHint.kt @@ -31,8 +31,10 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import de.tum.informatics.www1.artemis.native_app.feature.dashboard.R import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.SurveyHintService import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -102,7 +104,7 @@ private fun SurveyHintImpl( Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Survey available", + text = stringResource(R.string.survey_hint_text), ) Spacer(modifier = Modifier.weight(1f)) @@ -125,13 +127,13 @@ private fun SurveyDialog( AlertDialog( onDismissRequest = { onClose(false) }, - title = { Text(text = "We need your help!") }, + title = { Text(text = stringResource(R.string.survey_dialog_title)) }, text = { Column( modifier = Modifier, verticalArrangement = Arrangement.spacedBy(32.dp) ) { - Text(text = "Do you have 5-10 minutes to take a short survey and help us improve the app?") + Text(text = stringResource(R.string.survey_dialog_do_you_have_time)) Icon( modifier = Modifier @@ -142,14 +144,14 @@ private fun SurveyDialog( contentDescription = null, ) - Text("Thank you for your consideration!") + Text(stringResource(R.string.survey_dialog_thank_you)) } }, confirmButton = { Button( onClick = { onClose(true) } ) { - Text(text = "Participate") + Text(text = stringResource(R.string.survey_dialog_button_participate)) Icon( imageVector = Icons.AutoMirrored.Filled.OpenInNew, @@ -162,7 +164,7 @@ private fun SurveyDialog( TextButton( onClick = { onClose(false) } ) { - Text(text = "Not now") + Text(text = stringResource(R.string.survey_dialog_button_not_now)) } } ) diff --git a/feature/dashboard/src/main/res/values/survey_hint_strings.xml b/feature/dashboard/src/main/res/values/survey_hint_strings.xml new file mode 100644 index 000000000..b57eceac7 --- /dev/null +++ b/feature/dashboard/src/main/res/values/survey_hint_strings.xml @@ -0,0 +1,9 @@ + + + Survey available + We need your help! + Do you have 5–10 minutes to take a short survey and help us improve the app? + Thank you for your consideration! + Participate + Not now + \ No newline at end of file From 954d534b980e42bf4d1240a5816ee2333b59f430 Mon Sep 17 00:00:00 2001 From: Martin Felber Date: Fri, 29 Nov 2024 13:42:26 +0100 Subject: [PATCH 7/8] Add real survey link + dates --- .../feature/dashboard/service/impl/SurveyHintServiceImpl.kt | 5 ++--- .../artemis/native_app/feature/dashboard/ui/SurveyHint.kt | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/service/impl/SurveyHintServiceImpl.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/service/impl/SurveyHintServiceImpl.kt index 23d2102d6..f10b565cf 100644 --- a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/service/impl/SurveyHintServiceImpl.kt +++ b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/service/impl/SurveyHintServiceImpl.kt @@ -10,9 +10,8 @@ import kotlinx.coroutines.flow.map import java.time.LocalDate -// TODO: set the correct dates -private val SURVEY_START_DATE = LocalDate.of(2024, 1, 1) -private val SURVEY_END_DATE = LocalDate.of(2024, 12, 31) +private val SURVEY_START_DATE = LocalDate.of(2024, 11, 30) +private val SURVEY_END_DATE = LocalDate.of(2024, 12, 15) class SurveyHintServiceImpl( private val context: Context, diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/SurveyHint.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/SurveyHint.kt index 63f78a141..246f48113 100644 --- a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/SurveyHint.kt +++ b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/SurveyHint.kt @@ -39,7 +39,7 @@ import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.Surv import kotlinx.coroutines.delay import kotlinx.coroutines.launch -private const val SURVEY_URL = "https://example.com/survey" // TODO +private const val SURVEY_URL = "https://survey.ase.in.tum.de/index.php/767298?lang=en" @Composable fun SurveyHint( From 50e301e9a40dad3b858379e79e11c02cb724613a Mon Sep 17 00:00:00 2001 From: Martin Felber Date: Fri, 29 Nov 2024 14:23:40 +0100 Subject: [PATCH 8/8] Fixed merge issues --- .../native_app/android/ui/MainActivity.kt | 6 +- .../feature/dashboard/CoursesOverview.kt | 528 ------------------ .../service/impl/SurveyHintServiceImpl.kt | 4 +- .../feature/dashboard/ui/CoursesOverview.kt | 9 +- 4 files changed, 11 insertions(+), 536 deletions(-) delete mode 100644 feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/CoursesOverview.kt diff --git a/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/ui/MainActivity.kt b/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/ui/MainActivity.kt index 0668960d3..7954206ce 100644 --- a/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/ui/MainActivity.kt +++ b/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/ui/MainActivity.kt @@ -40,9 +40,9 @@ import de.tum.informatics.www1.artemis.native_app.feature.courseregistration.cou import de.tum.informatics.www1.artemis.native_app.feature.courseregistration.navigateToCourseRegistration import de.tum.informatics.www1.artemis.native_app.feature.courseview.ui.course_overview.course import de.tum.informatics.www1.artemis.native_app.feature.courseview.ui.course_overview.navigateToCourse -import de.tum.informatics.www1.artemis.native_app.feature.dashboard.DashboardScreen -import de.tum.informatics.www1.artemis.native_app.feature.dashboard.dashboard -import de.tum.informatics.www1.artemis.native_app.feature.dashboard.navigateToDashboard +import de.tum.informatics.www1.artemis.native_app.feature.dashboard.ui.DashboardScreen +import de.tum.informatics.www1.artemis.native_app.feature.dashboard.ui.dashboard +import de.tum.informatics.www1.artemis.native_app.feature.dashboard.ui.navigateToDashboard import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.ExerciseViewDestination import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.ExerciseViewMode import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.ExerciseViewUi diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/CoursesOverview.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/CoursesOverview.kt deleted file mode 100644 index 10aefcb08..000000000 --- a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/CoursesOverview.kt +++ /dev/null @@ -1,528 +0,0 @@ -package de.tum.informatics.www1.artemis.native_app.feature.dashboard - -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.consumeWindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material.icons.filled.Settings -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.Checkbox -import androidx.compose.material3.Divider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.rememberTopAppBarState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.min -import androidx.compose.ui.unit.sp -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavOptionsBuilder -import androidx.navigation.compose.composable -import de.tum.informatics.www1.artemis.native_app.core.model.Course -import de.tum.informatics.www1.artemis.native_app.core.model.CourseWithScore -import de.tum.informatics.www1.artemis.native_app.core.model.Dashboard -import de.tum.informatics.www1.artemis.native_app.core.ui.common.BasicDataStateUi -import de.tum.informatics.www1.artemis.native_app.core.ui.common.course.CompactCourseHeaderViewMode -import de.tum.informatics.www1.artemis.native_app.core.ui.common.course.CompactCourseItemHeader -import de.tum.informatics.www1.artemis.native_app.core.ui.common.course.CourseExerciseAndLectureCount -import de.tum.informatics.www1.artemis.native_app.core.ui.common.course.CourseItemGrid -import de.tum.informatics.www1.artemis.native_app.core.ui.common.course.ExpandedCourseItemHeader -import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.CoursePointsDecimalFormat -import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.BetaHintService -import kotlinx.coroutines.launch -import kotlinx.serialization.Serializable -import org.koin.androidx.compose.getViewModel -import org.koin.compose.koinInject -import java.text.DecimalFormat - -internal const val TEST_TAG_COURSE_LIST = "TEST_TAG_COURSE_LIST" - -internal fun testTagForCourse(courseId: Long) = "Course$courseId" - -@Serializable -data object DashboardScreen - -fun NavController.navigateToDashboard(builder: NavOptionsBuilder.() -> Unit) { - navigate(DashboardScreen, builder) -} - -fun NavGraphBuilder.dashboard( - onOpenSettings: () -> Unit, - onClickRegisterForCourse: () -> Unit, - onViewCourse: (courseId: Long) -> Unit -) { - composable { - CoursesOverview( - modifier = Modifier.fillMaxSize(), - viewModel = getViewModel(), - onOpenSettings = onOpenSettings, - onClickRegisterForCourse = onClickRegisterForCourse, - onViewCourse = onViewCourse - ) - } -} - -/** - * Displays the Course Overview screen. - * Uses Scaffold to display a Material Design TopAppBar. - */ -@Composable -internal fun CoursesOverview( - modifier: Modifier, - viewModel: CourseOverviewViewModel, - onOpenSettings: () -> Unit, - onClickRegisterForCourse: () -> Unit, - onViewCourse: (courseId: Long) -> Unit, - isBeta: Boolean = BuildConfig.isBeta, - betaHintService: BetaHintService = koinInject() -) { - val coursesDataState by viewModel.dashboard.collectAsState() - - //The course composable needs the serverUrl to build the correct url to fetch the course icon from. - val serverUrl by viewModel.serverUrl.collectAsState() - //The server wants an authorization token to send the course icon. - val authToken by viewModel.authToken.collectAsState() - - val topAppBarState = rememberTopAppBarState() - - val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior( - topAppBarState - ) - - val shouldDisplayBetaDialog by betaHintService.shouldShowBetaHint.collectAsState(initial = false) - var displayBetaDialog by rememberSaveable { mutableStateOf(false) } - - // Trigger the dialog if service sets value to true - LaunchedEffect(shouldDisplayBetaDialog) { - if (shouldDisplayBetaDialog) displayBetaDialog = true - } - - Scaffold( - modifier = modifier.then(Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)), - topBar = { - TopAppBar( - title = { - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Text( - modifier = Modifier.weight(1f, fill = false), - text = stringResource(id = R.string.course_overview_title), - maxLines = 1 - ) - - if (isBeta) { - Text( - modifier = Modifier - .border( - 1.dp, - color = MaterialTheme.colorScheme.outline, - shape = RoundedCornerShape(percent = 50) - ) - .padding(horizontal = 8.dp), - text = stringResource(id = R.string.dashboard_title_beta), - color = MaterialTheme.colorScheme.outline, - maxLines = 1 - ) - } - } - }, - actions = { - IconButton(onClick = viewModel::requestReloadDashboard) { - Icon( - imageVector = Icons.Default.Refresh, - contentDescription = null - ) - } - - IconButton(onClick = onClickRegisterForCourse) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = stringResource(id = R.string.course_overview_action_register) - ) - } - - IconButton(onClick = onOpenSettings) { - Icon(imageVector = Icons.Default.Settings, contentDescription = null) - } - }, - scrollBehavior = scrollBehavior - ) - } - ) { padding -> - BasicDataStateUi( - modifier = Modifier - .fillMaxSize() - .padding(top = padding.calculateTopPadding()) - .consumeWindowInsets(WindowInsets.systemBars), - dataState = coursesDataState, - loadingText = stringResource(id = R.string.courses_loading_loading), - failureText = stringResource(id = R.string.courses_loading_failure), - retryButtonText = stringResource(id = R.string.courses_loading_try_again), - onClickRetry = viewModel::requestReloadDashboard - ) { dashboard: Dashboard -> - if (dashboard.courses.isEmpty()) { - DashboardEmpty( - modifier = Modifier.fillMaxSize(), - onClickSignup = onClickRegisterForCourse - ) - } else { - CourseList( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 8.dp) - .testTag(TEST_TAG_COURSE_LIST), - courses = dashboard.courses, - serverUrl = serverUrl, - authorizationToken = authToken, - onClickOnCourse = { course -> onViewCourse(course.id ?: 0L) } - ) - } - } - } - - if (displayBetaDialog) { - val scope = rememberCoroutineScope() - - BetaHintDialog { dismissPermanently -> - if (dismissPermanently) { - scope.launch { - betaHintService.dismissBetaHintPermanently() - - displayBetaDialog = false - } - } else { - displayBetaDialog = false - } - } - } -} - -@Composable -private fun BetaHintDialog( - dismiss: (dismissPermanently: Boolean) -> Unit -) { - var isDismissPersistentlyChecked by remember { mutableStateOf(false) } - - AlertDialog( - onDismissRequest = { dismiss(false) }, - title = { Text(text = stringResource(id = R.string.dashboard_dialog_beta_title)) }, - text = { - Column( - modifier = Modifier, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text(text = stringResource(id = R.string.dashboard_dialog_beta_message)) - - Row( - modifier = Modifier - .fillMaxWidth() - .clickable( - role = Role.Checkbox, - onClick = { isDismissPersistentlyChecked = !isDismissPersistentlyChecked } - ), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox( - modifier = Modifier, - checked = isDismissPersistentlyChecked, - onCheckedChange = { isDismissPersistentlyChecked = it } - ) - - Text(text = stringResource(id = R.string.dashboard_dialog_beta_do_not_show_again)) - } - } - }, - confirmButton = { - TextButton( - onClick = { dismiss(isDismissPersistentlyChecked) } - ) { - Text(text = stringResource(id = R.string.dashboard_dialog_beta_positive)) - } - } - ) -} - -/** - * Displays a lazy list of all the courses supplied. - */ -@Composable -private fun CourseList( - modifier: Modifier, - courses: List, - serverUrl: String, - authorizationToken: String, - onClickOnCourse: (Course) -> Unit -) { - CourseItemGrid( - modifier = modifier, - courses = courses, - ) { dashboardCourse, courseItemModifier, isCompact -> - CourseItem( - modifier = courseItemModifier.testTag(testTagForCourse(dashboardCourse.course.id!!)), - courseWithScore = dashboardCourse, - serverUrl = serverUrl, - authorizationToken = authorizationToken, - onClick = { onClickOnCourse(dashboardCourse.course) }, - isCompact = isCompact - ) - } -} - -/** - * Displays course icon, title and description in a Material Design Card. - */ -@Composable -fun CourseItem( - modifier: Modifier, - isCompact: Boolean, - courseWithScore: CourseWithScore, - serverUrl: String, - authorizationToken: String, - onClick: () -> Unit -) { - val currentPoints = courseWithScore.totalScores.studentScores.absoluteScore - val maxPoints = courseWithScore.totalScores.maxPoints - - val currentPointsFormatted = remember(currentPoints) { - CoursePointsDecimalFormat.format(currentPoints) - } - val maxPointsFormatted = remember(maxPoints) { - CoursePointsDecimalFormat.format(maxPoints) - } - - val progress = if (maxPoints == 0f) 0f else currentPoints / maxPoints - - val progressPercentFormatted = remember(progress) { - DecimalFormat.getPercentInstance().format(progress) - } - - if (isCompact) { - CompactCourseItemHeader( - modifier = modifier, - course = courseWithScore.course, - serverUrl = serverUrl, - authorizationToken = authorizationToken, - onClick = onClick, - compactCourseHeaderViewMode = CompactCourseHeaderViewMode.EXERCISE_AND_LECTURE_COUNT, - content = { - Divider() - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp, horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - LinearProgressIndicator( - modifier = Modifier.weight(1f), - progress = progress, - trackColor = MaterialTheme.colorScheme.onPrimary - ) - - CourseProgressText( - modifier = Modifier, - currentPointsFormatted = currentPointsFormatted, - maxPointsFormatted = maxPointsFormatted, - progressPercentFormatted = progressPercentFormatted - ) - } - } - ) - } else { - ExpandedCourseItemHeader( - modifier = modifier, - course = courseWithScore.course, - serverUrl = serverUrl, - authorizationToken = authorizationToken, - onClick = onClick, - content = { - Box( - modifier = Modifier - .weight(1f) - .aspectRatio(1f) - .align(Alignment.CenterHorizontally) - ) { - CircularCourseProgress( - modifier = Modifier - .fillMaxSize(0.8f) - .align(Alignment.Center), - progress = progress, - currentPointsFormatted = currentPointsFormatted, - maxPointsFormatted = maxPointsFormatted, - progressPercentFormatted = progressPercentFormatted - ) - } - - CourseExerciseAndLectureCount( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(vertical = 8.dp), - exerciseCount = courseWithScore.course.exercises.size, - lectureCount = courseWithScore.course.lectures.size, - textStyle = MaterialTheme.typography.bodyLarge.copy(textAlign = TextAlign.Center), - alignment = Alignment.CenterHorizontally - ) - }, - rightHeaderContent = { } - ) - } -} - -@Composable -private fun CircularCourseProgress( - modifier: Modifier, - progress: Float, - currentPointsFormatted: String, - maxPointsFormatted: String, - progressPercentFormatted: String -) { - BoxWithConstraints(modifier = modifier) { - val progressBarWidthDp = min(24.dp, maxWidth * 0.1f) - val progressBarWidth = with(LocalDensity.current) { progressBarWidthDp.toPx() } - - Canvas( - modifier = Modifier - .fillMaxSize() - .padding(progressBarWidthDp) - ) { - drawArc( - color = Color.Green, - startAngle = 180f, - sweepAngle = 360f * progress, - useCenter = false, - style = Stroke(width = progressBarWidth) - ) - - drawArc( - color = Color.Red, - startAngle = 180f + 360f * progress, - sweepAngle = 360f * (1f - progress), - useCenter = false, - style = Stroke(width = progressBarWidth) - ) - } - - val (percentFontSize, ptsFontSize) = with(LocalDensity.current) { - val availableSpace = maxHeight - progressBarWidthDp * 2 - (availableSpace * 0.2f).toSp() to (availableSpace * 0.1f).toSp() - } - - Column( - modifier = Modifier.align(Alignment.Center), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = stringResource( - id = R.string.course_overview_course_progress_percentage, - progressPercentFormatted - ), - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, - fontSize = percentFontSize, - fontWeight = FontWeight.Normal - ) - - Text( - text = stringResource( - id = R.string.course_overview_course_progress_pts, - currentPointsFormatted, - maxPointsFormatted - ), - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, - fontSize = ptsFontSize, - fontWeight = FontWeight.Bold - ) - } - } -} - -@Composable -private fun CourseProgressText( - modifier: Modifier, - currentPointsFormatted: String, - maxPointsFormatted: String, - progressPercentFormatted: String -) { - Text( - modifier = modifier, - text = stringResource( - id = R.string.course_overview_course_progress, - currentPointsFormatted, - maxPointsFormatted, - progressPercentFormatted - ), - fontSize = 14.sp - ) -} - -@Composable -private fun DashboardEmpty(modifier: Modifier, onClickSignup: () -> Unit) { - Box(modifier = modifier) { - Column( - modifier = Modifier - .align(Alignment.Center) - .fillMaxWidth() - .padding(8.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - modifier = Modifier.fillMaxWidth(), - text = stringResource(id = R.string.courses_empty_text), - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center - ) - - Button(onClick = onClickSignup) { - Text(text = stringResource(id = R.string.courses_empty_register_now_button)) - } - } - } -} \ No newline at end of file diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/service/impl/SurveyHintServiceImpl.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/service/impl/SurveyHintServiceImpl.kt index f10b565cf..07c9b9aa3 100644 --- a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/service/impl/SurveyHintServiceImpl.kt +++ b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/service/impl/SurveyHintServiceImpl.kt @@ -10,8 +10,8 @@ import kotlinx.coroutines.flow.map import java.time.LocalDate -private val SURVEY_START_DATE = LocalDate.of(2024, 11, 30) -private val SURVEY_END_DATE = LocalDate.of(2024, 12, 15) +private val SURVEY_START_DATE = LocalDate.of(2024, 11, 28) +private val SURVEY_END_DATE = LocalDate.of(2024, 12, 16) class SurveyHintServiceImpl( private val context: Context, diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt index 6c4f99d5c..8bd4bfa86 100644 --- a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt +++ b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt @@ -51,16 +51,19 @@ import de.tum.informatics.www1.artemis.native_app.feature.dashboard.R import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.BetaHintService import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.SurveyHintService import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable import org.koin.androidx.compose.getViewModel import org.koin.compose.koinInject -const val DASHBOARD_DESTINATION = "dashboard" internal const val TEST_TAG_COURSE_LIST = "TEST_TAG_COURSE_LIST" internal fun testTagForCourse(courseId: Long) = "Course$courseId" +@Serializable +data object DashboardScreen + fun NavController.navigateToDashboard(builder: NavOptionsBuilder.() -> Unit) { - navigate(DASHBOARD_DESTINATION, builder) + navigate(DashboardScreen, builder) } fun NavGraphBuilder.dashboard( @@ -68,7 +71,7 @@ fun NavGraphBuilder.dashboard( onClickRegisterForCourse: () -> Unit, onViewCourse: (courseId: Long) -> Unit ) { - composable(DASHBOARD_DESTINATION) { + composable { CoursesOverview( modifier = Modifier.fillMaxSize(), viewModel = getViewModel(),