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..0668960d3 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 @@ -21,9 +21,9 @@ import androidx.compose.ui.res.stringResource import androidx.core.net.toUri import androidx.lifecycle.lifecycleScope import androidx.navigation.NavHostController -import androidx.navigation.NavOptions import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController +import androidx.navigation.navOptions import com.google.android.gms.oss.licenses.OssLicensesMenuActivity import de.tum.informatics.www1.artemis.native_app.android.BuildConfig import de.tum.informatics.www1.artemis.native_app.android.R @@ -40,16 +40,17 @@ 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.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.exerciseview.ExerciseViewDestination import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.ExerciseViewMode +import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.ExerciseViewUi import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.exercise import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.navigateToExercise import de.tum.informatics.www1.artemis.native_app.feature.lectureview.lecture import de.tum.informatics.www1.artemis.native_app.feature.lectureview.navigateToLecture -import de.tum.informatics.www1.artemis.native_app.feature.login.LOGIN_DESTINATION +import de.tum.informatics.www1.artemis.native_app.feature.login.LoginScreen import de.tum.informatics.www1.artemis.native_app.feature.login.loginScreen import de.tum.informatics.www1.artemis.native_app.feature.login.navigateToLogin import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.visiblemetiscontextreporter.ProvideLocalVisibleMetisContextManager @@ -96,8 +97,8 @@ class MainActivity : AppCompatActivity(), // When the user is logged in, immediately display the course overview. val startDestination = runBlocking { when (accountService.authenticationData.first()) { - is AccountService.AuthenticationData.LoggedIn -> DASHBOARD_DESTINATION - AccountService.AuthenticationData.NotLoggedIn -> LOGIN_DESTINATION + is AccountService.AuthenticationData.LoggedIn -> DashboardScreen + AccountService.AuthenticationData.NotLoggedIn -> LoginScreen(null) } } @@ -190,7 +191,7 @@ class MainActivity : AppCompatActivity(), } @Composable - private fun MainActivityComposeUi(startDestination: String, navController: NavHostController) { + private fun MainActivityComposeUi(startDestination: Any, navController: NavHostController) { // Listen for when the user get logged out (e.g. because their token has expired) // This only happens when the user has the app running for multiple days or the user logged out manually LaunchedEffect(Unit) { @@ -199,7 +200,7 @@ class MainActivity : AppCompatActivity(), .collect { (wasLoggedIn, isLoggedIn) -> if (wasLoggedIn == true && !isLoggedIn) { navController.navigateToLogin { - popUpTo(DASHBOARD_DESTINATION) { + popUpTo(DashboardScreen) { inclusive = true } } @@ -259,7 +260,7 @@ class MainActivity : AppCompatActivity(), if (deepLink == null) { // Navigate to the course overview and remove the login screen from the navigation stack. navController.navigateToDashboard { - popUpTo(LOGIN_DESTINATION) { + popUpTo { inclusive = true } } @@ -267,7 +268,9 @@ class MainActivity : AppCompatActivity(), try { navController.navigate( Uri.parse(deepLink), - NavOptions.Builder().setPopUpTo(LOGIN_DESTINATION, true).build() + navOptions { + popUpTo() + } ) } catch (_: IllegalArgumentException) { navController.navigateToDashboard { @@ -347,7 +350,7 @@ class MainActivity : AppCompatActivity(), quizParticipation( onLeaveQuiz = { val previousBackStackEntry = navController.previousBackStackEntry - if (previousBackStackEntry?.destination?.route == ExerciseViewDestination.EXERCISE_VIEW_ROUTE) { + if (previousBackStackEntry?.destination?.route == ExerciseViewUi::class.qualifiedName.orEmpty()) { previousBackStackEntry.savedStateHandle[ExerciseViewDestination.REQUIRE_RELOAD_KEY] = true } diff --git a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt index ddc610388..e12a57f74 100644 --- a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt @@ -7,6 +7,7 @@ import org.gradle.api.artifacts.VersionCatalogsExtension import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.getByType +import org.gradle.kotlin.dsl.kotlin import kotlin.Suppress import kotlin.apply import kotlin.with @@ -18,6 +19,7 @@ class AndroidFeatureConventionPlugin : Plugin { pluginManager.apply { apply("artemis.android.library") apply("org.gradle.jacoco") + apply("org.jetbrains.kotlin.plugin.serialization") } extensions.configure { diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/navigation/KSerializableNavType.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/navigation/KSerializableNavType.kt new file mode 100644 index 000000000..ab98f7664 --- /dev/null +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/navigation/KSerializableNavType.kt @@ -0,0 +1,30 @@ +package de.tum.informatics.www1.artemis.native_app.core.ui.navigation + +import android.os.Bundle +import androidx.navigation.NavType +import kotlinx.serialization.KSerializer +import kotlinx.serialization.json.Json + +class KSerializableNavType( + isNullableAllowed: Boolean, + private val serializer: KSerializer +) : NavType(isNullableAllowed) { + + companion object { + private val json = Json { + coerceInputValues = true + } + } + + override fun get(bundle: Bundle, key: String): T? { + return parseValue(bundle.getString(key) ?: return null) + } + + override fun parseValue(value: String): T { + return json.decodeFromString(serializer, value) + } + + override fun put(bundle: Bundle, key: String, value: T) { + bundle.putString(key, json.encodeToString(serializer, value)) + } +} diff --git a/feature/course-registration/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseregistration/RegisterForCourseUi.kt b/feature/course-registration/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseregistration/RegisterForCourseUi.kt index 44d0a3a63..d789f7f70 100644 --- a/feature/course-registration/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseregistration/RegisterForCourseUi.kt +++ b/feature/course-registration/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseregistration/RegisterForCourseUi.kt @@ -64,23 +64,25 @@ import de.tum.informatics.www1.artemis.native_app.core.ui.common.course.computeC import de.tum.informatics.www1.artemis.native_app.core.ui.getWindowSizeClass import de.tum.informatics.www1.artemis.native_app.core.ui.markdown.MarkdownText import kotlinx.coroutines.Deferred +import kotlinx.serialization.Serializable import org.koin.androidx.compose.getViewModel internal const val TEST_TAG_REGISTRABLE_COURSE_LIST = "registrable course list" internal fun testTagForRegistrableCourse(courseId: Long) = "registrableCourse$courseId" -private const val COURSE_REGISTRATION_DESTINATION = "courseRegistration" +@Serializable +private data object CourseRegistrationScreen fun NavController.navigateToCourseRegistration(builder: NavOptionsBuilder.() -> Unit) { - navigate(COURSE_REGISTRATION_DESTINATION, builder) + navigate(CourseRegistrationScreen, builder) } fun NavGraphBuilder.courseRegistration( onNavigateUp: () -> Unit, onRegisteredInCourse: (courseId: Long) -> Unit ) { - composable(COURSE_REGISTRATION_DESTINATION) { + composable { RegisterForCourseScreen( modifier = Modifier.fillMaxSize(), viewModel = getViewModel(), diff --git a/feature/course-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseview/ui/course_overview/CourseUiScreen.kt b/feature/course-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseview/ui/course_overview/CourseUiScreen.kt index 557480740..29d957e69 100644 --- a/feature/course-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseview/ui/course_overview/CourseUiScreen.kt +++ b/feature/course-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseview/ui/course_overview/CourseUiScreen.kt @@ -37,6 +37,7 @@ import androidx.navigation.NavType import androidx.navigation.compose.composable import androidx.navigation.navArgument import androidx.navigation.navDeepLink +import androidx.navigation.toRoute import de.tum.informatics.www1.artemis.native_app.core.data.DataState import de.tum.informatics.www1.artemis.native_app.core.model.Course import de.tum.informatics.www1.artemis.native_app.core.model.exercise.Exercise @@ -56,6 +57,7 @@ import de.tum.informatics.www1.artemis.native_app.feature.metis.ui.NavigateToUse import de.tum.informatics.www1.artemis.native_app.feature.metis.ui.NothingOpened import de.tum.informatics.www1.artemis.native_app.feature.metis.ui.OpenedConversation import de.tum.informatics.www1.artemis.native_app.feature.metis.ui.OpenedThread +import kotlinx.serialization.Serializable import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf @@ -66,8 +68,16 @@ internal const val TAB_COMMUNICATION = 2 internal const val DEFAULT_CONVERSATION_ID = -1L internal const val DEFAULT_POST_ID = -1L +@Serializable +private data class CourseUiScreen( + val courseId: Long, + val conversationId: Long = DEFAULT_CONVERSATION_ID, + val postId: Long = DEFAULT_POST_ID, + val username: String = "" +) + fun NavController.navigateToCourse(courseId: Long, builder: NavOptionsBuilder.() -> Unit) { - navigate("course/$courseId", builder) + navigate(CourseUiScreen(courseId), builder) } fun NavGraphBuilder.course( @@ -88,25 +98,15 @@ fun NavGraphBuilder.course( generateLinks("courses/{courseId}/exercises") + generateLinks("courses/{courseId}/messages?conversationId={conversationId}") + generateLinks("courses/{courseId}/messages?username={username}") - composable( - route = "course/{courseId}", - arguments = listOf( - navArgument("courseId") { type = NavType.LongType; nullable = false }, - navArgument("conversationId") { - type = NavType.LongType; defaultValue = DEFAULT_CONVERSATION_ID - }, - navArgument("postId") { type = NavType.LongType; defaultValue = DEFAULT_POST_ID }, - navArgument("username") { type = NavType.StringType; defaultValue = "" } - ), + composable( deepLinks = deepLinks ) { backStackEntry -> - val courseId = backStackEntry.arguments?.getLong("courseId") - checkNotNull(courseId) + val route: CourseUiScreen = backStackEntry.toRoute() + val courseId = route.courseId - val conversationId = - backStackEntry.arguments?.getLong("conversationId") ?: DEFAULT_CONVERSATION_ID - val postId = backStackEntry.arguments?.getLong("postId") ?: DEFAULT_POST_ID - val username = backStackEntry.arguments?.getString("username").orEmpty() + val conversationId = route.conversationId + val postId = route.postId + val username = route.username CourseUiScreen( modifier = Modifier.fillMaxSize(), diff --git a/feature/dashboard/build.gradle.kts b/feature/dashboard/build.gradle.kts index f4e3c194a..28440dc30 100644 --- a/feature/dashboard/build.gradle.kts +++ b/feature/dashboard/build.gradle.kts @@ -1,7 +1,6 @@ plugins { id("artemis.android.feature") id("artemis.android.library.compose") - kotlin("plugin.serialization") } android { 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 index dc93016f4..10aefcb08 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/CoursesOverview.kt @@ -75,17 +75,20 @@ import de.tum.informatics.www1.artemis.native_app.core.ui.common.course.Expanded 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 -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( @@ -93,7 +96,7 @@ fun NavGraphBuilder.dashboard( onClickRegisterForCourse: () -> Unit, onViewCourse: (courseId: Long) -> Unit ) { - composable(DASHBOARD_DESTINATION) { + composable { CoursesOverview( modifier = Modifier.fillMaxSize(), viewModel = getViewModel(), diff --git a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/ExerciseViewUi.kt b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/ExerciseViewUi.kt index 623108980..bd5798234 100644 --- a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/ExerciseViewUi.kt +++ b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/ExerciseViewUi.kt @@ -1,7 +1,6 @@ package de.tum.informatics.www1.artemis.native_app.feature.exerciseview import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect @@ -14,28 +13,26 @@ import androidx.compose.ui.res.stringResource import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptionsBuilder -import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument import androidx.navigation.navDeepLink +import androidx.navigation.toRoute import de.tum.informatics.www1.artemis.native_app.core.data.DataState import de.tum.informatics.www1.artemis.native_app.core.data.orNull import de.tum.informatics.www1.artemis.native_app.core.model.exercise.Exercise import de.tum.informatics.www1.artemis.native_app.core.ui.common.BasicDataStateUi import de.tum.informatics.www1.artemis.native_app.core.ui.common.EmptyDataStateUi import de.tum.informatics.www1.artemis.native_app.core.ui.generateLinks +import de.tum.informatics.www1.artemis.native_app.core.ui.navigation.KSerializableNavType import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.home.ExerciseScreen import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.participate.textexercise.TextExerciseParticipationScreen import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.viewresult.ViewResultScreen import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf -import java.net.URLDecoder -import java.net.URLEncoder +import kotlin.reflect.typeOf object ExerciseViewDestination { const val EXERCISE_VIEW_ROUTE = "exercise/{exerciseId}/{viewMode}" @@ -46,28 +43,37 @@ object ExerciseViewDestination { const val REQUIRE_RELOAD_KEY = "requireReload" } -/** - * Display the exercise view - */ -private const val NESTED_HOME_DESTINATION = "home" +@Serializable +sealed interface ExerciseViewUiNestedNavigation { + + /** + * Display the exercise view + */ + @Serializable + data object Home : ExerciseViewUiNestedNavigation -/** - * View the latest result - */ -private const val NESTED_EXERCISE_RESULT_DESTINATION = "view_result" + /** + * View the latest result + */ + @Serializable + data object Result : ExerciseViewUiNestedNavigation -private const val NESTED_PARTICIPATE_TEXT_EXERCISE_DESTINATION = - "participate/text_exercise/{participationId}" + @Serializable + data class ParticipateTextExercise(val participationId: Long) : ExerciseViewUiNestedNavigation +} + +@Serializable +data class ExerciseViewUi( + val exerciseId: Long, + val viewMode: ExerciseViewMode = ExerciseViewMode.Overview, +) fun NavController.navigateToExercise( exerciseId: Long, viewMode: ExerciseViewMode, builder: NavOptionsBuilder.() -> Unit ) { - val viewModeAsString = - URLEncoder.encode(Json.encodeToString(ExerciseViewMode.serializer(), viewMode), "UTF-8") - - navigate("exercise/$exerciseId/$viewModeAsString", builder) + navigate(ExerciseViewUi(exerciseId, viewMode), builder) } fun NavGraphBuilder.exercise( @@ -76,18 +82,12 @@ fun NavGraphBuilder.exercise( onParticipateInQuiz: (courseId: Long, exerciseId: Long, isPractice: Boolean) -> Unit, onClickViewQuizResults: (courseId: Long, exerciseId: Long) -> Unit ) { - composable( - route = ExerciseViewDestination.EXERCISE_VIEW_ROUTE, - arguments = listOf( - navArgument("exerciseId") { - type = NavType.LongType - nullable = false - }, - navArgument("viewMode") { - type = NavType.StringType - defaultValue = - Json.encodeToString(ExerciseViewMode.serializer(), ExerciseViewMode.Overview) - } + composable( + typeMap = mapOf( + typeOf() to KSerializableNavType( + isNullableAllowed = false, + ExerciseViewMode.Overview.serializer() + ) ), deepLinks = listOf( navDeepLink { @@ -95,13 +95,10 @@ fun NavGraphBuilder.exercise( } ) + generateLinks("courses/{courseId}/exercises/{exerciseId}") ) { backStackEntry -> - val exerciseId = - backStackEntry.arguments?.getLong("exerciseId") - checkNotNull(exerciseId) + val route: ExerciseViewUi = backStackEntry.toRoute() - val viewMode: ExerciseViewMode = backStackEntry.arguments?.getString("viewMode")?.let { - Json.decodeFromString(URLDecoder.decode(it, "UTF-8")) - } ?: ExerciseViewMode.Overview + val exerciseId = route.exerciseId + val viewMode: ExerciseViewMode = route.viewMode val exerciseViewModel = koinViewModel { parametersOf(exerciseId) } @@ -116,10 +113,13 @@ fun NavGraphBuilder.exercise( } } - val startDestination = when (viewMode) { - ExerciseViewMode.Overview -> NESTED_HOME_DESTINATION - is ExerciseViewMode.TextParticipation -> NESTED_PARTICIPATE_TEXT_EXERCISE_DESTINATION - ExerciseViewMode.ViewResult -> NESTED_EXERCISE_RESULT_DESTINATION + val startDestination: ExerciseViewUiNestedNavigation = when (viewMode) { + ExerciseViewMode.Overview -> ExerciseViewUiNestedNavigation.Home + is ExerciseViewMode.TextParticipation -> ExerciseViewUiNestedNavigation.ParticipateTextExercise( + participationId = viewMode.participationId + ) + + ExerciseViewMode.ViewResult -> ExerciseViewUiNestedNavigation.Result } val nestedNavigateUp: () -> Unit = { @@ -131,17 +131,21 @@ fun NavGraphBuilder.exercise( } NavHost(navController = nestedNavController, startDestination = startDestination) { - composable(NESTED_HOME_DESTINATION) { + composable { ExerciseScreen( modifier = Modifier.fillMaxSize(), viewModel = exerciseViewModel, onNavigateBack = nestedNavigateUp, onViewResult = { - nestedNavController.navigate(NESTED_EXERCISE_RESULT_DESTINATION) + nestedNavController.navigate(ExerciseViewUiNestedNavigation.Result) }, navController = navController, onViewTextExerciseParticipationScreen = { participationId -> - nestedNavController.navigate(createTextParticipationRoute(participationId)) + nestedNavController.navigate( + ExerciseViewUiNestedNavigation.ParticipateTextExercise( + participationId + ) + ) }, onParticipateInQuiz = { courseId, isPractice -> onParticipateInQuiz(courseId, exerciseId, isPractice) @@ -152,7 +156,7 @@ fun NavGraphBuilder.exercise( ) } - composable(NESTED_EXERCISE_RESULT_DESTINATION) { + composable { ViewResultScreen( modifier = Modifier.fillMaxSize(), viewModel = exerciseViewModel, @@ -160,21 +164,11 @@ fun NavGraphBuilder.exercise( ) } - composable( - NESTED_PARTICIPATE_TEXT_EXERCISE_DESTINATION, - arguments = listOf( - navArgument( - "participationId" - ) { - type = NavType.LongType - if (viewMode is ExerciseViewMode.TextParticipation) { - defaultValue = viewMode.participationId - } else nullable = false - } - ) - ) { backStackEntry -> - val participationId: Long = backStackEntry.arguments?.getLong("participationId") - ?: throw IllegalArgumentException() + composable { backStackEntry -> + val nestedRoute: ExerciseViewUiNestedNavigation.ParticipateTextExercise = + backStackEntry.toRoute() + + val participationId: Long = nestedRoute.participationId val exerciseDataState by exerciseViewModel.exerciseDataState.collectAsState() @@ -191,9 +185,6 @@ fun NavGraphBuilder.exercise( } } -private fun createTextParticipationRoute(participationId: Long) = - "participate/text_exercise/$participationId" - @Composable internal fun ExerciseDataStateUi( modifier: Modifier, @@ -216,7 +207,7 @@ internal fun ExerciseDataStateUi( sealed interface ExerciseViewMode { @Serializable @SerialName("overview") - object Overview : ExerciseViewMode + data object Overview : ExerciseViewMode @Serializable @SerialName("text_participation") @@ -224,7 +215,7 @@ sealed interface ExerciseViewMode { @Serializable @SerialName("view_result") - object ViewResult : ExerciseViewMode + data object ViewResult : ExerciseViewMode } internal val DataState.courseId: Long? diff --git a/feature/lecture-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/lectureview/LectureScreenUi.kt b/feature/lecture-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/lectureview/LectureScreenUi.kt index 3e693705e..e4884dab0 100644 --- a/feature/lecture-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/lectureview/LectureScreenUi.kt +++ b/feature/lecture-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/lectureview/LectureScreenUi.kt @@ -35,6 +35,7 @@ import androidx.navigation.NavType import androidx.navigation.compose.composable import androidx.navigation.navArgument import androidx.navigation.navDeepLink +import androidx.navigation.toRoute import io.github.fornewid.placeholder.material3.placeholder import de.tum.informatics.www1.artemis.native_app.core.model.lecture.Attachment import de.tum.informatics.www1.artemis.native_app.core.ui.LocalLinkOpener @@ -45,16 +46,20 @@ import de.tum.informatics.www1.artemis.native_app.feature.metis.ui.canDisplayMet import io.ktor.http.HttpHeaders import io.ktor.http.URLBuilder import io.ktor.http.appendPathSegments +import kotlinx.serialization.Serializable import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf const val METIS_RATIO = 0.3f +@Serializable +private data class LectureScreenUi(val lectureId: Long) + fun NavController.navigateToLecture( lectureId: Long, builder: NavOptionsBuilder.() -> Unit ) { - navigate("lecture/$lectureId", builder) + navigate(LectureScreenUi(lectureId), builder) } fun NavGraphBuilder.lecture( @@ -66,23 +71,16 @@ fun NavGraphBuilder.lecture( onParticipateInQuiz: (courseId: Long, exerciseId: Long, isPractice: Boolean) -> Unit, onClickViewQuizResults: (courseId: Long, exerciseId: Long) -> Unit, ) { - composable( - route = "lecture/{lectureId}", - arguments = listOf( - navArgument("lectureId") { - type = NavType.LongType - nullable = false - } - ), + composable( deepLinks = listOf( navDeepLink { uriPattern = "artemis://lectures/{lectureId}" } ) + generateLinks("courses/{courseId}/lectures/{lectureId}") ) { backStackEntry -> - val lectureId = - backStackEntry.arguments?.getLong("lectureId") - checkNotNull(lectureId) + val route: LectureScreenUi = backStackEntry.toRoute() + + val lectureId = route.lectureId val viewModel: LectureViewModel = koinViewModel { parametersOf(lectureId) } val lectureDataState by viewModel.lectureDataState.collectAsState() diff --git a/feature/login/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/login/AccountUi.kt b/feature/login/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/login/AccountUi.kt index d5ab2fc50..42342f213 100644 --- a/feature/login/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/login/AccountUi.kt +++ b/feature/login/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/login/AccountUi.kt @@ -60,6 +60,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument +import androidx.navigation.toRoute import de.tum.informatics.www1.artemis.native_app.core.data.DataState import de.tum.informatics.www1.artemis.native_app.core.datastore.ServerConfigurationService import de.tum.informatics.www1.artemis.native_app.core.datastore.defaults.ArtemisInstances @@ -78,39 +79,35 @@ import de.tum.informatics.www1.artemis.native_app.feature.login.service.ServerNo import io.ktor.http.encodeURLPathPart import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable import org.koin.androidx.compose.getViewModel import org.koin.androidx.compose.koinViewModel import org.koin.compose.koinInject import org.koin.core.parameter.parametersOf import java.io.IOException -private const val NAV_ARG_NEXT_DESTINATION = "next_destination" - -const val LOGIN_DESTINATION = "login/{$NAV_ARG_NEXT_DESTINATION}" private const val ARG_REMEMBER_ME = "rememberMe" private const val NESTED_SAML2_LOGIN_ROUTE = "saml2_login" -private enum class NestedDestination(val destination: String) { - INSTANCE_SELECTION("instance_selection"), - CUSTOM_INSTANCE_SELECTION("custom_instance_selection"), - HOME("nested_home"), - LOGIN("nested_login"), - REGISTER("nested_register"), - SAML2_LOGIN("$NESTED_SAML2_LOGIN_ROUTE/{$ARG_REMEMBER_ME}"); - - companion object { - fun getByRoute(route: String?): NestedDestination? = when (route) { - INSTANCE_SELECTION.destination -> INSTANCE_SELECTION - CUSTOM_INSTANCE_SELECTION.destination -> CUSTOM_INSTANCE_SELECTION - HOME.destination -> HOME - LOGIN.destination -> LOGIN - REGISTER.destination -> REGISTER - NESTED_SAML2_LOGIN_ROUTE -> SAML2_LOGIN - else -> null - } - } +@Serializable +private sealed interface NestedDestination { + @Serializable + data object InstanceSelection : NestedDestination + @Serializable + data object CustomInstanceSelection : NestedDestination + @Serializable + data object Home : NestedDestination + @Serializable + data object Login : NestedDestination + @Serializable + data object Register : NestedDestination + @Serializable + data class Saml2Login(val rememberMe: Boolean) : NestedDestination } +@Serializable +data class LoginScreen(val nextDestination: String?) + /** * @param nextDestination the deep link to a destination that should be opened after a successful login */ @@ -118,11 +115,13 @@ fun NavController.navigateToLogin( nextDestination: String? = null, builder: NavOptionsBuilder.() -> Unit ) { - if (nextDestination != null) { - navigate("login/${nextDestination.encodeURLPathPart()}", builder) + val screen = if (nextDestination != null) { + LoginScreen(nextDestination) } else { - navigate("login/null", builder) + LoginScreen(null) } + + navigate(screen, builder) } /** @@ -132,17 +131,9 @@ fun NavGraphBuilder.loginScreen( onFinishedLoginFlow: (deepLink: String?) -> Unit, onRequestOpenSettings: () -> Unit ) { - composable( - LOGIN_DESTINATION, - arguments = listOf( - navArgument(NAV_ARG_NEXT_DESTINATION) { - type = NavType.StringType - defaultValue = null - nullable = true - } - ) - ) { - val nextDestinationValue = it.arguments?.getString(NAV_ARG_NEXT_DESTINATION) + composable { + val screen = it.toRoute() + val nextDestinationValue = screen.nextDestination var nextDestination by remember(nextDestinationValue) { mutableStateOf(if (nextDestinationValue == null || nextDestinationValue == "null") null else nextDestinationValue) @@ -232,16 +223,14 @@ private fun LoginUiScreen( ?: return // Display nothing to avoid switching between destinations // Force recomposition + val currentBackStack by nestedNavController.currentBackStackEntryAsState() nestedNavController.currentBackStackEntryAsState().value val supportsBackNavigation = nestedNavController.previousBackStackEntry != null - val selectedDestination: NestedDestination? = - NestedDestination.getByRoute(nestedNavController.currentDestination?.route) + val selectedDestination: NestedDestination? = currentBackStack?.toRoute() val onClickSaml2Login: (rememberMe: Boolean) -> Unit = { rememberMe -> - nestedNavController.navigate( - createSaml2LoginRoute(rememberMe) - ) + nestedNavController.navigate(NestedDestination.Saml2Login(rememberMe)) } Scaffold( @@ -257,9 +246,9 @@ private fun LoginUiScreen( }, title = { val titleText: Int? = when (selectedDestination) { - NestedDestination.CUSTOM_INSTANCE_SELECTION -> R.string.account_select_custom_instance_selection_title - NestedDestination.LOGIN -> R.string.login_title - NestedDestination.REGISTER -> R.string.register_title + NestedDestination.CustomInstanceSelection -> R.string.account_select_custom_instance_selection_title + NestedDestination.Login -> R.string.login_title + NestedDestination.Register -> R.string.register_title else -> null } @@ -282,23 +271,23 @@ private fun LoginUiScreen( .consumeWindowInsets(WindowInsets.systemBars) .padding(top = paddingValues.calculateTopPadding()), navController = nestedNavController, - startDestination = if (hasSelectedInstance) NestedDestination.HOME.destination else NestedDestination.INSTANCE_SELECTION.destination + startDestination = if (hasSelectedInstance) NestedDestination.Home else NestedDestination.InstanceSelection ) { - composable(NestedDestination.HOME.destination) { + composable() { AccountScreen( modifier = Modifier.fillMaxSize(), canSwitchInstance = !BuildConfig.hasInstanceRestriction, onNavigateToLoginScreen = { - nestedNavController.navigate(NestedDestination.LOGIN.destination) + nestedNavController.navigate(NestedDestination.Login) }, onNavigateToRegisterScreen = { - nestedNavController.navigate(NestedDestination.REGISTER.destination) + nestedNavController.navigate(NestedDestination.Register) }, onNavigateToInstanceSelection = { onNavigatedToInstanceSelection() - nestedNavController.navigate(NestedDestination.INSTANCE_SELECTION.destination) { - popUpTo(NestedDestination.HOME.destination) { + nestedNavController.navigate(NestedDestination.InstanceSelection) { + popUpTo { inclusive = true } } @@ -308,21 +297,21 @@ private fun LoginUiScreen( ) } - composable(NestedDestination.CUSTOM_INSTANCE_SELECTION.destination) { + composable { CustomInstanceSelectionScreen( modifier = Modifier .fillMaxSize() .padding(horizontal = 16.dp) ) { - nestedNavController.navigate(NestedDestination.HOME.destination) { - popUpTo(NestedDestination.INSTANCE_SELECTION.destination) { + nestedNavController.navigate(NestedDestination.Home) { + popUpTo { inclusive = true } } } } - composable(NestedDestination.LOGIN.destination) { + composable { LoginScreen( modifier = Modifier.fillMaxSize(), viewModel = getViewModel(), @@ -331,12 +320,7 @@ private fun LoginUiScreen( ) } - composable( - route = NestedDestination.SAML2_LOGIN.destination, - arguments = listOf(navArgument("rememberMe") { - type = NavType.BoolType - }) - ) { backStack -> + composable { backStack -> val rememberMe = backStack.arguments?.getBoolean(ARG_REMEMBER_ME) checkNotNull(rememberMe) @@ -350,7 +334,7 @@ private fun LoginUiScreen( ) } - composable(NestedDestination.REGISTER.destination) { + composable { RegisterUi( modifier = Modifier .fillMaxSize() @@ -359,12 +343,12 @@ private fun LoginUiScreen( viewModel = koinViewModel(), onRegistered = { nestedNavController.popBackStack() - nestedNavController.navigate(NestedDestination.LOGIN.destination) + nestedNavController.navigate(NestedDestination.Login) } ) } - composable(NestedDestination.INSTANCE_SELECTION.destination) { + composable { val scope = rememberCoroutineScope() InstanceSelectionScreen( @@ -375,8 +359,8 @@ private fun LoginUiScreen( onSelectArtemisInstance = { serverUrl -> scope.launch { serverConfigurationService.updateServerUrl(serverUrl) - nestedNavController.navigate(NestedDestination.HOME.destination) { - popUpTo(NestedDestination.INSTANCE_SELECTION.destination) { + nestedNavController.navigate(NestedDestination.Home) { + popUpTo { inclusive = true } } @@ -384,7 +368,7 @@ private fun LoginUiScreen( }, onRequestOpenCustomInstanceSelection = { nestedNavController.navigate( - NestedDestination.CUSTOM_INSTANCE_SELECTION.destination + NestedDestination.CustomInstanceSelection ) } ) @@ -393,9 +377,6 @@ private fun LoginUiScreen( } } -private fun createSaml2LoginRoute(rememberMe: Boolean): String = - NestedDestination.SAML2_LOGIN.destination.replace("{$ARG_REMEMBER_ME}", rememberMe.toString()) - /** * Displays the screen to login and register. Also allows to change the artemis instance. */ diff --git a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/PostContextBottomSheet.kt b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/PostContextBottomSheet.kt index f5d170c34..077fa129c 100644 --- a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/PostContextBottomSheet.kt +++ b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/PostContextBottomSheet.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxHeight @@ -56,7 +55,6 @@ import de.tum.informatics.www1.artemis.native_app.core.ui.Spacings import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.R import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui.LocalEmojiProvider import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui.getUnicodeForEmojiId -import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.AnswerPost import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.IAnswerPost import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.IBasePost import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.IReaction @@ -77,7 +75,7 @@ internal fun PostContextBottomSheet( if (!displayAllEmojis) { ModalBottomSheet( modifier = Modifier.testTag(TEST_TAG_POST_CONTEXT_BOTTOM_SHEET), - windowInsets = WindowInsets.statusBars, + contentWindowInsets = { WindowInsets.statusBars }, sheetState = rememberModalBottomSheetState(), onDismissRequest = onDismissRequest ) { diff --git a/feature/metis/conversation/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ConversationAnswerMessagesUITest.kt b/feature/metis/conversation/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ConversationAnswerMessagesUITest.kt index b9a232f32..d71d75236 100644 --- a/feature/metis/conversation/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ConversationAnswerMessagesUITest.kt +++ b/feature/metis/conversation/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ConversationAnswerMessagesUITest.kt @@ -14,6 +14,7 @@ import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui. import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.IBasePost import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.db.pojo.AnswerPostPojo import kotlinx.coroutines.CompletableDeferred +import org.junit.Ignore import org.junit.Test import org.junit.experimental.categories.Category import org.junit.runner.RunWith @@ -21,6 +22,9 @@ import org.robolectric.RobolectricTestRunner @Category(UnitTest::class) @RunWith(RobolectricTestRunner::class) +@Ignore("There is an open issue about onClick events not working for the ModalBottomSheetLayout with" + + "the robolectric test runner. Enable this test again as soon as the following issue is resolved:" + + "https://github.com/robolectric/robolectric/issues/9595") class ConversationAnswerMessagesUITest : BaseThreadUITest() { private fun testTagForAnswerPost(answerPostId: String) = "answerPost$answerPostId" diff --git a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationOverviewBody.kt b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationOverviewBody.kt index cfed38cbc..d27066657 100644 --- a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationOverviewBody.kt +++ b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationOverviewBody.kt @@ -190,7 +190,7 @@ fun ConversationOverviewBody( if (showCodeOfConduct) { ModalBottomSheet( - windowInsets = WindowInsets.statusBars, + contentWindowInsets = { WindowInsets.statusBars }, onDismissRequest = { showCodeOfConduct = false } ) { CodeOfConductUi( diff --git a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/participation/QuizParticipationScreen.kt b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/participation/QuizParticipationScreen.kt index 50e90b59b..79dfe92d1 100644 --- a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/participation/QuizParticipationScreen.kt +++ b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/participation/QuizParticipationScreen.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -15,7 +14,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar 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 @@ -28,25 +26,25 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavType import androidx.navigation.compose.composable -import androidx.navigation.navArgument import androidx.navigation.navDeepLink +import androidx.navigation.toRoute import de.tum.informatics.www1.artemis.native_app.core.data.service.impl.JsonProvider import de.tum.informatics.www1.artemis.native_app.core.ui.AwaitDeferredCompletion import de.tum.informatics.www1.artemis.native_app.core.ui.alert.DestructiveMarkdownTextAlertDialog import de.tum.informatics.www1.artemis.native_app.core.ui.alert.TextAlertDialog import de.tum.informatics.www1.artemis.native_app.core.ui.common.ButtonWithLoadingAnimation +import de.tum.informatics.www1.artemis.native_app.core.ui.navigation.KSerializableNavType import de.tum.informatics.www1.artemis.native_app.feature.quiz.QuizType import de.tum.informatics.www1.artemis.native_app.feature.quiz.R import de.tum.informatics.www1.artemis.native_app.feature.quiz.view_result.ViewQuizResultScreen import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Job +import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json import org.koin.androidx.compose.koinViewModel import org.koin.compose.koinInject import org.koin.core.parameter.parametersOf +import kotlin.reflect.typeOf private val submitButtonColor: Color @Composable get() = if (isSystemInDarkTheme()) Color(0xff00bc8c) else Color(0xff28a745) @@ -54,31 +52,28 @@ private val submitButtonColor: Color private val submitButtonTextColor: Color @Composable get() = Color.White +@Serializable +private data class QuizParticipationScreen( + val courseId: Long, + val exerciseId: Long, + val quizType: QuizType.WorkableQuizType = QuizType.Live +) + fun NavController.navigateToQuizParticipation( courseId: Long, exerciseId: Long, quizType: QuizType.WorkableQuizType ) { - val quizTypeAsString = Json.encodeToString(QuizType.WorkableQuizType.serializer(), quizType) - - navigate("quiz-participation/$courseId/$exerciseId/$quizTypeAsString") + navigate(QuizParticipationScreen(courseId, exerciseId, quizType)) } fun NavGraphBuilder.quizParticipation(onLeaveQuiz: () -> Unit) { - composable( - route = "quiz-participation/{courseId}/{exerciseId}/{quizType}", - arguments = listOf( - navArgument("courseId") { - type = NavType.LongType - }, - navArgument("exerciseId") { - type = NavType.LongType - }, - navArgument("quizType") { - type = NavType.StringType - defaultValue = - Json.encodeToString(QuizType.WorkableQuizType.serializer(), QuizType.Live) - } + composable( + typeMap = mapOf( + typeOf() to KSerializableNavType( + isNullableAllowed = false, + QuizType.WorkableQuizType.serializer() + ) ), deepLinks = listOf( navDeepLink { @@ -86,15 +81,10 @@ fun NavGraphBuilder.quizParticipation(onLeaveQuiz: () -> Unit) { } ) ) { backStackEntry -> - val courseId = backStackEntry.arguments?.getLong("courseId") - val exerciseId = backStackEntry.arguments?.getLong("exerciseId") - val quizTypeString = backStackEntry.arguments?.getString("quizType") - - checkNotNull(courseId) - checkNotNull(exerciseId) - checkNotNull(quizTypeString) - - val quizType: QuizType.WorkableQuizType = Json.decodeFromString(quizTypeString) + val screen = backStackEntry.toRoute() + val courseId = screen.courseId + val exerciseId = screen.exerciseId + val quizType = screen.quizType val jsonProvider: JsonProvider = koinInject() diff --git a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/view_result/ViewQuizResultScreen.kt b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/view_result/ViewQuizResultScreen.kt index 363fa1050..c33d5b5d0 100644 --- a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/view_result/ViewQuizResultScreen.kt +++ b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/view_result/ViewQuizResultScreen.kt @@ -17,9 +17,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavType import androidx.navigation.compose.composable -import androidx.navigation.navArgument +import androidx.navigation.toRoute import de.tum.informatics.www1.artemis.native_app.core.data.DataState import de.tum.informatics.www1.artemis.native_app.core.data.join import de.tum.informatics.www1.artemis.native_app.core.model.exercise.QuizExercise @@ -29,37 +28,27 @@ import de.tum.informatics.www1.artemis.native_app.core.ui.common.BasicDataStateU import de.tum.informatics.www1.artemis.native_app.feature.quiz.QuizType import de.tum.informatics.www1.artemis.native_app.feature.quiz.R import de.tum.informatics.www1.artemis.native_app.feature.quiz.participation.QuizQuestionData +import kotlinx.serialization.Serializable import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf +@Serializable +private data class ViewQuizResultScreen(val courseId: Long, val exerciseId: Long) + fun NavController.navigateToQuizResult( courseId: Long, exerciseId: Long ) { - navigate("view-quiz-result/$courseId/$exerciseId") + navigate(ViewQuizResultScreen(courseId, exerciseId)) } fun NavGraphBuilder.quizResults(onRequestLeaveQuizResults: () -> Unit) { - composable( - "view-quiz-result/{courseId}/{exerciseId}", - arguments = listOf( - navArgument("courseId") { - type = NavType.LongType - }, - navArgument("exerciseId") { - type = NavType.LongType - } - ) - ) { backStackEntry -> - val courseId = backStackEntry.arguments?.getLong("courseId") - val exerciseId = backStackEntry.arguments?.getLong("exerciseId") - - checkNotNull(courseId) - checkNotNull(exerciseId) + composable { backStackEntry -> + val route: ViewQuizResultScreen = backStackEntry.toRoute() ViewQuizResultScreen( modifier = Modifier.fillMaxSize(), - exerciseId = exerciseId, + exerciseId = route.exerciseId, quizType = QuizType.ViewResults, onNavigateUp = onRequestLeaveQuizResults ) @@ -112,7 +101,14 @@ internal fun ViewQuizResultScreen( modifier = Modifier .fillMaxSize() .padding(padding), - dataState = joinMultiple(quizExercise, submission, result, quizQuestions, maxPoints, ::ResultData), + dataState = joinMultiple( + quizExercise, + submission, + result, + quizQuestions, + maxPoints, + ::ResultData + ), loadingText = stringResource(id = R.string.quiz_result_loading), failureText = stringResource(id = R.string.quiz_result_failure), retryButtonText = stringResource(id = R.string.quiz_result_try_again), diff --git a/feature/settings/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/settings/SettingsScreen.kt b/feature/settings/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/settings/SettingsScreen.kt index 00e78af6d..ed577395c 100644 --- a/feature/settings/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/settings/SettingsScreen.kt @@ -57,13 +57,17 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable import org.koin.compose.koinInject -private const val SETTINGS_DESTINATION = "settings" -private const val PUSH_NOTIFICATION_SETTINGS_DESTINATION = "push_notification_settings" +@Serializable +private data object SettingsScreen + +@Serializable +private data object PushNotificationSettingsScreen fun NavController.navigateToSettings(builder: NavOptionsBuilder.() -> Unit) { - navigate(SETTINGS_DESTINATION, builder) + navigate(SettingsScreen, builder) } /** @@ -77,7 +81,7 @@ fun NavGraphBuilder.settingsScreen( onLoggedOut: () -> Unit, onDisplayThirdPartyLicenses: () -> Unit ) { - composable(SETTINGS_DESTINATION) { + composable { SettingsScreen( modifier = Modifier.fillMaxSize(), versionCode = versionCode, @@ -86,11 +90,11 @@ fun NavGraphBuilder.settingsScreen( onLoggedOut = onLoggedOut, onDisplayThirdPartyLicenses = onDisplayThirdPartyLicenses ) { - navController.navigate(PUSH_NOTIFICATION_SETTINGS_DESTINATION) + navController.navigate(PushNotificationSettingsScreen) } } - composable(PUSH_NOTIFICATION_SETTINGS_DESTINATION) { + composable { PushNotificationSettingsScreen( modifier = Modifier.fillMaxSize(), onNavigateBack = onNavigateUp diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2bb45c294..48134c21d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,9 +8,9 @@ accompanist = "0.30.1" androidDesugarJdkLibs = "2.0.4" androidxActivity = "1.8.1" androidxAppCompat = "1.7.0" -androidxComposeBom = "2024.06.00" +androidxComposeBom = "2024.11.00" androidxLifecycle = "2.8.7" -androidxNavigation = "2.7.5" +androidxNavigation = "2.8.4" androidGradlePlugin = "8.7.0" androidxPaging = "3.3.4" androidxDataStore = "1.1.1"