Skip to content

Commit 4c1c94d

Browse files
committed
fix video playback
1 parent 10aa134 commit 4c1c94d

6 files changed

Lines changed: 154 additions & 107 deletions

File tree

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

Lines changed: 103 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import androidx.compose.animation.fadeIn
66
import androidx.compose.animation.fadeOut
77
import androidx.compose.animation.togetherWith
88
import androidx.compose.foundation.Canvas
9+
import androidx.compose.foundation.background
910
import androidx.compose.foundation.layout.Box
1011
import androidx.compose.foundation.layout.fillMaxSize
1112
import androidx.compose.foundation.layout.size
@@ -23,155 +24,175 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
2324
import coil3.compose.AsyncImage
2425
import io.github.kdroidfilter.composemediaplayer.rememberVideoPlayerState
2526
import kotlinx.coroutines.delay
27+
import kotlinx.coroutines.isActive
2628
import neth.iecal.trease.Constants
2729
import neth.iecal.trease.PlatformVideoPlayer
2830
import neth.iecal.trease.models.TimerStatus
2931
import neth.iecal.trease.models.TreeData
3032
import neth.iecal.trease.utils.getCachedVideoPath
3133
import 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
4043
fun 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
}

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import kotlin.collections.filter
2424
import kotlin.math.abs
2525
import kotlin.math.ceil
2626
import kotlin.math.max
27-
import kotlin.time.Clock
2827

2928
@Composable
3029
fun WeeklyActivityChart(allStats: List<FocusStats>) {
@@ -48,7 +47,7 @@ fun WeeklyActivityChart(allStats: List<FocusStats>) {
4847
val startOfWeek = endOfWeek.minus(DatePeriod(days = 6))
4948

5049
Text(
51-
text = "${startOfWeek.month.name} ${startOfWeek.day} - ${endOfWeek.month.name} ${endOfWeek.dayOfMonth}",
50+
text = "${startOfWeek.month.name} ${startOfWeek.dayOfMonth} - ${endOfWeek.month.name} ${endOfWeek.dayOfMonth}",
5251
style = MaterialTheme.typography.titleMedium,
5352
color = MaterialTheme.colorScheme.onSurface,
5453
modifier = Modifier.padding(bottom = 16.dp).align(Alignment.CenterHorizontally)

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import androidx.compose.ui.text.style.TextAlign
2424
import androidx.compose.ui.unit.dp
2525
import androidx.compose.ui.window.Dialog
2626
import coil3.compose.AsyncImage
27+
import neth.iecal.trease.Constants
2728
import neth.iecal.trease.viewmodels.HomeScreenViewModel
2829

2930

@@ -44,7 +45,7 @@ fun YouWon(viewModel: HomeScreenViewModel){
4445
Text("Wooww", style = MaterialTheme.typography.headlineLarge,textAlign = TextAlign.Center)
4546

4647
AsyncImage(
47-
model = "https://trease-focus.github.io/cache-trees/images/${viewModel.selectedTree.value.id}_{${viewModel.currentTreeSeedVariant.value}_grid.png",
48+
model = "${Constants.cdn}/images/${viewModel.selectedTree.value.id}_${viewModel.currentTreeSeedVariant.value}_grid.png",
4849
contentDescription = "Dead Tree",
4950
contentScale = ContentScale.Crop,
5051
modifier = Modifier.size(280.dp)

composeApp/src/commonMain/kotlin/neth/iecal/trease/utils/Time.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package neth.iecal.trease.utils
22

3+
import kotlinx.datetime.Clock
4+
import kotlinx.datetime.Instant
35
import kotlinx.datetime.LocalDate
46
import kotlinx.datetime.LocalDateTime
57
import kotlinx.datetime.TimeZone
@@ -9,8 +11,6 @@ import kotlinx.datetime.format.Padding
911
import kotlinx.datetime.format.char
1012
import kotlinx.datetime.number
1113
import kotlinx.datetime.toLocalDateTime
12-
import kotlin.time.Clock
13-
import kotlin.time.Instant
1414

1515
fun Long.getDayLabel(): String {
1616
val date = Instant.fromEpochMilliseconds(this)
@@ -27,7 +27,7 @@ fun Long.getDate(): String {
2727

2828
return date.format(
2929
LocalDateTime.Format {
30-
day(padding = Padding.ZERO)
30+
dayOfMonth(padding = Padding.ZERO)
3131
char('/')
3232
monthNumber(padding = Padding.ZERO)
3333
char('/')

0 commit comments

Comments
 (0)