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