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()

View File

@@ -0,0 +1,16 @@
<!--
~ 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/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#e3e3e3"
android:pathData="m438,548 l-57,-57q-12,-12 -28,-12t-28,12q-12,12 -12,28.5t12,28.5l85,86q12,12 28,12t28,-12l170,-170q12,-12 12,-28.5T636,407q-12,-12 -28.5,-12T579,407L438,548ZM480,880q-75,0 -140.5,-28.5t-114,-77q-48.5,-48.5 -77,-114T120,520q0,-75 28.5,-140.5t77,-114q48.5,-48.5 114,-77T480,160q75,0 140.5,28.5t114,77q48.5,48.5 77,114T840,520q0,75 -28.5,140.5t-77,114q-48.5,48.5 -114,77T480,880ZM82,292q-11,-11 -11,-28t11,-28l114,-114q11,-11 28,-11t28,11q11,11 11,28t-11,28L138,292q-11,11 -28,11t-28,-11ZM878,292q-11,11 -28,11t-28,-11L708,178q-11,-11 -11,-28t11,-28q11,-11 28,-11t28,11l114,114q11,11 11,28t-11,28Z" />
</vector>

View File

@@ -0,0 +1,16 @@
<!--
~ 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/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#e3e3e3"
android:pathData="m382,606 l339,-339q12,-12 28,-12t28,12q12,12 12,28.5T777,324L410,692q-12,12 -28,12t-28,-12L182,520q-12,-12 -11.5,-28.5T183,463q12,-12 28.5,-12t28.5,12l142,143Z" />
</vector>

View File

@@ -0,0 +1,16 @@
<!--
~ 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/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#e3e3e3"
android:pathData="M480,536 L284,732q-11,11 -28,11t-28,-11q-11,-11 -11,-28t11,-28l196,-196 -196,-196q-11,-11 -11,-28t11,-28q11,-11 28,-11t28,11l196,196 196,-196q11,-11 28,-11t28,11q11,11 11,28t-11,28L536,480l196,196q11,11 11,28t-11,28q-11,11 -28,11t-28,-11L480,536Z" />
</vector>

View File

@@ -0,0 +1,16 @@
<!--
~ 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/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#e3e3e3"
android:pathData="M320,840q-33,0 -56.5,-23.5T240,760v-560q0,-33 23.5,-56.5T320,120h320q33,0 56.5,23.5T720,200v560q0,33 -23.5,56.5T640,840L320,840ZM480,320q17,0 28.5,-11.5T520,280q0,-17 -11.5,-28.5T480,240q-17,0 -28.5,11.5T440,280q0,17 11.5,28.5T480,320ZM0,560v-160q0,-17 11.5,-28.5T40,360q17,0 28.5,11.5T80,400v160q0,17 -11.5,28.5T40,600q-17,0 -28.5,-11.5T0,560ZM120,640v-320q0,-17 11.5,-28.5T160,280q17,0 28.5,11.5T200,320v320q0,17 -11.5,28.5T160,680q-17,0 -28.5,-11.5T120,640ZM880,560v-160q0,-17 11.5,-28.5T920,360q17,0 28.5,11.5T960,400v160q0,17 -11.5,28.5T920,600q-17,0 -28.5,-11.5T880,560ZM760,640v-320q0,-17 11.5,-28.5T800,280q17,0 28.5,11.5T840,320v320q0,17 -11.5,28.5T800,680q-17,0 -28.5,-11.5T760,640Z" />
</vector>