Compare commits

10 Commits

7 changed files with 100 additions and 32 deletions

View File

@@ -15,8 +15,8 @@ android {
applicationId = "solutions.tretter.mindmachine"
minSdk = 26
targetSdk = 35
versionCode = 14
versionName = "1.0.9"
versionCode = 18
versionName = "1.0.13"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -35,6 +35,9 @@ android {
}
}
debug {
buildConfigField("boolean", "DEBUG_AD_FREE", "true")
}
release {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = false
@@ -42,6 +45,7 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
buildConfigField("boolean", "DEBUG_AD_FREE", "false")
}
}
compileOptions {
@@ -53,6 +57,7 @@ android {
}
buildFeatures {
compose = true
buildConfig = true
}
composeOptions {
kotlinCompilerExtensionVersion = "2.1.0"

View File

@@ -13,6 +13,7 @@ import android.util.Log
import android.view.View
import android.view.WindowManager
import androidx.activity.ComponentActivity
import solutions.tretter.mindmachine.BuildConfig
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.activity.viewModels
@@ -48,6 +49,7 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.Image
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Settings
@@ -172,7 +174,9 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!BuildConfig.DEBUG_AD_FREE) {
MobileAds.initialize(this) {}
}
setContent {
MaterialTheme(colorScheme = MindMachineColors) {
App(vm, billingManager)
@@ -227,7 +231,7 @@ fun App(vm: MainViewModel = viewModel(), billingManager: solutions.tretter.mindm
composable("home") {
var confirmDeleteProgramId by remember { mutableStateOf<String?>(null) }
var confirmDeleteProgramName by remember { mutableStateOf("") }
val interstitial = rememberInterstitialController(enabled = !ui.settings.noAds)
val interstitial = rememberInterstitialController(enabled = !ui.settings.noAds && !BuildConfig.DEBUG_AD_FREE)
Column(
Modifier
@@ -342,11 +346,13 @@ fun App(vm: MainViewModel = viewModel(), billingManager: solutions.tretter.mindm
}
}
if (!BuildConfig.DEBUG_AD_FREE) {
AdBanner(
enabled = !ui.settings.noAds,
modifier = Modifier.fillMaxWidth()
)
}
}
if (confirmDeleteProgramId != null) {
AlertDialog(
@@ -375,7 +381,7 @@ fun App(vm: MainViewModel = viewModel(), billingManager: solutions.tretter.mindm
}
}
composable("setup") {
val interstitial = rememberInterstitialController(enabled = !ui.settings.noAds)
val interstitial = rememberInterstitialController(enabled = !ui.settings.noAds && !BuildConfig.DEBUG_AD_FREE)
SetupScreen(
vm = vm,
onStart = {
@@ -597,6 +603,8 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToEntry:
}
var brightnessOverlayVisible by remember { mutableStateOf(false) }
var brightnessOverlayToken by remember { mutableIntStateOf(0) }
var stopButtonVisible by remember { mutableStateOf(false) }
var stopButtonToken by remember { mutableIntStateOf(0) }
val timingLogTag = "MindMachineTiming"
LaunchedEffect(brightnessOverlayToken) {
@@ -642,12 +650,24 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToEntry:
detectTapGestures {
brightnessOverlayVisible = true
brightnessOverlayToken++
if (ui.settings.showImmersiveStopButton && immersiveFullscreen) {
stopButtonVisible = true
stopButtonToken++
}
if (!immersiveFullscreen) {
showOverlay = !showOverlay
}
}
}
) {
// Auto-hide stop button after 3 seconds of visibility
LaunchedEffect(stopButtonToken) {
if (stopButtonVisible) {
delay(3000)
stopButtonVisible = false
}
}
if (isVisualMode) {
AndroidView(
modifier = Modifier.fillMaxSize(),
@@ -715,6 +735,26 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToEntry:
}
}
// Stop button in immersive mode (top-right)
if (stopButtonVisible && ui.settings.showImmersiveStopButton) {
IconButton(
onClick = {
vm.stop()
onFinish()
},
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 16.dp, end = 16.dp)
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = "Stop",
tint = Color.White,
modifier = Modifier.size(28.dp)
)
}
}
if (showOverlay) {
Column(
Modifier
@@ -1233,6 +1273,18 @@ fun SettingsScreen(vm: MainViewModel, onRemoveAds: () -> Unit) {
Text("Show immersive bottom progress bar", color = MaterialTheme.colorScheme.onBackground)
}
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = ui.settings.showImmersiveStopButton, onCheckedChange = vm::updateShowImmersiveStopButton)
Column {
Text("Show stop button in immersive mode", color = MaterialTheme.colorScheme.onBackground)
Text(
"If off, swipe from edge to display back button",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f)
)
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = ui.settings.preferLowestRefreshRate, onCheckedChange = vm::updatePreferLowestRefreshRate)
Text("Prefer lowest display refresh rate during sessions", color = MaterialTheme.colorScheme.onBackground)

View File

@@ -187,18 +187,18 @@ class BillingManager(
.setProductList(products)
.build()
billingClient.queryProductDetailsAsync(params) { result, queryResult ->
log("queryProductDetailsAsync($productType) result: ${result.responseCode} ${result.debugMessage}")
billingClient.queryProductDetailsAsync(params) { billingResult, queryResult ->
log("queryProductDetailsAsync($productType) result: ${billingResult.responseCode} ${billingResult.debugMessage}")
val detailsList = queryResult.productDetailsList
log("Fetched $productType products: ${detailsList.map { it.productId }}")
log("Fetched $productType products: ${detailsList?.map { it.productId } ?: emptyList()}")
if (queryResult.unfetchedProductList.isNotEmpty()) {
logError("Unfetched $productType products: ${queryResult.unfetchedProductList}")
}
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
_productDetails.value = _productDetails.value + detailsList.associateBy { it.productId }
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
_productDetails.value = _productDetails.value + (detailsList?.associateBy { it.productId } ?: emptyMap())
log("Product details cache now has: ${_productDetails.value.keys}")
}
}

View File

@@ -20,6 +20,7 @@ class SettingsRepository(private val context: Context) {
val countdownSeconds = intPreferencesKey("countdown_seconds")
val forceAudioWithoutHeadphones = booleanPreferencesKey("force_audio_without_headphones")
val showImmersiveProgressBar = booleanPreferencesKey("show_immersive_progress_bar")
val showImmersiveStopButton = booleanPreferencesKey("show_immersive_stop_button")
val preferLowestRefreshRate = booleanPreferencesKey("prefer_lowest_refresh_rate")
val monochromeFlashMode = booleanPreferencesKey("monochrome_flash_mode")
val immersiveBrightnessPercent = intPreferencesKey("immersive_brightness_percent")
@@ -36,6 +37,7 @@ class SettingsRepository(private val context: Context) {
?: 5,
forceAudioWithoutHeadphones = p[Keys.forceAudioWithoutHeadphones] ?: false,
showImmersiveProgressBar = p[Keys.showImmersiveProgressBar] ?: true,
showImmersiveStopButton = p[Keys.showImmersiveStopButton] ?: true,
preferLowestRefreshRate = p[Keys.preferLowestRefreshRate] ?: true,
monochromeFlashMode = p[Keys.monochromeFlashMode] ?: false,
immersiveBrightnessPercent = (p[Keys.immersiveBrightnessPercent] ?: 50).coerceIn(5, 100),
@@ -58,6 +60,9 @@ class SettingsRepository(private val context: Context) {
if (prefs[Keys.preferLowestRefreshRate] == null) {
prefs[Keys.preferLowestRefreshRate] = true
}
if (prefs[Keys.showImmersiveStopButton] == null) {
prefs[Keys.showImmersiveStopButton] = true
}
}
}
@@ -80,6 +85,10 @@ class SettingsRepository(private val context: Context) {
it[Keys.showImmersiveProgressBar] = value
}
suspend fun updateShowImmersiveStopButton(value: Boolean) = context.dataStore.edit {
it[Keys.showImmersiveStopButton] = value
}
suspend fun updatePreferLowestRefreshRate(value: Boolean) = context.dataStore.edit {
it[Keys.preferLowestRefreshRate] = value
}

View File

@@ -42,6 +42,7 @@ data class AppSettings(
val countdownSeconds: Int = 5,
val forceAudioWithoutHeadphones: Boolean = false,
val showImmersiveProgressBar: Boolean = true,
val showImmersiveStopButton: Boolean = true,
val preferLowestRefreshRate: Boolean = true,
val monochromeFlashMode: Boolean = false,
// Default used only before DataStore emits (and as a fallback).

View File

@@ -304,6 +304,7 @@ class MainViewModel(
settingsRepository.updateForceAudioWithoutHeadphones(value)
}
fun updateShowImmersiveProgressBar(value: Boolean) = viewModelScope.launch { settingsRepository.updateShowImmersiveProgressBar(value) }
fun updateShowImmersiveStopButton(value: Boolean) = viewModelScope.launch { settingsRepository.updateShowImmersiveStopButton(value) }
fun updatePreferLowestRefreshRate(value: Boolean) = viewModelScope.launch { settingsRepository.updatePreferLowestRefreshRate(value) }
fun updateMonochromeFlashMode(value: Boolean) = viewModelScope.launch { settingsRepository.updateMonochromeFlashMode(value) }
fun updateImmersiveBrightnessPercent(value: Int) = viewModelScope.launch {

View File

@@ -425,7 +425,7 @@ internal fun TimelineGraph(
height = h,
curveId = curveId,
)
val updated = targetCurve.movePointVerticalWithPropagation(pointIndex, constrainedY, h)
val updated = targetCurve.movePointVertical(pointIndex, constrainedY, h)
if (curveId == leftId) curveLeft = updated else curveRight = updated
currentOnUpdateCurve(curveId, updated)
onCurveEditedHaptic()