Auto commit (MindMachine) Sat Apr 11 01:01:02 PM CDT 2026
This commit is contained in:
@@ -80,6 +80,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -109,7 +110,6 @@ import solutions.tretter.mindmachine.session.FlashColor
|
||||
import solutions.tretter.mindmachine.session.MainViewModel
|
||||
import solutions.tretter.mindmachine.session.SplitFlashFrame
|
||||
import kotlinx.coroutines.launch
|
||||
import solutions.tretter.mindmachine.session.SplitFlashSequencer
|
||||
import solutions.tretter.mindmachine.session.flashIntervalMsToSeconds
|
||||
import solutions.tretter.mindmachine.session.flashIntervalSecondsToMs
|
||||
import solutions.tretter.mindmachine.session.formatDuration
|
||||
@@ -118,6 +118,7 @@ import solutions.tretter.mindmachine.session.shouldShowActiveControlsByDefault
|
||||
import solutions.tretter.mindmachine.session.shouldUseImmersiveFullscreen
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.roundToLong
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
@@ -806,33 +807,71 @@ private fun rememberSplitFlashFrame(
|
||||
val latestFlashOnMs = rememberUpdatedState(flashOnMs)
|
||||
val latestFlashOffMs = rememberUpdatedState(flashOffMs)
|
||||
|
||||
return produceState(initialValue = SplitFlashFrame(FlashColor.BLACK, FlashColor.BLACK), isRunning) {
|
||||
// The screen can only change on vsync boundaries. If the configured interval is not a
|
||||
// multiple of the refresh period, the *visual* interval will wobble by ~1 frame.
|
||||
// Snapping intervals to whole frames makes the blink look uniform.
|
||||
val refreshRateHz = LocalView.current.display?.refreshRate ?: 60f
|
||||
val framePeriodNanos = remember(refreshRateHz) {
|
||||
(1_000_000_000.0 / refreshRateHz.coerceAtLeast(1f)).roundToLong().coerceAtLeast(1L)
|
||||
}
|
||||
|
||||
return produceState(initialValue = SplitFlashFrame(FlashColor.BLACK, FlashColor.BLACK), isRunning, framePeriodNanos) {
|
||||
if (!isRunning) {
|
||||
value = SplitFlashFrame(FlashColor.BLACK, FlashColor.BLACK)
|
||||
return@produceState
|
||||
}
|
||||
|
||||
// Lock the sequencer's time base to Compose's frame clock.
|
||||
// Mixing System.nanoTime() (start) with withFrameNanos() (updates) can introduce
|
||||
// an arbitrary offset that looks like irregular intervals.
|
||||
val sequencer = SplitFlashSequencer(
|
||||
startTimeNanos = 0L,
|
||||
flashOnMs = latestFlashOnMs.value,
|
||||
flashOffMs = latestFlashOffMs.value,
|
||||
)
|
||||
|
||||
// Drive the blink schedule from the frame clock, but keep phase boundaries aligned
|
||||
// to a monotonic time base (frameTimeNanos) so the *configured timing* stays steady
|
||||
// even if some frames are late.
|
||||
//
|
||||
// Key idea: instead of "advance by whatever elapsed since last frame", we keep an
|
||||
// explicit nextToggleTime and only advance the phase when now >= nextToggleTime.
|
||||
var started = false
|
||||
var phase = 0 // 0=RG, 1=BK, 2=GR, 3=BK
|
||||
var nextToggleNanos = 0L
|
||||
var onNanos = 0L
|
||||
var offNanos = 0L
|
||||
|
||||
fun frameForPhase(p: Int): SplitFlashFrame = when (p) {
|
||||
0 -> SplitFlashFrame(left = FlashColor.RED, right = FlashColor.GREEN)
|
||||
1 -> SplitFlashFrame(left = FlashColor.BLACK, right = FlashColor.BLACK)
|
||||
2 -> SplitFlashFrame(left = FlashColor.GREEN, right = FlashColor.RED)
|
||||
else -> SplitFlashFrame(left = FlashColor.BLACK, right = FlashColor.BLACK)
|
||||
}
|
||||
|
||||
fun updateDurations() {
|
||||
val rawOn = latestFlashOnMs.value.coerceIn(50, 2000).toLong() * 1_000_000L
|
||||
val rawOff = latestFlashOffMs.value.coerceIn(50, 2000).toLong() * 1_000_000L
|
||||
|
||||
val onFrames = ((rawOn + framePeriodNanos / 2) / framePeriodNanos).coerceAtLeast(1L)
|
||||
val offFrames = ((rawOff + framePeriodNanos / 2) / framePeriodNanos).coerceAtLeast(1L)
|
||||
|
||||
onNanos = onFrames * framePeriodNanos
|
||||
offNanos = offFrames * framePeriodNanos
|
||||
}
|
||||
|
||||
while (true) {
|
||||
withFrameNanos { frameTimeNanos ->
|
||||
withFrameNanos { now ->
|
||||
if (!started) {
|
||||
// Initialize phaseStartNanos to the first real frame timestamp.
|
||||
// This keeps phase transitions aligned to the same monotonic clock.
|
||||
sequencer.frameAt(frameTimeNanos)
|
||||
updateDurations()
|
||||
phase = 0
|
||||
nextToggleNanos = now + onNanos
|
||||
value = frameForPhase(phase)
|
||||
started = true
|
||||
return@withFrameNanos
|
||||
}
|
||||
sequencer.updateIntervals(latestFlashOnMs.value, latestFlashOffMs.value)
|
||||
value = sequencer.frameAt(frameTimeNanos)
|
||||
|
||||
updateDurations()
|
||||
|
||||
// Catch up if we missed one or more phase boundaries due to frame delays.
|
||||
while (now >= nextToggleNanos) {
|
||||
phase = (phase + 1) % 4
|
||||
val dur = if (phase == 0 || phase == 2) onNanos else offNanos
|
||||
nextToggleNanos += dur
|
||||
}
|
||||
|
||||
value = frameForPhase(phase)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -891,6 +930,19 @@ fun RemoveAdsScreen(billingManager: solutions.tretter.mindmachine.billing.Billin
|
||||
val ui by vm.ui.collectAsStateWithLifecycle()
|
||||
val activity = LocalContext.current as? Activity
|
||||
val details by billingManager.productDetails.collectAsStateWithLifecycle()
|
||||
val logs by billingManager.logs.collectAsStateWithLifecycle()
|
||||
|
||||
val subDetails = details[solutions.tretter.mindmachine.billing.BillingProducts.SUB_REMOVE_ADS_MONTHLY]
|
||||
val subPrice = subDetails
|
||||
?.subscriptionOfferDetails
|
||||
?.firstOrNull()
|
||||
?.pricingPhases
|
||||
?.pricingPhaseList
|
||||
?.firstOrNull()
|
||||
?.formattedPrice
|
||||
|
||||
val inAppDetails = details[solutions.tretter.mindmachine.billing.BillingProducts.INAPP_REMOVE_ADS_FOREVER]
|
||||
val inAppPrice = inAppDetails?.oneTimePurchaseOfferDetails?.formattedPrice
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
@@ -912,7 +964,7 @@ fun RemoveAdsScreen(billingManager: solutions.tretter.mindmachine.billing.Billin
|
||||
activity?.let { billingManager.launchPurchase(it, solutions.tretter.mindmachine.billing.BillingProducts.SUB_REMOVE_ADS_MONTHLY) }
|
||||
}
|
||||
) {
|
||||
Text(details[solutions.tretter.mindmachine.billing.BillingProducts.SUB_REMOVE_ADS_MONTHLY]?.name ?: "Subscribe: $1/month")
|
||||
Text("Subscribe: ${subPrice ?: "$0.99"}/month")
|
||||
}
|
||||
|
||||
Button(
|
||||
@@ -921,7 +973,7 @@ fun RemoveAdsScreen(billingManager: solutions.tretter.mindmachine.billing.Billin
|
||||
activity?.let { billingManager.launchPurchase(it, solutions.tretter.mindmachine.billing.BillingProducts.INAPP_REMOVE_ADS_FOREVER) }
|
||||
}
|
||||
) {
|
||||
Text(details[solutions.tretter.mindmachine.billing.BillingProducts.INAPP_REMOVE_ADS_FOREVER]?.name ?: "Buy: $20 one-time")
|
||||
Text("Buy: ${inAppPrice ?: "$19.99"} one-time")
|
||||
}
|
||||
|
||||
TextButton(onClick = { billingManager.refresh() }) { Text("Restore / Refresh purchases") }
|
||||
@@ -930,6 +982,21 @@ fun RemoveAdsScreen(billingManager: solutions.tretter.mindmachine.billing.Billin
|
||||
"Note: Purchases only work when the app is installed from Google Play (Internal testing/Production). Debug sideloaded builds won’t complete real billing flows.",
|
||||
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.8f)
|
||||
)
|
||||
|
||||
// Debug logs
|
||||
Text("Billing logs:", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onBackground)
|
||||
Column(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.2f))
|
||||
.padding(8.dp)
|
||||
) {
|
||||
logs.forEach { log ->
|
||||
Text(log, color = MaterialTheme.colorScheme.onBackground, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user