Skip to content

Commit cd77dcd

Browse files
committed
add stats
1 parent 5782783 commit cd77dcd

15 files changed

Lines changed: 702 additions & 294 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
2+
3+
<path android:fillColor="#FFFFFF" android:pathData="M400,880L0,480L400,80L471,151L142,480L471,809L400,880Z"/>
4+
5+
</vector>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
2+
3+
<path android:fillColor="#FFFFFF" android:pathData="M321,880L250,809L579,480L250,151L321,80L721,480L321,880Z"/>
4+
5+
</vector>

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import androidx.navigation.compose.rememberNavController
66
import kotlinx.serialization.Serializable
77
import neth.iecal.trease.ui.screens.GardenScreen
88
import neth.iecal.trease.ui.screens.HomeScreen
9-
import neth.iecal.trease.ui.theme.ZenTheme
9+
import neth.iecal.trease.ui.theme.MainTheme
1010

1111
@Serializable
1212
data object Home
@@ -16,7 +16,7 @@ data object Garden
1616
@Composable
1717
fun App() {
1818
val navController = rememberNavController()
19-
ZenTheme() {
19+
MainTheme() {
2020
NavHost(navController = navController, startDestination = Home) {
2121
composable<Home> {
2222
HomeScreen(navController)

composeApp/src/commonMain/kotlin/neth/iecal/trease/models/FocusStats.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package neth.iecal.trease.models
22

33
import kotlinx.serialization.Serializable
44
import kotlinx.serialization.Serializer
5-
import kotlin.time.Clock
65
import kotlin.uuid.ExperimentalUuidApi
76
import kotlin.uuid.Uuid
87

@@ -13,6 +12,6 @@ data class FocusStats @OptIn(ExperimentalUuidApi::class) constructor(
1312
val isFailed: Boolean,
1413
val failureTree: String = "weathered",
1514
val id:String = Uuid.generateV7().toString(),
16-
val completedOn: Long = Clock.System.now().toEpochMilliseconds(),
15+
val completedOn: Long = kotlin.time.Clock.System.now().toEpochMilliseconds(),
1716

18-
)
17+
)

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ import androidx.compose.material3.MaterialTheme
1111
import androidx.compose.runtime.Composable
1212
import androidx.compose.ui.Modifier
1313
import androidx.compose.ui.draw.clip
14-
import androidx.compose.ui.graphics.Color
1514
import androidx.compose.ui.unit.dp
16-
import neth.iecal.trease.ui.theme.ZenTheme
1715

1816
@Composable
1917
fun ProgressBar(progress: Float) {

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

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,20 @@ import kotlin.math.ceil
2424
import kotlin.math.min
2525
import kotlin.math.sqrt
2626

27-
// --- Configuration ---
28-
// Identical constants to the Node.js script
27+
2928
const val SCALE = 4f
3029
const val TILE_WIDTH = 100f * SCALE
3130
const val TILE_HEIGHT = TILE_WIDTH / 2f
3231
const val SOURCE_ANCHOR_Y_OFFSET = (150f * SCALE) + (TILE_WIDTH / 4f)
3332

34-
data class PlacedTree(
33+
private data class PlacedTree(
3534
val id: String,
3635
val image: ImageBitmap,
3736
val drawX: Float,
3837
val drawY: Float
3938
)
4039

41-
data class ForestLayout(
40+
private data class ForestLayout(
4241
val trees: List<PlacedTree>,
4342
val totalWidth: Float,
4443
val totalHeight: Float
@@ -48,21 +47,17 @@ data class ForestLayout(
4847
fun IsometricForest(treeIds: List<String>) {
4948
val context = LocalPlatformContext.current
5049

51-
// State to hold the calculated forest
5250
var forestLayout by remember { mutableStateOf<ForestLayout?>(null) }
5351
var isLoading by remember { mutableStateOf(true) }
5452

55-
// Logic: Load & Calculate Positions (Background Thread)
5653
LaunchedEffect(treeIds) {
5754
isLoading = true
5855

5956
forestLayout = withContext(Dispatchers.Default) {
6057
if (treeIds.isEmpty()) return@withContext null
6158

62-
// 1. Calculate Grid Dimensions
6359
val gridSize = ceil(sqrt(treeIds.size.toDouble())).toInt()
6460

65-
// 2. Load Images Parallel
6661
val imageLoader = ImageLoader(context)
6762

6863
val deferredLoad = treeIds.mapIndexed { index, id ->
@@ -91,7 +86,7 @@ fun IsometricForest(treeIds: List<String>) {
9186

9287
val results = deferredLoad.awaitAll().filterNotNull()
9388

94-
// 3. Sort by Depth (Painter's Algorithm)
89+
// Sort by Depth (Painter's Algorithm)
9590
val sortedResults = results.sortedWith(Comparator { a, b ->
9691
val (posA, _) = a
9792
val (posB, _) = b
@@ -101,7 +96,6 @@ fun IsometricForest(treeIds: List<String>) {
10196
if (depthA != depthB) depthA - depthB else posA.first - posB.first
10297
})
10398

104-
// 4. Calculate Canvas Bounds (Virtual Size)
10599
val isoWidth = (gridSize * 2) * (TILE_WIDTH / 2f)
106100
val isoHeight = gridSize * TILE_HEIGHT
107101

@@ -113,7 +107,7 @@ fun IsometricForest(treeIds: List<String>) {
113107
val originX = totalWidth / 2f
114108
val originY = 150f * SCALE
115109

116-
// 5. Create PlacedTree objects with exact coordinates
110+
// Create PlacedTree objects with exact coordinates
117111
val placements = sortedResults.map { (pos, bitmap, id) ->
118112
val (gridX, gridY) = pos
119113

@@ -141,7 +135,6 @@ fun IsometricForest(treeIds: List<String>) {
141135
isLoading = false
142136
}
143137

144-
// --- Rendering ---
145138
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
146139
if (isLoading) {
147140
CircularProgressIndicator()
@@ -153,33 +146,28 @@ fun IsometricForest(treeIds: List<String>) {
153146
val screenHeight = constraints.maxHeight.toFloat()
154147

155148
Canvas(modifier = Modifier.fillMaxSize()) {
156-
// 1. Calculate Scale to Fit
157-
// We want the whole 'layout.totalWidth' to fit inside 'screenWidth' (and same for height)
149+
// Calculate Scale to Fit
158150
val scaleX = screenWidth / layout.totalWidth
159151
val scaleY = screenHeight / layout.totalHeight
160152

161153
// Use the smaller scale to ensure it fits both dimensions (Fit Center)
162154
val scale = min(scaleX, scaleY)
163155

164-
// 2. Calculate Centering Offsets
165-
// How much empty space is left?
156+
// Calculate Centering Offsets
166157
val usedWidth = layout.totalWidth * scale
167158
val usedHeight = layout.totalHeight * scale
168159

169160
val translateX = (screenWidth - usedWidth) / 2f
170161
val translateY = (screenHeight - usedHeight) / 2f
171162

172-
// 3. Apply Transform and Draw
173163
withTransform({
174164
translate(left = translateX, top = translateY)
175-
// We must define both scaleX and scaleY explicitly
176165
scale(
177166
scaleX = scale,
178167
scaleY = scale,
179168
pivot = Offset.Zero
180169
)
181170
}) {
182-
// "Very efficient": No math here, just drawing pre-calc coordinates
183171
layout.trees.forEach { tree ->
184172
drawImage(
185173
image = tree.image,
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package neth.iecal.trease.ui.components.stats
2+
3+
import androidx.compose.foundation.Canvas
4+
import androidx.compose.foundation.background
5+
import androidx.compose.foundation.layout.Arrangement
6+
import androidx.compose.foundation.layout.Box
7+
import androidx.compose.foundation.layout.Column
8+
import androidx.compose.foundation.layout.Row
9+
import androidx.compose.foundation.layout.Spacer
10+
import androidx.compose.foundation.layout.fillMaxSize
11+
import androidx.compose.foundation.layout.size
12+
import androidx.compose.foundation.layout.width
13+
import androidx.compose.foundation.shape.CircleShape
14+
import androidx.compose.material3.MaterialTheme
15+
import androidx.compose.material3.Text
16+
import androidx.compose.runtime.Composable
17+
import androidx.compose.ui.Alignment
18+
import androidx.compose.ui.Modifier
19+
import androidx.compose.ui.draw.clip
20+
import androidx.compose.ui.graphics.Color
21+
import androidx.compose.ui.graphics.StrokeCap
22+
import androidx.compose.ui.graphics.drawscope.Stroke
23+
import androidx.compose.ui.text.font.FontWeight
24+
import androidx.compose.ui.unit.dp
25+
import neth.iecal.trease.models.FocusStats
26+
27+
@Composable
28+
fun EfficiencyDonutChart(stats: List<FocusStats>) {
29+
val total = stats.size
30+
val failed = stats.count { it.isFailed }
31+
val success = total - failed
32+
33+
val successColor = MaterialTheme.colorScheme.primary
34+
val failColor = MaterialTheme.colorScheme.error
35+
val emptyColor = MaterialTheme.colorScheme.surfaceContainerHigh
36+
37+
if (total == 0) {
38+
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
39+
Text("Start planting to see health", style = MaterialTheme.typography.bodyMedium)
40+
}
41+
return
42+
}
43+
44+
Row(
45+
modifier = Modifier.fillMaxSize(),
46+
verticalAlignment = Alignment.CenterVertically
47+
) {
48+
Box(modifier = Modifier.size(120.dp), contentAlignment = Alignment.Center) {
49+
Canvas(modifier = Modifier.fillMaxSize()) {
50+
val strokeWidth = 16.dp.toPx()
51+
val radius = size.minDimension / 2 - strokeWidth / 2
52+
53+
drawCircle(color = emptyColor, radius = radius, style = Stroke(width = strokeWidth))
54+
55+
val successSweep = (success.toFloat() / total) * 360f
56+
drawArc(
57+
color = successColor,
58+
startAngle = -90f,
59+
sweepAngle = successSweep,
60+
useCenter = false,
61+
style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
62+
)
63+
}
64+
Text(
65+
text = "${((success.toFloat()/total)*100).toInt()}%",
66+
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold)
67+
)
68+
}
69+
70+
Spacer(modifier = Modifier.width(42.dp))
71+
72+
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
73+
MetricLegendItem(color = successColor, text = "Thriving ($success)")
74+
MetricLegendItem(color = failColor, text = "Withered ($failed)")
75+
}
76+
}
77+
}
78+
79+
@Composable
80+
private fun MetricLegendItem(color: Color, text: String) {
81+
Row(verticalAlignment = Alignment.CenterVertically) {
82+
Box(modifier = Modifier.size(10.dp).clip(CircleShape).background(color))
83+
Spacer(modifier = Modifier.width(8.dp))
84+
Text(text, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface)
85+
}
86+
}

0 commit comments

Comments
 (0)