11package 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
27import androidx.compose.foundation.combinedClickable
38import androidx.compose.foundation.layout.*
9+ import androidx.compose.foundation.lazy.LazyRow
410import androidx.compose.foundation.lazy.grid.*
11+ import androidx.compose.foundation.shape.CircleShape
12+ import androidx.compose.foundation.shape.RoundedCornerShape
513import androidx.compose.material3.*
614import 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
1315import androidx.compose.ui.Alignment
16+ import androidx.compose.ui.ExperimentalComposeUiApi
1417import 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
1522import 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
1629import kotlinx.serialization.encodeToString
1730import kotlinx.serialization.json.Json
1831import neth.iecal.trease.Constants
@@ -21,58 +34,269 @@ import neth.iecal.trease.models.TreeData
2134import neth.iecal.trease.models.TreeUiState
2235import 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}
0 commit comments