11 Commits

Author SHA1 Message Date
Hermes
c2a78e1a73 ci: align MindMachine Android workflow with e-Sun setup
Some checks failed
Android build / build (push) Successful in 15m28s
Android build / build (release) Failing after 8m10s
2026-08-29 13:30:22 -05:00
Tretzi
9a6f28d39f Auto commit (MindMachine) Sun Apr 19 10:01:02 AM CDT 2026 2026-04-19 10:01:02 -05:00
Tretzi
0357da5158 Bump versionCode to 18, versionName to 1.0.13 for AAB release 2026-04-19 10:00:10 -05:00
Tretzi
c468840359 Auto commit (MindMachine) Thu Apr 16 09:01:01 PM CDT 2026 2026-04-16 21:01:01 -05:00
Tretzi
260d0a1e84 Auto commit (MindMachine) Thu Apr 16 08:01:01 PM CDT 2026 2026-04-16 20:01:01 -05:00
Tretzi
7d7057f640 Auto commit (MindMachine) Thu Apr 16 07:01:02 PM CDT 2026 2026-04-16 19:01:02 -05:00
Tretzi
a1a3f2ab8c Bump versionCode to 17, versionName to 1.0.12 for ad-free debug builds 2026-04-16 18:58:58 -05:00
Tretzi
d3f21094ff Add DEBUG_AD_FREE build config flag to disable ads in debug builds 2026-04-16 18:55:50 -05:00
Tretzi
3876d68183 Bump versionCode to 16, versionName to 1.0.11 2026-04-16 18:48:39 -05:00
Tretzi
6ccf19707b Add showImmersiveStopButton setting and top-right X button overlay 2026-04-16 18:48:30 -05:00
Tretzi
95cb3de029 Bump versionCode to 15, versionName to 1.1.0 for AAB release 2026-04-16 18:12:00 -05:00
8 changed files with 262 additions and 45 deletions

View File

