@@ -450,15 +450,19 @@ class TimerService : Service() {
|
||||
}
|
||||
|
||||
private fun initializeMediaPlayer(): MediaPlayer? {
|
||||
val settingsState = _settingsState.value
|
||||
return try {
|
||||
MediaPlayer().apply {
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.setUsage(AudioAttributes.USAGE_ALARM)
|
||||
.setUsage(
|
||||
if (settingsState.mediaVolumeForAlarm) AudioAttributes.USAGE_MEDIA
|
||||
else AudioAttributes.USAGE_ALARM
|
||||
)
|
||||
.build()
|
||||
)
|
||||
_settingsState.value.alarmSoundUri?.let {
|
||||
settingsState.alarmSoundUri?.let {
|
||||
setDataSource(applicationContext, it)
|
||||
prepare()
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import androidx.annotation.StringRes
|
||||
data class SettingsSwitchItem(
|
||||
val checked: Boolean,
|
||||
val enabled: Boolean = true,
|
||||
val collapsible: Boolean = false,
|
||||
@param:DrawableRes val icon: Int,
|
||||
@param:StringRes val label: Int,
|
||||
@param:StringRes val description: Int,
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -43,6 +44,7 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.LargeFlexibleTopAppBar
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme.motionScheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
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.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
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.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
|
||||
|
||||
@@ -127,25 +132,36 @@ fun AlarmSettings(
|
||||
}
|
||||
|
||||
val switchItems = remember(
|
||||
settingsState.blackTheme,
|
||||
settingsState.aodEnabled,
|
||||
settingsState.alarmEnabled,
|
||||
settingsState.vibrateEnabled
|
||||
settingsState.vibrateEnabled,
|
||||
settingsState.mediaVolumeForAlarm
|
||||
) {
|
||||
listOf(
|
||||
SettingsSwitchItem(
|
||||
checked = settingsState.alarmEnabled,
|
||||
icon = R.drawable.alarm_on,
|
||||
label = R.string.sound,
|
||||
description = R.string.alarm_desc,
|
||||
onClick = { onAction(SettingsAction.SaveAlarmEnabled(it)) }
|
||||
listOf(
|
||||
SettingsSwitchItem(
|
||||
checked = settingsState.alarmEnabled,
|
||||
icon = R.drawable.alarm_on,
|
||||
label = R.string.sound,
|
||||
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(
|
||||
checked = settingsState.vibrateEnabled,
|
||||
icon = R.drawable.mobile_vibrate,
|
||||
label = R.string.vibrate,
|
||||
description = R.string.vibrate_desc,
|
||||
onClick = { onAction(SettingsAction.SaveVibrateEnabled(it)) }
|
||||
listOf(
|
||||
SettingsSwitchItem(
|
||||
checked = settingsState.mediaVolumeForAlarm,
|
||||
collapsible = true,
|
||||
icon = R.drawable.music_note,
|
||||
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) })
|
||||
)
|
||||
}
|
||||
itemsIndexed(switchItems) { index, item ->
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(painterResource(item.icon), contentDescription = null)
|
||||
},
|
||||
headlineContent = { Text(stringResource(item.label)) },
|
||||
supportingContent = { Text(stringResource(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) {
|
||||
switchItems.lastIndex -> bottomListItemShape
|
||||
else -> middleListItemShape
|
||||
switchItems.fastForEach { items ->
|
||||
itemsIndexed(items) { index, item ->
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(painterResource(item.icon), contentDescription = null)
|
||||
},
|
||||
headlineContent = { Text(stringResource(item.label)) },
|
||||
supportingContent = {
|
||||
if (item.collapsible) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
Text(
|
||||
stringResource(item.description),
|
||||
maxLines = if (expanded) Int.MAX_VALUE else 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.clickable { expanded = !expanded }
|
||||
.animateContentSize(motionScheme.defaultSpatialSpec())
|
||||
)
|
||||
} else {
|
||||
Text(stringResource(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 {
|
||||
items.size == 1 -> cardShape
|
||||
index == items.lastIndex -> bottomListItemShape
|
||||
else -> middleListItemShape
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
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 SaveAodEnabled(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 SaveTheme(val theme: String) : SettingsAction
|
||||
data class SaveColorScheme(val color: Color) : SettingsAction
|
||||
|
||||
@@ -31,6 +31,7 @@ data class SettingsState(
|
||||
val alarmEnabled: Boolean = true,
|
||||
val vibrateEnabled: Boolean = true,
|
||||
val dndEnabled: Boolean = false,
|
||||
val mediaVolumeForAlarm: Boolean = false,
|
||||
|
||||
val focusTime: Long = 25 * 60 * 1000L,
|
||||
val shortBreakTime: Long = 5 * 60 * 1000L,
|
||||
|
||||
@@ -109,6 +109,7 @@ class SettingsViewModel(
|
||||
is SettingsAction.SaveAlarmEnabled -> saveAlarmEnabled(action.enabled)
|
||||
is SettingsAction.SaveVibrateEnabled -> saveVibrateEnabled(action.enabled)
|
||||
is SettingsAction.SaveDndEnabled -> saveDndEnabled(action.enabled)
|
||||
is SettingsAction.SaveMediaVolumeForAlarm -> saveMediaVolumeForAlarm(action.enabled)
|
||||
is SettingsAction.SaveColorScheme -> saveColorScheme(action.color)
|
||||
is SettingsAction.SaveTheme -> saveTheme(action.theme)
|
||||
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() {
|
||||
var settingsState = _settingsState.value
|
||||
val focusTime =
|
||||
@@ -316,6 +329,12 @@ class SettingsViewModel(
|
||||
)
|
||||
val dndEnabled = preferenceRepository.getBooleanPreference("dnd_enabled")
|
||||
?: 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 ->
|
||||
currentState.copy(
|
||||
@@ -330,7 +349,8 @@ class SettingsViewModel(
|
||||
aodEnabled = aodEnabled,
|
||||
alarmEnabled = alarmEnabled,
|
||||
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="tomato_plus_desc">Customize further with Tomato+</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>
|
||||
Reference in New Issue
Block a user