diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/exercise/ExerciseActionButtons.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/exercise/ExerciseActionButtons.kt index 67580b3b1..0049d6e2e 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/exercise/ExerciseActionButtons.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/exercise/ExerciseActionButtons.kt @@ -6,8 +6,9 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.outlined.Info import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -15,7 +16,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -29,6 +29,7 @@ import de.tum.informatics.www1.artemis.native_app.core.model.exercise.latestPart import de.tum.informatics.www1.artemis.native_app.core.model.exercise.participation.Participation import de.tum.informatics.www1.artemis.native_app.core.ui.R import de.tum.informatics.www1.artemis.native_app.core.ui.date.hasPassed +import de.tum.informatics.www1.artemis.native_app.core.ui.material.colors.ParticipationNotPossibleInfoMessageCardColors /** * This composable composes up to two buttons. The modifier parameter is applied to every button @@ -96,43 +97,50 @@ fun ExerciseActionButtons( } if (templateStatus != null) { - if (exercise is TextExercise) { - if (latestParticipation?.initializationState == Participation.InitializationState.INITIALIZED) { - Button( - modifier = modifier, - onClick = { - actions.onClickOpenTextExercise( - latestParticipation.id ?: return@Button + when (exercise) { + is TextExercise -> { + if (latestParticipation?.initializationState == Participation.InitializationState.INITIALIZED) { + Button( + modifier = modifier, + onClick = { + actions.onClickOpenTextExercise( + latestParticipation.id ?: return@Button + ) + }, + enabled = !exercise.teamMode + ) { + Text( + text = stringResource(id = R.string.exercise_actions_open_exercise_button) ) - }, - enabled = !exercise.teamMode - ) { - Text( - text = stringResource(id = R.string.exercise_actions_open_exercise_button) - ) + } } - } - if (latestParticipation?.initializationState == Participation.InitializationState.FINISHED && - (latestParticipation.results.isNullOrEmpty() || !showResult) - ) { - Button( - modifier = modifier, - onClick = { - actions.onClickOpenTextExercise( - latestParticipation.id ?: return@Button - ) - }, - enabled = !exercise.teamMode + if (latestParticipation?.initializationState == Participation.InitializationState.FINISHED && + (latestParticipation.results.isNullOrEmpty() || !showResult) ) { - Text( - text = stringResource(id = R.string.exercise_actions_view_submission_button) - ) + Button( + modifier = modifier, + onClick = { + actions.onClickOpenTextExercise( + latestParticipation.id ?: return@Button + ) + }, + enabled = !exercise.teamMode + ) { + Text( + text = stringResource(id = R.string.exercise_actions_view_submission_button) + ) + } } } - } else { - Row(modifier=Modifier.padding(top=2.dp, bottom = 2.dp)) { - InfoMessageCard() + // TODO: The following code is temporarily disabled. See https://github.com/ls1intum/artemis-android/issues/107 + //is QuizExercise -> { + // Do not show participation not possible info card for quiz exercises + //} + else -> { + Row(modifier=Modifier.padding(top=2.dp, bottom = 2.dp)) { + ParticipationNotPossibleInfoMessageCard() + } } } @@ -209,25 +217,29 @@ class BoundExerciseActions( @Composable -fun InfoMessageCard() { +fun ParticipationNotPossibleInfoMessageCard() { Box( modifier = Modifier - .border(width = 2.dp, color = Color.LightGray) - .background(Color(0xFFB3E5FC)) // Light sky blue background - .padding(10.dp) + .border( + width = 1.dp, + color = ParticipationNotPossibleInfoMessageCardColors.border, + shape = RoundedCornerShape(4.dp) + ) + .background(ParticipationNotPossibleInfoMessageCardColors.background) + .padding(8.dp) .fillMaxWidth() ) { Row(verticalAlignment = Alignment.CenterVertically) { Icon( - imageVector = Icons.Filled.Info, - contentDescription = "Information", + imageVector = Icons.Outlined.Info, + contentDescription = null, modifier = Modifier.padding(end = 8.dp), - tint = Color(0xFF0288D1) + tint = ParticipationNotPossibleInfoMessageCardColors.text ) Text( text = stringResource(id = R.string.exercise_participation_not_possible), fontSize = 16.sp, - color = Color.Black + color = ParticipationNotPossibleInfoMessageCardColors.text ) } } diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/exercise/ExerciseListItem.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/exercise/ExerciseListItem.kt index f7279e9c9..45fdccc46 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/exercise/ExerciseListItem.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/exercise/ExerciseListItem.kt @@ -29,9 +29,7 @@ import de.tum.informatics.www1.artemis.native_app.core.model.exercise.Exercise import de.tum.informatics.www1.artemis.native_app.core.ui.R import de.tum.informatics.www1.artemis.native_app.core.ui.date.getRelativeTime import de.tum.informatics.www1.artemis.native_app.core.ui.getWindowSizeClass -import de.tum.informatics.www1.artemis.native_app.core.ui.material.easyColor -import de.tum.informatics.www1.artemis.native_app.core.ui.material.hardColor -import de.tum.informatics.www1.artemis.native_app.core.ui.material.mediumColor +import de.tum.informatics.www1.artemis.native_app.core.ui.material.colors.DifficultyColors /** * Display a single exercise. @@ -113,13 +111,13 @@ private fun DifficultyRectangle(modifier: Modifier, difficulty: Exercise.Difficu .background( color = when (difficulty) { Exercise.Difficulty.EASY -> - easyColor + DifficultyColors.easy Exercise.Difficulty.MEDIUM -> - mediumColor + DifficultyColors.medium Exercise.Difficulty.HARD -> - hardColor + DifficultyColors.hard } ) ) diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/material/colors/DifficultyColors.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/material/colors/DifficultyColors.kt new file mode 100644 index 000000000..c9be88a03 --- /dev/null +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/material/colors/DifficultyColors.kt @@ -0,0 +1,13 @@ +package de.tum.informatics.www1.artemis.native_app.core.ui.material.colors + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +object DifficultyColors { + val hard: Color + @Composable get() = Color(0xffdc3545) + val medium: Color + @Composable get() = Color(0xffffc107) + val easy: Color + @Composable get() = Color(0xff28a745) +} \ No newline at end of file diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/material/colors/ParticipationNotPossibleInfoMessageCardColors.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/material/colors/ParticipationNotPossibleInfoMessageCardColors.kt new file mode 100644 index 000000000..847655e58 --- /dev/null +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/material/colors/ParticipationNotPossibleInfoMessageCardColors.kt @@ -0,0 +1,14 @@ +package de.tum.informatics.www1.artemis.native_app.core.ui.material.colors + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +object ParticipationNotPossibleInfoMessageCardColors { + val background: Color + @Composable get() = if(isSystemInDarkTheme()) Color(0xFF062A30) else Color(0xFFD1ECF1) + val border: Color + @Composable get() = if(isSystemInDarkTheme()) Color(0xFF148EA1) else Color(0xFFA2DAE3) + val text: Color + @Composable get() = if(isSystemInDarkTheme()) Color(0xFF36CEE6) else Color(0xFF09414A) +} \ No newline at end of file diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/material/text_colors.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/material/colors/text_colors.kt similarity index 96% rename from core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/material/text_colors.kt rename to core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/material/colors/text_colors.kt index 50c755297..2ccad4b89 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/material/text_colors.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/material/colors/text_colors.kt @@ -1,4 +1,4 @@ -package de.tum.informatics.www1.artemis.native_app.core.ui.material +package de.tum.informatics.www1.artemis.native_app.core.ui.material.colors import androidx.compose.material3.ColorScheme import androidx.compose.runtime.Composable diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/material/difficulty_colors.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/material/difficulty_colors.kt deleted file mode 100644 index 52f862fc0..000000000 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/material/difficulty_colors.kt +++ /dev/null @@ -1,11 +0,0 @@ -package de.tum.informatics.www1.artemis.native_app.core.ui.material - -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color - -val hardColor: Color - @Composable get() = Color(0xffdc3545) -val mediumColor: Color - @Composable get() = Color(0xffffc107) -val easyColor: Color - @Composable get() = Color(0xff28a745) \ No newline at end of file diff --git a/core/ui/src/main/res/values/exercise_strings.xml b/core/ui/src/main/res/values/exercise_strings.xml index 540edd2d4..c391b8652 100644 --- a/core/ui/src/main/res/values/exercise_strings.xml +++ b/core/ui/src/main/res/values/exercise_strings.xml @@ -39,8 +39,8 @@ View result - Participating this exercise is currently - not possible in the mobile app. + Participating in this exercise is currently + not possible in the mobile app Start exercise Open exercise View submission diff --git a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/ExerciseScreen.kt b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/ExerciseScreen.kt index 625fd6ea7..8c6168f81 100644 --- a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/ExerciseScreen.kt +++ b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/ExerciseScreen.kt @@ -8,12 +8,9 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface -import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.compositionLocalOf @@ -21,7 +18,6 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector @@ -29,7 +25,6 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.google.accompanist.web.WebViewState -import de.tum.informatics.www1.artemis.native_app.core.data.isSuccess import de.tum.informatics.www1.artemis.native_app.core.data.orNull import de.tum.informatics.www1.artemis.native_app.core.model.exercise.ProgrammingExercise import de.tum.informatics.www1.artemis.native_app.core.model.exercise.latestParticipation @@ -42,10 +37,7 @@ import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.getProble import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.MetisContext import de.tum.informatics.www1.artemis.native_app.feature.metis.ui.canDisplayMetisOnDisplaySide import kotlinx.coroutines.Deferred -import me.onebone.toolbar.CollapsingToolbarScaffold import me.onebone.toolbar.ExperimentalToolbarApi -import me.onebone.toolbar.ScrollStrategy -import me.onebone.toolbar.rememberCollapsingToolbarScaffoldState val LocalExerciseScreenFloatingActionButton = compositionLocalOf { ExerciseScreenFloatingActionButtonProvider() } @@ -126,59 +118,8 @@ internal fun ExerciseScreen( metisContentRatio = METIS_RATIO ) - // Only collapse toolbar if otherwise too much of the screen would be occupied by it - val isToolbarCollapsible = windowSizeClass.heightSizeClass < WindowHeightSizeClass.Expanded - val isLongToolbar = windowSizeClass.widthSizeClass >= WindowWidthSizeClass.Medium - // Keep state when device configuration changes - val body = @Composable { modifier: Modifier -> - val onParticipateInQuizDelegate = { isPractice: Boolean -> - courseId?.let { - onParticipateInQuiz(it, isPractice) - } - } - - val actions = remember( - courseId, - onViewTextExerciseParticipationScreen, - onParticipateInQuizDelegate, - onViewResult, - viewModel - ) { - ExerciseActions( - onClickStartTextExercise = { - startExerciseParticipationDeferred = viewModel.startExercise() - }, - onClickPracticeQuiz = { onParticipateInQuizDelegate(true) }, - onClickOpenQuiz = { onParticipateInQuizDelegate(false) }, - onClickStartQuiz = { onParticipateInQuizDelegate(false) }, - onClickViewResult = onViewResult, - onClickOpenTextExercise = onViewTextExerciseParticipationScreen, - onClickViewQuizResults = { - courseId?.let { - onClickViewQuizResults(it) - } - } - ) - } - - ExerciseScreenBody( - modifier = modifier, - exerciseDataState = exerciseDataState, - displayCommunicationOnSide = displayCommunicationOnSide, - navController = navController, - metisContext = metisContext, - actions = actions, - webViewState = webViewState, - setWebView = { savedWebView = it }, - webView = savedWebView, - onClickRetry = viewModel::requestReloadExercise, - serverUrl = serverUrl, - authToken = authToken - ) - } - val currentExerciseScreenFloatingActionButton = remember { ExerciseScreenFloatingActionButtonProvider() } @@ -197,65 +138,66 @@ internal fun ExerciseScreen( } } - if (isToolbarCollapsible) { - val state = rememberCollapsingToolbarScaffoldState() - // On the first load, we need to expand the toolbar, as otherwise content may be hidden - var hasExecutedInitialExpand by rememberSaveable { mutableStateOf(false) } + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + ExerciseScreenTopAppBar( + modifier = Modifier.fillMaxWidth(), + onNavigateBack = onNavigateBack, + exerciseDataState = exerciseDataState, + onRequestReloadExercise = viewModel::requestReloadExercise + ) + }, + floatingActionButton = floatingActionButton + ) { padding -> - LaunchedEffect(exerciseDataState, hasExecutedInitialExpand) { - if (exerciseDataState.isSuccess && !hasExecutedInitialExpand) { - state.toolbarState.expand() - hasExecutedInitialExpand = true + val onParticipateInQuizDelegate = { isPractice: Boolean -> + courseId?.let { + onParticipateInQuiz(it, isPractice) } } - Scaffold( - modifier = Modifier.fillMaxSize(), - floatingActionButton = floatingActionButton - ) { padding -> - CollapsingToolbarScaffold( - modifier = Modifier - .fillMaxSize() - .padding(padding), - state = state, - toolbar = { - ExerciseScreenCollapsingTopBar( - modifier = Modifier.fillMaxWidth(), - state = state, - exercise = exerciseDataState, - onNavigateBack = onNavigateBack, - onRequestRefresh = viewModel::requestReloadExercise, - isLongToolbar = isLongToolbar - ) + val actions = remember( + courseId, + onViewTextExerciseParticipationScreen, + onParticipateInQuizDelegate, + onViewResult, + viewModel + ) { + ExerciseActions( + onClickStartTextExercise = { + startExerciseParticipationDeferred = viewModel.startExercise() }, - scrollStrategy = ScrollStrategy.ExitUntilCollapsed, - body = { - Surface(Modifier.fillMaxSize()) { - body(Modifier.fillMaxSize()) + onClickPracticeQuiz = { onParticipateInQuizDelegate(true) }, + onClickOpenQuiz = { onParticipateInQuizDelegate(false) }, + onClickStartQuiz = { onParticipateInQuizDelegate(false) }, + onClickViewResult = onViewResult, + onClickOpenTextExercise = onViewTextExerciseParticipationScreen, + onClickViewQuizResults = { + courseId?.let { + onClickViewQuizResults(it) } } ) } - } else { - Scaffold( - modifier = Modifier.fillMaxSize(), - topBar = { - StaticTopAppBar( - modifier = Modifier.fillMaxWidth(), - onNavigateBack = onNavigateBack, - exerciseDataState = exerciseDataState, - isLongToolbar = isLongToolbar, - onRequestReloadExercise = viewModel::requestReloadExercise - ) - }, - floatingActionButton = floatingActionButton - ) { padding -> - body( - Modifier - .fillMaxSize() - .padding(padding) - ) - } + + ExerciseScreenBody( + modifier = Modifier + .fillMaxSize() + .padding(padding), + exerciseDataState = exerciseDataState, + isLongToolbar = isLongToolbar, + displayCommunicationOnSide = displayCommunicationOnSide, + navController = navController, + metisContext = metisContext, + actions = actions, + webViewState = webViewState, + setWebView = { savedWebView = it }, + webView = savedWebView, + onClickRetry = viewModel::requestReloadExercise, + serverUrl = serverUrl, + authToken = authToken + ) } } } diff --git a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/ExerciseScreenBody.kt b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/ExerciseScreenBody.kt index 1bd9ba8fd..c55085050 100644 --- a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/ExerciseScreenBody.kt +++ b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/ExerciseScreenBody.kt @@ -37,6 +37,7 @@ const val METIS_RATIO = 0.3f internal fun ExerciseScreenBody( modifier: Modifier, exerciseDataState: DataState, + isLongToolbar: Boolean, displayCommunicationOnSide: Boolean, navController: NavController, metisContext: MetisContext?, @@ -60,6 +61,7 @@ internal fun ExerciseScreenBody( ExerciseOverviewTab( modifier = modifier, exercise = exercise, + isLongToolbar = isLongToolbar, webViewState = webViewState, setWebView = setWebView, webView = webView, @@ -73,6 +75,7 @@ internal fun ExerciseScreenBody( Modifier .fillMaxSize() .padding(horizontal = 8.dp) + .padding(bottom = 8.dp) ) // Commented out as we may need that code again once we display communications for exercises diff --git a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/ExerciseScreenTopAppBar.kt b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/ExerciseScreenTopAppBar.kt index 31c6dba97..b4a28fb04 100644 --- a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/ExerciseScreenTopAppBar.kt +++ b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/ExerciseScreenTopAppBar.kt @@ -1,39 +1,20 @@ package de.tum.informatics.www1.artemis.native_app.feature.exerciseview.home -import androidx.annotation.StringRes -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.layout.layout -import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.AnnotatedString @@ -41,68 +22,34 @@ import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.TextUnit import com.google.accompanist.placeholder.material.placeholder 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.model.exercise.currentUserPoints -import de.tum.informatics.www1.artemis.native_app.core.ui.common.EmptyDataStateUi -import de.tum.informatics.www1.artemis.native_app.core.ui.date.getRelativeTime -import de.tum.informatics.www1.artemis.native_app.core.ui.date.hasPassed -import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.ExerciseCategoryChipData -import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.ExerciseCategoryChipRow -import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.ExerciseInfoChip -import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.ExerciseInfoChipTextHorizontalPadding -import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.ExercisePointsDecimalFormat import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.getExerciseTypeIconPainter -import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.R -import kotlinx.datetime.Instant -import me.onebone.toolbar.CollapsingToolbarScaffoldState -import me.onebone.toolbar.CollapsingToolbarScope -/** - * Display a collapsing top app bar with an incollapsible [TopAppBar] and [TopBarExerciseInformation] as the collapsible part in a column. - */ + @Composable -internal fun CollapsingToolbarScope.ExerciseScreenCollapsingTopBar( +internal fun ExerciseScreenTopAppBar( modifier: Modifier, - state: CollapsingToolbarScaffoldState, - isLongToolbar: Boolean, - exercise: DataState, + exerciseDataState: DataState, onNavigateBack: () -> Unit, - onRequestRefresh: () -> Unit + onRequestReloadExercise: () -> Unit ) { - TopAppBar( - modifier = modifier, - title = { - TitleText( - modifier = Modifier.graphicsLayer { alpha = 1f - state.toolbarState.progress }, - exerciseDataState = exercise, - maxLines = 1 - ) - }, - navigationIcon = { - TopAppBarNavigationIcon(onNavigateBack) - }, - actions = { - TopAppBarActions(onRequestRefresh = onRequestRefresh) - } - ) - - TopBarExerciseInformation( - modifier = Modifier - .fillMaxWidth() - .padding(top = 64.dp) - .background(MaterialTheme.colorScheme.surface) - .padding(horizontal = 16.dp) - .parallax(0f), - titleTextAlpha = state.toolbarState.progress, - exercise = exercise, - isLongToolbar = isLongToolbar - ) + Column(modifier = modifier) { + TopAppBar( + modifier = Modifier.fillMaxWidth(), + title = { TitleText(modifier = modifier, maxLines = 1, exerciseDataState = exerciseDataState) }, + navigationIcon = { + TopAppBarNavigationIcon(onNavigateBack = onNavigateBack) + }, + actions = { + TopAppBarActions(onRequestRefresh = onRequestReloadExercise) + } + ) + } } @Composable @@ -119,230 +66,6 @@ private fun TopAppBarActions(onRequestRefresh: () -> Unit) { } } -private val placeholderCategoryChips = listOf( - ExerciseCategoryChipData("WWWW", Color.Cyan), - ExerciseCategoryChipData("WWWW", Color.Cyan), - ExerciseCategoryChipData("WWWW", Color.Cyan) -) - -/** - * @param isLongToolbar if the deadline information is displayed on the right side of the toolbar. - * If false, the information is instead displayed in the column - */ -@Composable -internal fun TopBarExerciseInformation( - modifier: Modifier, - titleTextAlpha: Float, - exercise: DataState, - isLongToolbar: Boolean -) { - val dueDate = exercise.bind { it.dueDate }.orElse(null) - val assessmentDueData = exercise.bind { it.assessmentDueDate }.orElse(null) - val releaseData = exercise.bind { it.releaseDate }.orElse(null) - - var maxWidth: Int by remember { mutableIntStateOf(0) } - val updateMaxWidth = { new: Int -> maxWidth = new } - - val dueDateTopBarTextInformation = - @Composable { date: Instant, hintRes: @receiver:StringRes Int -> - TopBarTextInformation( - modifier = Modifier.fillMaxWidth().padding(bottom = 1.dp), - hintColumnWidth = maxWidth, - hint = stringResource(id = hintRes), - dataText = getRelativeTime(to = date).toString(), - dataColor = getDueDateColor(date), - updateHintColumnWidth = updateMaxWidth - ) - } - - val exerciseInfoUi = @Composable { - EmptyDataStateUi( - dataState = exercise, - otherwise = { - ExerciseCategoryChipRow( - modifier = Modifier - .fillMaxWidth() - .placeholder(true), - chips = placeholderCategoryChips - ) - } - ) { loadedExercise -> - ExerciseCategoryChipRow( - modifier = Modifier.fillMaxWidth(), - exercise = loadedExercise - ) - } - - val currentUserPoints = exercise.bind { exercise -> - exercise.currentUserPoints?.let(ExercisePointsDecimalFormat::format) - }.orElse(null) - val maxPoints = exercise.bind { exercise -> - exercise.maxPoints?.let(ExercisePointsDecimalFormat::format) - }.orElse(null) - - val pointsHintText = when { - currentUserPoints != null && maxPoints != null -> stringResource( - id = R.string.exercise_view_overview_points_reached, - currentUserPoints, - maxPoints - ) - - maxPoints != null -> stringResource( - id = R.string.exercise_view_overview_points_max, - maxPoints - ) - - else -> stringResource(id = R.string.exercise_view_overview_points_none) - } - - Text( - modifier = Modifier - .placeholder(exercise !is DataState.Success) - .padding(bottom = 4.dp), - text = pointsHintText, - style = MaterialTheme.typography.bodyLarge - ) - - releaseData?.let { - dueDateTopBarTextInformation( - it, - R.string.exercise_view_overview_hint_assessment_release_date - ) - } - - } - - val dueDateColumnUi = @Composable { contentModifier: Modifier -> - - Column( - modifier = contentModifier, - ) { - - dueDate?.let { - dueDateTopBarTextInformation( - it, - R.string.exercise_view_overview_hint_submission_due_date - ) - } - - assessmentDueData?.let { - dueDateTopBarTextInformation( - it, - R.string.exercise_view_overview_hint_assessment_due_date - ) - } - - val complaintPossible = exercise.bind { exercise -> - exercise.allowComplaintsForAutomaticAssessments - }.orElse(false) - - Text( - modifier = Modifier - .placeholder(exercise !is DataState.Success) - .padding(bottom = 4.dp), - text = "Complaint possible: " + if (complaintPossible == true) "Yes" else "No", - style = MaterialTheme.typography.bodyLarge - ) - - } - } - - // Actual UI - Column( - modifier = modifier.padding(10.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - - Text( - text = "Exercise Details", - style = MaterialTheme.typography.titleMedium, - modifier = Modifier - .padding(bottom = 1.dp) - .fillMaxWidth(), - textAlign = TextAlign.Start - ) - Divider( - color = Color.Black, - thickness = 3.dp, - modifier = Modifier.padding(vertical = 0.dp) - ) - - // Here we make the distinction in the layout between long toolbar and short toolbar - - if (isLongToolbar) { - Row(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.weight(1f)) { - exerciseInfoUi() - } - - dueDateColumnUi( - Modifier - .width(IntrinsicSize.Max) - .align(Alignment.Bottom) - ) - } - } else { - Column(modifier = Modifier.fillMaxWidth()) { - exerciseInfoUi() - dueDateColumnUi(Modifier.fillMaxWidth()) - } - } - } -} - -/** - * Text information composable that achieves a table like layout, where the hint is the first column - * and the data is the second column. - */ -@Composable -private fun TopBarTextInformation( - modifier: Modifier, - hintColumnWidth: Int, - hint: String, - dataText: String, - dataColor: Color?, - updateHintColumnWidth: (Int) -> Unit -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - modifier = Modifier.layout { measurable, constraints -> - val placeable = measurable.measure(constraints) - - val assignedWidth = maxOf(hintColumnWidth, placeable.width) - if (assignedWidth > hintColumnWidth) { - updateHintColumnWidth(assignedWidth) - } - - layout(width = assignedWidth, height = placeable.height) { - placeable.placeRelative(0, 0) - } - }, - text = hint, - style = MaterialTheme.typography.bodyLarge, - ) - - val dataModifier = Modifier - - if (dataColor != null) { - ExerciseInfoChip(modifier = dataModifier, color = dataColor, text = dataText) - } else { - Text( - modifier = dataModifier.padding(horizontal = ExerciseInfoChipTextHorizontalPadding), - text = dataText, - style = MaterialTheme.typography.bodyMedium - ) - } - } -} - -@Composable -private fun getDueDateColor(dueDate: Instant): Color = - if (dueDate.hasPassed()) Color.Red else Color.Green - @Composable private fun TitleText( modifier: Modifier, @@ -352,40 +75,26 @@ private fun TitleText( ) { val fontSize = style.fontSize - val (titleText, inlineContent) = remember(exerciseDataState) { - val text = buildAnnotatedString { - appendInlineContent("icon") - append(" ") - append( - exerciseDataState.bind { it.title }.orNull() - ?: "Exercise name placeholder" - ) - } - - val inlineContent = mapOf( - "icon" to InlineTextContent( - Placeholder( - fontSize, - fontSize, - PlaceholderVerticalAlign.TextCenter - ) - ) { - Icon( - painter = getExerciseTypeIconPainter(exerciseDataState.orNull()), - contentDescription = null - ) - } - ) - - text to inlineContent - } + val (titleText, inlineContent) = rememberTitleTextWithInlineContent(exerciseDataState, fontSize) Text( text = titleText, inlineContent = inlineContent, modifier = modifier .placeholder(exerciseDataState !is DataState.Success) - .semantics { set(SemanticsProperties.Text, listOf(AnnotatedString(exerciseDataState.bind { it.title }.orNull().orEmpty()))) }, + .semantics { + set( + SemanticsProperties.Text, + listOf( + AnnotatedString( + exerciseDataState + .bind { it.title } + .orNull() + .orEmpty() + ) + ) + ) + }, style = style, maxLines = maxLines, overflow = TextOverflow.Ellipsis @@ -393,33 +102,33 @@ private fun TitleText( } @Composable -internal fun StaticTopAppBar( - modifier: Modifier, +private fun rememberTitleTextWithInlineContent( exerciseDataState: DataState, - isLongToolbar: Boolean, - onNavigateBack: () -> Unit, - onRequestReloadExercise: () -> Unit -) { - Column(modifier = modifier) { - TopAppBar( - modifier = Modifier.fillMaxWidth(), - title = { TitleText(modifier = modifier, maxLines = 1, exerciseDataState = exerciseDataState) }, - navigationIcon = { - TopAppBarNavigationIcon(onNavigateBack = onNavigateBack) - }, - actions = { - TopAppBarActions(onRequestRefresh = onRequestReloadExercise) - } - ) - TopBarExerciseInformation( - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.surface) - .padding(horizontal = 16.dp) - .border(2.dp, Color.Black, RoundedCornerShape(4.dp)), - titleTextAlpha = 1f, - exercise = exerciseDataState, - isLongToolbar = isLongToolbar + fontSize: TextUnit +) = remember(exerciseDataState) { + val text = buildAnnotatedString { + appendInlineContent("icon") + append(" ") + append( + exerciseDataState.bind { it.title }.orNull() + ?: "Exercise name placeholder" ) } -} \ No newline at end of file + + val inlineContent = mapOf( + "icon" to InlineTextContent( + Placeholder( + fontSize, + fontSize, + PlaceholderVerticalAlign.TextCenter + ) + ) { + Icon( + painter = getExerciseTypeIconPainter(exerciseDataState.orNull()), + contentDescription = null + ) + } + ) + + text to inlineContent +} diff --git a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/overview/ExerciseOverviewTab.kt b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/overview/ExerciseOverviewTab.kt index b351c303f..037c131d1 100644 --- a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/overview/ExerciseOverviewTab.kt +++ b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/overview/ExerciseOverviewTab.kt @@ -2,24 +2,55 @@ package de.tum.informatics.www1.artemis.native_app.feature.exerciseview.home.ove import android.annotation.SuppressLint import android.webkit.WebView +import androidx.annotation.StringRes import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Divider +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.layout +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.google.accompanist.web.WebViewState import de.tum.informatics.www1.artemis.native_app.core.model.exercise.Exercise import de.tum.informatics.www1.artemis.native_app.core.model.exercise.QuizExercise +import de.tum.informatics.www1.artemis.native_app.core.model.exercise.currentUserPoints +import de.tum.informatics.www1.artemis.native_app.core.ui.date.getRelativeTime +import de.tum.informatics.www1.artemis.native_app.core.ui.date.hasPassed import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.ExerciseActions +import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.ExerciseCategoryChipRow +import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.ExerciseInfoChip +import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.ExerciseInfoChipTextHorizontalPadding +import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.ExercisePointsDecimalFormat import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.ArtemisWebView +import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.R +import kotlinx.datetime.Instant + @SuppressLint("SetJavaScriptEnabled") @Composable internal fun ExerciseOverviewTab( modifier: Modifier = Modifier, exercise: Exercise, + isLongToolbar: Boolean, webViewState: WebViewState?, serverUrl: String, authToken: String, @@ -29,11 +60,18 @@ internal fun ExerciseOverviewTab( ) { Column( modifier = modifier - .fillMaxSize() - .background(Color.White), + .fillMaxSize(), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - + ExerciseInformation( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surface) + .padding(horizontal = 16.dp) + .border(2.dp, Color.Black, RoundedCornerShape(4.dp)), + exercise = exercise, + isLongToolbar = isLongToolbar + ) ParticipationStatusUi( modifier = Modifier @@ -56,7 +94,7 @@ internal fun ExerciseOverviewTab( ) } else { Text( - text = "No problem statement available.", + text = stringResource(id = R.string.exercise_view_overview_problem_statement_not_available), modifier = Modifier .fillMaxWidth() .padding(16.dp), @@ -65,3 +103,213 @@ internal fun ExerciseOverviewTab( } } } + + +/** + * @param isLongToolbar if the deadline information is displayed on the right side of the toolbar. + * If false, the information is instead displayed in the column + */ +@Composable +private fun ExerciseInformation( + modifier: Modifier, + exercise: Exercise, + isLongToolbar: Boolean +) { + var maxWidth: Int by remember { mutableIntStateOf(0) } + val updateMaxWidth = { new: Int -> maxWidth = new } + + val nullableDueDateTextInfo = @Composable { dueDate: Instant?, hintRes: @receiver:StringRes Int -> + if (dueDate != null) { + DueDateTextInfo( + dueDate = dueDate, + hintRes = hintRes, + maxWidth = maxWidth, + updateMaxWidth = updateMaxWidth + ) + } + } + + val categoryPointsReleaseDateUi = @Composable { + ExerciseCategoryChipRow( + modifier = Modifier.fillMaxWidth(), + exercise = exercise + ) + + ExercisePointInfo(exercise) + + nullableDueDateTextInfo( + exercise.releaseDate, + R.string.exercise_view_overview_hint_assessment_release_date + ) + } + + val dueDateColumnUi = @Composable { contentModifier: Modifier -> + Column( + modifier = contentModifier, + ) { + nullableDueDateTextInfo( + exercise.assessmentDueDate, + R.string.exercise_view_overview_hint_assessment_due_date + ) + + nullableDueDateTextInfo( + exercise.assessmentDueDate, + R.string.exercise_view_overview_hint_assessment_due_date + ) + + ExerciseCompliantPossibleInfo(exercise) + } + } + + // Actual UI + Column( + modifier = modifier.padding(10.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = stringResource(R.string.exercise_view_overview_title), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier + .padding(bottom = 1.dp) + .fillMaxWidth(), + textAlign = TextAlign.Start + ) + Divider( + color = Color.Black, + thickness = 2.dp, + modifier = Modifier.padding(vertical = 0.dp) + ) + + // Here we make the distinction in the layout between long toolbar and short toolbar + if (isLongToolbar) { + Row(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.weight(1f)) { + categoryPointsReleaseDateUi() + } + + dueDateColumnUi( + Modifier + .width(IntrinsicSize.Max) + .align(Alignment.Bottom) + ) + } + } else { + Column(modifier = Modifier.fillMaxWidth()) { + categoryPointsReleaseDateUi() + dueDateColumnUi(Modifier.fillMaxWidth()) + } + } + } +} + +@Composable +private fun ExerciseCompliantPossibleInfo(exercise: Exercise) { + val complaintPossible = exercise.allowComplaintsForAutomaticAssessments ?: false + val complaintPossibleText = stringResource( + R.string.exercise_view_overview_hint_assessment_complaint_possible, + stringResource(if (complaintPossible) R.string.exercise_view_overview_hint_assessment_complaint_possible_yes else R.string.exercise_view_overview_hint_assessment_complaint_possible_no) + ) + + Text( + modifier = Modifier.padding(bottom = 4.dp), + text = complaintPossibleText, + style = MaterialTheme.typography.bodyLarge + ) +} + +@Composable +private fun ExercisePointInfo(exercise: Exercise) { + val currentUserPoints = exercise.currentUserPoints?.let(ExercisePointsDecimalFormat::format) + val maxPoints = exercise.maxPoints?.let(ExercisePointsDecimalFormat::format) + + val pointsHintText = when { + currentUserPoints != null && maxPoints != null -> stringResource( + id = R.string.exercise_view_overview_points_reached, + currentUserPoints, + maxPoints + ) + + maxPoints != null -> stringResource( + id = R.string.exercise_view_overview_points_max, + maxPoints + ) + + else -> stringResource(id = R.string.exercise_view_overview_points_none) + } + + Text( + modifier = Modifier.padding(bottom = 4.dp), + text = pointsHintText, + style = MaterialTheme.typography.bodyLarge + ) +} + +@Composable +private fun DueDateTextInfo( + dueDate: Instant, + @StringRes hintRes: Int, + maxWidth: Int, + updateMaxWidth: (Int) -> Unit +) = TextInformation( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 1.dp), + hintColumnWidth = maxWidth, + hint = stringResource(id = hintRes), + dataText = getRelativeTime(to = dueDate).toString(), + dataColor = getDueDateColor(dueDate), + updateHintColumnWidth = updateMaxWidth + ) + +/** + * Text information composable that achieves a table like layout, where the hint is the first column + * and the data is the second column. + */ +@Composable +private fun TextInformation( + modifier: Modifier, + hintColumnWidth: Int, + hint: String, + dataText: String, + dataColor: Color?, + updateHintColumnWidth: (Int) -> Unit +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + modifier = Modifier.layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + + val assignedWidth = maxOf(hintColumnWidth, placeable.width) + if (assignedWidth > hintColumnWidth) { + updateHintColumnWidth(assignedWidth) + } + + layout(width = assignedWidth, height = placeable.height) { + placeable.placeRelative(0, 0) + } + }, + text = hint, + style = MaterialTheme.typography.bodyLarge, + ) + + val dataModifier = Modifier + + if (dataColor != null) { + ExerciseInfoChip(modifier = dataModifier, color = dataColor, text = dataText) + } else { + Text( + modifier = dataModifier.padding(horizontal = ExerciseInfoChipTextHorizontalPadding), + text = dataText, + style = MaterialTheme.typography.bodyMedium + ) + } + } +} + +@Composable +private fun getDueDateColor(dueDate: Instant): Color = + if (dueDate.hasPassed()) Color.Red else Color.Green diff --git a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/overview/ParticipationStatus.kt b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/overview/ParticipationStatus.kt index d07acca26..441bb6388 100644 --- a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/overview/ParticipationStatus.kt +++ b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/overview/ParticipationStatus.kt @@ -1,5 +1,7 @@ package de.tum.informatics.www1.artemis.native_app.feature.exerciseview.home.overview +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp diff --git a/feature/exercise-view/src/main/res/values/exercise_view_strings.xml b/feature/exercise-view/src/main/res/values/exercise_view_strings.xml index 8580ba5ff..9c82f6693 100644 --- a/feature/exercise-view/src/main/res/values/exercise_view_strings.xml +++ b/feature/exercise-view/src/main/res/values/exercise_view_strings.xml @@ -8,15 +8,23 @@ Overview Communication + Exercise Details + Points: %1$s Points: %1$s / %2$s No points Submission due: Assessment due: + Complaint possible: %1$s Release date: + Yes + No + Your exercise status: + No problem statement available. + Exercise info Type Points 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 42342f213..646e47f87 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 @@ -67,7 +67,7 @@ import de.tum.informatics.www1.artemis.native_app.core.datastore.defaults.Artemi import de.tum.informatics.www1.artemis.native_app.core.model.server_config.ProfileInfo import de.tum.informatics.www1.artemis.native_app.core.ui.Spacings import de.tum.informatics.www1.artemis.native_app.core.ui.common.BasicDataStateUi -import de.tum.informatics.www1.artemis.native_app.core.ui.material.linkTextColor +import de.tum.informatics.www1.artemis.native_app.core.ui.material.colors.linkTextColor import de.tum.informatics.www1.artemis.native_app.feature.login.custom_instance_selection.CustomInstanceSelectionScreen import de.tum.informatics.www1.artemis.native_app.feature.login.instance_selection.InstanceSelectionScreen import de.tum.informatics.www1.artemis.native_app.feature.login.login.LoginScreen diff --git a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationList.kt b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationList.kt index 4863750f6..9c1be4744 100644 --- a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationList.kt +++ b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationList.kt @@ -277,7 +277,7 @@ private fun LazyListScope.conversationList( onNavigateToConversation: (conversationId: Long) -> Unit, onToggleMarkAsFavourite: (conversationId: Long, favorite: Boolean) -> Unit, onToggleHidden: (conversationId: Long, hidden: Boolean) -> Unit, - onToggleMuted: (conversationId: Long, muted: Boolean) -> Unit, + onToggleMuted: (conversationId: Long, muted: Boolean) -> Unit ) { if (!conversations.isExpanded) return items( @@ -300,7 +300,7 @@ private fun LazyListScope.conversationList( ) }, onToggleHidden = { onToggleHidden(conversation.id, !conversation.isHidden) }, - onToggleMuted = { onToggleMuted(conversation.id, !conversation.isMuted) }, + onToggleMuted = { onToggleMuted(conversation.id, !conversation.isMuted) } ) } } @@ -314,7 +314,7 @@ private fun ConversationListItem( onNavigateToConversation: () -> Unit, onToggleMarkAsFavourite: () -> Unit, onToggleHidden: () -> Unit, - onToggleMuted: () -> Unit, + onToggleMuted: () -> Unit ) { var isContextDialogShown by remember { mutableStateOf(false) } val onDismissRequest = { isContextDialogShown = false } @@ -547,4 +547,4 @@ private fun String.removeSectionPrefix(): String { } } return result.trim() -} +} \ No newline at end of file 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 d27066657..a2fc5f4ed 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 @@ -181,10 +181,10 @@ fun ConversationOverviewBody( } ConversationFabMenu( + canCreateChannel = canCreateChannel, onCreateChat = onRequestCreatePersonalConversation, onBrowseChannels = onRequestBrowseChannel, - onCreateChannel = onRequestAddChannel, - canCreateChannel = canCreateChannel + onCreateChannel = onRequestAddChannel ) } @@ -207,10 +207,10 @@ fun ConversationOverviewBody( @Composable fun ConversationFabMenu( + canCreateChannel: Boolean, onCreateChat: () -> Unit, onBrowseChannels: () -> Unit, - onCreateChannel: () -> Unit, - canCreateChannel: Boolean + onCreateChannel: () -> Unit ) { var expanded by remember { mutableStateOf(false) } diff --git a/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/communication_module.kt b/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/communication_module.kt index 06e05ea65..f3e253d19 100644 --- a/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/communication_module.kt +++ b/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/communication_module.kt @@ -5,6 +5,7 @@ import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.con import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.manageConversationsModule import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.sharedConversationModule import de.tum.informatics.www1.artemis.native_app.feature.metis.ui.NavigateToUserConversationViewModel +import de.tum.informatics.www1.artemis.native_app.feature.metis.ui.SinglePageConversationBodyViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module @@ -22,4 +23,15 @@ val communicationModule = module { get() ) } + + viewModel { params -> + SinglePageConversationBodyViewModel( + params[0], + get(), + get(), + get(), + get(), + get() + ) + } } diff --git a/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/ConversationFacadeUi.kt b/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/ConversationFacadeUi.kt index 973363b8f..90694625c 100644 --- a/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/ConversationFacadeUi.kt +++ b/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/ConversationFacadeUi.kt @@ -4,7 +4,10 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import de.tum.informatics.www1.artemis.native_app.feature.metis.codeofconduct.ui.CodeOfConductFacadeUi +import org.koin.androidx.compose.getViewModel +import org.koin.androidx.compose.koinViewModel import org.koin.compose.koinInject +import org.koin.core.parameter.parametersOf /** * Displays the conversation ui. If the code of conduct has not yet been accepted, displays a code @@ -22,13 +25,9 @@ fun ConversationFacadeUi( codeOfConductAcceptedContent = { SinglePageConversationBody( modifier = Modifier.fillMaxSize(), + viewModel = koinViewModel { parametersOf(courseId) }, courseId = courseId, initialConfiguration = initialConfiguration, - accountService = koinInject(), - accountDataService = koinInject(), - courseService = koinInject(), - networkStatusProvider = koinInject(), - serverConfigurationService = koinInject() ) } ) diff --git a/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/SinglePageConversationBody.kt b/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/SinglePageConversationBody.kt index cfe7a811b..56da5fc4c 100644 --- a/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/SinglePageConversationBody.kt +++ b/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/SinglePageConversationBody.kt @@ -4,23 +4,13 @@ import android.os.Parcelable import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.padding 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.Modifier import androidx.compose.ui.unit.dp -import de.tum.informatics.www1.artemis.native_app.core.common.flatMapLatest -import de.tum.informatics.www1.artemis.native_app.core.data.retryOnInternet -import de.tum.informatics.www1.artemis.native_app.core.data.service.network.AccountDataService -import de.tum.informatics.www1.artemis.native_app.core.data.service.network.CourseService -import de.tum.informatics.www1.artemis.native_app.core.datastore.AccountService -import de.tum.informatics.www1.artemis.native_app.core.datastore.ServerConfigurationService -import de.tum.informatics.www1.artemis.native_app.core.datastore.authToken -import de.tum.informatics.www1.artemis.native_app.core.device.NetworkStatusProvider -import de.tum.informatics.www1.artemis.native_app.core.model.account.isAtLeastTutorInCourse import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui.ConversationScreen import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ui.conversation.browse_channels.BrowseChannelsScreen import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ui.conversation.create_channel.CreateChannelScreen @@ -30,20 +20,14 @@ import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversati import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ui.conversation.settings.members.ConversationMembersScreen import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ui.conversation.settings.overview.ConversationSettingsScreen import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.StandalonePostId -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize @Composable internal fun SinglePageConversationBody( modifier: Modifier, + viewModel: SinglePageConversationBodyViewModel, courseId: Long, - initialConfiguration: ConversationConfiguration = NothingOpened, - accountService: AccountService, - serverConfigurationService: ServerConfigurationService, - courseService: CourseService, - accountDataService: AccountDataService, - networkStatusProvider: NetworkStatusProvider + initialConfiguration: ConversationConfiguration = NothingOpened ) { var configuration: ConversationConfiguration by rememberSaveable(initialConfiguration) { mutableStateOf(initialConfiguration) @@ -56,30 +40,7 @@ internal fun SinglePageConversationBody( } } - var canCreateChannel by rememberSaveable { mutableStateOf(false) } - val coroutineScope = rememberCoroutineScope() - - LaunchedEffect(courseId) { - coroutineScope.launch { - val flow = flatMapLatest( - serverConfigurationService.serverUrl, - accountService.authToken - ) { serverUrl, authToken -> - retryOnInternet(networkStatusProvider.currentNetworkStatus) { - courseService.getCourse(courseId, serverUrl, authToken) - .then { courseWithScore -> - accountDataService - .getAccountData(serverUrl, authToken) - .bind { it.isAtLeastTutorInCourse(courseWithScore.course) } - } - }.map { it.orElse(false) } - } - - flow.collect { value -> - canCreateChannel = value - } - } - } + val canCreateChannel by viewModel.canCreateChannel.collectAsState() BackHandler(configuration != NothingOpened) { when (val config = configuration) { diff --git a/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/SinglePageConversationBodyViewModel.kt b/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/SinglePageConversationBodyViewModel.kt new file mode 100644 index 000000000..fc007eed6 --- /dev/null +++ b/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/SinglePageConversationBodyViewModel.kt @@ -0,0 +1,44 @@ +package de.tum.informatics.www1.artemis.native_app.feature.metis.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import de.tum.informatics.www1.artemis.native_app.core.common.flatMapLatest +import de.tum.informatics.www1.artemis.native_app.core.data.retryOnInternet +import de.tum.informatics.www1.artemis.native_app.core.data.service.network.AccountDataService +import de.tum.informatics.www1.artemis.native_app.core.data.service.network.CourseService +import de.tum.informatics.www1.artemis.native_app.core.datastore.AccountService +import de.tum.informatics.www1.artemis.native_app.core.datastore.ServerConfigurationService +import de.tum.informatics.www1.artemis.native_app.core.datastore.authToken +import de.tum.informatics.www1.artemis.native_app.core.device.NetworkStatusProvider +import de.tum.informatics.www1.artemis.native_app.core.model.account.isAtLeastTutorInCourse +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +internal class SinglePageConversationBodyViewModel( + courseId: Long, + accountService: AccountService, + serverConfigurationService: ServerConfigurationService, + courseService: CourseService, + accountDataService: AccountDataService, + networkStatusProvider: NetworkStatusProvider, + private val coroutineContext: CoroutineContext = EmptyCoroutineContext +) : ViewModel() { + + val canCreateChannel: StateFlow = flatMapLatest( + serverConfigurationService.serverUrl, + accountService.authToken + ) { serverUrl, authToken -> + retryOnInternet(networkStatusProvider.currentNetworkStatus) { + courseService.getCourse(courseId, serverUrl, authToken) + .then { courseWithScore -> + accountDataService + .getAccountData(serverUrl, authToken) + .bind { it.isAtLeastTutorInCourse(courseWithScore.course) } + } + }.map { it.orElse(false) } + }.stateIn(viewModelScope, SharingStarted.Eagerly, false) +} \ No newline at end of file diff --git a/feature/push/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/push/ui/PushNotificationSettingsUi.kt b/feature/push/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/push/ui/PushNotificationSettingsUi.kt index bafafbcf2..9aa5e26aa 100644 --- a/feature/push/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/push/ui/PushNotificationSettingsUi.kt +++ b/feature/push/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/push/ui/PushNotificationSettingsUi.kt @@ -31,7 +31,7 @@ import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import de.tum.informatics.www1.artemis.native_app.core.data.DataState import de.tum.informatics.www1.artemis.native_app.core.ui.alert.TextAlertDialog -import de.tum.informatics.www1.artemis.native_app.core.ui.material.linkTextColor +import de.tum.informatics.www1.artemis.native_app.core.ui.material.colors.linkTextColor import de.tum.informatics.www1.artemis.native_app.feature.push.R import kotlinx.coroutines.Job import org.koin.androidx.compose.koinViewModel