@@ -450,15 +450,19 @@ class TimerService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initializeMediaPlayer(): MediaPlayer? {
|
private fun initializeMediaPlayer(): MediaPlayer? {
|
||||||
|
val settingsState = _settingsState.value
|
||||||
return try {
|
return try {
|
||||||
MediaPlayer().apply {
|
MediaPlayer().apply {
|
||||||
setAudioAttributes(
|
setAudioAttributes(
|
||||||
AudioAttributes.Builder()
|
AudioAttributes.Builder()
|
||||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||||
.setUsage(AudioAttributes.USAGE_ALARM)
|
.setUsage(
|
||||||
|
if (settingsState.mediaVolumeForAlarm) AudioAttributes.USAGE_MEDIA
|
||||||
|
else AudioAttributes.USAGE_ALARM
|
||||||
|
)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
_settingsState.value.alarmSoundUri?.let {
|
settingsState.alarmSoundUri?.let {
|
||||||
setDataSource(applicationContext, it)
|
setDataSource(applicationContext, it)
|
||||||
prepare()
|
prepare()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import androidx.annotation.StringRes
|
|||||||
data class SettingsSwitchItem(
|
data class SettingsSwitchItem(
|
||||||
val checked: Boolean,
|
val checked: Boolean,
|
||||||
val enabled: Boolean = true,
|
val enabled: Boolean = true,
|
||||||
|
val collapsible: Boolean = false,
|
||||||
@param:DrawableRes val icon: Int,
|
@param:DrawableRes val icon: Int,
|
||||||
@param:StringRes val label: Int,
|
@param:StringRes val label: Int,
|
||||||
@param:StringRes val description: Int,
|
@param:StringRes val description: Int,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import android.net.Uri
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -43,6 +44,7 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButtonDefaults
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
import androidx.compose.material3.LargeFlexibleTopAppBar
|
import androidx.compose.material3.LargeFlexibleTopAppBar
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme.motionScheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.SwitchDefaults
|
import androidx.compose.material3.SwitchDefaults
|
||||||
@@ -60,8 +62,10 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.util.fastForEach
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.nsh07.pomodoro.R
|
import org.nsh07.pomodoro.R
|
||||||
@@ -74,6 +78,7 @@ import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
|
|||||||
import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors
|
import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors
|
||||||
import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
|
import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
|
||||||
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
|
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.middleListItemShape
|
||||||
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
|
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
|
||||||
|
|
||||||
@@ -127,25 +132,36 @@ fun AlarmSettings(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val switchItems = remember(
|
val switchItems = remember(
|
||||||
settingsState.blackTheme,
|
|
||||||
settingsState.aodEnabled,
|
|
||||||
settingsState.alarmEnabled,
|
settingsState.alarmEnabled,
|
||||||
settingsState.vibrateEnabled
|
settingsState.vibrateEnabled,
|
||||||
|
settingsState.mediaVolumeForAlarm
|
||||||
) {
|
) {
|
||||||
listOf(
|
listOf(
|
||||||
SettingsSwitchItem(
|
listOf(
|
||||||
checked = settingsState.alarmEnabled,
|
SettingsSwitchItem(
|
||||||
icon = R.drawable.alarm_on,
|
checked = settingsState.alarmEnabled,
|
||||||
label = R.string.sound,
|
icon = R.drawable.alarm_on,
|
||||||
description = R.string.alarm_desc,
|
label = R.string.sound,
|
||||||
onClick = { onAction(SettingsAction.SaveAlarmEnabled(it)) }
|
description = R.string.alarm_desc,
|
||||||
|
onClick = { onAction(SettingsAction.SaveAlarmEnabled(it)) }
|
||||||
|
),
|
||||||
|
SettingsSwitchItem(
|
||||||
|
checked = settingsState.vibrateEnabled,
|
||||||
|
icon = R.drawable.mobile_vibrate,
|
||||||
|
label = R.string.vibrate,
|
||||||
|
description = R.string.vibrate_desc,
|
||||||
|
onClick = { onAction(SettingsAction.SaveVibrateEnabled(it)) }
|
||||||
|
)
|
||||||
),
|
),
|
||||||
SettingsSwitchItem(
|
listOf(
|
||||||
checked = settingsState.vibrateEnabled,
|
SettingsSwitchItem(
|
||||||
icon = R.drawable.mobile_vibrate,
|
checked = settingsState.mediaVolumeForAlarm,
|
||||||
label = R.string.vibrate,
|
collapsible = true,
|
||||||
description = R.string.vibrate_desc,
|
icon = R.drawable.music_note,
|
||||||
onClick = { onAction(SettingsAction.SaveVibrateEnabled(it)) }
|
label = R.string.media_volume_for_alarm,
|
||||||
|
description = R.string.media_volume_for_alarm_desc,
|
||||||
|
onClick = { onAction(SettingsAction.SaveMediaVolumeForAlarm(it)) }
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -203,44 +219,65 @@ fun AlarmSettings(
|
|||||||
.clickable(onClick = { ringtonePickerLauncher.launch(ringtonePickerIntent) })
|
.clickable(onClick = { ringtonePickerLauncher.launch(ringtonePickerIntent) })
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
itemsIndexed(switchItems) { index, item ->
|
switchItems.fastForEach { items ->
|
||||||
ListItem(
|
itemsIndexed(items) { index, item ->
|
||||||
leadingContent = {
|
ListItem(
|
||||||
Icon(painterResource(item.icon), contentDescription = null)
|
leadingContent = {
|
||||||
},
|
Icon(painterResource(item.icon), contentDescription = null)
|
||||||
headlineContent = { Text(stringResource(item.label)) },
|
},
|
||||||
supportingContent = { Text(stringResource(item.description)) },
|
headlineContent = { Text(stringResource(item.label)) },
|
||||||
trailingContent = {
|
supportingContent = {
|
||||||
Switch(
|
if (item.collapsible) {
|
||||||
checked = item.checked,
|
var expanded by remember { mutableStateOf(false) }
|
||||||
onCheckedChange = { item.onClick(it) },
|
Text(
|
||||||
thumbContent = {
|
stringResource(item.description),
|
||||||
if (item.checked) {
|
maxLines = if (expanded) Int.MAX_VALUE else 2,
|
||||||
Icon(
|
overflow = TextOverflow.Ellipsis,
|
||||||
painter = painterResource(R.drawable.check),
|
modifier = Modifier
|
||||||
contentDescription = null,
|
.clickable { expanded = !expanded }
|
||||||
modifier = Modifier.size(SwitchDefaults.IconSize),
|
.animateContentSize(motionScheme.defaultSpatialSpec())
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Icon(
|
Text(stringResource(item.description))
|
||||||
painter = painterResource(R.drawable.clear),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(SwitchDefaults.IconSize),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors = switchColors
|
|
||||||
)
|
|
||||||
},
|
|
||||||
colors = listItemColors,
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(
|
|
||||||
when (index) {
|
|
||||||
switchItems.lastIndex -> bottomListItemShape
|
|
||||||
else -> middleListItemShape
|
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
)
|
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 {
|
||||||
|
items.size == 1 -> cardShape
|
||||||
|
index == items.lastIndex -> bottomListItemShape
|
||||||
|
else -> middleListItemShape
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item { Spacer(Modifier.height(12.dp)) }
|
item { Spacer(Modifier.height(12.dp)) }
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ sealed interface SettingsAction {
|
|||||||
data class SaveBlackTheme(val enabled: Boolean) : SettingsAction
|
data class SaveBlackTheme(val enabled: Boolean) : SettingsAction
|
||||||
data class SaveAodEnabled(val enabled: Boolean) : SettingsAction
|
data class SaveAodEnabled(val enabled: Boolean) : SettingsAction
|
||||||
data class SaveDndEnabled(val enabled: Boolean) : SettingsAction
|
data class SaveDndEnabled(val enabled: Boolean) : SettingsAction
|
||||||
|
data class SaveMediaVolumeForAlarm(val enabled: Boolean) : SettingsAction
|
||||||
data class SaveAlarmSound(val uri: Uri?) : SettingsAction
|
data class SaveAlarmSound(val uri: Uri?) : SettingsAction
|
||||||
data class SaveTheme(val theme: String) : SettingsAction
|
data class SaveTheme(val theme: String) : SettingsAction
|
||||||
data class SaveColorScheme(val color: Color) : SettingsAction
|
data class SaveColorScheme(val color: Color) : SettingsAction
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ data class SettingsState(
|
|||||||
val alarmEnabled: Boolean = true,
|
val alarmEnabled: Boolean = true,
|
||||||
val vibrateEnabled: Boolean = true,
|
val vibrateEnabled: Boolean = true,
|
||||||
val dndEnabled: Boolean = false,
|
val dndEnabled: Boolean = false,
|
||||||
|
val mediaVolumeForAlarm: Boolean = false,
|
||||||
|
|
||||||
val focusTime: Long = 25 * 60 * 1000L,
|
val focusTime: Long = 25 * 60 * 1000L,
|
||||||
val shortBreakTime: Long = 5 * 60 * 1000L,
|
val shortBreakTime: Long = 5 * 60 * 1000L,
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ class SettingsViewModel(
|
|||||||
is SettingsAction.SaveAlarmEnabled -> saveAlarmEnabled(action.enabled)
|
is SettingsAction.SaveAlarmEnabled -> saveAlarmEnabled(action.enabled)
|
||||||
is SettingsAction.SaveVibrateEnabled -> saveVibrateEnabled(action.enabled)
|
is SettingsAction.SaveVibrateEnabled -> saveVibrateEnabled(action.enabled)
|
||||||
is SettingsAction.SaveDndEnabled -> saveDndEnabled(action.enabled)
|
is SettingsAction.SaveDndEnabled -> saveDndEnabled(action.enabled)
|
||||||
|
is SettingsAction.SaveMediaVolumeForAlarm -> saveMediaVolumeForAlarm(action.enabled)
|
||||||
is SettingsAction.SaveColorScheme -> saveColorScheme(action.color)
|
is SettingsAction.SaveColorScheme -> saveColorScheme(action.color)
|
||||||
is SettingsAction.SaveTheme -> saveTheme(action.theme)
|
is SettingsAction.SaveTheme -> saveTheme(action.theme)
|
||||||
is SettingsAction.SaveBlackTheme -> saveBlackTheme(action.enabled)
|
is SettingsAction.SaveBlackTheme -> saveBlackTheme(action.enabled)
|
||||||
@@ -260,6 +261,18 @@ class SettingsViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun saveMediaVolumeForAlarm(mediaVolumeForAlarm: Boolean) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_settingsState.update { currentState ->
|
||||||
|
currentState.copy(mediaVolumeForAlarm = mediaVolumeForAlarm)
|
||||||
|
}
|
||||||
|
preferenceRepository.saveBooleanPreference(
|
||||||
|
"media_volume_for_alarm",
|
||||||
|
mediaVolumeForAlarm
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun reloadSettings() {
|
suspend fun reloadSettings() {
|
||||||
var settingsState = _settingsState.value
|
var settingsState = _settingsState.value
|
||||||
val focusTime =
|
val focusTime =
|
||||||
@@ -316,6 +329,12 @@ class SettingsViewModel(
|
|||||||
)
|
)
|
||||||
val dndEnabled = preferenceRepository.getBooleanPreference("dnd_enabled")
|
val dndEnabled = preferenceRepository.getBooleanPreference("dnd_enabled")
|
||||||
?: preferenceRepository.saveBooleanPreference("dnd_enabled", settingsState.dndEnabled)
|
?: preferenceRepository.saveBooleanPreference("dnd_enabled", settingsState.dndEnabled)
|
||||||
|
val mediaVolumeForAlarm =
|
||||||
|
preferenceRepository.getBooleanPreference("media_volume_for_alarm")
|
||||||
|
?: preferenceRepository.saveBooleanPreference(
|
||||||
|
"media_volume_for_alarm",
|
||||||
|
settingsState.mediaVolumeForAlarm
|
||||||
|
)
|
||||||
|
|
||||||
_settingsState.update { currentState ->
|
_settingsState.update { currentState ->
|
||||||
currentState.copy(
|
currentState.copy(
|
||||||
@@ -330,7 +349,8 @@ class SettingsViewModel(
|
|||||||
aodEnabled = aodEnabled,
|
aodEnabled = aodEnabled,
|
||||||
alarmEnabled = alarmEnabled,
|
alarmEnabled = alarmEnabled,
|
||||||
vibrateEnabled = vibrateEnabled,
|
vibrateEnabled = vibrateEnabled,
|
||||||
dndEnabled = dndEnabled
|
dndEnabled = dndEnabled,
|
||||||
|
mediaVolumeForAlarm = mediaVolumeForAlarm
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
26
app/src/main/res/drawable/music_note.xml
Normal file
26
app/src/main/res/drawable/music_note.xml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<!--
|
||||||
|
~ Copyright (c) 2025 Nishant Mishra
|
||||||
|
~
|
||||||
|
~ This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
|
~
|
||||||
|
~ Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
~ General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
~ License, or (at your option) any later version.
|
||||||
|
~
|
||||||
|
~ Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
~ Public License for more details.
|
||||||
|
~
|
||||||
|
~ You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
~ 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="M400,840q-66,0 -113,-47t-47,-113q0,-66 47,-113t113,-47q23,0 42.5,5.5T480,542v-382q0,-17 11.5,-28.5T520,120h160q17,0 28.5,11.5T720,160v80q0,17 -11.5,28.5T680,280L560,280v400q0,66 -47,113t-113,47Z" />
|
||||||
|
</vector>
|
||||||
@@ -103,4 +103,6 @@
|
|||||||
<string name="bmc_desc">Support me with a small donation</string>
|
<string name="bmc_desc">Support me with a small donation</string>
|
||||||
<string name="tomato_plus_desc">Customize further with Tomato+</string>
|
<string name="tomato_plus_desc">Customize further with Tomato+</string>
|
||||||
<string name="license">License</string>
|
<string name="license">License</string>
|
||||||
|
<string name="media_volume_for_alarm">Headphone mode</string>
|
||||||
|
<string name="media_volume_for_alarm_desc">Plays on headphones only. If headphones are disconnected, alarm plays through speaker at media volume.</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user