@@ -6,6 +6,7 @@ import androidx.compose.animation.fadeIn
66import androidx.compose.animation.fadeOut
77import androidx.compose.animation.togetherWith
88import androidx.compose.foundation.Canvas
9+ import androidx.compose.foundation.background
910import androidx.compose.foundation.layout.Box
1011import androidx.compose.foundation.layout.fillMaxSize
1112import androidx.compose.foundation.layout.size
@@ -23,155 +24,175 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
2324import coil3.compose.AsyncImage
2425import io.github.kdroidfilter.composemediaplayer.rememberVideoPlayerState
2526import kotlinx.coroutines.delay
27+ import kotlinx.coroutines.isActive
2628import neth.iecal.trease.Constants
2729import neth.iecal.trease.PlatformVideoPlayer
2830import neth.iecal.trease.models.TimerStatus
2931import neth.iecal.trease.models.TreeData
3032import neth.iecal.trease.utils.getCachedVideoPath
3133import neth.iecal.trease.viewmodels.HomeScreenViewModel
3234
33-
34- private data class PlayerState (
35+ // Helper for animation state
36+ private data class OverlayState (
3537 val status : TimerStatus ,
3638 val tree : TreeData ? ,
3739 val seed : Int
3840)
41+
3942@Composable
4043fun TreeGrowthPlayer (
4144 viewModel : HomeScreenViewModel ,
4245 scale : Float ,
4346) {
44- var isReady by remember { mutableStateOf(false ) }
47+ var isVideoLoaded by remember { mutableStateOf(false ) }
4548 val statePlayer = rememberVideoPlayerState()
4649 val crntStatus by viewModel.timerStatus.collectAsStateWithLifecycle()
4750
51+ val selectedMinutes by viewModel.selectedMinutes.collectAsStateWithLifecycle()
4852 val selectedTree by viewModel.selectedTree.collectAsStateWithLifecycle()
4953 val selectedSeed by viewModel.currentTreeSeedVariant.collectAsStateWithLifecycle()
50- val selectedMinutes by viewModel.selectedMinutes.collectAsStateWithLifecycle()
51-
52- val combinedState = remember(crntStatus, selectedTree,selectedSeed) {
53- PlayerState (crntStatus, selectedTree, selectedSeed)
54- }
55- val infiniteTransition = rememberInfiniteTransition(label = " GlowTransition" )
56-
57- val glowScale by infiniteTransition.animateFloat(
58- initialValue = 0.1f ,
59- targetValue = 0.8f ,
60- animationSpec = infiniteRepeatable(
61- animation = tween(4000 , easing = EaseInOutSine ), // 4s slow breath
62- repeatMode = RepeatMode .Reverse
63- ),
64- label = " GlowScale"
65- )
66-
67- val glowAlpha by infiniteTransition.animateFloat(
68- initialValue = 0.1f ,
69- targetValue = 0.3f ,
70- animationSpec = infiniteRepeatable(
71- animation = tween(4000 , easing = EaseInOutSine ),
72- repeatMode = RepeatMode .Reverse
73- ),
74- label = " GlowAlpha"
75- )
7654
7755 LaunchedEffect (selectedTree, selectedSeed) {
56+ isVideoLoaded = false
7857 val remoteUrl = " ${Constants .cdn} /video/${selectedTree?.id} _${selectedSeed} .webm"
7958
80- println (" loading tree $remoteUrl " )
8159 try {
82- val localPath = getCachedVideoPath(remoteUrl, selectedTree?.id ? : " tree" ,selectedSeed)
60+ val localPath = getCachedVideoPath(remoteUrl, selectedTree?.id ? : " tree" , selectedSeed)
8361 statePlayer.openUri(localPath)
84- isReady = true
62+ isVideoLoaded = true
63+ } catch (e: Exception ) {
64+ e.printStackTrace()
65+ }
66+ }
67+
68+ LaunchedEffect (crntStatus, isVideoLoaded, selectedMinutes) {
69+ if (! isVideoLoaded) return @LaunchedEffect
8570
86- val originalDurationMs = 30_000L
87- println (" duration ${statePlayer.durationText} " )
88- val targetDurationMs = selectedMinutes * 60_000L
89- val stretchFactor = targetDurationMs.toDouble() / originalDurationMs
71+ if (crntStatus == TimerStatus .Running ) {
72+ statePlayer.play()
73+ delay(600 ) // Let engine warm up
9074
91- val playChunkMs = 100L
92- val pauseChunkMs = ((playChunkMs * stretchFactor) - playChunkMs).toLong().coerceAtLeast( 0 )
75+ val videoDurationMs = 30_000L // 30 seconds
76+ val targetDurationMs = (selectedMinutes ? : 1 ) * 60_000L // e.g., 25 mins -> 1,500,000ms
9377
94- // Monotonic video loop (no animation logic inside here anymore)
95- while (true ) {
78+ val stretchFactor = (targetDurationMs.toDouble() / videoDurationMs).coerceAtLeast(1.0 )
79+
80+ val playChunk = 200L
81+ // Formula: TotalStepTime = Play * Factor. Pause = Total - Play.
82+ val pauseChunk = ((playChunk * stretchFactor) - playChunk).toLong().coerceAtLeast(0L )
83+
84+ println (" TreeGrowth: Target=${selectedMinutes} m ($targetDurationMs ms). Factor=$stretchFactor . Play=$playChunk , Pause=$pauseChunk " )
85+
86+ while (isActive && crntStatus == TimerStatus .Running ) {
9687 statePlayer.play()
97- delay(playChunkMs)
98- statePlayer.pause()
99- delay(pauseChunkMs)
88+ delay(playChunk)
89+
90+ if (isActive && crntStatus == TimerStatus .Running && pauseChunk > 0 ) {
91+ statePlayer.pause()
92+ delay(pauseChunk)
93+ }
10094 }
101- } catch (e : Exception ) {
102- e.printStackTrace ()
95+ } else {
96+ statePlayer.pause ()
10397 }
10498 }
10599
106100 Box (
107- modifier = Modifier .fillMaxSize().scale(scale),
101+ modifier = Modifier
102+ .fillMaxSize()
103+ .scale(scale),
108104 contentAlignment = Alignment .Center
109105 ) {
106+ // We render this DIRECTLY. No If-statements, no Animations.
107+ // It sits in the background persistently.
108+ PlatformVideoPlayer (
109+ statePlayer,
110+ Modifier .fillMaxSize()
111+ )
112+
113+ val overlayState = remember(crntStatus, selectedTree, selectedSeed) {
114+ OverlayState (crntStatus, selectedTree, selectedSeed)
115+ }
116+
110117 AnimatedContent (
111- targetState = combinedState,
112- transitionSpec = {
113- fadeIn(tween(800 )) togetherWith fadeOut(tween(800 ))
114- },
118+ targetState = overlayState,
119+ transitionSpec = { fadeIn(tween(500 )) togetherWith fadeOut(tween(500 )) },
115120 modifier = Modifier .fillMaxSize(),
116- label = " StateTransition "
117- ) { status ->
118- when (status .status) {
121+ label = " OverlayTransition "
122+ ) { state ->
123+ when (state .status) {
119124 TimerStatus .Running -> {
120- Box (modifier = Modifier .fillMaxSize(), contentAlignment = Alignment .Center ) {
121- PlatformVideoPlayer (
122- statePlayer,
123- Modifier .fillMaxSize()
124- )
125-
126- Canvas (modifier = Modifier .size(350 .dp)) {
127- drawCircle(
128- brush = Brush .radialGradient(
129- colors = listOf (
130- Color (0xFFFFF9C4 ).copy(alpha = glowAlpha), // Soft Sunlight Yellow
131- Color (0xFFFFD54F ).copy(alpha = glowAlpha * 0.5f ), // Warm Amber
132- Color .Transparent
133- ),
134- center = center,
135- radius = (size.minDimension / 2 ) * glowScale
136- ),
137- radius = (size.minDimension / 2 ) * glowScale,
138- center = center
139- )
140- }
125+ Box (Modifier .fillMaxSize(), contentAlignment = Alignment .Center ) {
126+ BreathingGlow ()
141127 }
142128 }
143129
144130 TimerStatus .POST_QUIT , TimerStatus .HAS_QUIT -> {
131+ // Dead Tree Image (Opaque)
145132 AsyncImage (
146133 model = " ${Constants .cdn} /images/weathered_grid.png" ,
147- contentDescription = status.tree?.id ,
134+ contentDescription = " Withered " ,
148135 contentScale = ContentScale .Crop ,
149136 modifier = Modifier .fillMaxSize(),
150137 filterQuality = FilterQuality .High
151138 )
152139 }
140+
153141 else -> {
154142 Box (Modifier .fillMaxSize()) {
155143 AsyncImage (
156- model = " ${Constants .cdn} /images/${status .tree?.id} _0_grid.png" ,
157- contentDescription = status.tree?.name ,
144+ model = " ${Constants .cdn} /images/${state .tree?.id} _0_grid.png" ,
145+ contentDescription = " Preview " ,
158146 contentScale = ContentScale .Crop ,
159147 modifier = Modifier .fillMaxSize(),
160148 filterQuality = FilterQuality .High
161149 )
162150
163- if (! isReady ) {
151+ if (! isVideoLoaded ) {
164152 CircularProgressIndicator (
165- modifier = Modifier
166- .size(48 .dp)
167- .align(Alignment .Center ),
153+ modifier = Modifier .align(Alignment .Center ),
168154 color = Color .White .copy(alpha = 0.7f )
169155 )
170156 }
171157 }
172158 }
173-
174159 }
175160 }
176161 }
162+ }
163+
164+ @Composable
165+ fun BreathingGlow () {
166+ val infiniteTransition = rememberInfiniteTransition(label = " GlowTransition" )
167+ val glowScale by infiniteTransition.animateFloat(
168+ initialValue = 0.6f ,
169+ targetValue = 0.8f ,
170+ animationSpec = infiniteRepeatable(
171+ animation = tween(4000 , easing = EaseInOutSine ),
172+ repeatMode = RepeatMode .Reverse
173+ ),
174+ label = " GlowScale"
175+ )
176+ val glowAlpha by infiniteTransition.animateFloat(
177+ initialValue = 0.05f , // Very subtle
178+ targetValue = 0.2f ,
179+ animationSpec = infiniteRepeatable(
180+ animation = tween(4000 , easing = EaseInOutSine ),
181+ repeatMode = RepeatMode .Reverse
182+ ),
183+ label = " GlowAlpha"
184+ )
185+
186+ Canvas (modifier = Modifier .size(350 .dp)) {
187+ drawCircle(
188+ brush = Brush .radialGradient(
189+ colors = listOf (
190+ Color (0xFFFFF9C4 ).copy(alpha = glowAlpha),
191+ Color .Transparent
192+ ),
193+ center = center,
194+ radius = (size.minDimension / 2 ) * glowScale
195+ )
196+ )
197+ }
177198}
0 commit comments