From 8cbb7d03212ce41780022bab2e0263747dfa30b2 Mon Sep 17 00:00:00 2001 From: Ankit Suda Date: Tue, 22 Nov 2022 01:22:27 +0530 Subject: [PATCH] Improve calendar --- .../rebound/data/db/daos/WorkoutsDao.kt | 3 + .../data/repositories/WorkoutsRepository.kt | 6 +- .../ui/calendar/CalendarPagingDataSource.kt | 64 ++++++++ .../rebound/ui/calendar/CalendarScreen.kt | 142 ++++++++++-------- .../ui/calendar/CalendarScreenViewModel.kt | 80 ++++------ .../ui/history/HistoryScreenViewModel.kt | 12 +- 6 files changed, 186 insertions(+), 121 deletions(-) create mode 100644 modules/ui-calendar/src/main/java/com/ankitsuda/rebound/ui/calendar/CalendarPagingDataSource.kt diff --git a/modules/core-data/src/main/java/com/ankitsuda/rebound/data/db/daos/WorkoutsDao.kt b/modules/core-data/src/main/java/com/ankitsuda/rebound/data/db/daos/WorkoutsDao.kt index af74d2d9..21e0dad1 100644 --- a/modules/core-data/src/main/java/com/ankitsuda/rebound/data/db/daos/WorkoutsDao.kt +++ b/modules/core-data/src/main/java/com/ankitsuda/rebound/data/db/daos/WorkoutsDao.kt @@ -148,6 +148,9 @@ interface WorkoutsDao { @RawQuery(observedEntities = [Workout::class, ExerciseLogEntry::class, ExerciseWorkoutJunction::class]) fun getAllWorkoutsRawQueryPaged(query: SupportSQLiteQuery): PagingSource + @Query("SELECT COUNT(*) as count, start_at as date FROM workouts WHERE is_hidden = 0 AND in_progress = 0 GROUP BY start_at") + fun getWorkoutsCount(): Flow> + @Query("SELECT COUNT(*) as count, start_at as date FROM workouts WHERE date(start_at / 1000,'unixepoch') >= date(:dateStart / 1000,'unixepoch') AND date(start_at / 1000,'unixepoch') <= date(:dateEnd / 1000,'unixepoch') AND is_hidden = 0 AND in_progress = 0 GROUP BY start_at") fun getWorkoutsCountOnDateRange(dateStart: Long, dateEnd: Long): Flow> diff --git a/modules/core-data/src/main/java/com/ankitsuda/rebound/data/repositories/WorkoutsRepository.kt b/modules/core-data/src/main/java/com/ankitsuda/rebound/data/repositories/WorkoutsRepository.kt index 55627af8..378190aa 100644 --- a/modules/core-data/src/main/java/com/ankitsuda/rebound/data/repositories/WorkoutsRepository.kt +++ b/modules/core-data/src/main/java/com/ankitsuda/rebound/data/repositories/WorkoutsRepository.kt @@ -290,7 +290,8 @@ class WorkoutsRepository @Inject constructor( workoutsDao.getWorkoutsWithExtraInfoAltPaged() } } - .flow.map { + .flow + .map { it.map { item -> val logEntries = item.junctions?.flatMap { j -> j.logEntries } WorkoutWithExtraInfo( @@ -316,6 +317,9 @@ class WorkoutsRepository @Inject constructor( } } + fun getWorkoutsCount() = + workoutsDao.getWorkoutsCount() + fun getWorkoutsCountOnDateRange(dateStart: LocalDate, dateEnd: LocalDate) = workoutsDao.getWorkoutsCountOnDateRange( dateStart = dateStart.toEpochMillis(), diff --git a/modules/ui-calendar/src/main/java/com/ankitsuda/rebound/ui/calendar/CalendarPagingDataSource.kt b/modules/ui-calendar/src/main/java/com/ankitsuda/rebound/ui/calendar/CalendarPagingDataSource.kt new file mode 100644 index 00000000..d76fe35c --- /dev/null +++ b/modules/ui-calendar/src/main/java/com/ankitsuda/rebound/ui/calendar/CalendarPagingDataSource.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2022 Ankit Suda. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + */ + +package com.ankitsuda.rebound.ui.calendar + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.ankitsuda.rebound.ui.calendar.models.CalendarMonth +import com.ankitsuda.rebound.ui.calendar.models.InDateStyle +import com.ankitsuda.rebound.ui.calendar.models.MonthConfig +import com.ankitsuda.rebound.ui.calendar.models.OutDateStyle +import kotlinx.coroutines.Job +import java.time.DayOfWeek +import java.time.Month +import java.time.YearMonth + +class CalendarPagingDataSource( + private val startYear: Int, + private val firstDayOfWeek: DayOfWeek, +) : + PagingSource() { + override suspend fun load(params: LoadParams): LoadResult { + val year = params.key ?: startYear + return try { + val monthConfig = MonthConfig( + outDateStyle = OutDateStyle.NONE, + inDateStyle = InDateStyle.ALL_MONTHS, + startMonth = YearMonth.of(year, Month.JANUARY), + endMonth = YearMonth.of(year, Month.DECEMBER), + hasBoundaries = true, + maxRowCount = Int.MAX_VALUE, + firstDayOfWeek = firstDayOfWeek, + job = Job() + ) + + + LoadResult.Page( + data = monthConfig.months, + prevKey = year - 1, + nextKey = year + 1 + ) + } catch (e: Exception) { + LoadResult.Error(e) + } + } + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) + ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) + } + } +} \ No newline at end of file diff --git a/modules/ui-calendar/src/main/java/com/ankitsuda/rebound/ui/calendar/CalendarScreen.kt b/modules/ui-calendar/src/main/java/com/ankitsuda/rebound/ui/calendar/CalendarScreen.kt index 7253b756..eee1940e 100644 --- a/modules/ui-calendar/src/main/java/com/ankitsuda/rebound/ui/calendar/CalendarScreen.kt +++ b/modules/ui-calendar/src/main/java/com/ankitsuda/rebound/ui/calendar/CalendarScreen.kt @@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ArrowBack import androidx.compose.material.icons.outlined.Today @@ -29,19 +28,21 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.items import com.ankitsuda.base.utils.generateId -import com.ankitsuda.base.utils.toLocalDate import com.ankitsuda.navigation.* import com.ankitsuda.rebound.ui.calendar.components.CalendarMonthItem import com.ankitsuda.rebound.ui.calendar.components.CalendarYearHeader +import com.ankitsuda.rebound.ui.calendar.models.CalendarMonth import com.ankitsuda.rebound.ui.components.TopBar import com.ankitsuda.rebound.ui.components.TopBarIconButton +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import me.onebone.toolbar.CollapsingToolbarScaffold import me.onebone.toolbar.ScrollStrategy import me.onebone.toolbar.rememberCollapsingToolbarScaffoldState import java.time.LocalDate -import java.time.Month import java.time.Year @Composable @@ -56,79 +57,88 @@ fun CalendarScreen( val collapsingState = rememberCollapsingToolbarScaffoldState() val scrollState = rememberLazyListState() - val mCalendar by viewModel.calendar.collectAsState(null) - val countsWithDate by viewModel.workoutsCountOnDates.collectAsState() + val calendar = viewModel.calendar.collectAsLazyPagingItems() + val countsWithDate by viewModel.workoutsCountOnDates.collectAsState(emptyList()) val today = LocalDate.now() - val coroutine = rememberCoroutineScope() + val coroutineScope = rememberCoroutineScope() - LaunchedEffect(mCalendar) { - mCalendar?.let { calendar -> - if (calendar.isNotEmpty() && !didFirstAutoScroll) { - try { - scrollState.scrollToItem(calendar.indexOf((calendar.filter { - it.month == today.month.value && today.year == today.year - }[0]))) - } catch (e: Exception) { - e.printStackTrace() + suspend fun scrollToToday(smooth: Boolean = true) { + for (i in 0 until calendar.itemCount) { + val item = calendar.peek(i) + if (item is CalendarMonth && item.yearMonth.year == today.year && item.month == today.monthValue) { + if (smooth) { + scrollState.animateScrollToItem(i) + } else { + scrollState.scrollToItem(i) } + break + } + } + } + + LaunchedEffect(calendar.itemCount) { + calendar.let { calendar -> + if (calendar.itemCount > 0 && !didFirstAutoScroll) { + delay(100) + scrollToToday() didFirstAutoScroll = true } } } - mCalendar?.let { calendar -> - CollapsingToolbarScaffold( - scrollStrategy = ScrollStrategy.EnterAlwaysCollapsed, - state = collapsingState, - toolbar = { - TopBar( - title = stringResource(id = R.string.calendar), - strictLeftIconAlignToStart = true, - leftIconBtn = { - TopBarIconButton( - icon = Icons.Outlined.ArrowBack, - title = stringResource(id = R.string.back) - ) { - navController.popBackStack() - } - }, - rightIconBtn = { - TopBarIconButton( - icon = Icons.Outlined.Today, - title = stringResource(id = R.string.jump_to_today), - onClick = { - coroutine.launch { - scrollState.animateScrollToItem(calendar.indexOf((calendar.filter { - it.month == today.month.value && it.year == today.year - }[0]))) - } - }) - }) - }, - modifier = Modifier.background(MaterialTheme.colors.background) + CollapsingToolbarScaffold( + scrollStrategy = ScrollStrategy.EnterAlwaysCollapsed, + state = collapsingState, + toolbar = { + TopBar( + title = stringResource(id = R.string.calendar), + strictLeftIconAlignToStart = true, + leftIconBtn = { + TopBarIconButton( + icon = Icons.Outlined.ArrowBack, + title = stringResource(id = R.string.back) + ) { + navController.popBackStack() + } + }, + rightIconBtn = { + TopBarIconButton( + icon = Icons.Outlined.Today, + title = stringResource(id = R.string.jump_to_today), + onClick = { + coroutineScope.launch { + scrollToToday() + } + }) + }) + }, + modifier = Modifier.background(MaterialTheme.colors.background) + ) { + LazyColumn( + state = scrollState, modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colors.background) ) { - LazyColumn( - state = scrollState, modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colors.background) - ) { - for (item in calendar) { - if (item.month == Month.JANUARY.value) { - item(key = "year_header_${item.year}") { - CalendarYearHeader( - year = Year.of(item.year), - onClick = { - navigator.navigate( - LeafScreen.History.createRoute( - year = item.year, - ) - ) - } + items(calendar, key = { + when (it) { + is CalendarMonth -> "month_block_${it.month}_${it.year}" + is Int -> "year_header_$it" + else -> generateId() + } + }) { item -> + when (item) { + is Int -> CalendarYearHeader( + year = Year.of(item), + onClick = { + navigator.navigate( + LeafScreen.History.createRoute( + year = item, + ) ) } - } - item(key = "month_block_${item.month}_${item.year}") { + ) + is CalendarMonth -> CalendarMonthItem( month = item, selectedDate = today, @@ -151,10 +161,10 @@ fun CalendarScreen( ) } ) - } } - } + } } + } } diff --git a/modules/ui-calendar/src/main/java/com/ankitsuda/rebound/ui/calendar/CalendarScreenViewModel.kt b/modules/ui-calendar/src/main/java/com/ankitsuda/rebound/ui/calendar/CalendarScreenViewModel.kt index c425342b..13fcae68 100644 --- a/modules/ui-calendar/src/main/java/com/ankitsuda/rebound/ui/calendar/CalendarScreenViewModel.kt +++ b/modules/ui-calendar/src/main/java/com/ankitsuda/rebound/ui/calendar/CalendarScreenViewModel.kt @@ -14,25 +14,19 @@ package com.ankitsuda.rebound.ui.calendar -import androidx.compose.runtime.collectAsState import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.* import com.ankitsuda.base.utils.extensions.shareWhileObserved import com.ankitsuda.rebound.data.datastore.PrefStorage import com.ankitsuda.rebound.data.repositories.WorkoutsRepository -import com.ankitsuda.rebound.domain.entities.CountWithDate import com.ankitsuda.rebound.ui.calendar.models.CalendarMonth -import com.ankitsuda.rebound.ui.calendar.models.InDateStyle -import com.ankitsuda.rebound.ui.calendar.models.MonthConfig -import com.ankitsuda.rebound.ui.calendar.models.OutDateStyle import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import java.time.DayOfWeek -import java.time.Month import java.time.Year -import java.time.YearMonth import javax.inject.Inject @HiltViewModel @@ -40,14 +34,14 @@ class CalendarScreenViewModel @Inject constructor( private val workoutsRepository: WorkoutsRepository, private val prefs: PrefStorage ) : ViewModel() { - private var _calendar = MutableStateFlow?>(null) - val calendar = _calendar.asStateFlow().shareWhileObserved(viewModelScope) + // 💀 Hack + private var _calendar = flow> {} - private var _workoutsCountOnDates = MutableStateFlow?>(null) - val workoutsCountOnDates = _workoutsCountOnDates.asStateFlow() + var calendar = _calendar + .shareWhileObserved(viewModelScope) - private var calendarJob: Job? = null - private var countJob: Job? = null + val workoutsCountOnDates = + workoutsRepository.getWorkoutsCount().shareWhileObserved(viewModelScope) private var firstDayOfWeek = DayOfWeek.MONDAY @@ -55,44 +49,32 @@ class CalendarScreenViewModel @Inject constructor( viewModelScope.launch { prefs.firstDayOfWeek.collect { firstDayOfWeek = DayOfWeek.of(it) - getCalendar() + calendar = Pager( + config = PagingConfig(pageSize = 12, prefetchDistance = 2), + initialKey = Year.now().value, + pagingSourceFactory = { + CalendarPagingDataSource( + startYear = Year.now().value, + firstDayOfWeek = firstDayOfWeek + ) + } + ).flow + .map { + mapCalendar(it) + } + .cachedIn(viewModelScope) + .shareWhileObserved(viewModelScope) } } } - fun getCalendar( - ) { - calendarJob?.cancel() - calendarJob = viewModelScope.launch { - val monthConfig = MonthConfig( - outDateStyle = OutDateStyle.NONE, - inDateStyle = InDateStyle.ALL_MONTHS, - startMonth = YearMonth.of(Year.now().value, Month.JANUARY), - endMonth = YearMonth.of(Year.now().value, Month.DECEMBER), - hasBoundaries = true, - maxRowCount = Int.MAX_VALUE, - firstDayOfWeek = firstDayOfWeek, - job = Job() - ) - - refreshCounts() - _calendar.emit(monthConfig.months) - } - } - - private fun refreshCounts() { - countJob?.cancel() - countJob = viewModelScope.launch { - val counts = workoutsRepository.getWorkoutsCountOnDateRange( - dateStart = YearMonth.of( - Year.now().value, - Month.JANUARY - ).atDay(1), - dateEnd = YearMonth.of(Year.now().value, Month.DECEMBER).atEndOfMonth() - ).firstOrNull() - - _workoutsCountOnDates.value = counts + private fun mapCalendar(pagingData: PagingData): PagingData { + return pagingData.insertSeparators { before, after -> + if (after != null && before?.year != after.year) { + after.year + } else { + null + } } } - } \ No newline at end of file diff --git a/modules/ui-history/src/main/java/com/ankitsuda/rebound/ui/history/HistoryScreenViewModel.kt b/modules/ui-history/src/main/java/com/ankitsuda/rebound/ui/history/HistoryScreenViewModel.kt index 26bd225e..cbcfc44d 100644 --- a/modules/ui-history/src/main/java/com/ankitsuda/rebound/ui/history/HistoryScreenViewModel.kt +++ b/modules/ui-history/src/main/java/com/ankitsuda/rebound/ui/history/HistoryScreenViewModel.kt @@ -22,15 +22,16 @@ import androidx.paging.cachedIn import androidx.paging.insertSeparators import com.ankitsuda.base.utils.extensions.shareWhileObserved import com.ankitsuda.base.utils.toEpochMillis -import com.ankitsuda.navigation.* +import com.ankitsuda.navigation.DAY_KEY +import com.ankitsuda.navigation.MONTH_KEY +import com.ankitsuda.navigation.YEAR_KEY import com.ankitsuda.rebound.data.repositories.WorkoutsRepository import com.ankitsuda.rebound.domain.entities.CountWithDate import com.ankitsuda.rebound.domain.entities.WorkoutWithExtraInfo import com.ankitsuda.rebound.ui.history.enums.WorkoutsDateRangeType import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import timber.log.Timber +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map import java.time.LocalDate import java.time.Month import java.time.Year @@ -94,7 +95,8 @@ class HistoryScreenViewModel @Inject constructor( if ( dateRangeType != WorkoutsDateRangeType.YEAR && dateRangeType != WorkoutsDateRangeType.ALL && - dateStart != null && dateEnd != null) { + dateStart != null && dateEnd != null + ) { workoutsRepository.getWorkoutsCountOnMonthOnDateRangeAlt( dateStart, dateEnd