@@ -0,0 +1,138 @@
name: Android build
permissions:
contents: read
releases: write
on:
push:
branches:
- '**'
pull_request:
release:
types:
- published
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Java 17
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '17'
- name: Install Android SDK command-line tools and packages
shell: bash
run: |
set -euxo pipefail
export ANDROID_SDK_ROOT="$HOME/.android/sdk"
export ANDROID_HOME="$ANDROID_SDK_ROOT"
mkdir -p "$ANDROID_SDK_ROOT/cmdline-tools"
if [ ! -x "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" ]; then
tools_url="$(curl -fsSL https://developer.android.com/studio | grep -o 'https://dl.google.com/android/repository/commandlinetools-linux-[0-9][0-9]*_latest.zip' | head -n 1)"
test -n "$tools_url"
curl -fsSL -o /tmp/commandlinetools.zip "$tools_url"
rm -rf "$ANDROID_SDK_ROOT/cmdline-tools/latest" "$ANDROID_SDK_ROOT/cmdline-tools/cmdline-tools"
unzip -q /tmp/commandlinetools.zip -d "$ANDROID_SDK_ROOT/cmdline-tools"
mv "$ANDROID_SDK_ROOT/cmdline-tools/cmdline-tools" "$ANDROID_SDK_ROOT/cmdline-tools/latest"
fi
export PATH="$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$ANDROID_SDK_ROOT/platform-tools:$PATH"
set +o pipefail
yes | sdkmanager --licenses >/dev/null
set -o pipefail
sdkmanager \
"platform-tools" \
"platforms;android-35" \
"build-tools;35.0.0"
- name: Prepare optional release signing
if: github.event_name == 'release'
continue-on-error: true
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
shell: bash
run: |
set -euo pipefail
missing=()
for key in ANDROID_KEYSTORE_BASE64 ANDROID_KEYSTORE_PASSWORD ANDROID_KEY_ALIAS ANDROID_KEY_PASSWORD; do
if [ -z "${!key:-}" ]; then
missing+=("$key")
fi
done
if [ ${#missing[@]} -gt 0 ]; then
printf -v missing_csv '%s, ' "${missing[@]}"
missing_csv="${missing_csv%, }"
echo "::error::Release signing secrets missing: ${missing_csv}. Continuing with unsigned/default-signed release AAB."
exit 1
fi
mkdir -p .ci-secrets
printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 -d > .ci-secrets/release-keystore.jks
cat > keystore.properties <<EOF
storeFile=.ci-secrets/release-keystore.jks
storePassword=$ANDROID_KEYSTORE_PASSWORD
keyAlias=$ANDROID_KEY_ALIAS
keyPassword=$ANDROID_KEY_PASSWORD
EOF
- name: Make Gradle wrapper executable
run: chmod +x gradlew
- name: Write Android SDK location
shell: bash
run: |
set -euxo pipefail
printf 'sdk.dir=%s/.android/sdk\n' "$HOME" > local.properties
- name: Build debug APK
if: github.event_name != 'release'
shell: bash
run: |
set -euxo pipefail
export ANDROID_SDK_ROOT="$HOME/.android/sdk"
export ANDROID_HOME="$ANDROID_SDK_ROOT"
./gradlew --no-daemon clean assembleDebug
- name: Build release AAB
if: github.event_name == 'release'
shell: bash
run: |
set -euxo pipefail
export ANDROID_SDK_ROOT="$HOME/.android/sdk"
export ANDROID_HOME="$ANDROID_SDK_ROOT"
./gradlew --no-daemon bundleRelease
- name: Upload debug APK
if: github.event_name != 'release'
uses: actions/upload-artifact@v3
with:
name: debug-apk
path: app/build/outputs/apk/debug/*.apk
if-no-files-found: error
- name: Attach release AAB to Gitea release
if: github.event_name == 'release'
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
shell: bash
run: |
set -euxo pipefail
release_id="$(python3 -c 'import json, os; print(json.load(open(os.environ["GITHUB_EVENT_PATH"], encoding="utf-8"))["release"]["id"])')"
aab="$(find app/build/outputs/bundle/release -name '*.aab' | head -n 1)"
curl --fail-with-body \
--request POST \
--header "Authorization: token ${GITEA_TOKEN}" \
--form "attachment=@${aab}" \
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/${release_id}/assets?name=$(basename "$aab")"

View File

@@ -1,3 +1,5 @@
import java.util.Properties
plugins { plugins {
id("com.android.application") id("com.android.application")
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
@@ -5,7 +7,17 @@ plugins {
id("org.jetbrains.kotlin.plugin.compose") id("org.jetbrains.kotlin.plugin.compose")
} }
import java.util.Properties val keystorePropertiesFile = rootProject.file("keystore.properties")
val keystoreProperties = Properties()
if (keystorePropertiesFile.exists()) {
keystorePropertiesFile.inputStream().use { keystoreProperties.load(it) }
}
val hasReleaseSigning = listOf(
"storeFile",
"storePassword",
"keyAlias",
"keyPassword",
).all { !keystoreProperties.getProperty(it).isNullOrBlank() }
android { android {
namespace = "solutions.tretter.mindmachine" namespace = "solutions.tretter.mindmachine"
@@ -15,33 +27,37 @@ android {
applicationId = "solutions.tretter.mindmachine" applicationId = "solutions.tretter.mindmachine"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 14 versionCode = 18
versionName = "1.0.9" versionName = "1.0.13"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
buildTypes { signingConfigs {
signingConfigs { if (hasReleaseSigning) {
create("release") { create("release") {
val propsFile = rootProject.file("keystore/upload-keystore.properties") storeFile = file(keystoreProperties.getProperty("storeFile"))
if (propsFile.exists()) { storePassword = keystoreProperties.getProperty("storePassword")
val props = Properties().apply { propsFile.inputStream().use { this.load(it) } } keyAlias = keystoreProperties.getProperty("keyAlias")
storeFile = rootProject.file(props.getProperty("storeFile")) keyPassword = keystoreProperties.getProperty("keyPassword")
storePassword = props.getProperty("storePassword")
keyAlias = props.getProperty("keyAlias")
keyPassword = props.getProperty("keyPassword")
}
} }
} }
}
buildTypes {
debug {
buildConfigField("boolean", "DEBUG_AD_FREE", "true")
}
release { release {
signingConfig = signingConfigs.getByName("release") if (hasReleaseSigning) {
signingConfig = signingConfigs.getByName("release")
}
isMinifyEnabled = false isMinifyEnabled = false
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro"
) )
buildConfigField("boolean", "DEBUG_AD_FREE", "false")
} }
} }
compileOptions { compileOptions {
@@ -53,6 +69,7 @@ android {
} }
buildFeatures { buildFeatures {
compose = true compose = true
buildConfig = true
} }
composeOptions { composeOptions {
kotlinCompilerExtensionVersion = "2.1.0" kotlinCompilerExtensionVersion = "2.1.0"
@@ -83,7 +100,6 @@ dependencies {
implementation("androidx.compose.material:material-icons-extended") implementation("androidx.compose.material:material-icons-extended")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
// Monetization
implementation("com.google.android.gms:play-services-ads:23.1.0") implementation("com.google.android.gms:play-services-ads:23.1.0")
implementation("com.android.billingclient:billing-ktx:8.0.0") implementation("com.android.billingclient:billing-ktx:8.0.0")

View File

@@ -13,6 +13,7 @@ import android.util.Log
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import solutions.tretter.mindmachine.BuildConfig
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.viewModels import androidx.activity.viewModels
@@ -48,6 +49,7 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add 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.Delete
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
@@ -172,7 +174,9 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
MobileAds.initialize(this) {} if (!BuildConfig.DEBUG_AD_FREE) {
MobileAds.initialize(this) {}
}
setContent { setContent {
MaterialTheme(colorScheme = MindMachineColors) { MaterialTheme(colorScheme = MindMachineColors) {
App(vm, billingManager) App(vm, billingManager)
@@ -227,7 +231,7 @@ fun App(vm: MainViewModel = viewModel(), billingManager: solutions.tretter.mindm
composable("home") { composable("home") {
var confirmDeleteProgramId by remember { mutableStateOf<String?>(null) } var confirmDeleteProgramId by remember { mutableStateOf<String?>(null) }
var confirmDeleteProgramName by remember { mutableStateOf("") } var confirmDeleteProgramName by remember { mutableStateOf("") }
val interstitial = rememberInterstitialController(enabled = !ui.settings.noAds) val interstitial = rememberInterstitialController(enabled = !ui.settings.noAds && !BuildConfig.DEBUG_AD_FREE)
Column( Column(
Modifier Modifier
@@ -342,10 +346,12 @@ fun App(vm: MainViewModel = viewModel(), billingManager: solutions.tretter.mindm
} }
} }
AdBanner( if (!BuildConfig.DEBUG_AD_FREE) {
enabled = !ui.settings.noAds, AdBanner(
modifier = Modifier.fillMaxWidth() enabled = !ui.settings.noAds,
) modifier = Modifier.fillMaxWidth()
)
}
} }
if (confirmDeleteProgramId != null) { if (confirmDeleteProgramId != null) {
@@ -375,7 +381,7 @@ fun App(vm: MainViewModel = viewModel(), billingManager: solutions.tretter.mindm
} }
} }
composable("setup") { composable("setup") {
val interstitial = rememberInterstitialController(enabled = !ui.settings.noAds) val interstitial = rememberInterstitialController(enabled = !ui.settings.noAds && !BuildConfig.DEBUG_AD_FREE)
SetupScreen( SetupScreen(
vm = vm, vm = vm,
onStart = { onStart = {
@@ -597,6 +603,8 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToEntry:
} }
var brightnessOverlayVisible by remember { mutableStateOf(false) } var brightnessOverlayVisible by remember { mutableStateOf(false) }
var brightnessOverlayToken by remember { mutableIntStateOf(0) } var brightnessOverlayToken by remember { mutableIntStateOf(0) }
var stopButtonVisible by remember { mutableStateOf(false) }
var stopButtonToken by remember { mutableIntStateOf(0) }
val timingLogTag = "MindMachineTiming" val timingLogTag = "MindMachineTiming"
LaunchedEffect(brightnessOverlayToken) { LaunchedEffect(brightnessOverlayToken) {
@@ -642,12 +650,24 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToEntry:
detectTapGestures { detectTapGestures {
brightnessOverlayVisible = true brightnessOverlayVisible = true
brightnessOverlayToken++ brightnessOverlayToken++
if (ui.settings.showImmersiveStopButton && immersiveFullscreen) {
stopButtonVisible = true
stopButtonToken++
}
if (!immersiveFullscreen) { if (!immersiveFullscreen) {
showOverlay = !showOverlay showOverlay = !showOverlay
} }
} }
} }
) { ) {
// Auto-hide stop button after 3 seconds of visibility
LaunchedEffect(stopButtonToken) {
if (stopButtonVisible) {
delay(3000)
stopButtonVisible = false
}
}
if (isVisualMode) { if (isVisualMode) {
AndroidView( AndroidView(
modifier = Modifier.fillMaxSize(), 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) { if (showOverlay) {
Column( Column(
Modifier Modifier
@@ -1233,6 +1273,18 @@ fun SettingsScreen(vm: MainViewModel, onRemoveAds: () -> Unit) {
Text("Show immersive bottom progress bar", color = MaterialTheme.colorScheme.onBackground) 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) { Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = ui.settings.preferLowestRefreshRate, onCheckedChange = vm::updatePreferLowestRefreshRate) Checkbox(checked = ui.settings.preferLowestRefreshRate, onCheckedChange = vm::updatePreferLowestRefreshRate)
Text("Prefer lowest display refresh rate during sessions", color = MaterialTheme.colorScheme.onBackground) Text("Prefer lowest display refresh rate during sessions", color = MaterialTheme.colorScheme.onBackground)

View File

@@ -54,36 +54,36 @@ class BillingManager(
@Volatile @Volatile
private var isConnecting = false private var isConnecting = false
fun start() { fun start() {
log("BillingManager.start() - isReady=${billingClient.isReady}, isConnecting=$isConnecting") log("BillingManager.start() - isReady=${billingClient.isReady}, isConnecting=$isConnecting")
if (billingClient.isReady) { if (billingClient.isReady) {
log("Billing client already ready") log("Billing client already ready")
refresh() refresh()
return return
} }
if (isConnecting) { if (isConnecting) {
log("Billing client connection already in progress") log("Billing client connection already in progress")
return return
} }
isConnecting = true isConnecting = true
log("Calling startConnection()") log("Calling startConnection()")
billingClient.startConnection(object : BillingClientStateListener { billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(result: BillingResult) { override fun onBillingSetupFinished(result: BillingResult) {
isConnecting = false isConnecting = false
log("Billing setup finished: ${result.responseCode} ${result.debugMessage}") log("Billing setup finished: ${result.responseCode} ${result.debugMessage}")
if (result.responseCode == BillingClient.BillingResponseCode.OK) { if (result.responseCode == BillingClient.BillingResponseCode.OK) {
refresh() refresh()
} else { } else {
logError("Billing setup failed: ${result.responseCode} ${result.debugMessage}") logError("Billing setup failed: ${result.responseCode} ${result.debugMessage}")
} }
} }
override fun onBillingServiceDisconnected() { override fun onBillingServiceDisconnected() {
isConnecting = false isConnecting = false
log("Billing service disconnected") log("Billing service disconnected")
@@ -150,55 +150,55 @@ class BillingManager(
log("Billing client not ready") log("Billing client not ready")
return return
} }
val featureResult = billingClient.isFeatureSupported(BillingClient.FeatureType.PRODUCT_DETAILS) val featureResult = billingClient.isFeatureSupported(BillingClient.FeatureType.PRODUCT_DETAILS)
log("PRODUCT_DETAILS support: ${featureResult.responseCode} ${featureResult.debugMessage}") log("PRODUCT_DETAILS support: ${featureResult.responseCode} ${featureResult.debugMessage}")
if (featureResult.responseCode != BillingClient.BillingResponseCode.OK) { if (featureResult.responseCode != BillingClient.BillingResponseCode.OK) {
logError("PRODUCT_DETAILS feature not supported") logError("PRODUCT_DETAILS feature not supported")
return return
} }
queryProductDetailsForType( queryProductDetailsForType(
productType = BillingClient.ProductType.SUBS, productType = BillingClient.ProductType.SUBS,
productIds = listOf(BillingProducts.SUB_REMOVE_ADS_MONTHLY) productIds = listOf(BillingProducts.SUB_REMOVE_ADS_MONTHLY)
) )
queryProductDetailsForType( queryProductDetailsForType(
productType = BillingClient.ProductType.INAPP, productType = BillingClient.ProductType.INAPP,
productIds = listOf(BillingProducts.INAPP_REMOVE_ADS_FOREVER) productIds = listOf(BillingProducts.INAPP_REMOVE_ADS_FOREVER)
) )
} }
private fun queryProductDetailsForType( private fun queryProductDetailsForType(
productType: String, productType: String,
productIds: List<String> productIds: List<String>
) { ) {
try { try {
log("Querying $productType product details for $productIds") log("Querying $productType product details for $productIds")
val products = productIds.map { productId -> val products = productIds.map { productId ->
QueryProductDetailsParams.Product.newBuilder() QueryProductDetailsParams.Product.newBuilder()
.setProductId(productId) .setProductId(productId)
.setProductType(productType) .setProductType(productType)
.build() .build()
} }
val params = QueryProductDetailsParams.newBuilder() val params = QueryProductDetailsParams.newBuilder()
.setProductList(products) .setProductList(products)
.build() .build()
billingClient.queryProductDetailsAsync(params) { result, queryResult -> billingClient.queryProductDetailsAsync(params) { billingResult, queryResult ->
log("queryProductDetailsAsync($productType) result: ${result.responseCode} ${result.debugMessage}") log("queryProductDetailsAsync($productType) result: ${billingResult.responseCode} ${billingResult.debugMessage}")
val detailsList = queryResult.productDetailsList 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()) { if (queryResult.unfetchedProductList.isNotEmpty()) {
logError("Unfetched $productType products: ${queryResult.unfetchedProductList}") logError("Unfetched $productType products: ${queryResult.unfetchedProductList}")
} }
if (result.responseCode == BillingClient.BillingResponseCode.OK) { if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
_productDetails.value = _productDetails.value + detailsList.associateBy { it.productId } _productDetails.value = _productDetails.value + (detailsList?.associateBy { it.productId } ?: emptyMap())
log("Product details cache now has: ${_productDetails.value.keys}") 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 countdownSeconds = intPreferencesKey("countdown_seconds")
val forceAudioWithoutHeadphones = booleanPreferencesKey("force_audio_without_headphones") val forceAudioWithoutHeadphones = booleanPreferencesKey("force_audio_without_headphones")
val showImmersiveProgressBar = booleanPreferencesKey("show_immersive_progress_bar") val showImmersiveProgressBar = booleanPreferencesKey("show_immersive_progress_bar")
val showImmersiveStopButton = booleanPreferencesKey("show_immersive_stop_button")
val preferLowestRefreshRate = booleanPreferencesKey("prefer_lowest_refresh_rate") val preferLowestRefreshRate = booleanPreferencesKey("prefer_lowest_refresh_rate")
val monochromeFlashMode = booleanPreferencesKey("monochrome_flash_mode") val monochromeFlashMode = booleanPreferencesKey("monochrome_flash_mode")
val immersiveBrightnessPercent = intPreferencesKey("immersive_brightness_percent") val immersiveBrightnessPercent = intPreferencesKey("immersive_brightness_percent")
@@ -36,6 +37,7 @@ class SettingsRepository(private val context: Context) {
?: 5, ?: 5,
forceAudioWithoutHeadphones = p[Keys.forceAudioWithoutHeadphones] ?: false, forceAudioWithoutHeadphones = p[Keys.forceAudioWithoutHeadphones] ?: false,
showImmersiveProgressBar = p[Keys.showImmersiveProgressBar] ?: true, showImmersiveProgressBar = p[Keys.showImmersiveProgressBar] ?: true,
showImmersiveStopButton = p[Keys.showImmersiveStopButton] ?: true,
preferLowestRefreshRate = p[Keys.preferLowestRefreshRate] ?: true, preferLowestRefreshRate = p[Keys.preferLowestRefreshRate] ?: true,
monochromeFlashMode = p[Keys.monochromeFlashMode] ?: false, monochromeFlashMode = p[Keys.monochromeFlashMode] ?: false,
immersiveBrightnessPercent = (p[Keys.immersiveBrightnessPercent] ?: 50).coerceIn(5, 100), immersiveBrightnessPercent = (p[Keys.immersiveBrightnessPercent] ?: 50).coerceIn(5, 100),
@@ -58,6 +60,9 @@ class SettingsRepository(private val context: Context) {
if (prefs[Keys.preferLowestRefreshRate] == null) { if (prefs[Keys.preferLowestRefreshRate] == null) {
prefs[Keys.preferLowestRefreshRate] = true 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 it[Keys.showImmersiveProgressBar] = value
} }
suspend fun updateShowImmersiveStopButton(value: Boolean) = context.dataStore.edit {
it[Keys.showImmersiveStopButton] = value
}
suspend fun updatePreferLowestRefreshRate(value: Boolean) = context.dataStore.edit { suspend fun updatePreferLowestRefreshRate(value: Boolean) = context.dataStore.edit {
it[Keys.preferLowestRefreshRate] = value it[Keys.preferLowestRefreshRate] = value
} }

View File

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

View File

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

View File

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