CourseDetailsScreen improvements
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user