diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/courses/CoursesScreen.kt b/app/src/main/java/de/sebse/fuplanner2/ui/courses/CoursesScreen.kt index 7e95128..0c16660 100644 --- a/app/src/main/java/de/sebse/fuplanner2/ui/courses/CoursesScreen.kt +++ b/app/src/main/java/de/sebse/fuplanner2/ui/courses/CoursesScreen.kt @@ -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>, 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( diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/details/CourseDetailsScreen.kt b/app/src/main/java/de/sebse/fuplanner2/ui/details/CourseDetailsScreen.kt index 3908c3a..fcf5441 100644 --- a/app/src/main/java/de/sebse/fuplanner2/ui/details/CourseDetailsScreen.kt +++ b/app/src/main/java/de/sebse/fuplanner2/ui/details/CourseDetailsScreen.kt @@ -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?, id: Long) { - Column { - QuickLinks(courseId = id) - Text( - text = stringResource(R.string.lecturers), - style = MaterialTheme.typography.h5 - ) - LazyColumn { - items(course?.lecturers ?: listOf()) { - LecturerItem(lecturer = it, courseTitle = course?.title ?: "") - } + 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 + ) } - Text( - text = stringResource(R.string.announcements), - style = MaterialTheme.typography.h5 - ) - LazyColumn { - items(announcement ?: listOf()) { - AnnouncementItem(it) - } + items(course?.lecturers ?: listOf()) { + LecturerItem(lecturer = it, courseTitle = course?.title ?: "") + } + item { + Text( + text = stringResource(R.string.announcements), + style = MaterialTheme.typography.h5 + ) + } + items(items = announcements) { + AnnouncementItem(it) } // TODO: Add latest announcements, current assignments, upcoming events } diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/details/components/QuickLinks.kt b/app/src/main/java/de/sebse/fuplanner2/ui/details/components/QuickLinks.kt index 38a70fd..ab1235e 100644 --- a/app/src/main/java/de/sebse/fuplanner2/ui/details/components/QuickLinks.kt +++ b/app/src/main/java/de/sebse/fuplanner2/ui/details/components/QuickLinks.kt @@ -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, diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/details_announcements/AnnouncementsViewModel.kt b/app/src/main/java/de/sebse/fuplanner2/ui/details_announcements/AnnouncementsViewModel.kt index 7971057..006ec7f 100644 --- a/app/src/main/java/de/sebse/fuplanner2/ui/details_announcements/AnnouncementsViewModel.kt +++ b/app/src/main/java/de/sebse/fuplanner2/ui/details_announcements/AnnouncementsViewModel.kt @@ -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 create(modelClass: Class): 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(ctx) { - it.setInputData(workDataOf( - AbstractAccountWorker.KEY_ACCOUNT_NAME to AppAccounts.getInstance().selectedAccount?.name, - AbstractAccountWorker.KEY_COURSE_ID to courseId - )) - } - } - val events: LiveData> = LivePagedListBuilder(factory, 50).build() } diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/shared/VerticalGrid.kt b/app/src/main/java/de/sebse/fuplanner2/ui/shared/VerticalGrid.kt new file mode 100644 index 0000000..37f2639 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/ui/shared/VerticalGrid.kt @@ -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 + } + } + } +} diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/tools/viewmodels/DetailsViewModel.kt b/app/src/main/java/de/sebse/fuplanner2/ui/tools/viewmodels/DetailsViewModel.kt index 21dbed1..71f09ac 100644 --- a/app/src/main/java/de/sebse/fuplanner2/ui/tools/viewmodels/DetailsViewModel.kt +++ b/app/src/main/java/de/sebse/fuplanner2/ui/tools/viewmodels/DetailsViewModel.kt @@ -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 create(modelClass: Class): T = DetailsViewModel(courseId) as T } -class DetailsViewModel(courseId: Long) : ViewModel() { +class DetailsViewModel(private val courseId: Long) : ViewModel() { + fun refresh(ctx: Context) { + enqueueOneTimeWork(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 = AppDatabase.getInstance().courseDao().getCourseById(courseId) + val announcements: LiveData> = LivePagedListBuilder( + AppDatabase.getInstance().announcementDao().getAll1(courseId), + 50 + ).build() } diff --git a/app/src/main/java/de/sebse/fuplanner2/worker/AbstractAccountWorker.kt b/app/src/main/java/de/sebse/fuplanner2/worker/AbstractAccountWorker.kt index 775094e..3624257 100644 --- a/app/src/main/java/de/sebse/fuplanner2/worker/AbstractAccountWorker.kt +++ b/app/src/main/java/de/sebse/fuplanner2/worker/AbstractAccountWorker.kt @@ -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 { diff --git a/app/src/main/java/de/sebse/fuplanner2/worker/CourseWorker.kt b/app/src/main/java/de/sebse/fuplanner2/worker/CourseWorker.kt index d906d22..b1af73d 100644 --- a/app/src/main/java/de/sebse/fuplanner2/worker/CourseWorker.kt +++ b/app/src/main/java/de/sebse/fuplanner2/worker/CourseWorker.kt @@ -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) }