Skip to content

Commit d5d2c1b

Browse files
committed
add tree stats
1 parent cfe4186 commit d5d2c1b

5 files changed

Lines changed: 266 additions & 41 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ data class FocusStats @OptIn(ExperimentalUuidApi::class) constructor(
1010
val duration:Long, // in seconds
1111
val treeId: String,
1212
val isFailed: Boolean,
13-
val failureTree: String = "weathered",
13+
val failureTree: String = "weathered_0",
1414
val id:String = Uuid.generateV7().toString(),
1515
val completedOn: Long = kotlin.time.Clock.System.now().toEpochMilliseconds(),
1616
val treeSeed: Int = 0,
Lines changed: 261 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,31 @@
11
package neth.iecal.trease.ui.bottomsheet
2+
3+
import androidx.compose.animation.*
4+
import androidx.compose.animation.core.tween
5+
import androidx.compose.foundation.ExperimentalFoundationApi
6+
import androidx.compose.foundation.background
27
import androidx.compose.foundation.combinedClickable
38
import androidx.compose.foundation.layout.*
9+
import androidx.compose.foundation.lazy.LazyRow
410
import androidx.compose.foundation.lazy.grid.*
11+
import androidx.compose.foundation.shape.CircleShape
12+
import androidx.compose.foundation.shape.RoundedCornerShape
513
import androidx.compose.material3.*
614
import androidx.compose.runtime.*
7-
import androidx.compose.ui.draw.clip
8-
import androidx.compose.ui.unit.dp
9-
import coil3.compose.AsyncImage
10-
11-
12-
import androidx.compose.foundation.shape.RoundedCornerShape
1315
import androidx.compose.ui.Alignment
16+
import androidx.compose.ui.ExperimentalComposeUiApi
1417
import androidx.compose.ui.Modifier
18+
import androidx.compose.ui.draw.clip
19+
import androidx.compose.ui.graphics.Brush
20+
import androidx.compose.ui.graphics.Color
21+
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
1522
import androidx.compose.ui.layout.ContentScale
23+
import androidx.compose.ui.platform.LocalHapticFeedback
24+
import androidx.compose.ui.text.font.FontWeight
25+
import androidx.compose.ui.text.style.TextAlign
26+
import androidx.compose.ui.text.style.TextOverflow
27+
import androidx.compose.ui.unit.dp
28+
import coil3.compose.AsyncImage
1629
import kotlinx.serialization.encodeToString
1730
import kotlinx.serialization.json.Json
1831
import neth.iecal.trease.Constants
@@ -21,58 +34,269 @@ import neth.iecal.trease.models.TreeData
2134
import neth.iecal.trease.models.TreeUiState
2235
import neth.iecal.trease.utils.CacheManager
2336

24-
25-
@OptIn(ExperimentalMaterial3Api::class)
37+
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
2638
@Composable
27-
fun TreeBottomSheet(onDismiss: () -> Unit,onSelected: (TreeData) -> Unit) {
39+
fun TreeBottomSheet(
40+
onDismiss: () -> Unit,
41+
onSelected: (TreeData) -> Unit
42+
) {
43+
// State
2844
var uiState by remember { mutableStateOf<TreeUiState>(TreeUiState.Loading) }
45+
var selectedTree by remember { mutableStateOf<TreeData?>(null) }
2946

47+
// Data Loading Logic
3048
LaunchedEffect(Unit) {
3149
val cacheManager = CacheManager()
32-
val cache = cacheManager.readFile("tree.json")
33-
if(cache!=null) {
34-
uiState = TreeUiState.Success(
35-
Json.decodeFromString(cache)
36-
)
50+
// Optimistic load from cache
51+
cacheManager.readFile("tree.json")?.let {
52+
uiState = TreeUiState.Success(Json.decodeFromString(it))
3753
}
38-
try {
54+
55+
// Fetch fresh data
56+
try {
3957
val trees = TreeRepository.fetchTrees()
4058
uiState = TreeUiState.Success(trees)
41-
cacheManager.saveFile("tree.json",Json.encodeToString(trees))
59+
cacheManager.saveFile("tree.json", Json.encodeToString(trees))
4260
} catch (e: Exception) {
43-
uiState = TreeUiState.Error("Failed to load trees $e")
61+
if (uiState !is TreeUiState.Success) {
62+
uiState = TreeUiState.Error("Unable to load forest: ${e.message}")
63+
}
4464
}
4565
}
4666

47-
ModalBottomSheet(onDismissRequest = onDismiss) {
48-
LazyVerticalGrid(
49-
columns = GridCells.Adaptive(100.dp),
50-
contentPadding = PaddingValues(16.dp),
51-
modifier = Modifier.fillMaxWidth().heightIn(min = 300.dp)
52-
) {
53-
if (uiState is TreeUiState.Success) {
54-
items((uiState as TreeUiState.Success).trees.filter { it.isGrowable }) { treeId ->
55-
TreeItem(treeId,onSelected)
56-
}
67+
ModalBottomSheet(
68+
onDismissRequest = onDismiss,
69+
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
70+
containerColor = MaterialTheme.colorScheme.surface,
71+
tonalElevation = 0.dp // Cleaner flat look
72+
) {
73+
// Smart Transition between List and Details
74+
AnimatedContent(
75+
targetState = selectedTree,
76+
transitionSpec = {
77+
if (targetState != null) {
78+
slideInHorizontally { it } + fadeIn() togetherWith slideOutHorizontally { -it / 2 } + fadeOut()
79+
} else {
80+
slideInHorizontally { -it / 2 } + fadeIn() togetherWith slideOutHorizontally { it } + fadeOut()
81+
}.using(SizeTransform(clip = false))
82+
},
83+
label = "TreeSheetContent"
84+
) { tree ->
85+
if (tree != null) {
86+
TreeDetailView(
87+
tree = tree,
88+
onBack = { selectedTree = null },
89+
onSelect = { onSelected(tree) }
90+
)
91+
} else {
92+
TreeGridView(
93+
uiState = uiState,
94+
onTreeSelected = { onSelected(it) },
95+
onTreeLongPress = { selectedTree = it }
96+
)
5797
}
5898
}
5999
}
60100
}
61101

102+
62103
@Composable
63-
fun TreeItem(treeId: TreeData, onSelected: (TreeData) -> Unit) {
104+
private fun TreeGridView(
105+
uiState: TreeUiState,
106+
onTreeSelected: (TreeData) -> Unit,
107+
onTreeLongPress: (TreeData) -> Unit
108+
) {
64109
Column(
65-
modifier = Modifier.padding(8.dp).combinedClickable(true,onClick = { onSelected(treeId) }),
110+
modifier = Modifier
111+
.fillMaxWidth()
112+
.heightIn(min = 400.dp)
113+
.padding(horizontal = 24.dp),
66114
horizontalAlignment = Alignment.CenterHorizontally
67115
) {
68-
val treeUrl = "${Constants.cdn}/images/${treeId.id}_${treeId.variants-1}.png"
69-
print("Displaying tree $treeUrl")
70-
AsyncImage(
71-
model =treeUrl,
72-
contentDescription = treeId.name,
73-
modifier = Modifier.size(80.dp).clip(RoundedCornerShape(8.dp)),
74-
contentScale = ContentScale.Fit
116+
Text(
117+
text = "Select a Tree",
118+
style = MaterialTheme.typography.headlineSmall,
119+
fontWeight = FontWeight.Bold
120+
)
121+
Text(
122+
text = "Tap to plant, hold to view details",
123+
style = MaterialTheme.typography.bodySmall,
124+
)
125+
126+
Spacer(modifier = Modifier.height(16.dp))
127+
128+
when (uiState) {
129+
is TreeUiState.Loading -> Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
130+
CircularProgressIndicator(strokeWidth = 2.dp)
131+
}
132+
is TreeUiState.Error -> Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
133+
Text(text = uiState.message, color = MaterialTheme.colorScheme.error)
134+
}
135+
is TreeUiState.Success -> {
136+
LazyVerticalGrid(
137+
columns = GridCells.Adaptive(minSize = 100.dp),
138+
contentPadding = PaddingValues(bottom = 24.dp),
139+
verticalArrangement = Arrangement.spacedBy(16.dp),
140+
horizontalArrangement = Arrangement.spacedBy(16.dp)
141+
) {
142+
items(
143+
items = uiState.trees.filter { it.isGrowable },
144+
key = { it.id }
145+
) { tree ->
146+
TreeGridItem(tree, onTreeSelected, onTreeLongPress)
147+
}
148+
}
149+
}
150+
}
151+
}
152+
}
153+
154+
@Composable
155+
private fun TreeDetailView(
156+
tree: TreeData,
157+
onBack: () -> Unit,
158+
onSelect: () -> Unit
159+
) {
160+
Column(
161+
modifier = Modifier
162+
.fillMaxWidth()
163+
.padding(horizontal = 24.dp)
164+
.padding(bottom = 32.dp)
165+
) {
166+
SuggestionChip(
167+
onClick = {onBack() },
168+
label = { Text("Go Back") },
169+
)
170+
171+
// Main Info
172+
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp),) {
173+
// Main Preview Image
174+
AsyncImage(
175+
model = "${Constants.cdn}/images/${tree.id}_${tree.variants - 1}.png",
176+
contentDescription = null,
177+
contentScale = ContentScale.Fit,
178+
modifier = Modifier.size(200.dp)
179+
)
180+
181+
Column(horizontalAlignment = Alignment.CenterHorizontally,
182+
verticalArrangement = Arrangement.Center,) {
183+
Text(tree.name, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
184+
Text(
185+
text = "By ${tree.creator}",
186+
style = MaterialTheme.typography.labelLarge,
187+
)
188+
}
189+
}
190+
191+
Spacer(Modifier.height(16.dp))
192+
193+
Text(
194+
text = tree.description,
195+
style = MaterialTheme.typography.bodyMedium,
196+
)
197+
198+
Spacer(Modifier.height(24.dp))
199+
200+
Text("Tree Variants", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.Bold)
201+
Spacer(Modifier.height(12.dp))
202+
203+
LazyRow(
204+
horizontalArrangement = Arrangement.spacedBy(12.dp),
205+
modifier = Modifier.fillMaxWidth()
206+
) {
207+
items(tree.variants) { index ->
208+
VariantItem(tree.id, index, isRare = index == tree.variants-1)
209+
}
210+
}
211+
212+
Spacer(Modifier.height(32.dp))
213+
214+
// Action Button
215+
Button(
216+
onClick = onSelect,
217+
modifier = Modifier.fillMaxWidth().height(50.dp),
218+
shape = RoundedCornerShape(12.dp)
219+
) {
220+
Text("Select This Tree")
221+
}
222+
}
223+
}
224+
225+
@OptIn(ExperimentalFoundationApi::class)
226+
@Composable
227+
private fun TreeGridItem(
228+
tree: TreeData,
229+
onClick: (TreeData) -> Unit,
230+
onLongClick: (TreeData) -> Unit
231+
) {
232+
val haptic = LocalHapticFeedback.current
233+
234+
Column(
235+
horizontalAlignment = Alignment.CenterHorizontally,
236+
modifier = Modifier
237+
.clip(RoundedCornerShape(16.dp))
238+
.combinedClickable(
239+
onClick = { onClick(tree) },
240+
onLongClick = {
241+
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
242+
onLongClick(tree)
243+
}
244+
)
245+
.padding(8.dp)
246+
) {
247+
// Image Container
248+
Box(
249+
modifier = Modifier
250+
.size(90.dp)
251+
.padding(8.dp),
252+
contentAlignment = Alignment.Center
253+
) {
254+
AsyncImage(
255+
model = "${Constants.cdn}/images/${tree.id}_${tree.variants - 1}.png",
256+
contentDescription = tree.name,
257+
modifier = Modifier.fillMaxSize(),
258+
contentScale = ContentScale.Fit
259+
)
260+
}
261+
262+
Spacer(Modifier.height(8.dp))
263+
264+
Text(
265+
text = tree.name,
266+
style = MaterialTheme.typography.labelMedium,
267+
textAlign = TextAlign.Center,
268+
maxLines = 1,
269+
overflow = TextOverflow.Ellipsis
270+
)
271+
}
272+
}
273+
274+
@Composable
275+
private fun VariantItem(treeId: String, variantIndex: Int,isRare: Boolean ) {
276+
Column(horizontalAlignment = Alignment.CenterHorizontally) {
277+
Box(
278+
modifier = Modifier
279+
.size(64.dp)
280+
.clip(RoundedCornerShape(12.dp))
281+
.background(MaterialTheme.colorScheme.surfaceVariant),
282+
contentAlignment = Alignment.Center
283+
) {
284+
AsyncImage(
285+
model = "${Constants.cdn}/images/${treeId}_${variantIndex}.png",
286+
contentDescription = "Stage $variantIndex",
287+
modifier = Modifier.padding(4.dp).fillMaxSize(),
288+
contentScale = ContentScale.Fit
289+
)
290+
}
291+
Spacer(Modifier.height(4.dp))
292+
Text(
293+
text = "#${variantIndex + 1}",
294+
style = MaterialTheme.typography.labelSmall,
295+
)
296+
Text(
297+
text = if (isRare) "Rare" else " ",
298+
style = MaterialTheme.typography.labelSmall,
299+
color = MaterialTheme.colorScheme.error
75300
)
76-
Text(treeId.name, style = MaterialTheme.typography.bodySmall)
77301
}
78302
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ fun TreeGrowthPlayer(
130130
TimerStatus.POST_QUIT, TimerStatus.HAS_QUIT -> {
131131
// Dead Tree Image (Opaque)
132132
AsyncImage(
133-
model = "${Constants.cdn}/images/weathered_grid.png",
133+
model = "${Constants.cdn}/images/weathered_0_grid.png",
134134
contentDescription = "Withered",
135135
contentScale = ContentScale.Crop,
136136
modifier = Modifier.fillMaxSize(),

composeApp/src/commonMain/kotlin/neth/iecal/trease/ui/dialogs/YouLost.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ fun YouLost(viewModel: HomeScreenViewModel) {
7070
)
7171

7272
AsyncImage(
73-
model = "${Constants.cdn}/images/weathered_grid.png",
73+
model = "${Constants.cdn}/images/weathered_0_grid.png",
7474
contentDescription = "Dead Tree",
7575
contentScale = ContentScale.Crop,
7676
modifier = Modifier.size(280.dp)

composeApp/src/commonMain/kotlin/neth/iecal/trease/viewmodels/HomeScreenViewModel.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ class HomeScreenViewModel : ViewModel() {
4848
creator = "nethical",
4949
donate = "tree",
5050
variants = 4,
51-
basePrice = 0
51+
basePrice = 0,
52+
isGrowable = true
5253
))
5354

5455
var viewModelCoroutineScope = CoroutineScope(Dispatchers.Default)

0 commit comments

Comments
 (0)