Collapse interval bell behind underlined dropdown; 5 selectable gong sounds with preview
All checks were successful
Build & Release APK / build (push) Successful in 6m11s
All checks were successful
Build & Release APK / build (push) Successful in 6m11s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,7 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
@@ -187,6 +188,7 @@ private fun TimerTab() {
|
|||||||
var durationMin by rememberSaveable { mutableIntStateOf(20) }
|
var durationMin by rememberSaveable { mutableIntStateOf(20) }
|
||||||
val settings = remember { context.getSharedPreferences("settings", android.content.Context.MODE_PRIVATE) }
|
val settings = remember { context.getSharedPreferences("settings", android.content.Context.MODE_PRIVATE) }
|
||||||
var intervalMin by rememberSaveable { mutableIntStateOf(settings.getInt("interval_min", 0)) }
|
var intervalMin by rememberSaveable { mutableIntStateOf(settings.getInt("interval_min", 0)) }
|
||||||
|
var gongKey by rememberSaveable { mutableStateOf(settings.getString("gong_sound", "bowl") ?: "bowl") }
|
||||||
|
|
||||||
val phase = timer.phase
|
val phase = timer.phase
|
||||||
val selectedTotalMs = durationMin * 60_000L
|
val selectedTotalMs = durationMin * 60_000L
|
||||||
@@ -222,7 +224,15 @@ private fun TimerTab() {
|
|||||||
settings.edit().putInt("interval_min", it).apply()
|
settings.edit().putInt("interval_min", it).apply()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(20.dp))
|
Spacer(Modifier.height(6.dp))
|
||||||
|
GongSoundRow(
|
||||||
|
soundKey = gongKey,
|
||||||
|
onChange = {
|
||||||
|
gongKey = it
|
||||||
|
settings.edit().putString("gong_sound", it).apply()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
Button(
|
Button(
|
||||||
onClick = { TimerService.start(context, selectedTotalMs, intervalMin) },
|
onClick = { TimerService.start(context, selectedTotalMs, intervalMin) },
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = Gold, contentColor = DeepBlue),
|
colors = ButtonDefaults.buttonColors(containerColor = Gold, contentColor = DeepBlue),
|
||||||
@@ -274,27 +284,79 @@ private val INTERVAL_OPTIONS = listOf(0, 5, 10, 15, 20, 30)
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun IntervalBellRow(intervalMin: Int, onChange: (Int) -> Unit) {
|
private fun IntervalBellRow(intervalMin: Int, onChange: (Int) -> Unit) {
|
||||||
|
// Collapsed by default — a small underlined label that expands the options.
|
||||||
|
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
Text(
|
Text(
|
||||||
"INTERVAL BELL",
|
text = if (intervalMin == 0) "Interval Timer" else "Interval Timer · ${intervalMin}m",
|
||||||
color = Color.White.copy(alpha = 0.35f),
|
color = Gold.copy(alpha = if (expanded) 0.9f else 0.55f),
|
||||||
fontSize = 11.sp,
|
fontSize = 13.sp,
|
||||||
letterSpacing = 2.sp,
|
textDecoration = TextDecoration.Underline,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable { expanded = !expanded }
|
||||||
|
.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(8.dp))
|
if (expanded) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Spacer(Modifier.height(6.dp))
|
||||||
INTERVAL_OPTIONS.forEach { min ->
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
val selected = intervalMin == min
|
INTERVAL_OPTIONS.forEach { min ->
|
||||||
Text(
|
val selected = intervalMin == min
|
||||||
text = if (min == 0) "Off" else "${min}m",
|
Text(
|
||||||
color = if (selected) DeepBlue else Gold.copy(alpha = 0.7f),
|
text = if (min == 0) "Off" else "${min}m",
|
||||||
fontSize = 13.sp,
|
color = if (selected) DeepBlue else Gold.copy(alpha = 0.7f),
|
||||||
modifier = Modifier
|
fontSize = 13.sp,
|
||||||
.clip(RoundedCornerShape(14.dp))
|
modifier = Modifier
|
||||||
.background(if (selected) Gold else Color.White.copy(alpha = 0.06f))
|
.clip(RoundedCornerShape(14.dp))
|
||||||
.clickable { onChange(min) }
|
.background(if (selected) Gold else Color.White.copy(alpha = 0.06f))
|
||||||
.padding(horizontal = 12.dp, vertical = 6.dp),
|
.clickable {
|
||||||
)
|
onChange(min)
|
||||||
|
expanded = false
|
||||||
|
}
|
||||||
|
.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun GongSoundRow(soundKey: String, onChange: (String) -> Unit) {
|
||||||
|
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||||
|
val context = LocalContext.current
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Text(
|
||||||
|
text = "Gong Sound · ${GONG_SOUNDS.firstOrNull { it.first == soundKey }?.second ?: "Bowl"}",
|
||||||
|
color = Gold.copy(alpha = if (expanded) 0.9f else 0.55f),
|
||||||
|
fontSize = 13.sp,
|
||||||
|
textDecoration = TextDecoration.Underline,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable { expanded = !expanded }
|
||||||
|
.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
|
)
|
||||||
|
if (expanded) {
|
||||||
|
Spacer(Modifier.height(6.dp))
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
GONG_SOUNDS.forEach { (key, label) ->
|
||||||
|
val selected = soundKey == key
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
color = if (selected) DeepBlue else Gold.copy(alpha = 0.7f),
|
||||||
|
fontSize = 13.sp,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(14.dp))
|
||||||
|
.background(if (selected) Gold else Color.White.copy(alpha = 0.06f))
|
||||||
|
.clickable {
|
||||||
|
onChange(key)
|
||||||
|
// Preview the chosen gong immediately.
|
||||||
|
android.media.MediaPlayer.create(context, gongRes(key))?.apply {
|
||||||
|
setOnCompletionListener { it.release() }
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
app/src/main/java/com/marcus/meditationtimer/Sounds.kt
Normal file
18
app/src/main/java/com/marcus/meditationtimer/Sounds.kt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package com.marcus.meditationtimer
|
||||||
|
|
||||||
|
/** Selectable session-gong sounds (key → label). Key persisted in settings. */
|
||||||
|
val GONG_SOUNDS = listOf(
|
||||||
|
"bowl" to "Bowl",
|
||||||
|
"deep" to "Deep",
|
||||||
|
"bright" to "Bright",
|
||||||
|
"bell" to "Bell",
|
||||||
|
"twin" to "Twin",
|
||||||
|
)
|
||||||
|
|
||||||
|
fun gongRes(key: String): Int = when (key) {
|
||||||
|
"deep" -> R.raw.gong_deep
|
||||||
|
"bright" -> R.raw.gong_bright
|
||||||
|
"bell" -> R.raw.gong_bell
|
||||||
|
"twin" -> R.raw.gong_twin
|
||||||
|
else -> R.raw.gong
|
||||||
|
}
|
||||||
@@ -69,6 +69,7 @@ class TimerService : Service() {
|
|||||||
private var pauseCount = 0
|
private var pauseCount = 0
|
||||||
private var intervalMin = 0
|
private var intervalMin = 0
|
||||||
private var nextBellElapsedMs = 0L
|
private var nextBellElapsedMs = 0L
|
||||||
|
private var gongResId = R.raw.gong
|
||||||
|
|
||||||
override fun onBind(intent: Intent?) = null
|
override fun onBind(intent: Intent?) = null
|
||||||
|
|
||||||
@@ -89,6 +90,9 @@ class TimerService : Service() {
|
|||||||
pauseCount = 0
|
pauseCount = 0
|
||||||
intervalMin = intent.getIntExtra(EXTRA_INTERVAL_MIN, 0)
|
intervalMin = intent.getIntExtra(EXTRA_INTERVAL_MIN, 0)
|
||||||
nextBellElapsedMs = if (intervalMin > 0) intervalMin * 60_000L else Long.MAX_VALUE
|
nextBellElapsedMs = if (intervalMin > 0) intervalMin * 60_000L else Long.MAX_VALUE
|
||||||
|
gongResId = gongRes(
|
||||||
|
getSharedPreferences("settings", MODE_PRIVATE).getString("gong_sound", "bowl") ?: "bowl"
|
||||||
|
)
|
||||||
startForeground(NOTIF_ID, buildNotification(remainingMs, paused = false))
|
startForeground(NOTIF_ID, buildNotification(remainingMs, paused = false))
|
||||||
acquireWakeLock(totalMs)
|
acquireWakeLock(totalMs)
|
||||||
playGong()
|
playGong()
|
||||||
@@ -185,7 +189,7 @@ class TimerService : Service() {
|
|||||||
|
|
||||||
private fun playGong(onComplete: (() -> Unit)? = null) {
|
private fun playGong(onComplete: (() -> Unit)? = null) {
|
||||||
player?.release()
|
player?.release()
|
||||||
player = MediaPlayer.create(this, R.raw.gong)?.apply {
|
player = MediaPlayer.create(this, gongResId)?.apply {
|
||||||
setOnCompletionListener {
|
setOnCompletionListener {
|
||||||
it.release()
|
it.release()
|
||||||
if (player == it) player = null
|
if (player == it) player = null
|
||||||
|
|||||||
BIN
app/src/main/res/raw/gong_bell.wav
Normal file
BIN
app/src/main/res/raw/gong_bell.wav
Normal file
Binary file not shown.
BIN
app/src/main/res/raw/gong_bright.wav
Normal file
BIN
app/src/main/res/raw/gong_bright.wav
Normal file
Binary file not shown.
BIN
app/src/main/res/raw/gong_deep.wav
Normal file
BIN
app/src/main/res/raw/gong_deep.wav
Normal file
Binary file not shown.
BIN
app/src/main/res/raw/gong_twin.wav
Normal file
BIN
app/src/main/res/raw/gong_twin.wav
Normal file
Binary file not shown.
Reference in New Issue
Block a user