From 481654609f5b0fee862bd36a309ac10b352d6f5e Mon Sep 17 00:00:00 2001 From: Joseph Roque Date: Thu, 14 Mar 2024 23:56:34 -0700 Subject: [PATCH] feat(android): pipeline for statistics chart to UI, basic UI for empty widgets --- .../utils/StatisticsChartDataModelProducer.kt | 8 + .../feature/bowlerdetails/build.gradle.kts | 3 + .../bowlerdetails/BowlerDetailsViewModel.kt | 45 +++-- .../feature/bowlerdetails/ui/build.gradle.kts | 2 + android/feature/overview/build.gradle.kts | 2 + .../feature/overview/OverviewViewModel.kt | 46 +++-- android/feature/overview/ui/build.gradle.kts | 1 + .../feature/statisticswidget/build.gradle.kts | 3 + .../editor/StatisticsWidgetEditorViewModel.kt | 18 +- .../StatisticsWidgetLayoutEditorViewModel.kt | 19 +- .../statisticswidget/ui/build.gradle.kts | 2 + .../ui/StatisticsWidgetTimelineTitle.kt | 11 ++ .../ui/editor/StatisticsWidgetEditor.kt | 30 +--- .../ui/editor/StatisticsWidgetEditorUi.kt | 10 +- .../ui/layout/StatisticsWidgetLayout.kt | 8 +- .../ui/layout/StatisticsWidgetLayoutUi.kt | 10 +- .../editor/StatisticsWidgetLayoutEditor.kt | 5 +- .../editor/StatisticsWidgetLayoutEditorUi.kt | 10 +- .../ui/widget/StatisticsWidgetCard.kt | 168 +++++++++++++++++- .../res/drawable/ic_exclamation_point.xml | 12 ++ .../main/res/drawable/ic_question_mark.xml | 12 ++ .../ui/src/main/res/values/strings.xml | 5 + 22 files changed, 363 insertions(+), 67 deletions(-) create mode 100644 android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/StatisticsWidgetTimelineTitle.kt create mode 100644 android/feature/statisticswidget/ui/src/main/res/drawable/ic_exclamation_point.xml create mode 100644 android/feature/statisticswidget/ui/src/main/res/drawable/ic_question_mark.xml diff --git a/android/core/statistics/charts/src/main/java/ca/josephroque/bowlingcompanion/core/statistics/charts/utils/StatisticsChartDataModelProducer.kt b/android/core/statistics/charts/src/main/java/ca/josephroque/bowlingcompanion/core/statistics/charts/utils/StatisticsChartDataModelProducer.kt index b8d6dddf6..e8470527c 100644 --- a/android/core/statistics/charts/src/main/java/ca/josephroque/bowlingcompanion/core/statistics/charts/utils/StatisticsChartDataModelProducer.kt +++ b/android/core/statistics/charts/src/main/java/ca/josephroque/bowlingcompanion/core/statistics/charts/utils/StatisticsChartDataModelProducer.kt @@ -14,6 +14,14 @@ fun StatisticChartContent.getModelEntries(): List = when (this) { else -> throw IllegalStateException("Unsupported chart type: $this") } +fun StatisticChartContent.hasModelEntries(): Boolean = when (this) { + is StatisticChartContent.CountableChart, + is StatisticChartContent.AveragingChart, + is StatisticChartContent.PercentageChart, + -> true + else -> false +} + fun CountableChartData.getModelEntries(): List = entries.map { entryOf(it.key.value, it.value.toFloat()) } diff --git a/android/feature/bowlerdetails/build.gradle.kts b/android/feature/bowlerdetails/build.gradle.kts index 5bbe97af9..c2b31f14a 100644 --- a/android/feature/bowlerdetails/build.gradle.kts +++ b/android/feature/bowlerdetails/build.gradle.kts @@ -9,8 +9,11 @@ android { dependencies { implementation(project(":core:statistics")) + implementation(project(":core:statistics:charts")) implementation(project(":feature:bowlerdetails:ui")) implementation(project(":feature:leagueslist:ui")) implementation(project(":feature:gearlist:ui")) implementation(project(":feature:statisticswidget:ui")) + + implementation(libs.vico.compose) } diff --git a/android/feature/bowlerdetails/src/main/java/ca/josephroque/bowlingcompanion/feature/bowlerdetails/BowlerDetailsViewModel.kt b/android/feature/bowlerdetails/src/main/java/ca/josephroque/bowlingcompanion/feature/bowlerdetails/BowlerDetailsViewModel.kt index 829d6a5b1..ba78f33e2 100644 --- a/android/feature/bowlerdetails/src/main/java/ca/josephroque/bowlingcompanion/feature/bowlerdetails/BowlerDetailsViewModel.kt +++ b/android/feature/bowlerdetails/src/main/java/ca/josephroque/bowlingcompanion/feature/bowlerdetails/BowlerDetailsViewModel.kt @@ -14,7 +14,8 @@ import ca.josephroque.bowlingcompanion.core.data.repository.UserDataRepository import ca.josephroque.bowlingcompanion.core.model.LeagueListItem import ca.josephroque.bowlingcompanion.core.model.LeagueRecurrence import ca.josephroque.bowlingcompanion.core.navigation.Route -import ca.josephroque.bowlingcompanion.core.statistics.models.StatisticChartContent +import ca.josephroque.bowlingcompanion.core.statistics.charts.utils.getModelEntries +import ca.josephroque.bowlingcompanion.core.statistics.charts.utils.hasModelEntries import ca.josephroque.bowlingcompanion.feature.bowlerdetails.ui.BowlerDetailsTopBarUiState import ca.josephroque.bowlingcompanion.feature.bowlerdetails.ui.BowlerDetailsUiAction import ca.josephroque.bowlingcompanion.feature.bowlerdetails.ui.BowlerDetailsUiState @@ -23,6 +24,7 @@ import ca.josephroque.bowlingcompanion.feature.leagueslist.ui.LeaguesListUiActio import ca.josephroque.bowlingcompanion.feature.leagueslist.ui.LeaguesListUiState import ca.josephroque.bowlingcompanion.feature.statisticswidget.ui.layout.StatisticsWidgetLayoutUiAction import ca.josephroque.bowlingcompanion.feature.statisticswidget.ui.layout.StatisticsWidgetLayoutUiState +import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer import dagger.hilt.android.lifecycle.HiltViewModel import java.util.UUID import javax.inject.Inject @@ -79,29 +81,34 @@ class BowlerDetailsViewModel @Inject constructor( } } - private val widgetCharts: MutableStateFlow> = MutableStateFlow( - emptyMap(), - ) + private val widgetCharts: MutableStateFlow> = + MutableStateFlow(emptyMap()) - val uiState: StateFlow = combine( - leaguesListState, + private val widgetLayoutState = combine( widgets, widgetCharts, + ) { widgets, widgetCharts -> + widgets?.let { + StatisticsWidgetLayoutUiState( + widgets = it, + widgetCharts = widgetCharts, + ) + } + } + + val uiState: StateFlow = combine( + leaguesListState, + widgetLayoutState, bowlersRepository.getBowlerDetails(bowlerId), gearRepository.getBowlerPreferredGear(bowlerId), - ) { leaguesList, widgets, widgetCharts, bowlerDetails, gearList -> + ) { leaguesList, widgets, bowlerDetails, gearList -> BowlerDetailsScreenUiState.Loaded( bowler = BowlerDetailsUiState( bowler = bowlerDetails, leaguesList = leaguesList, gearList = GearListUiState(gearList, gearToDelete = null), topBar = BowlerDetailsTopBarUiState(bowlerDetails.name), - widgets = widgets?.let { - StatisticsWidgetLayoutUiState( - widgets = it, - widgetCharts = widgetCharts, - ) - }, + widgets = widgets, ), ) }.stateIn( @@ -120,8 +127,18 @@ class BowlerDetailsViewModel @Inject constructor( widgets?.forEach { widget -> launch { val chart = statisticsWidgetsRepository.getStatisticsWidgetChart(widget) + widgetCharts.update { - it + (widget.id to chart) + val widgetChart = it[widget.id] ?: StatisticsWidgetLayoutUiState.ChartContent( + chart, + ChartEntryModelProducer(), + ) + + if (widgetChart.chart.hasModelEntries()) { + widgetChart.modelProducer.setEntries(chart.getModelEntries()) + } + + it + (widget.id to widgetChart.copy(chart = chart)) } } } diff --git a/android/feature/bowlerdetails/ui/build.gradle.kts b/android/feature/bowlerdetails/ui/build.gradle.kts index fa61b1c47..49d49c376 100644 --- a/android/feature/bowlerdetails/ui/build.gradle.kts +++ b/android/feature/bowlerdetails/ui/build.gradle.kts @@ -11,4 +11,6 @@ dependencies { implementation(project(":feature:gearlist:ui")) implementation(project(":feature:leagueslist:ui")) implementation(project(":feature:statisticswidget:ui")) + + implementation(libs.vico.compose) } diff --git a/android/feature/overview/build.gradle.kts b/android/feature/overview/build.gradle.kts index 981d567c5..8e56ba7ef 100644 --- a/android/feature/overview/build.gradle.kts +++ b/android/feature/overview/build.gradle.kts @@ -9,9 +9,11 @@ android { dependencies { implementation(project(":core:statistics")) + implementation(project(":core:statistics:charts")) implementation(project(":feature:bowlerslist:ui")) implementation(project(":feature:overview:ui")) implementation(project(":feature:statisticswidget:ui")) implementation(libs.kotlinx.datetime) + implementation(libs.vico.compose) } diff --git a/android/feature/overview/src/main/java/ca/josephroque/bowlingcompanion/feature/overview/OverviewViewModel.kt b/android/feature/overview/src/main/java/ca/josephroque/bowlingcompanion/feature/overview/OverviewViewModel.kt index 48dcdec22..7a9565788 100644 --- a/android/feature/overview/src/main/java/ca/josephroque/bowlingcompanion/feature/overview/OverviewViewModel.kt +++ b/android/feature/overview/src/main/java/ca/josephroque/bowlingcompanion/feature/overview/OverviewViewModel.kt @@ -10,13 +10,15 @@ import ca.josephroque.bowlingcompanion.core.data.repository.StatisticsWidgetsRep import ca.josephroque.bowlingcompanion.core.data.repository.UserDataRepository import ca.josephroque.bowlingcompanion.core.model.BowlerKind import ca.josephroque.bowlingcompanion.core.model.BowlerListItem -import ca.josephroque.bowlingcompanion.core.statistics.models.StatisticChartContent +import ca.josephroque.bowlingcompanion.core.statistics.charts.utils.getModelEntries +import ca.josephroque.bowlingcompanion.core.statistics.charts.utils.hasModelEntries import ca.josephroque.bowlingcompanion.feature.bowlerslist.ui.BowlersListUiAction import ca.josephroque.bowlingcompanion.feature.bowlerslist.ui.BowlersListUiState import ca.josephroque.bowlingcompanion.feature.overview.ui.OverviewUiAction import ca.josephroque.bowlingcompanion.feature.overview.ui.OverviewUiState import ca.josephroque.bowlingcompanion.feature.statisticswidget.ui.layout.StatisticsWidgetLayoutUiAction import ca.josephroque.bowlingcompanion.feature.statisticswidget.ui.layout.StatisticsWidgetLayoutUiState +import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer import dagger.hilt.android.lifecycle.HiltViewModel import java.util.UUID import javax.inject.Inject @@ -66,25 +68,30 @@ class OverviewViewModel @Inject constructor( } } - private val widgetCharts: MutableStateFlow> = MutableStateFlow( - emptyMap(), - ) + private val widgetCharts: MutableStateFlow> = + MutableStateFlow(emptyMap()) - val uiState: StateFlow = combine( - bowlersListState, + private val widgetLayoutState = combine( widgets, widgetCharts, + ) { widgets, widgetCharts -> + widgets?.let { + StatisticsWidgetLayoutUiState( + widgets = it, + widgetCharts = widgetCharts, + ) + } + } + + val uiState: StateFlow = combine( + bowlersListState, + widgetLayoutState, isGameInProgressSnackBarVisible, - ) { bowlersList, widgets, widgetCharts, isGameInProgressSnackBarVisible -> + ) { bowlersList, widgets, isGameInProgressSnackBarVisible -> OverviewScreenUiState.Loaded( overview = OverviewUiState( bowlersList = bowlersList, - widgets = widgets?.let { - StatisticsWidgetLayoutUiState( - widgets = it, - widgetCharts = widgetCharts, - ) - }, + widgets = widgets, ), isGameInProgressSnackBarVisible = isGameInProgressSnackBarVisible, ) @@ -95,13 +102,24 @@ class OverviewViewModel @Inject constructor( ) init { + // FIXME: Share with BowlerDetailsViewModel, StatisticsWidgetLayoutEditorViewModel? viewModelScope.launch { widgets.collect { widgets -> widgets?.forEach { widget -> launch { val chart = statisticsWidgetsRepository.getStatisticsWidgetChart(widget) + widgetCharts.update { - it + (widget.id to chart) + val widgetChart = it[widget.id] ?: StatisticsWidgetLayoutUiState.ChartContent( + chart, + ChartEntryModelProducer(), + ) + + if (widgetChart.chart.hasModelEntries()) { + widgetChart.modelProducer.setEntries(chart.getModelEntries()) + } + + it + (widget.id to widgetChart.copy(chart = chart)) } } } diff --git a/android/feature/overview/ui/build.gradle.kts b/android/feature/overview/ui/build.gradle.kts index f1c4715bf..ca490eeaf 100644 --- a/android/feature/overview/ui/build.gradle.kts +++ b/android/feature/overview/ui/build.gradle.kts @@ -13,4 +13,5 @@ dependencies { implementation(libs.compose.reorderable) implementation(libs.swipe) + implementation(libs.vico.compose) } diff --git a/android/feature/statisticswidget/build.gradle.kts b/android/feature/statisticswidget/build.gradle.kts index 4d8e22b02..0193c13db 100644 --- a/android/feature/statisticswidget/build.gradle.kts +++ b/android/feature/statisticswidget/build.gradle.kts @@ -9,5 +9,8 @@ android { dependencies { implementation(project(":core:statistics")) + implementation(project(":core:statistics:charts")) implementation(project(":feature:statisticswidget:ui")) + + implementation(libs.vico.compose) } diff --git a/android/feature/statisticswidget/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/editor/StatisticsWidgetEditorViewModel.kt b/android/feature/statisticswidget/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/editor/StatisticsWidgetEditorViewModel.kt index d8c7bda47..0d0daf8ae 100644 --- a/android/feature/statisticswidget/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/editor/StatisticsWidgetEditorViewModel.kt +++ b/android/feature/statisticswidget/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/editor/StatisticsWidgetEditorViewModel.kt @@ -13,7 +13,8 @@ import ca.josephroque.bowlingcompanion.core.model.LeagueSummary import ca.josephroque.bowlingcompanion.core.navigation.Route import ca.josephroque.bowlingcompanion.core.statistics.Statistic import ca.josephroque.bowlingcompanion.core.statistics.allStatistics -import ca.josephroque.bowlingcompanion.core.statistics.models.StatisticChartContent +import ca.josephroque.bowlingcompanion.core.statistics.charts.utils.getModelEntries +import ca.josephroque.bowlingcompanion.core.statistics.charts.utils.hasModelEntries import ca.josephroque.bowlingcompanion.core.statistics.models.StatisticsWidget import ca.josephroque.bowlingcompanion.core.statistics.models.StatisticsWidgetCreate import ca.josephroque.bowlingcompanion.core.statistics.models.StatisticsWidgetSource @@ -21,6 +22,7 @@ import ca.josephroque.bowlingcompanion.core.statistics.models.StatisticsWidgetTi import ca.josephroque.bowlingcompanion.core.statistics.trackable.overall.GameAverageStatistic import ca.josephroque.bowlingcompanion.feature.statisticswidget.ui.editor.StatisticsWidgetEditorUiAction import ca.josephroque.bowlingcompanion.feature.statisticswidget.ui.editor.StatisticsWidgetEditorUiState +import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer import dagger.hilt.android.lifecycle.HiltViewModel import java.util.UUID import javax.inject.Inject @@ -113,9 +115,19 @@ class StatisticsWidgetEditorViewModel @Inject constructor( ) } - private val preview: Flow = widget.map { + private val preview: Flow = widget.map { it ?: return@map null - statisticsWidgetsRepository.getStatisticsWidgetChart(it) + val chart = statisticsWidgetsRepository.getStatisticsWidgetChart(it) + val modelProducer = ChartEntryModelProducer() + + if (chart.hasModelEntries()) { + modelProducer.setEntries(chart.getModelEntries()) + } + + StatisticsWidgetEditorUiState.ChartContent( + chart = chart, + modelProducer = modelProducer, + ) } val uiState: StateFlow = combine( diff --git a/android/feature/statisticswidget/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/layout/StatisticsWidgetLayoutEditorViewModel.kt b/android/feature/statisticswidget/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/layout/StatisticsWidgetLayoutEditorViewModel.kt index 3ea148261..4ab7b39f2 100644 --- a/android/feature/statisticswidget/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/layout/StatisticsWidgetLayoutEditorViewModel.kt +++ b/android/feature/statisticswidget/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/layout/StatisticsWidgetLayoutEditorViewModel.kt @@ -9,11 +9,14 @@ import ca.josephroque.bowlingcompanion.core.analytics.trackable.widget.WidgetLay import ca.josephroque.bowlingcompanion.core.common.viewmodel.ApproachViewModel import ca.josephroque.bowlingcompanion.core.data.repository.StatisticsWidgetsRepository import ca.josephroque.bowlingcompanion.core.navigation.Route +import ca.josephroque.bowlingcompanion.core.statistics.charts.utils.getModelEntries +import ca.josephroque.bowlingcompanion.core.statistics.charts.utils.hasModelEntries import ca.josephroque.bowlingcompanion.core.statistics.models.StatisticsWidget import ca.josephroque.bowlingcompanion.feature.statisticswidget.editor.StatisticsWidgetInitialSource import ca.josephroque.bowlingcompanion.feature.statisticswidget.ui.layout.editor.StatisticsWidgetLayoutEditorTopBarUiState import ca.josephroque.bowlingcompanion.feature.statisticswidget.ui.layout.editor.StatisticsWidgetLayoutEditorUiAction import ca.josephroque.bowlingcompanion.feature.statisticswidget.ui.layout.editor.StatisticsWidgetLayoutEditorUiState +import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer import dagger.hilt.android.lifecycle.HiltViewModel import java.util.UUID import javax.inject.Inject @@ -82,9 +85,23 @@ class StatisticsWidgetLayoutEditorViewModel @Inject constructor( launch { val chart = statisticsWidgetRepository.getStatisticsWidgetChart(widget) _uiState.updateWidgets { + val widgetChart = it.layoutEditor.widgetCharts[widget.id] + ?: StatisticsWidgetLayoutEditorUiState.ChartContent( + chart, + ChartEntryModelProducer(), + ) + + if (widgetChart.chart.hasModelEntries()) { + widgetChart.modelProducer.setEntries(chart.getModelEntries()) + } + it.copy( layoutEditor = it.layoutEditor.copy( - widgetCharts = it.layoutEditor.widgetCharts + (widget.id to chart), + widgetCharts = it.layoutEditor.widgetCharts + ( + widget.id to widgetChart.copy( + chart = chart, + ) + ), ), ) } diff --git a/android/feature/statisticswidget/ui/build.gradle.kts b/android/feature/statisticswidget/ui/build.gradle.kts index 6229f07a4..2b4786990 100644 --- a/android/feature/statisticswidget/ui/build.gradle.kts +++ b/android/feature/statisticswidget/ui/build.gradle.kts @@ -9,6 +9,8 @@ android { dependencies { implementation(project(":core:statistics")) + implementation(project(":core:statistics:charts")) implementation(libs.compose.reorderable) + implementation(libs.vico.compose) } diff --git a/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/StatisticsWidgetTimelineTitle.kt b/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/StatisticsWidgetTimelineTitle.kt new file mode 100644 index 000000000..f1b6fd54e --- /dev/null +++ b/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/StatisticsWidgetTimelineTitle.kt @@ -0,0 +1,11 @@ +package ca.josephroque.bowlingcompanion.feature.statisticswidget.ui + +import ca.josephroque.bowlingcompanion.core.statistics.models.StatisticsWidgetTimeline + +fun StatisticsWidgetTimeline.titleResourceId(): Int = when (this) { + StatisticsWidgetTimeline.ALL_TIME -> R.string.statistics_widget_timeline_all_time + StatisticsWidgetTimeline.ONE_YEAR -> R.string.statistics_widget_timeline_one_year + StatisticsWidgetTimeline.ONE_MONTH -> R.string.statistics_widget_timeline_one_month + StatisticsWidgetTimeline.SIX_MONTHS -> R.string.statistics_widget_timeline_six_months + StatisticsWidgetTimeline.THREE_MONTHS -> R.string.statistics_widget_timeline_three_months +} diff --git a/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/editor/StatisticsWidgetEditor.kt b/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/editor/StatisticsWidgetEditor.kt index 321ca4b23..46a6632b5 100644 --- a/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/editor/StatisticsWidgetEditor.kt +++ b/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/editor/StatisticsWidgetEditor.kt @@ -19,6 +19,7 @@ import ca.josephroque.bowlingcompanion.core.designsystem.components.form.Pickabl import ca.josephroque.bowlingcompanion.core.designsystem.components.list.ListSectionFooter import ca.josephroque.bowlingcompanion.core.statistics.models.StatisticsWidgetTimeline import ca.josephroque.bowlingcompanion.feature.statisticswidget.ui.R +import ca.josephroque.bowlingcompanion.feature.statisticswidget.ui.titleResourceId import ca.josephroque.bowlingcompanion.feature.statisticswidget.ui.widget.StatisticsWidgetCard @Composable @@ -35,7 +36,8 @@ fun StatisticsWidgetEditor( if (state.widget != null) { StatisticsWidgetCard( widget = state.widget, - chart = state.preview, + chart = state.preview?.chart, + chartEntryModelProducer = state.preview?.modelProducer, modifier = Modifier .aspectRatio(2f) .padding(horizontal = 16.dp), @@ -90,28 +92,10 @@ fun StatisticsWidgetEditor( options = StatisticsWidgetTimeline.entries.toTypedArray(), selected = state.timeline, titleForOption = { - when (it) { - StatisticsWidgetTimeline.ONE_MONTH -> stringResource( - R.string.statistics_widget_timeline_one_month, - ) - - StatisticsWidgetTimeline.THREE_MONTHS -> stringResource( - R.string.statistics_widget_timeline_three_months, - ) - - StatisticsWidgetTimeline.SIX_MONTHS -> stringResource( - R.string.statistics_widget_timeline_six_months, - ) - - StatisticsWidgetTimeline.ONE_YEAR -> stringResource( - R.string.statistics_widget_timeline_one_year, - ) - - StatisticsWidgetTimeline.ALL_TIME -> stringResource( - R.string.statistics_widget_timeline_all_time, - ) - - null -> "" + if (it == null) { + "" + } else { + stringResource(it.titleResourceId()) } }, onOptionSelected = { diff --git a/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/editor/StatisticsWidgetEditorUi.kt b/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/editor/StatisticsWidgetEditorUi.kt index e414a9afd..bfd5fba7f 100644 --- a/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/editor/StatisticsWidgetEditorUi.kt +++ b/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/editor/StatisticsWidgetEditorUi.kt @@ -8,6 +8,7 @@ import ca.josephroque.bowlingcompanion.core.statistics.models.StatisticsWidget import ca.josephroque.bowlingcompanion.core.statistics.models.StatisticsWidgetSource import ca.josephroque.bowlingcompanion.core.statistics.models.StatisticsWidgetTimeline import ca.josephroque.bowlingcompanion.core.statistics.trackable.overall.GameAverageStatistic +import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer data class StatisticsWidgetEditorUiState( val source: StatisticsWidgetSource? = null, @@ -16,8 +17,13 @@ data class StatisticsWidgetEditorUiState( val bowler: BowlerSummary? = null, val league: LeagueSummary? = null, val widget: StatisticsWidget? = null, - val preview: StatisticChartContent? = null, -) + val preview: ChartContent? = null, +) { + data class ChartContent( + val chart: StatisticChartContent, + val modelProducer: ChartEntryModelProducer, + ) +} sealed interface StatisticsWidgetEditorUiAction { data object BackClicked : StatisticsWidgetEditorUiAction diff --git a/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/layout/StatisticsWidgetLayout.kt b/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/layout/StatisticsWidgetLayout.kt index bd079c786..f3124ff8a 100644 --- a/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/layout/StatisticsWidgetLayout.kt +++ b/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/layout/StatisticsWidgetLayout.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import ca.josephroque.bowlingcompanion.core.statistics.StatisticID -import ca.josephroque.bowlingcompanion.core.statistics.models.StatisticChartContent import ca.josephroque.bowlingcompanion.core.statistics.models.StatisticsWidget import ca.josephroque.bowlingcompanion.core.statistics.models.StatisticsWidgetSource import ca.josephroque.bowlingcompanion.core.statistics.models.StatisticsWidgetTimeline @@ -72,7 +71,7 @@ fun StatisticsWidgetLayout( @Composable private fun StatisticsWidgetRow( widgets: List, - widgetCharts: Map, + widgetCharts: Map, onAction: (StatisticsWidgetLayoutUiAction) -> Unit, modifier: Modifier = Modifier, ) { @@ -81,9 +80,12 @@ private fun StatisticsWidgetRow( modifier = modifier.fillMaxWidth(), ) { widgets.forEach { widget -> + val chart = widgetCharts[widget.id] + StatisticsWidgetCard( widget = widget, - chart = widgetCharts[widget.id], + chart = chart?.chart, + chartEntryModelProducer = chart?.modelProducer, onClick = { onAction(StatisticsWidgetLayoutUiAction.WidgetClicked(widget)) }, modifier = Modifier .weight(1f) diff --git a/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/layout/StatisticsWidgetLayoutUi.kt b/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/layout/StatisticsWidgetLayoutUi.kt index 048714198..db6731edb 100644 --- a/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/layout/StatisticsWidgetLayoutUi.kt +++ b/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/layout/StatisticsWidgetLayoutUi.kt @@ -2,12 +2,18 @@ package ca.josephroque.bowlingcompanion.feature.statisticswidget.ui.layout import ca.josephroque.bowlingcompanion.core.statistics.models.StatisticChartContent import ca.josephroque.bowlingcompanion.core.statistics.models.StatisticsWidget +import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer import java.util.UUID data class StatisticsWidgetLayoutUiState( val widgets: List, - val widgetCharts: Map, -) + val widgetCharts: Map, +) { + data class ChartContent( + val chart: StatisticChartContent, + val modelProducer: ChartEntryModelProducer, + ) +} sealed interface StatisticsWidgetLayoutUiAction { data object ChangeLayoutClicked : StatisticsWidgetLayoutUiAction diff --git a/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/layout/editor/StatisticsWidgetLayoutEditor.kt b/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/layout/editor/StatisticsWidgetLayoutEditor.kt index 416946f2f..5cab670dd 100644 --- a/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/layout/editor/StatisticsWidgetLayoutEditor.kt +++ b/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/layout/editor/StatisticsWidgetLayoutEditor.kt @@ -76,9 +76,12 @@ fun StatisticsWidgetLayoutEditor( contentAlignment = Alignment.TopEnd, modifier = modifier, ) { + val chart = state.widgetCharts[widget.id] + StatisticsWidgetCard( widget = widget, - chart = state.widgetCharts[widget.id], + chart = chart?.chart, + chartEntryModelProducer = chart?.modelProducer, onClick = { onAction(StatisticsWidgetLayoutEditorUiAction.WidgetClicked(widget)) }, modifier = Modifier .aspectRatio(1f) diff --git a/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/layout/editor/StatisticsWidgetLayoutEditorUi.kt b/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/layout/editor/StatisticsWidgetLayoutEditorUi.kt index 3bf6b07a8..45f09dc8a 100644 --- a/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/layout/editor/StatisticsWidgetLayoutEditorUi.kt +++ b/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/layout/editor/StatisticsWidgetLayoutEditorUi.kt @@ -2,13 +2,19 @@ package ca.josephroque.bowlingcompanion.feature.statisticswidget.ui.layout.edito import ca.josephroque.bowlingcompanion.core.statistics.models.StatisticChartContent import ca.josephroque.bowlingcompanion.core.statistics.models.StatisticsWidget +import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer import java.util.UUID data class StatisticsWidgetLayoutEditorUiState( val widgets: List = emptyList(), - val widgetCharts: Map = emptyMap(), + val widgetCharts: Map = emptyMap(), val isDeleteModeEnabled: Boolean = false, -) +) { + data class ChartContent( + val chart: StatisticChartContent, + val modelProducer: ChartEntryModelProducer, + ) +} data class StatisticsWidgetLayoutEditorTopBarUiState( val isDeleteModeEnabled: Boolean = false, diff --git a/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/widget/StatisticsWidgetCard.kt b/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/widget/StatisticsWidgetCard.kt index d7037b1d9..9bf6bcb73 100644 --- a/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/widget/StatisticsWidgetCard.kt +++ b/android/feature/statisticswidget/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/statisticswidget/ui/widget/StatisticsWidgetCard.kt @@ -1,25 +1,189 @@ package ca.josephroque.bowlingcompanion.feature.statisticswidget.ui.widget +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import ca.josephroque.bowlingcompanion.core.designsystem.components.LoadingState +import ca.josephroque.bowlingcompanion.core.statistics.StatisticID +import ca.josephroque.bowlingcompanion.core.statistics.charts.CountingChart import ca.josephroque.bowlingcompanion.core.statistics.models.StatisticChartContent import ca.josephroque.bowlingcompanion.core.statistics.models.StatisticsWidget +import ca.josephroque.bowlingcompanion.core.statistics.models.StatisticsWidgetSource +import ca.josephroque.bowlingcompanion.core.statistics.models.StatisticsWidgetTimeline +import ca.josephroque.bowlingcompanion.feature.statisticswidget.ui.R +import ca.josephroque.bowlingcompanion.feature.statisticswidget.ui.titleResourceId +import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer +import java.util.UUID @Composable fun StatisticsWidgetCard( widget: StatisticsWidget, chart: StatisticChartContent?, + chartEntryModelProducer: ChartEntryModelProducer?, modifier: Modifier = Modifier, onClick: (() -> Unit)? = null, ) { Card(modifier = modifier, onClick = onClick ?: {}) { - if (chart == null) { + if (chart == null || chartEntryModelProducer == null) { LoadingState() } else { - Text("Chart") + when (chart) { + is StatisticChartContent.AveragingChart -> Unit + is StatisticChartContent.PercentageChart -> Unit + is StatisticChartContent.CountableChart -> CountingChartWidget( + widget, + chart, + chartEntryModelProducer, + ) + is StatisticChartContent.DataMissing -> DataMissingWidget(chart.id) + is StatisticChartContent.ChartUnavailable -> UnavailableWidget(chart.id) + } } } } + +@Composable +private fun CountingChartWidget( + widget: StatisticsWidget, + chart: StatisticChartContent.CountableChart, + chartEntryModelProducer: ChartEntryModelProducer, +) { + Widget( + title = stringResource(widget.statistic.titleResourceId), + footer = { + Timeline(widget.timeline) + }, + ) { + CountingChart(chartData = chart.data, chartModel = chartEntryModelProducer) + } +} + +@Composable +private fun DataMissingWidget(statistic: StatisticID) { + Widget( + title = stringResource(statistic.titleResourceId), + subtitle = stringResource(R.string.statistics_widget_chart_data_missing), + footer = { + WhatDoesThisMeanFooter() + }, + ) { + } +} + +@Composable +private fun UnavailableWidget(statistic: StatisticID) { + Widget( + title = stringResource(statistic.titleResourceId), + subtitle = stringResource(R.string.statistics_widget_chart_unavailable), + footer = { + WhatDoesThisMeanFooter() + }, + ) { + Spacer(modifier = Modifier.weight(1f)) + + Image( + painterResource(id = R.drawable.ic_exclamation_point), + contentDescription = null, + modifier = Modifier + .size(20.dp) + .align(Alignment.CenterHorizontally) + .padding(8.dp), + ) + + Spacer(modifier = Modifier.weight(1f)) + } +} + +@Composable +private fun Widget( + title: String, + modifier: Modifier = Modifier, + subtitle: String? = null, + footer: @Composable ColumnScope.() -> Unit = {}, + content: @Composable ColumnScope.() -> Unit, +) { + Column( + modifier = modifier + .fillMaxSize() + .padding(8.dp), + ) { + Text( + text = title, + style = MaterialTheme.typography.bodyMedium, + ) + + if (subtitle != null) { + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(top = 2.dp), + ) + } + + Column(modifier = Modifier.padding(vertical = 4.dp)) { + content() + } + + footer() + } +} + +@Composable +private fun WhatDoesThisMeanFooter() { + Row { + Icon( + Icons.Default.Info, + contentDescription = null, + ) + + Text( + text = stringResource(R.string.statistics_widget_what_does_this_mean), + style = MaterialTheme.typography.labelSmall, + ) + } +} + +@Composable +private fun Timeline(timeline: StatisticsWidgetTimeline) { + Text( + text = stringResource(timeline.titleResourceId()), + style = MaterialTheme.typography.bodyMedium, + ) +} + +@Preview +@Composable +private fun StatisticsWidgetCardPreview() { + StatisticsWidgetCard( + widget = StatisticsWidget( + id = UUID.randomUUID(), + source = StatisticsWidgetSource.Bowler(UUID.randomUUID()), + priority = 0, + statistic = StatisticID.ACES, + timeline = StatisticsWidgetTimeline.ONE_YEAR, + context = "", + ), + chart = StatisticChartContent.DataMissing(StatisticID.ACES), + chartEntryModelProducer = ChartEntryModelProducer(), + modifier = Modifier.aspectRatio(2f), + ) +} diff --git a/android/feature/statisticswidget/ui/src/main/res/drawable/ic_exclamation_point.xml b/android/feature/statisticswidget/ui/src/main/res/drawable/ic_exclamation_point.xml new file mode 100644 index 000000000..ea07cda6a --- /dev/null +++ b/android/feature/statisticswidget/ui/src/main/res/drawable/ic_exclamation_point.xml @@ -0,0 +1,12 @@ + + + + diff --git a/android/feature/statisticswidget/ui/src/main/res/drawable/ic_question_mark.xml b/android/feature/statisticswidget/ui/src/main/res/drawable/ic_question_mark.xml new file mode 100644 index 000000000..e094735f0 --- /dev/null +++ b/android/feature/statisticswidget/ui/src/main/res/drawable/ic_question_mark.xml @@ -0,0 +1,12 @@ + + + + diff --git a/android/feature/statisticswidget/ui/src/main/res/values/strings.xml b/android/feature/statisticswidget/ui/src/main/res/values/strings.xml index ac3256ec6..1fda19c18 100644 --- a/android/feature/statisticswidget/ui/src/main/res/values/strings.xml +++ b/android/feature/statisticswidget/ui/src/main/res/values/strings.xml @@ -33,4 +33,9 @@ Tap here to configure the stats you want to see Tap to change layout + + There doesn\'t seem to be any data available. + You may not have bowled enough games yet, or your filters are excluding too many games. + Not enough data + What does this mean? \ No newline at end of file