16 Commits

Author SHA1 Message Date
Hermes
cc2a16bf66 ci: broaden release asset upload token fallback
All checks were successful
Android build / build (push) Successful in 10m54s
Android build / build (release) Successful in 10m14s
2026-08-31 16:12:26 -05:00
Hermes
56a7103503 ci: bump APK versionCode and timestamp AAB names
Some checks failed
Android build / build (push) Successful in 15m22s
Android build / build (release) Failing after 12m34s
2026-08-31 15:06:03 -05:00
Hermes
f7722216a0 build: upgrade AGP for Android 16 support
All checks were successful
Android build / build (push) Successful in 16m36s
Android build / build (release) Successful in 13m37s
2026-08-31 13:31:01 -05:00
Hermes
8af42dbe48 build: target Android 16 / API 36
All checks were successful
Android build / build (push) Successful in 19m59s
2026-08-31 12:18:31 -05:00
Hermes
ebd7910b32 fix: resolve MindMachine release keystore path from repo root
All checks were successful
Android build / build (push) Successful in 11m29s
Android build / build (release) Successful in 11m56s
2026-08-29 14:03:23 -05:00
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
10 changed files with 307 additions and 49 deletions

View File

@@ -0,0 +1,179 @@
name: Android build
permissions:
contents: write
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-36" \
"build-tools;36.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: Prepare CI build metadata
id: build_meta
shell: bash
run: |
set -euxo pipefail
timestamp_utc="$(date -u +%Y%m%d-%H%M%S)"
version_code="$(date -u +%s)"
project_name="$(basename "$GITHUB_REPOSITORY")"
python3 - "$version_code" <<'PY'
from pathlib import Path
import re, sys
path = Path('app/build.gradle.kts')
text = path.read_text(encoding='utf-8')
new_text, count = re.subn(r'(\bversionCode\s*=\s*)\d+', rf'\g<1>{sys.argv[1]}', text, count=1)
if count != 1:
raise SystemExit('versionCode not found in app/build.gradle.kts')
path.write_text(new_text, encoding='utf-8')
PY
echo "Using CI versionCode: $version_code"
echo "project_name=$project_name" >> "$GITHUB_OUTPUT"
echo "timestamp_utc=$timestamp_utc" >> "$GITHUB_OUTPUT"
echo "version_code=$version_code" >> "$GITHUB_OUTPUT"
- 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: Rename release AAB for upload
if: github.event_name == 'release'
id: release_aab
shell: bash
run: |
set -euxo pipefail
aab="$(find app/build/outputs/bundle/release -name '*.aab' | head -n 1)"
renamed="app/build/outputs/bundle/release/${{ steps.build_meta.outputs.project_name }}-${{ steps.build_meta.outputs.timestamp_utc }}-release.aab"
mv -f "$aab" "$renamed"
echo "Renamed release AAB to $(basename "$renamed")"
echo "path=$renamed" >> "$GITHUB_OUTPUT"
- 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 }}
GITHUB_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euxo pipefail
token="${GITEA_TOKEN:-${GITHUB_TOKEN:-}}"
if [ -z "$token" ]; then
echo "::error::No release upload token is available from secrets.GITEA_TOKEN or github.token"
exit 1
fi
release_id="$(python3 -c 'import json, os; print(json.load(open(os.environ["GITHUB_EVENT_PATH"], encoding="utf-8"))["release"]["id"])')"
aab="${{ steps.release_aab.outputs.path }}"
curl --fail-with-body \
--request POST \
--header "Authorization: token ${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,43 +7,57 @@ 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"
compileSdk = 35 compileSdk = 36
defaultConfig { defaultConfig {
applicationId = "solutions.tretter.mindmachine" applicationId = "solutions.tretter.mindmachine"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 36
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 = rootProject.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 {
if (hasReleaseSigning) {
signingConfig = signingConfigs.getByName("release") 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)
if (!BuildConfig.DEBUG_AD_FREE) {
MobileAds.initialize(this) {} 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,11 +346,13 @@ fun App(vm: MainViewModel = viewModel(), billingManager: solutions.tretter.mindm
} }
} }
if (!BuildConfig.DEBUG_AD_FREE) {
AdBanner( AdBanner(
enabled = !ui.settings.noAds, enabled = !ui.settings.noAds,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
} }
}
if (confirmDeleteProgramId != null) { if (confirmDeleteProgramId != null) {
AlertDialog( AlertDialog(
@@ -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

@@ -187,18 +187,18 @@ class BillingManager(
.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()

View File

@@ -1,5 +1,5 @@
plugins { plugins {
id("com.android.application") version "8.5.2" apply false id("com.android.application") version "8.10.0" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false id("org.jetbrains.kotlin.android") version "2.1.0" apply false
id("org.jetbrains.kotlin.plugin.serialization") version "2.1.0" apply false id("org.jetbrains.kotlin.plugin.serialization") version "2.1.0" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.1.0" apply false id("org.jetbrains.kotlin.plugin.compose") version "2.1.0" apply false

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME