feat: Add options in the settings menu to disable alarm and vibration

#36
This commit is contained in:
Nishant Mishra
2025-09-15 11:37:27 +05:30
parent 66dd419de4
commit 6fd8ee43be
11 changed files with 275 additions and 25 deletions

View File

@@ -13,8 +13,8 @@ import kotlinx.coroutines.withContext
/**
* Interface for reading/writing app preferences to the app's database. This style of storage aims
* to mimic the Preferences DataStore library, preventing the requirement of migration if the
* database schema changes.
* to mimic the Preferences DataStore library, preventing the requirement of migration if new
* preferences are added
*/
interface PreferenceRepository {
/**

View File

@@ -17,6 +17,8 @@ interface TimerRepository {
var longBreakTime: Long
var sessionLength: Int
var timerFrequency: Float
var alarmEnabled: Boolean
var vibrateEnabled: Boolean
}
/**
@@ -28,4 +30,6 @@ class AppTimerRepository : TimerRepository {
override var longBreakTime = 15 * 60 * 1000L
override var sessionLength = 4
override var timerFrequency: Float = 10f
override var alarmEnabled = true
override var vibrateEnabled = true
}

View File

@@ -344,23 +344,28 @@ class TimerService : Service() {
}
fun startAlarm() {
alarm.start()
if (timerRepository.alarmEnabled) alarm.start()
if (!vibrator.hasVibrator()) {
return
if (timerRepository.vibrateEnabled) {
if (!vibrator.hasVibrator()) {
return
}
val vibrationPattern = longArrayOf(0, 1000, 1000, 1000)
val repeat = 2
val effect = VibrationEffect.createWaveform(vibrationPattern, repeat)
vibrator.vibrate(effect)
}
val vibrationPattern = longArrayOf(0, 1000, 1000, 1000)
val repeat = 2
val effect = VibrationEffect.createWaveform(vibrationPattern, repeat)
vibrator.vibrate(effect)
}
fun stopAlarm() {
alarm.pause()
alarm.seekTo(0)
vibrator.cancel()
if (timerRepository.alarmEnabled) {
alarm.pause()
alarm.seekTo(0)
}
if (timerRepository.vibrateEnabled) {
vibrator.cancel()
}
_timerState.update { currentState ->
currentState.copy(alarmRinging = false)

View File

@@ -7,6 +7,7 @@
package org.nsh07.pomodoro.ui.settingsScreen
import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
@@ -18,8 +19,10 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.input.TextFieldState
@@ -32,10 +35,11 @@ import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.ListItem
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.shapes
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderState
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
@@ -56,12 +60,17 @@ import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.cardShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoTheme
@OptIn(ExperimentalMaterial3Api::class)
@@ -80,6 +89,9 @@ fun SettingsScreenRoot(
viewModel.longBreakTimeTextFieldState
}
val alarmEnabled = viewModel.alarmEnabled.collectAsStateWithLifecycle()
val vibrateEnabled = viewModel.vibrateEnabled.collectAsStateWithLifecycle()
val sessionsSliderState = rememberSaveable(
saver = SliderState.Saver(
viewModel.sessionsSliderState.onValueChangeFinished,
@@ -94,6 +106,10 @@ fun SettingsScreenRoot(
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
sessionsSliderState = sessionsSliderState,
alarmEnabled = alarmEnabled.value,
vibrateEnabled = vibrateEnabled.value,
onAlarmEnabledChange = viewModel::saveAlarmEnabled,
onVibrateEnabledChange = viewModel::saveVibrateEnabled,
modifier = modifier
)
}
@@ -105,9 +121,35 @@ private fun SettingsScreen(
shortBreakTimeInputFieldState: TextFieldState,
longBreakTimeInputFieldState: TextFieldState,
sessionsSliderState: SliderState,
alarmEnabled: Boolean,
vibrateEnabled: Boolean,
onAlarmEnabledChange: (Boolean) -> Unit,
onVibrateEnabledChange: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
val switchColors = SwitchDefaults.colors(
checkedIconColor = colorScheme.primary,
)
val switchItems = remember(alarmEnabled, vibrateEnabled) {
listOf(
SettingsSwitchItem(
checked = alarmEnabled,
icon = R.drawable.alarm_on,
label = "Alarm",
description = "Ring your system alarm sound when a timer completes",
onClick = onAlarmEnabledChange
),
SettingsSwitchItem(
checked = vibrateEnabled,
icon = R.drawable.mobile_vibrate,
label = "Vibrate",
description = "Vibrate in a repeating pattern when a timer completes",
onClick = onVibrateEnabledChange
)
)
}
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
TopAppBar(
@@ -155,10 +197,10 @@ private fun SettingsScreen(
MinuteInputField(
state = focusTimeInputFieldState,
shape = RoundedCornerShape(
topStart = 16.dp,
bottomStart = 16.dp,
topEnd = 4.dp,
bottomEnd = 4.dp
topStart = topListItemShape.topStart,
bottomStart = topListItemShape.topStart,
topEnd = topListItemShape.bottomStart,
bottomEnd = topListItemShape.bottomStart
),
imeAction = ImeAction.Next
)
@@ -174,7 +216,7 @@ private fun SettingsScreen(
)
MinuteInputField(
state = shortBreakTimeInputFieldState,
shape = RoundedCornerShape(4.dp),
shape = RoundedCornerShape(middleListItemShape.topStart),
imeAction = ImeAction.Next
)
}
@@ -190,10 +232,10 @@ private fun SettingsScreen(
MinuteInputField(
state = longBreakTimeInputFieldState,
shape = RoundedCornerShape(
topStart = 4.dp,
bottomStart = 4.dp,
topEnd = 16.dp,
bottomEnd = 16.dp
topStart = bottomListItemShape.topStart,
bottomStart = bottomListItemShape.topStart,
topEnd = bottomListItemShape.bottomStart,
bottomEnd = bottomListItemShape.bottomStart
),
imeAction = ImeAction.Done
)
@@ -224,7 +266,48 @@ private fun SettingsScreen(
}
},
colors = listItemColors,
modifier = Modifier.clip(shapes.large)
modifier = Modifier.clip(cardShape)
)
}
item { Spacer(Modifier.height(12.dp)) }
itemsIndexed(switchItems) { index, item ->
ListItem(
leadingContent = {
Icon(painterResource(item.icon), contentDescription = null)
},
headlineContent = { Text(item.label) },
supportingContent = { Text(item.description) },
trailingContent = {
Switch(
checked = item.checked,
onCheckedChange = { item.onClick(it) },
thumbContent = {
if (item.checked) {
Icon(
painter = painterResource(R.drawable.check),
contentDescription = null,
modifier = Modifier.size(SwitchDefaults.IconSize),
)
} else {
Icon(
painter = painterResource(R.drawable.clear),
contentDescription = null,
modifier = Modifier.size(SwitchDefaults.IconSize),
)
}
},
colors = switchColors
)
},
colors = listItemColors,
modifier = Modifier
.clip(
when (index) {
0 -> topListItemShape
switchItems.lastIndex -> bottomListItemShape
else -> middleListItemShape
}
)
)
}
item {
@@ -275,7 +358,19 @@ fun SettingsScreenPreview() {
shortBreakTimeInputFieldState = rememberTextFieldState((5 * 60 * 1000).toString()),
longBreakTimeInputFieldState = rememberTextFieldState((15 * 60 * 1000).toString()),
sessionsSliderState = rememberSliderState(value = 3f, steps = 3, valueRange = 1f..5f),
alarmEnabled = true,
vibrateEnabled = true,
onAlarmEnabledChange = {},
onVibrateEnabledChange = {},
modifier = Modifier.fillMaxSize()
)
}
}
data class SettingsSwitchItem(
val checked: Boolean,
@DrawableRes val icon: Int,
val label: String,
val description: String,
val onClick: (Boolean) -> Unit
)

View File

@@ -19,6 +19,9 @@ import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.launch
import org.nsh07.pomodoro.TomatoApplication
@@ -44,6 +47,14 @@ class SettingsViewModel(
onValueChangeFinished = ::updateSessionLength
)
private val _alarmEnabled: MutableStateFlow<Boolean> =
MutableStateFlow(timerRepository.alarmEnabled)
val alarmEnabled: StateFlow<Boolean> = _alarmEnabled.asStateFlow()
private val _vibrateEnabled: MutableStateFlow<Boolean> =
MutableStateFlow(timerRepository.alarmEnabled)
val vibrateEnabled: StateFlow<Boolean> = _vibrateEnabled.asStateFlow()
init {
viewModelScope.launch(Dispatchers.IO) {
snapshotFlow { focusTimeTextFieldState.text }
@@ -92,6 +103,22 @@ class SettingsViewModel(
}
}
fun saveAlarmEnabled(enabled: Boolean) {
viewModelScope.launch {
timerRepository.alarmEnabled = preferenceRepository
.saveIntPreference("alarm_enabled", if (enabled) 1 else 0) == 1
_alarmEnabled.value = enabled
}
}
fun saveVibrateEnabled(enabled: Boolean) {
viewModelScope.launch {
timerRepository.vibrateEnabled = preferenceRepository
.saveIntPreference("vibrate_enabled", if (enabled) 1 else 0) == 1
_vibrateEnabled.value = enabled
}
}
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {

View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2025 Nishant Mishra
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.ui.theme
import androidx.compose.foundation.shape.CornerBasedShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme.shapes
import androidx.compose.runtime.Composable
object TomatoShapeDefaults {
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
val topListItemShape: RoundedCornerShape
@Composable get() =
RoundedCornerShape(
topStart = shapes.largeIncreased.topStart,
topEnd = shapes.largeIncreased.topEnd,
bottomStart = shapes.extraSmall.bottomStart,
bottomEnd = shapes.extraSmall.bottomStart
)
val middleListItemShape: RoundedCornerShape
@Composable get() = RoundedCornerShape(shapes.extraSmall.topStart)
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
val bottomListItemShape: RoundedCornerShape
@Composable get() =
RoundedCornerShape(
topStart = shapes.extraSmall.topStart,
topEnd = shapes.extraSmall.topEnd,
bottomStart = shapes.largeIncreased.bottomStart,
bottomEnd = shapes.largeIncreased.bottomEnd
)
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
val cardShape: CornerBasedShape
@Composable get() = shapes.largeIncreased
}

View File

@@ -77,6 +77,18 @@ class TimerViewModel(
timerRepository.sessionLength
)
timerRepository.alarmEnabled = (preferenceRepository.getIntPreference("alarm_enabled")
?: preferenceRepository.saveIntPreference(
"alarm_enabled",
1
)) == 1
timerRepository.vibrateEnabled =
(preferenceRepository.getIntPreference("vibrate_enabled")
?: preferenceRepository.saveIntPreference(
"vibrate_enabled",
1
)) == 1
resetTimer()
var lastDate = statRepository.getLastDate()