feat(ui): implement separate settings pages
This commit is contained in:
@@ -1,8 +1,18 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Nishant Mishra
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
*
|
||||||
|
* 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/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.nsh07.pomodoro.ui.settingsScreen
|
package org.nsh07.pomodoro.ui.settingsScreen
|
||||||
@@ -36,7 +46,6 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import org.nsh07.pomodoro.BuildConfig
|
import org.nsh07.pomodoro.BuildConfig
|
||||||
import org.nsh07.pomodoro.R
|
import org.nsh07.pomodoro.R
|
||||||
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexHeadline
|
|
||||||
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
||||||
|
|
||||||
// Taken from https://github.com/shub39/Grit/blob/master/app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt
|
// Taken from https://github.com/shub39/Grit/blob/master/app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt
|
||||||
@@ -71,10 +80,7 @@ fun AboutCard(modifier: Modifier = Modifier) {
|
|||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
fontFamily = robotoFlexTopBar
|
fontFamily = robotoFlexTopBar
|
||||||
)
|
)
|
||||||
Text(
|
Text(text = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
||||||
text = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
|
|
||||||
fontFamily = robotoFlexHeadline
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.nsh07.pomodoro.ui.settingsScreen
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.media.RingtoneManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LargeFlexibleTopAppBar
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.SwitchDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
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.unit.dp
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.nsh07.pomodoro.R
|
||||||
|
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState
|
||||||
|
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
||||||
|
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.middleListItemShape
|
||||||
|
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun AlarmSettings(
|
||||||
|
preferencesState: PreferencesState,
|
||||||
|
alarmEnabled: Boolean,
|
||||||
|
vibrateEnabled: Boolean,
|
||||||
|
alarmSound: String,
|
||||||
|
onAlarmEnabledChange: (Boolean) -> Unit,
|
||||||
|
onVibrateEnabledChange: (Boolean) -> Unit,
|
||||||
|
onAlarmSoundChanged: (Uri?) -> Unit,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
var alarmName by remember { mutableStateOf("...") }
|
||||||
|
|
||||||
|
LaunchedEffect(alarmSound) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
alarmName =
|
||||||
|
RingtoneManager.getRingtone(context, alarmSound.toUri())?.getTitle(context) ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val ringtonePickerLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
val uri =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
result.data?.getParcelableExtra(
|
||||||
|
RingtoneManager.EXTRA_RINGTONE_PICKED_URI,
|
||||||
|
Uri::class.java
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
result.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
|
||||||
|
}
|
||||||
|
onAlarmSoundChanged(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val ringtonePickerIntent = remember(alarmSound) {
|
||||||
|
Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
|
||||||
|
putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM)
|
||||||
|
putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, context.getString(R.string.alarm_sound))
|
||||||
|
putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, alarmSound.toUri())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val switchItems = remember(
|
||||||
|
preferencesState.blackTheme,
|
||||||
|
preferencesState.aodEnabled,
|
||||||
|
alarmEnabled,
|
||||||
|
vibrateEnabled
|
||||||
|
) {
|
||||||
|
listOf(
|
||||||
|
SettingsSwitchItem(
|
||||||
|
checked = alarmEnabled,
|
||||||
|
icon = R.drawable.alarm_on,
|
||||||
|
label = R.string.alarm,
|
||||||
|
description = R.string.alarm_desc,
|
||||||
|
onClick = onAlarmEnabledChange
|
||||||
|
),
|
||||||
|
SettingsSwitchItem(
|
||||||
|
checked = vibrateEnabled,
|
||||||
|
icon = R.drawable.mobile_vibrate,
|
||||||
|
label = R.string.vibrate,
|
||||||
|
description = R.string.vibrate_desc,
|
||||||
|
onClick = onVibrateEnabledChange
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
|
||||||
|
LargeFlexibleTopAppBar(
|
||||||
|
title = {
|
||||||
|
Text("Alarm", fontFamily = robotoFlexTopBar)
|
||||||
|
},
|
||||||
|
subtitle = {
|
||||||
|
Text("Settings")
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onBack) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.arrow_back),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = topBarColors,
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.background(topBarColors.containerColor)
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Spacer(Modifier.height(14.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
ListItem(
|
||||||
|
leadingContent = {
|
||||||
|
Icon(painterResource(R.drawable.alarm), null)
|
||||||
|
},
|
||||||
|
headlineContent = { Text(stringResource(R.string.alarm_sound)) },
|
||||||
|
supportingContent = { Text(alarmName) },
|
||||||
|
colors = listItemColors,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(topListItemShape)
|
||||||
|
.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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item { Spacer(Modifier.height(12.dp)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.nsh07.pomodoro.ui.settingsScreen
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LargeFlexibleTopAppBar
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.SwitchDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
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.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.nsh07.pomodoro.R
|
||||||
|
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState
|
||||||
|
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
||||||
|
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.middleListItemShape
|
||||||
|
import org.nsh07.pomodoro.utils.toColor
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun AppearanceSettings(
|
||||||
|
preferencesState: PreferencesState,
|
||||||
|
onBlackThemeChange: (Boolean) -> Unit,
|
||||||
|
onThemeChange: (String) -> Unit,
|
||||||
|
onColorSchemeChange: (Color) -> Unit,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val themeMap: Map<String, Pair<Int, Int>> = remember {
|
||||||
|
mapOf(
|
||||||
|
"auto" to Pair(
|
||||||
|
R.drawable.brightness_auto,
|
||||||
|
R.string.system_default
|
||||||
|
),
|
||||||
|
"light" to Pair(R.drawable.light_mode, R.string.light),
|
||||||
|
"dark" to Pair(R.drawable.dark_mode, R.string.dark)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val reverseThemeMap: Map<String, String> = remember {
|
||||||
|
mapOf(
|
||||||
|
context.getString(R.string.system_default) to "auto",
|
||||||
|
context.getString(R.string.light) to "light",
|
||||||
|
context.getString(R.string.dark) to "dark"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
|
||||||
|
|
||||||
|
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
|
||||||
|
LargeFlexibleTopAppBar(
|
||||||
|
title = {
|
||||||
|
Text("Appearance", fontFamily = robotoFlexTopBar)
|
||||||
|
},
|
||||||
|
subtitle = {
|
||||||
|
Text("Settings")
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onBack) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.arrow_back),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = topBarColors,
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.background(topBarColors.containerColor)
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Spacer(Modifier.height(14.dp))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
ColorSchemePickerListItem(
|
||||||
|
color = preferencesState.colorScheme.toColor(),
|
||||||
|
items = 3,
|
||||||
|
index = 0,
|
||||||
|
onColorChange = onColorSchemeChange
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
ThemePickerListItem(
|
||||||
|
theme = preferencesState.theme,
|
||||||
|
themeMap = themeMap,
|
||||||
|
reverseThemeMap = reverseThemeMap,
|
||||||
|
onThemeChange = onThemeChange,
|
||||||
|
items = 3,
|
||||||
|
index = 1,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(middleListItemShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
val item = SettingsSwitchItem(
|
||||||
|
checked = preferencesState.blackTheme,
|
||||||
|
icon = R.drawable.contrast,
|
||||||
|
label = R.string.black_theme,
|
||||||
|
description = R.string.black_theme_desc,
|
||||||
|
onClick = onBlackThemeChange
|
||||||
|
)
|
||||||
|
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(bottomListItemShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item { Spacer(Modifier.height(12.dp)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun AppearanceSettingsPreview() {
|
||||||
|
val preferencesState = PreferencesState()
|
||||||
|
AppearanceSettings(
|
||||||
|
preferencesState = preferencesState,
|
||||||
|
onBlackThemeChange = {},
|
||||||
|
onThemeChange = {},
|
||||||
|
onColorSchemeChange = {},
|
||||||
|
onBack = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.nsh07.pomodoro.ui.settingsScreen
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
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.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.FilledTonalIconToggleButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
|
import androidx.compose.material3.LargeFlexibleTopAppBar
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||||
|
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.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.nsh07.pomodoro.R
|
||||||
|
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
||||||
|
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
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun TimerSettings(
|
||||||
|
aodEnabled: Boolean,
|
||||||
|
focusTimeInputFieldState: TextFieldState,
|
||||||
|
shortBreakTimeInputFieldState: TextFieldState,
|
||||||
|
longBreakTimeInputFieldState: TextFieldState,
|
||||||
|
sessionsSliderState: SliderState,
|
||||||
|
onAodEnabledChange: (Boolean) -> Unit,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
|
||||||
|
|
||||||
|
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
|
||||||
|
LargeFlexibleTopAppBar(
|
||||||
|
title = {
|
||||||
|
Text("Timer", fontFamily = robotoFlexTopBar)
|
||||||
|
},
|
||||||
|
subtitle = {
|
||||||
|
Text("Settings")
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onBack) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.arrow_back),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = topBarColors,
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.background(topBarColors.containerColor)
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Spacer(Modifier.height(14.dp))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.horizontalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.focus),
|
||||||
|
style = typography.titleSmallEmphasized
|
||||||
|
)
|
||||||
|
MinuteInputField(
|
||||||
|
state = focusTimeInputFieldState,
|
||||||
|
shape = RoundedCornerShape(
|
||||||
|
topStart = topListItemShape.topStart,
|
||||||
|
bottomStart = topListItemShape.topStart,
|
||||||
|
topEnd = topListItemShape.bottomStart,
|
||||||
|
bottomEnd = topListItemShape.bottomStart
|
||||||
|
),
|
||||||
|
imeAction = ImeAction.Next
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(2.dp))
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.short_break),
|
||||||
|
style = typography.titleSmallEmphasized
|
||||||
|
)
|
||||||
|
MinuteInputField(
|
||||||
|
state = shortBreakTimeInputFieldState,
|
||||||
|
shape = RoundedCornerShape(middleListItemShape.topStart),
|
||||||
|
imeAction = ImeAction.Next
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(2.dp))
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.long_break),
|
||||||
|
style = typography.titleSmallEmphasized
|
||||||
|
)
|
||||||
|
MinuteInputField(
|
||||||
|
state = longBreakTimeInputFieldState,
|
||||||
|
shape = RoundedCornerShape(
|
||||||
|
topStart = bottomListItemShape.topStart,
|
||||||
|
bottomStart = bottomListItemShape.topStart,
|
||||||
|
topEnd = bottomListItemShape.bottomStart,
|
||||||
|
bottomEnd = bottomListItemShape.bottomStart
|
||||||
|
),
|
||||||
|
imeAction = ImeAction.Done
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
ListItem(
|
||||||
|
leadingContent = {
|
||||||
|
Icon(painterResource(R.drawable.clocks), null)
|
||||||
|
},
|
||||||
|
headlineContent = {
|
||||||
|
Text(stringResource(R.string.session_length))
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
stringResource(
|
||||||
|
R.string.session_length_desc,
|
||||||
|
sessionsSliderState.value.toInt()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Slider(
|
||||||
|
state = sessionsSliderState,
|
||||||
|
modifier = Modifier.padding(vertical = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = listItemColors,
|
||||||
|
modifier = Modifier.clip(cardShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item { Spacer(Modifier.height(12.dp)) }
|
||||||
|
item {
|
||||||
|
val item = SettingsSwitchItem(
|
||||||
|
checked = aodEnabled,
|
||||||
|
icon = R.drawable.aod,
|
||||||
|
label = R.string.always_on_display,
|
||||||
|
description = R.string.always_on_display_desc,
|
||||||
|
onClick = onAodEnabledChange
|
||||||
|
)
|
||||||
|
ListItem(
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
painterResource(item.icon),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.padding(top = 4.dp)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
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(cardShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.End,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = 6.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
FilledTonalIconToggleButton(
|
||||||
|
checked = expanded,
|
||||||
|
onCheckedChange = { expanded = it },
|
||||||
|
shapes = IconButtonDefaults.toggleableShapes(),
|
||||||
|
modifier = Modifier.width(52.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.info),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AnimatedVisibility(expanded) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.pomodoro_info),
|
||||||
|
style = typography.bodyMedium,
|
||||||
|
color = colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item { Spacer(Modifier.height(12.dp)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun TimerSettingsPreview() {
|
||||||
|
val focusTimeInputFieldState = TextFieldState("25")
|
||||||
|
val shortBreakTimeInputFieldState = TextFieldState("5")
|
||||||
|
val longBreakTimeInputFieldState = TextFieldState("15")
|
||||||
|
val sessionsSliderState = SliderState(
|
||||||
|
value = 4f,
|
||||||
|
valueRange = 1f..8f,
|
||||||
|
steps = 6
|
||||||
|
)
|
||||||
|
TimerSettings(
|
||||||
|
focusTimeInputFieldState = focusTimeInputFieldState,
|
||||||
|
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
|
||||||
|
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
|
||||||
|
sessionsSliderState = sessionsSliderState,
|
||||||
|
aodEnabled = true,
|
||||||
|
onBack = {},
|
||||||
|
onAodEnabledChange = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,9 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.nsh07.pomodoro.ui.theme
|
package org.nsh07.pomodoro.ui.theme
|
||||||
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ListItemColors
|
import androidx.compose.material3.ListItemColors
|
||||||
import androidx.compose.material3.ListItemDefaults
|
import androidx.compose.material3.ListItemDefaults
|
||||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||||
|
import androidx.compose.material3.SwitchColors
|
||||||
|
import androidx.compose.material3.SwitchDefaults
|
||||||
import androidx.compose.material3.TopAppBarColors
|
import androidx.compose.material3.TopAppBarColors
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -41,4 +60,9 @@ object CustomColors {
|
|||||||
supportingColor = colorScheme.onSecondaryFixedVariant,
|
supportingColor = colorScheme.onSecondaryFixedVariant,
|
||||||
trailingIconColor = colorScheme.onSecondaryFixedVariant
|
trailingIconColor = colorScheme.onSecondaryFixedVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val switchColors: SwitchColors
|
||||||
|
@Composable get() = SwitchDefaults.colors(
|
||||||
|
checkedIconColor = colorScheme.primary,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user