CourseDetailsScreen improvements

This commit is contained in:
Sebastian Seedorf
2021-11-19 21:06:34 +01:00
parent cb905fc9a6
commit adff1fea0c
8 changed files with 163 additions and 60 deletions

View File

@@ -4,11 +4,9 @@ import android.content.res.Configuration
import android.util.Log
import androidx.annotation.StringRes
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Card
@@ -22,6 +20,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
@@ -29,13 +28,14 @@ import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import de.sebse.fuplanner2.MenuItem
import de.sebse.fuplanner2.R
import de.sebse.fuplanner2.Tools
import de.sebse.fuplanner2.database.Course
import de.sebse.fuplanner2.ui.theme.AppTheme
import de.sebse.fuplanner2.ui.theme.*
import de.sebse.fuplanner2.ui.tools.previews.CoursePreviewProvider
import de.sebse.fuplanner2.ui.tools.viewmodels.CoursesViewModel
import de.sebse.fuplanner2.utils.color.getColor
@@ -67,10 +67,23 @@ fun CoursesScreen(tools: Tools) {
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun GroupedCourseList(groups: Map<String, List<Course>>, onclick: (course: Course) -> Unit) {
val headerBg =
if (MaterialTheme.colors.isLight) md_theme_light_secondaryContainer
else md_theme_dark_secondaryContainer
val headerColor =
if (MaterialTheme.colors.isLight) md_theme_light_onSecondaryContainer
else md_theme_dark_onSecondaryContainer
LazyColumn {
for ((key, value) in groups.entries) {
stickyHeader(key = key) {
Text(text = key)
Text(
text = key,
color = headerColor,
modifier = Modifier
.fillMaxWidth()
.background(headerBg)
.padding()
)
}
items(value) { course ->
CourseItem(course) { onclick(course) }
@@ -109,6 +122,7 @@ fun CourseItem(course: Course, onclick: () -> Unit) {
@Composable
fun CourseItem(id: Long?, title: String, lecturers: String, type: String, onclick: () -> Unit) {
val color = Color(getColor(LocalContext.current, id ?: 0, highContrast = true))
Card(
modifier = Modifier
.fillMaxWidth()
@@ -116,6 +130,13 @@ fun CourseItem(id: Long?, title: String, lecturers: String, type: String, onclic
.padding(dimensionResource(R.dimen.card_view_margin)),
elevation = dimensionResource(R.dimen.card_view_elevation)
) {
Column(
modifier = Modifier
.height(1.5.dp)
.background(Brush.horizontalGradient(
colors = listOf(color, MaterialTheme.colors.surface)
))
) {}
Column(
modifier = Modifier
.fillMaxWidth()
@@ -123,7 +144,7 @@ fun CourseItem(id: Long?, title: String, lecturers: String, type: String, onclic
) {
Text(
text = title,
color = Color(getColor(LocalContext.current, id ?: 0, highContrast = true)),
color = MaterialTheme.colors.primaryVariant,
style = MaterialTheme.typography.h6
)
CourseItemHint(

View File

@@ -1,13 +1,14 @@
package de.sebse.fuplanner2.ui.details
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@@ -15,7 +16,6 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import de.sebse.fuplanner2.R
import de.sebse.fuplanner2.Tools
import de.sebse.fuplanner2.database.Announcement
import de.sebse.fuplanner2.database.AppDatabase
import de.sebse.fuplanner2.database.Course
import de.sebse.fuplanner2.ui.details.components.AnnouncementItem
import de.sebse.fuplanner2.ui.details.components.LecturerItem
@@ -25,40 +25,46 @@ import de.sebse.fuplanner2.ui.tools.previews.AnnouncementPreviewProvider
import de.sebse.fuplanner2.ui.tools.previews.CoursePreviewProvider
import de.sebse.fuplanner2.ui.tools.viewmodels.DetailsViewModel
import de.sebse.fuplanner2.ui.tools.viewmodels.DetailsViewModelFactory
import kotlin.math.min
@Composable
fun CourseDetailsScreen(tools: Tools, id: Long) {
val coursesViewModel: DetailsViewModel = viewModel(factory = DetailsViewModelFactory(id))
val state = coursesViewModel.course.observeAsState()
val announce = AppDatabase.getInstance().announcementDao().getAll3(id).observeAsState()
val title = state.value?.title
val course by coursesViewModel.course.observeAsState()
val announcements by coursesViewModel.announcements.observeAsState()
val title = course?.title
val context = LocalContext.current
LaunchedEffect(title) {
title?.let { tools.setTitle(it) }
}
CourseDetailsScreen(state.value, announce.value, id)
LaunchedEffect(true) {
coursesViewModel.refresh(context)
}
CourseDetailsScreen(course, announcements, id)
}
@Composable
fun CourseDetailsScreen(course: Course?, announcement: List<Announcement>?, id: Long) {
Column {
val announcements = announcement?.subList(0, min(announcement.size, 3)) ?: listOf()
LazyColumn {
item {
QuickLinks(courseId = id)
Text(
text = stringResource(R.string.lecturers),
style = MaterialTheme.typography.h5
)
LazyColumn {
}
items(course?.lecturers ?: listOf()) {
LecturerItem(lecturer = it, courseTitle = course?.title ?: "")
}
}
item {
Text(
text = stringResource(R.string.announcements),
style = MaterialTheme.typography.h5
)
LazyColumn {
items(announcement ?: listOf()) {
AnnouncementItem(it)
}
items(items = announcements) {
AnnouncementItem(it)
}
// TODO: Add latest announcements, current assignments, upcoming events
}

View File

@@ -2,11 +2,9 @@ package de.sebse.fuplanner2.ui.details.components
import androidx.annotation.StringRes
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.GridCells
import androidx.compose.foundation.lazy.LazyVerticalGrid
import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
@@ -18,6 +16,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import de.sebse.fuplanner2.R
import de.sebse.fuplanner2.ui.shared.VerticalGrid
import de.sebse.fuplanner2.ui.theme.AppTheme
@@ -33,14 +32,10 @@ fun QuickLinks(courseId: Long) {
QuickLinkProps(R.string.announcements, "courses/$courseId/announcements"),
QuickLinkProps(R.string.events, "courses/$courseId/events")
)
LazyVerticalGrid(
cells = GridCells.Adaptive(150.dp),
contentPadding = PaddingValues(
horizontal = 12.dp,
vertical = 16.dp
),
VerticalGrid(
cells = GridCells.Adaptive(150.dp)
) {
items(list.size) { index ->
list.forEach {
Card(
backgroundColor = MaterialTheme.colors.secondary,
modifier = Modifier
@@ -49,7 +44,7 @@ fun QuickLinks(courseId: Long) {
elevation = dimensionResource(R.dimen.card_view_elevation),
) {
Text(
text = stringResource(list[index].name),
text = stringResource(it.name),
color = MaterialTheme.colors.onSecondary,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.h6,

View File

@@ -1,18 +1,12 @@
package de.sebse.fuplanner2.ui.details_announcements
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import androidx.work.workDataOf
import de.sebse.fuplanner2.auth.AppAccounts
import de.sebse.fuplanner2.database.Announcement
import de.sebse.fuplanner2.database.AppDatabase
import de.sebse.fuplanner2.utils.enqueueOneTimeWork
import de.sebse.fuplanner2.worker.AbstractAccountWorker
import de.sebse.fuplanner2.worker.AnnouncementWorker
class AnnouncementsViewModelFactory(private val courseId: Long): ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T = AnnouncementsViewModel(courseId) as T
@@ -21,14 +15,5 @@ class AnnouncementsViewModelFactory(private val courseId: Long): ViewModelProvid
class AnnouncementsViewModel(private val courseId: Long) : ViewModel() {
private val factory = AppDatabase.getInstance().announcementDao().getAll1(courseId)
fun refresh(ctx: Context) {
enqueueOneTimeWork<AnnouncementWorker>(ctx) {
it.setInputData(workDataOf(
AbstractAccountWorker.KEY_ACCOUNT_NAME to AppAccounts.getInstance().selectedAccount?.name,
AbstractAccountWorker.KEY_COURSE_ID to courseId
))
}
}
val events: LiveData<PagedList<Announcement>> = LivePagedListBuilder(factory, 50).build()
}

View File

@@ -0,0 +1,64 @@
package de.sebse.fuplanner2.ui.shared
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.GridCells
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun VerticalGrid(
cells: GridCells,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
val columns: Int = when (cells) {
is GridCells.Fixed -> {
cells.count
}
is GridCells.Adaptive -> {
val width = LocalContext.current.resources.displayMetrics.widthPixels
val columnWidthPx = with(LocalDensity.current) { cells.minSize.toPx() }
((width / columnWidthPx).toInt()).coerceAtLeast(1)
}
}
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
val itemWidth = constraints.maxWidth / columns
// Keep given height constraints, but set an exact width
val itemConstraints = constraints.copy(
minWidth = itemWidth,
maxWidth = itemWidth
)
// Measure each item with these constraints
val placeables = measurables.map { measurable ->
measurable.measure(itemConstraints)
}
// Track each columns height so we can calculate the overall height
val columnHeights = Array(columns) { 0 }
placeables.forEachIndexed { index, placeable ->
val column = index % columns
columnHeights[column] += placeable.height
}
val height = (columnHeights.maxOrNull() ?: constraints.minHeight)
.coerceAtMost(constraints.maxHeight)
layout(constraints.maxWidth, height) {
// Track the Y co-ord per column we have placed up to
val columnY = Array(columns) { 0 }
placeables.forEachIndexed { index, placeable ->
val column = index % columns
placeable.place(
x = column * itemWidth,
y = columnY[column]
)
columnY[column] += placeable.height
}
}
}
}

View File

@@ -1,16 +1,40 @@
package de.sebse.fuplanner2.ui.tools.viewmodels
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import androidx.work.workDataOf
import de.sebse.fuplanner2.auth.AppAccounts
import de.sebse.fuplanner2.database.Announcement
import de.sebse.fuplanner2.database.AppDatabase
import de.sebse.fuplanner2.database.Course
import de.sebse.fuplanner2.utils.enqueueOneTimeWork
import de.sebse.fuplanner2.worker.AbstractAccountWorker
import de.sebse.fuplanner2.worker.CourseWorker
class DetailsViewModelFactory(private val courseId: Long): ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T = DetailsViewModel(courseId) as T
}
class DetailsViewModel(courseId: Long) : ViewModel() {
val course: LiveData<Course> = AppDatabase.getInstance().courseDao().getCourseById(courseId)
class DetailsViewModel(private val courseId: Long) : ViewModel() {
fun refresh(ctx: Context) {
enqueueOneTimeWork<CourseWorker>(ctx) {
it.setInputData(workDataOf(
AbstractAccountWorker.KEY_ACCOUNT_NAME to AppAccounts.getInstance().selectedAccount?.name,
AbstractAccountWorker.KEY_COURSE_ID to courseId,
AbstractAccountWorker.KEY_FORCE_FETCH to true
)
)
}
}
val course: LiveData<Course> = AppDatabase.getInstance().courseDao().getCourseById(courseId)
val announcements: LiveData<PagedList<Announcement>> = LivePagedListBuilder(
AppDatabase.getInstance().announcementDao().getAll1(courseId),
50
).build()
}

View File

@@ -16,7 +16,8 @@ abstract class AbstractAccountWorker(context: Context, params: WorkerParameters)
companion object {
const val KEY_ACCOUNT_NAME = AccountManager.KEY_ACCOUNT_NAME
const val KEY_COURSE_ID = "KEY_COURSE_ID"
const val OUT_ERROR_CODE = "OUT_CODE"
const val OUT_ERROR_CODE = "OUT_ERROR_CODE"
const val KEY_FORCE_FETCH = "KEY_FORCE_FETCH"
}
enum class ErrorCodes {

View File

@@ -33,10 +33,17 @@ class CourseWorker(context: Context, params: WorkerParameters) : AbstractAccount
override suspend fun doActualWork(database: AppDatabase, account: Account, user: User): Data {
val courseId = inputData.getLong(KEY_COURSE_ID, -1)
.let { if (it == -1L) null else it }
val isForceFetch = inputData.getBoolean(KEY_FORCE_FETCH, false)
val updates = work(applicationContext, database, user, courseId)
val latestSemester = database.courseDao().getLatestSemesterName()
updates.added.forEach {
if (it.isSummerSemester == latestSemester.semester && it.year == latestSemester.year) {
val detailsToFetch =
if (isForceFetch) updates.added + updates.updated
else updates.added
detailsToFetch.forEach {
val isLatestSemester = it.isSummerSemester == latestSemester.semester && it.year == latestSemester.year
if (isForceFetch || isLatestSemester) {
EventWorker.work(applicationContext, database, user, it)
AnnouncementWorker.work(applicationContext, database, user, it)
}