Skip to content

Commit 07024e0

Browse files
committed
add fullscreen stats
1 parent 81e1ed8 commit 07024e0

5 files changed

Lines changed: 476 additions & 133 deletions

File tree

composeApp/src/commonMain/kotlin/neth/iecal/trease/App.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@ import kotlinx.serialization.Serializable
77
import neth.iecal.trease.ui.screens.GardenScreen
88
import neth.iecal.trease.ui.screens.HomeScreen
99
import neth.iecal.trease.ui.theme.MainTheme
10+
import neth.iecal.trease.GardenFullScreen
11+
import neth.iecal.trease.ui.screens.FullScreenGarden
1012

1113
@Serializable
1214
data object Home
1315
@Serializable
1416
data object Garden
1517

18+
@Serializable
19+
data object GardenFullScreen
1620
@Composable
1721
fun App() {
1822
val navController = rememberNavController()
@@ -24,6 +28,9 @@ fun App() {
2428
composable<Garden> {
2529
GardenScreen(navController)
2630
}
31+
composable<GardenFullScreen> {
32+
FullScreenGarden(navController)
33+
}
2734

2835
}
2936

composeApp/src/commonMain/kotlin/neth/iecal/trease/ui/components/stats/HourlyStatChart.kt

Lines changed: 84 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@ import androidx.compose.ui.geometry.CornerRadius
1111
import androidx.compose.ui.geometry.Offset
1212
import androidx.compose.ui.geometry.Size
1313
import androidx.compose.ui.graphics.PathEffect
14+
import androidx.compose.ui.text.TextStyle
1415
import androidx.compose.ui.text.drawText
16+
import androidx.compose.ui.text.font.FontWeight
1517
import androidx.compose.ui.text.rememberTextMeasurer
1618
import androidx.compose.ui.unit.dp
19+
import androidx.compose.ui.unit.sp
1720
import neth.iecal.trease.models.FocusStats
1821
import neth.iecal.trease.utils.getHourOfDay
1922
import kotlin.math.ceil
@@ -30,120 +33,144 @@ fun HourlyFocusChart(stats: List<FocusStats>) {
3033
buckets.toList()
3134
}
3235

33-
Column(Modifier.fillMaxSize()) {
34-
Text(
35-
text = "Productivity by Hour",
36-
style = MaterialTheme.typography.titleMedium,
37-
color = MaterialTheme.colorScheme.onSurface,
38-
modifier = Modifier.padding(bottom = 24.dp).align(Alignment.CenterHorizontally)
39-
)
36+
BoxWithConstraints(Modifier.fillMaxSize()) {
37+
val isCompact = maxHeight < 180.dp || maxWidth < 260.dp
38+
39+
Column(Modifier.fillMaxSize()) {
40+
val headerStyle = if (isCompact) MaterialTheme.typography.labelSmall else MaterialTheme.typography.titleMedium
41+
val headerPadding = if (isCompact) 4.dp else 24.dp
42+
val headerText = if (isCompact) "Peak Hours" else "Productivity by Hour"
43+
44+
Text(
45+
text = headerText,
46+
style = headerStyle,
47+
color = MaterialTheme.colorScheme.onSurface,
48+
modifier = Modifier
49+
.padding(bottom = headerPadding)
50+
.align(Alignment.CenterHorizontally)
51+
)
4052

41-
HourlyBarChartCanvas(hourlyData)
53+
HourlyBarChartCanvas(
54+
data = hourlyData,
55+
isCompact = isCompact
56+
)
57+
}
4258
}
4359
}
4460

4561
@Composable
46-
fun HourlyBarChartCanvas(data: List<Long>) {
62+
fun HourlyBarChartCanvas(
63+
data: List<Long>,
64+
isCompact: Boolean
65+
) {
4766
val textMeasurer = rememberTextMeasurer()
4867

4968
val barColor = MaterialTheme.colorScheme.primary
5069
val trackColor = MaterialTheme.colorScheme.surfaceContainerHigh
51-
val gridColor = MaterialTheme.colorScheme.outlineVariant
70+
val gridColor = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)
5271
val labelColor = MaterialTheme.colorScheme.onSurfaceVariant
53-
val labelStyle = MaterialTheme.typography.labelSmall
5472

55-
Canvas(
56-
modifier = Modifier
57-
.fillMaxSize()
58-
.padding(start = 40.dp, end = 16.dp, top = 16.dp, bottom = 32.dp)
59-
) {
60-
if (size.width <= 0 || size.height <= 0) return@Canvas
73+
val labelStyle = if (isCompact)
74+
TextStyle(fontSize = 8.sp, fontWeight = FontWeight.Medium)
75+
else
76+
MaterialTheme.typography.labelSmall
6177

62-
val maxVal = data.maxOfOrNull { it } ?: 0L
78+
val leftPadding = if (isCompact) 24.dp else 40.dp
79+
val bottomPadding = if (isCompact) 16.dp else 32.dp
80+
val barCornerRadius = if (isCompact) 1.5.dp else 3.dp
6381

64-
val yMax = max(60L, (ceil(maxVal / 60.0) * 60).toLong())
82+
val gridSteps = if (isCompact) 2 else 4
6583

66-
val chartHeight = size.height
67-
val chartWidth = size.width
84+
Canvas(modifier = Modifier.fillMaxSize()) {
85+
val width = size.width
86+
val height = size.height
6887

88+
val leftPadPx = leftPadding.toPx()
89+
val botPadPx = bottomPadding.toPx()
6990

70-
val barWidth = (chartWidth / 24) * 0.6f
71-
val spacing = (chartWidth / 24)
91+
val chartWidth = width - leftPadPx
92+
val chartHeight = height - botPadPx
93+
94+
if (chartWidth <= 0 || chartHeight <= 0) return@Canvas
95+
96+
val maxVal = data.maxOfOrNull { it } ?: 0L
97+
val yMax = max(60L, (ceil(maxVal / 60.0) * 60).toLong())
7298

73-
val steps = 4
74-
val stepHeight = chartHeight / steps
75-
val stepValue = yMax / steps
99+
val stepHeight = chartHeight / gridSteps
100+
val stepValue = yMax / gridSteps
76101

77-
for (i in 0..steps) {
102+
for (i in 0..gridSteps) {
78103
val y = chartHeight - (i * stepHeight)
79104
val value = i * stepValue
80105

81-
82106
drawLine(
83107
color = gridColor,
84-
start = Offset(0f, y),
85-
end = Offset(chartWidth, y),
108+
start = Offset(leftPadPx, y),
109+
end = Offset(width, y),
86110
strokeWidth = 1.dp.toPx(),
87111
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f))
88112
)
89113

90-
// Y-Label
91114
if (i > 0) {
92115
val label = if (value >= 60) "${value / 60}h" else "${value}m"
93116

94-
val textLayoutResult = textMeasurer.measure(
95-
text = label,
96-
style = labelStyle.copy(color = labelColor)
97-
)
117+
val textResult = textMeasurer.measure(label, labelStyle.copy(color = labelColor))
98118

99119
drawText(
100-
textLayoutResult = textLayoutResult,
120+
textLayoutResult = textResult,
101121
topLeft = Offset(
102-
x = -35.dp.toPx(),
103-
y = y - (textLayoutResult.size.height / 2)
122+
x = (leftPadPx - textResult.size.width) - 4.dp.toPx(),
123+
y = y - (textResult.size.height / 2)
104124
)
105125
)
106126
}
107127
}
108128

129+
val barSpaceAllocated = chartWidth / 24
130+
val fillRatio = if (isCompact) 0.75f else 0.6f
131+
val barWidth = barSpaceAllocated * fillRatio
132+
val halfBarWidth = barWidth / 2
109133

110134
data.forEachIndexed { hour, minutes ->
111-
val x = (hour * spacing) + (spacing / 2)
135+
val centerX = leftPadPx + (hour * barSpaceAllocated) + (barSpaceAllocated / 2)
112136
val barHeight = (minutes / yMax.toFloat()) * chartHeight
113137

114-
// Track (Background Bar)
115138
drawRoundRect(
116139
color = trackColor.copy(alpha = 0.3f),
117-
topLeft = Offset(x - barWidth / 2, 0f),
140+
topLeft = Offset(centerX - halfBarWidth, 0f),
118141
size = Size(barWidth, chartHeight),
119-
cornerRadius = CornerRadius(2.dp.toPx()) // Slightly sharper corners for thinner bars
142+
cornerRadius = CornerRadius(barCornerRadius.toPx())
120143
)
121144

122-
// Active Bar
123-
drawRoundRect(
124-
color = barColor,
125-
topLeft = Offset(x - barWidth / 2, chartHeight - barHeight),
126-
size = Size(barWidth, barHeight),
127-
cornerRadius = CornerRadius(2.dp.toPx())
128-
)
145+
if (barHeight > 0) {
146+
drawRoundRect(
147+
color = barColor,
148+
topLeft = Offset(centerX - halfBarWidth, chartHeight - barHeight),
149+
size = Size(barWidth, barHeight),
150+
cornerRadius = CornerRadius(barCornerRadius.toPx())
151+
)
152+
}
129153

154+
// Show every 6 hours (0, 6, 12, 18) to prevent clutter
130155
if (hour % 6 == 0) {
131156
val timeLabel = when(hour) {
132157
0 -> "12am"
133158
12 -> "12pm"
134-
else -> "${hour % 12}${if (hour < 12) "am" else "pm"}"
159+
else -> "${hour % 12}${if (isCompact) "" else if (hour < 12) "am" else "pm"}"
160+
// In compact mode, just show "6", "12" etc to save space, or keep AM/PM if it fits.
161+
// Let's keep it simple: "6" for compact, "6am" for full
135162
}
136163

137-
val textLayoutResult = textMeasurer.measure(
138-
text = timeLabel,
139-
style = labelStyle.copy(color = labelColor)
140-
)
164+
// If compact, simplify text further
165+
val finalLabel = if (isCompact && timeLabel.length > 2) timeLabel.filter { it.isDigit() } else timeLabel
166+
167+
val textResult = textMeasurer.measure(finalLabel, labelStyle.copy(color = labelColor))
141168

142169
drawText(
143-
textLayoutResult = textLayoutResult,
170+
textLayoutResult = textResult,
144171
topLeft = Offset(
145-
x = x - (textLayoutResult.size.width / 2),
146-
y = chartHeight + 8.dp.toPx()
172+
x = centerX - (textResult.size.width / 2),
173+
y = chartHeight + 6.dp.toPx()
147174
)
148175
)
149176
}

0 commit comments

Comments
 (0)