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

View File

@@ -1,13 +1,14 @@
package de.sebse.fuplanner2.ui.details package de.sebse.fuplanner2.ui.details
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter 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.R
import de.sebse.fuplanner2.Tools import de.sebse.fuplanner2.Tools
import de.sebse.fuplanner2.database.Announcement import de.sebse.fuplanner2.database.Announcement
import de.sebse.fuplanner2.database.AppDatabase
import de.sebse.fuplanner2.database.Course import de.sebse.fuplanner2.database.Course
import de.sebse.fuplanner2.ui.details.components.AnnouncementItem import de.sebse.fuplanner2.ui.details.components.AnnouncementItem
import de.sebse.fuplanner2.ui.details.components.LecturerItem 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.previews.CoursePreviewProvider
import de.sebse.fuplanner2.ui.tools.viewmodels.DetailsViewModel import de.sebse.fuplanner2.ui.tools.viewmodels.DetailsViewModel
import de.sebse.fuplanner2.ui.tools.viewmodels.DetailsViewModelFactory import de.sebse.fuplanner2.ui.tools.viewmodels.DetailsViewModelFactory
import kotlin.math.min
@Composable @Composable
fun CourseDetailsScreen(tools: Tools, id: Long) { fun CourseDetailsScreen(tools: Tools, id: Long) {
val coursesViewModel: DetailsViewModel = viewModel(factory = DetailsViewModelFactory(id)) val coursesViewModel: DetailsViewModel = viewModel(factory = DetailsViewModelFactory(id))
val state = coursesViewModel.course.observeAsState() val course by coursesViewModel.course.observeAsState()
val announce = AppDatabase.getInstance().announcementDao().getAll3(id).observeAsState() val announcements by coursesViewModel.announcements.observeAsState()
val title = state.value?.title val title = course?.title
val context = LocalContext.current
LaunchedEffect(title) { LaunchedEffect(title) {
title?.let { tools.setTitle(it) } title?.let { tools.setTitle(it) }
} }
CourseDetailsScreen(state.value, announce.value, id) LaunchedEffect(true) {
coursesViewModel.refresh(context)
}
CourseDetailsScreen(course, announcements, id)
} }
@Composable @Composable
fun CourseDetailsScreen(course: Course?, announcement: List<Announcement>?, id: Long) { 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) QuickLinks(courseId = id)
Text( Text(
text = stringResource(R.string.lecturers), text = stringResource(R.string.lecturers),
style = MaterialTheme.typography.h5 style = MaterialTheme.typography.h5
) )
LazyColumn { }
items(course?.lecturers ?: listOf()) { items(course?.lecturers ?: listOf()) {
LecturerItem(lecturer = it, courseTitle = course?.title ?: "") LecturerItem(lecturer = it, courseTitle = course?.title ?: "")
} }
} item {
Text( Text(
text = stringResource(R.string.announcements), text = stringResource(R.string.announcements),
style = MaterialTheme.typography.h5 style = MaterialTheme.typography.h5
) )
LazyColumn {
items(announcement ?: listOf()) {
AnnouncementItem(it)
} }
items(items = announcements) {
AnnouncementItem(it)
} }
// TODO: Add latest announcements, current assignments, upcoming events // 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.annotation.StringRes
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.GridCells import androidx.compose.foundation.lazy.GridCells
import androidx.compose.foundation.lazy.LazyVerticalGrid
import androidx.compose.material.Card import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import de.sebse.fuplanner2.R import de.sebse.fuplanner2.R
import de.sebse.fuplanner2.ui.shared.VerticalGrid
import de.sebse.fuplanner2.ui.theme.AppTheme 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.announcements, "courses/$courseId/announcements"),
QuickLinkProps(R.string.events, "courses/$courseId/events") QuickLinkProps(R.string.events, "courses/$courseId/events")
) )
LazyVerticalGrid( VerticalGrid(
cells = GridCells.Adaptive(150.dp), cells = GridCells.Adaptive(150.dp)
contentPadding = PaddingValues(
horizontal = 12.dp,
vertical = 16.dp
),
) { ) {
items(list.size) { index -> list.forEach {
Card( Card(
backgroundColor = MaterialTheme.colors.secondary, backgroundColor = MaterialTheme.colors.secondary,
modifier = Modifier modifier = Modifier
@@ -49,7 +44,7 @@ fun QuickLinks(courseId: Long) {
elevation = dimensionResource(R.dimen.card_view_elevation), elevation = dimensionResource(R.dimen.card_view_elevation),
) { ) {
Text( Text(
text = stringResource(list[index].name), text = stringResource(it.name),
color = MaterialTheme.colors.onSecondary, color = MaterialTheme.colors.onSecondary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = MaterialTheme.typography.h6, style = MaterialTheme.typography.h6,

View File

@@ -1,18 +1,12 @@
package de.sebse.fuplanner2.ui.details_announcements package de.sebse.fuplanner2.ui.details_announcements
import android.content.Context
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.paging.LivePagedListBuilder import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList 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.Announcement
import de.sebse.fuplanner2.database.AppDatabase 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() { class AnnouncementsViewModelFactory(private val courseId: Long): ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T = AnnouncementsViewModel(courseId) as T 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() { class AnnouncementsViewModel(private val courseId: Long) : ViewModel() {
private val factory = AppDatabase.getInstance().announcementDao().getAll1(courseId) 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() 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 package de.sebse.fuplanner2.ui.tools.viewmodels
import android.content.Context
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider 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.AppDatabase
import de.sebse.fuplanner2.database.Course 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() { class DetailsViewModelFactory(private val courseId: Long): ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T = DetailsViewModel(courseId) as T override fun <T : ViewModel> create(modelClass: Class<T>): T = DetailsViewModel(courseId) as T
} }
class DetailsViewModel(courseId: Long) : ViewModel() { class DetailsViewModel(private val courseId: Long) : ViewModel() {
val course: LiveData<Course> = AppDatabase.getInstance().courseDao().getCourseById(courseId) 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 { companion object {
const val KEY_ACCOUNT_NAME = AccountManager.KEY_ACCOUNT_NAME const val KEY_ACCOUNT_NAME = AccountManager.KEY_ACCOUNT_NAME
const val KEY_COURSE_ID = "KEY_COURSE_ID" 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 { 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 { override suspend fun doActualWork(database: AppDatabase, account: Account, user: User): Data {
val courseId = inputData.getLong(KEY_COURSE_ID, -1) val courseId = inputData.getLong(KEY_COURSE_ID, -1)
.let { if (it == -1L) null else it } .let { if (it == -1L) null else it }
val isForceFetch = inputData.getBoolean(KEY_FORCE_FETCH, false)
val updates = work(applicationContext, database, user, courseId) val updates = work(applicationContext, database, user, courseId)
val latestSemester = database.courseDao().getLatestSemesterName() 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) EventWorker.work(applicationContext, database, user, it)
AnnouncementWorker.work(applicationContext, database, user, it) AnnouncementWorker.work(applicationContext, database, user, it)
} }