Merge branch 'dev'
This commit is contained in:
@@ -33,8 +33,8 @@ android {
|
|||||||
applicationId = "org.nsh07.pomodoro"
|
applicationId = "org.nsh07.pomodoro"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 7
|
versionCode = 8
|
||||||
versionName = "1.3.0"
|
versionName = "1.4.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
@@ -86,6 +86,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.navigation3.ui)
|
implementation(libs.androidx.navigation3.ui)
|
||||||
|
|
||||||
implementation(libs.vico.compose.m3)
|
implementation(libs.vico.compose.m3)
|
||||||
|
implementation(libs.material.kolor)
|
||||||
|
|
||||||
implementation(libs.androidx.room.runtime)
|
implementation(libs.androidx.room.runtime)
|
||||||
implementation(libs.androidx.room.ktx)
|
implementation(libs.androidx.room.ktx)
|
||||||
|
|||||||
@@ -5,19 +5,23 @@ import androidx.activity.ComponentActivity
|
|||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import org.nsh07.pomodoro.ui.AppScreen
|
import org.nsh07.pomodoro.ui.AppScreen
|
||||||
import org.nsh07.pomodoro.ui.NavItem
|
import org.nsh07.pomodoro.ui.NavItem
|
||||||
import org.nsh07.pomodoro.ui.Screen
|
import org.nsh07.pomodoro.ui.Screen
|
||||||
import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel
|
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
|
||||||
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
||||||
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
|
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
|
||||||
|
import org.nsh07.pomodoro.utils.toColor
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
private val timerViewModel: TimerViewModel by viewModels(factoryProducer = { TimerViewModel.Factory })
|
private val timerViewModel: TimerViewModel by viewModels(factoryProducer = { TimerViewModel.Factory })
|
||||||
private val statsViewModel: StatsViewModel by viewModels(factoryProducer = { StatsViewModel.Factory })
|
private val settingsViewModel: SettingsViewModel by viewModels(factoryProducer = { SettingsViewModel.Factory })
|
||||||
|
|
||||||
private val appContainer by lazy {
|
private val appContainer by lazy {
|
||||||
(application as TomatoApplication).container
|
(application as TomatoApplication).container
|
||||||
@@ -27,14 +31,27 @@ class MainActivity : ComponentActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
TomatoTheme {
|
val preferencesState by settingsViewModel.preferencesState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
val darkTheme = when (preferencesState.theme) {
|
||||||
|
"dark" -> true
|
||||||
|
"light" -> false
|
||||||
|
else -> isSystemInDarkTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
val seed = preferencesState.colorScheme.toColor()
|
||||||
|
|
||||||
|
TomatoTheme(
|
||||||
|
darkTheme = darkTheme,
|
||||||
|
seedColor = seed,
|
||||||
|
blackTheme = preferencesState.blackTheme
|
||||||
|
) {
|
||||||
val colorScheme = colorScheme
|
val colorScheme = colorScheme
|
||||||
LaunchedEffect(colorScheme) {
|
LaunchedEffect(colorScheme) {
|
||||||
appContainer.appTimerRepository.colorScheme = colorScheme
|
appContainer.appTimerRepository.colorScheme = colorScheme
|
||||||
}
|
}
|
||||||
|
|
||||||
timerViewModel.setCompositionLocals(colorScheme)
|
AppScreen(timerViewModel = timerViewModel)
|
||||||
AppScreen(timerViewModel = timerViewModel, statsViewModel = statsViewModel)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,10 +42,10 @@ interface StatDao {
|
|||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT " +
|
"SELECT " +
|
||||||
"AVG(focusTimeQ1) AS focusTimeQ1, " +
|
"AVG(NULLIF(focusTimeQ1,0)) AS focusTimeQ1, " +
|
||||||
"AVG(focusTimeQ2) AS focusTimeQ2, " +
|
"AVG(NULLIF(focusTimeQ2,0)) AS focusTimeQ2, " +
|
||||||
"AVG(focusTimeQ3) AS focusTimeQ3, " +
|
"AVG(NULLIF(focusTimeQ3,0)) AS focusTimeQ3, " +
|
||||||
"AVG(focusTimeQ4) AS focusTimeQ4 " +
|
"AVG(NULLIF(focusTimeQ4,0)) AS focusTimeQ4 " +
|
||||||
"FROM (SELECT focusTimeQ1, focusTimeQ2, focusTimeQ3, focusTimeQ4 FROM stat ORDER BY date DESC LIMIT :n)"
|
"FROM (SELECT focusTimeQ1, focusTimeQ2, focusTimeQ3, focusTimeQ4 FROM stat ORDER BY date DESC LIMIT :n)"
|
||||||
)
|
)
|
||||||
fun getLastNDaysAvgFocusTimes(n: Int): Flow<StatFocusTime?>
|
fun getLastNDaysAvgFocusTimes(n: Int): Flow<StatFocusTime?>
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ interface TimerRepository {
|
|||||||
var colorScheme: ColorScheme
|
var colorScheme: ColorScheme
|
||||||
|
|
||||||
var alarmSoundUri: Uri?
|
var alarmSoundUri: Uri?
|
||||||
|
|
||||||
|
var serviceRunning: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,4 +49,5 @@ class AppTimerRepository : TimerRepository {
|
|||||||
override var colorScheme = lightColorScheme()
|
override var colorScheme = lightColorScheme()
|
||||||
override var alarmSoundUri: Uri? =
|
override var alarmSoundUri: Uri? =
|
||||||
Settings.System.DEFAULT_ALARM_ALERT_URI ?: Settings.System.DEFAULT_RINGTONE_URI
|
Settings.System.DEFAULT_ALARM_ALERT_URI ?: Settings.System.DEFAULT_RINGTONE_URI
|
||||||
|
override var serviceRunning = false
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ fun NotificationCompat.Builder.addTimerActions(
|
|||||||
)
|
)
|
||||||
.addAction(
|
.addAction(
|
||||||
R.drawable.restart,
|
R.drawable.restart,
|
||||||
"Reset",
|
"Exit",
|
||||||
PendingIntent.getService(
|
PendingIntent.getService(
|
||||||
context,
|
context,
|
||||||
0,
|
0,
|
||||||
|
|||||||
@@ -69,15 +69,29 @@ class TimerService : Service() {
|
|||||||
|
|
||||||
private val cs by lazy { timerRepository.colorScheme }
|
private val cs by lazy { timerRepository.colorScheme }
|
||||||
|
|
||||||
|
private lateinit var notificationStyle: NotificationCompat.ProgressStyle
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? {
|
override fun onBind(intent: Intent?): IBinder? {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
timerRepository.serviceRunning = true
|
||||||
alarm = MediaPlayer.create(this, timerRepository.alarmSoundUri)
|
alarm = MediaPlayer.create(this, timerRepository.alarmSoundUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
timerRepository.serviceRunning = false
|
||||||
|
runBlocking {
|
||||||
|
job.cancel()
|
||||||
|
saveTimeToDb()
|
||||||
|
notificationManager.cancel(1)
|
||||||
|
alarm?.release()
|
||||||
|
}
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
when (intent?.action) {
|
when (intent?.action) {
|
||||||
Actions.TOGGLE.toString() -> {
|
Actions.TOGGLE.toString() -> {
|
||||||
@@ -101,6 +115,8 @@ class TimerService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleTimer() {
|
private fun toggleTimer() {
|
||||||
|
updateProgressSegments()
|
||||||
|
|
||||||
if (timerState.value.timerRunning) {
|
if (timerState.value.timerRunning) {
|
||||||
notificationBuilder.clearActions().addTimerActions(
|
notificationBuilder.clearActions().addTimerActions(
|
||||||
this, R.drawable.play, "Start"
|
this, R.drawable.play, "Start"
|
||||||
@@ -194,42 +210,7 @@ class TimerService : Service() {
|
|||||||
)
|
)
|
||||||
.setContentText("Up next: $nextTimer (${timerState.value.nextTimeStr})")
|
.setContentText("Up next: $nextTimer (${timerState.value.nextTimeStr})")
|
||||||
.setStyle(
|
.setStyle(
|
||||||
NotificationCompat.ProgressStyle()
|
notificationStyle
|
||||||
.also {
|
|
||||||
// Add all the Focus, Short break and long break intervals in order
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
|
|
||||||
// Android 16 and later supports live updates
|
|
||||||
// Set progress bar sections if on Baklava or later
|
|
||||||
for (i in 0..<timerRepository.sessionLength * 2) {
|
|
||||||
if (i % 2 == 0) it.addProgressSegment(
|
|
||||||
NotificationCompat.ProgressStyle.Segment(
|
|
||||||
timerRepository.focusTime.toInt()
|
|
||||||
)
|
|
||||||
.setColor(cs.primary.toArgb())
|
|
||||||
)
|
|
||||||
else if (i != (timerRepository.sessionLength * 2 - 1)) it.addProgressSegment(
|
|
||||||
NotificationCompat.ProgressStyle.Segment(
|
|
||||||
timerRepository.shortBreakTime.toInt()
|
|
||||||
).setColor(cs.tertiary.toArgb())
|
|
||||||
)
|
|
||||||
else it.addProgressSegment(
|
|
||||||
NotificationCompat.ProgressStyle.Segment(
|
|
||||||
timerRepository.longBreakTime.toInt()
|
|
||||||
).setColor(cs.tertiary.toArgb())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
it.addProgressSegment(
|
|
||||||
NotificationCompat.ProgressStyle.Segment(
|
|
||||||
when (timerState.value.timerMode) {
|
|
||||||
TimerMode.FOCUS -> timerRepository.focusTime.toInt()
|
|
||||||
TimerMode.SHORT_BREAK -> timerRepository.shortBreakTime.toInt()
|
|
||||||
else -> timerRepository.longBreakTime.toInt()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setProgress( // Set the current progress by filling the previous intervals and part of the current interval
|
.setProgress( // Set the current progress by filling the previous intervals and part of the current interval
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
|
||||||
(totalTime - remainingTime) + ((cycles + 1) / 2) * timerRepository.focusTime.toInt() + (cycles / 2) * timerRepository.shortBreakTime.toInt()
|
(totalTime - remainingTime) + ((cycles + 1) / 2) * timerRepository.focusTime.toInt() + (cycles / 2) * timerRepository.shortBreakTime.toInt()
|
||||||
@@ -249,6 +230,45 @@ class TimerService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateProgressSegments() {
|
||||||
|
notificationStyle = NotificationCompat.ProgressStyle()
|
||||||
|
.also {
|
||||||
|
// Add all the Focus, Short break and long break intervals in order
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
|
||||||
|
// Android 16 and later supports live updates
|
||||||
|
// Set progress bar sections if on Baklava or later
|
||||||
|
for (i in 0..<timerRepository.sessionLength * 2) {
|
||||||
|
if (i % 2 == 0) it.addProgressSegment(
|
||||||
|
NotificationCompat.ProgressStyle.Segment(
|
||||||
|
timerRepository.focusTime.toInt()
|
||||||
|
)
|
||||||
|
.setColor(cs.primary.toArgb())
|
||||||
|
)
|
||||||
|
else if (i != (timerRepository.sessionLength * 2 - 1)) it.addProgressSegment(
|
||||||
|
NotificationCompat.ProgressStyle.Segment(
|
||||||
|
timerRepository.shortBreakTime.toInt()
|
||||||
|
).setColor(cs.tertiary.toArgb())
|
||||||
|
)
|
||||||
|
else it.addProgressSegment(
|
||||||
|
NotificationCompat.ProgressStyle.Segment(
|
||||||
|
timerRepository.longBreakTime.toInt()
|
||||||
|
).setColor(cs.tertiary.toArgb())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
it.addProgressSegment(
|
||||||
|
NotificationCompat.ProgressStyle.Segment(
|
||||||
|
when (timerState.value.timerMode) {
|
||||||
|
TimerMode.FOCUS -> timerRepository.focusTime.toInt()
|
||||||
|
TimerMode.SHORT_BREAK -> timerRepository.shortBreakTime.toInt()
|
||||||
|
else -> timerRepository.longBreakTime.toInt()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun resetTimer() {
|
private fun resetTimer() {
|
||||||
skipScope.launch {
|
skipScope.launch {
|
||||||
saveTimeToDb()
|
saveTimeToDb()
|
||||||
@@ -275,6 +295,7 @@ class TimerService : Service() {
|
|||||||
private fun skipTimer(fromButton: Boolean = false) {
|
private fun skipTimer(fromButton: Boolean = false) {
|
||||||
skipScope.launch {
|
skipScope.launch {
|
||||||
saveTimeToDb()
|
saveTimeToDb()
|
||||||
|
updateProgressSegments()
|
||||||
showTimerNotification(0, paused = true, complete = !fromButton)
|
showTimerNotification(0, paused = true, complete = !fromButton)
|
||||||
startTime = 0L
|
startTime = 0L
|
||||||
pauseTime = 0L
|
pauseTime = 0L
|
||||||
@@ -380,16 +401,6 @@ class TimerService : Service() {
|
|||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
runBlocking {
|
|
||||||
job.cancel()
|
|
||||||
saveTimeToDb()
|
|
||||||
notificationManager.cancel(1)
|
|
||||||
alarm?.release()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class Actions {
|
enum class Actions {
|
||||||
TOGGLE, SKIP, RESET, STOP_ALARM, UPDATE_ALARM_TONE
|
TOGGLE, SKIP, RESET, STOP_ALARM, UPDATE_ALARM_TONE
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ import androidx.compose.ui.platform.LocalLayoutDirection
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation3.runtime.entry
|
|
||||||
import androidx.navigation3.runtime.entryProvider
|
import androidx.navigation3.runtime.entryProvider
|
||||||
import androidx.navigation3.runtime.rememberNavBackStack
|
import androidx.navigation3.runtime.rememberNavBackStack
|
||||||
import androidx.navigation3.ui.NavDisplay
|
import androidx.navigation3.ui.NavDisplay
|
||||||
@@ -46,7 +45,6 @@ import org.nsh07.pomodoro.MainActivity.Companion.screens
|
|||||||
import org.nsh07.pomodoro.service.TimerService
|
import org.nsh07.pomodoro.service.TimerService
|
||||||
import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot
|
import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot
|
||||||
import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot
|
import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot
|
||||||
import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel
|
|
||||||
import org.nsh07.pomodoro.ui.timerScreen.AlarmDialog
|
import org.nsh07.pomodoro.ui.timerScreen.AlarmDialog
|
||||||
import org.nsh07.pomodoro.ui.timerScreen.TimerScreen
|
import org.nsh07.pomodoro.ui.timerScreen.TimerScreen
|
||||||
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction
|
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction
|
||||||
@@ -56,8 +54,7 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
|
|||||||
@Composable
|
@Composable
|
||||||
fun AppScreen(
|
fun AppScreen(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory),
|
timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory)
|
||||||
statsViewModel: StatsViewModel = viewModel(factory = StatsViewModel.Factory)
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
@@ -194,7 +191,6 @@ fun AppScreen(
|
|||||||
entry<Screen.Stats> {
|
entry<Screen.Stats> {
|
||||||
StatsScreenRoot(
|
StatsScreenRoot(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
viewModel = statsViewModel,
|
|
||||||
modifier = modifier.padding(
|
modifier = modifier.padding(
|
||||||
start = contentPadding.calculateStartPadding(layoutDirection),
|
start = contentPadding.calculateStartPadding(layoutDirection),
|
||||||
end = contentPadding.calculateEndPadding(layoutDirection),
|
end = contentPadding.calculateEndPadding(layoutDirection),
|
||||||
|
|||||||
80
app/src/main/java/org/nsh07/pomodoro/ui/ClickableListItem.kt
Normal file
80
app/src/main/java/org/nsh07/pomodoro/ui/ClickableListItem.kt
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package org.nsh07.pomodoro.ui
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.ListItemColors
|
||||||
|
import androidx.compose.material3.ListItemDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme.motionScheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ClickableListItem(
|
||||||
|
headlineContent: @Composable (() -> Unit),
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
overlineContent: @Composable (() -> Unit)? = null,
|
||||||
|
supportingContent: @Composable (() -> Unit)? = null,
|
||||||
|
leadingContent: @Composable (() -> Unit)? = null,
|
||||||
|
trailingContent: @Composable (() -> Unit)? = null,
|
||||||
|
colors: ListItemColors = ListItemDefaults.colors(),
|
||||||
|
tonalElevation: Dp = ListItemDefaults.Elevation,
|
||||||
|
shadowElevation: Dp = ListItemDefaults.Elevation,
|
||||||
|
items: Int,
|
||||||
|
index: Int,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val isPressed by interactionSource.collectIsPressedAsState()
|
||||||
|
|
||||||
|
val top by animateDpAsState(
|
||||||
|
if (isPressed) 40.dp
|
||||||
|
else {
|
||||||
|
if (items == 1 || index == 0) 20.dp
|
||||||
|
else 4.dp
|
||||||
|
},
|
||||||
|
motionScheme.fastSpatialSpec()
|
||||||
|
)
|
||||||
|
val bottom by animateDpAsState(
|
||||||
|
if (isPressed) 40.dp
|
||||||
|
else {
|
||||||
|
if (items == 1 || index == items - 1) 20.dp
|
||||||
|
else 4.dp
|
||||||
|
},
|
||||||
|
motionScheme.fastSpatialSpec()
|
||||||
|
)
|
||||||
|
|
||||||
|
ListItem(
|
||||||
|
headlineContent = headlineContent,
|
||||||
|
modifier = modifier
|
||||||
|
.clip(
|
||||||
|
RoundedCornerShape(
|
||||||
|
topStart = top,
|
||||||
|
topEnd = top,
|
||||||
|
bottomStart = bottom,
|
||||||
|
bottomEnd = bottom
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.clickable(
|
||||||
|
onClick = onClick,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
),
|
||||||
|
overlineContent = overlineContent,
|
||||||
|
supportingContent = supportingContent,
|
||||||
|
leadingContent = leadingContent,
|
||||||
|
trailingContent = trailingContent,
|
||||||
|
colors = colors,
|
||||||
|
tonalElevation = tonalElevation,
|
||||||
|
shadowElevation = shadowElevation
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
package org.nsh07.pomodoro.ui.settingsScreen
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
|
import androidx.compose.material3.AlertDialogDefaults
|
||||||
|
import androidx.compose.material3.BasicAlertDialog
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||||
|
import androidx.compose.material3.MaterialTheme.shapes
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
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.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.util.fastForEach
|
||||||
|
import org.nsh07.pomodoro.R
|
||||||
|
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ColorPickerButton(
|
||||||
|
color: Color,
|
||||||
|
isSelected: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
shapes = IconButtonDefaults.shapes(),
|
||||||
|
colors = IconButtonDefaults.iconButtonColors(containerColor = color),
|
||||||
|
modifier = modifier.size(48.dp),
|
||||||
|
onClick = onClick
|
||||||
|
) {
|
||||||
|
AnimatedContent(isSelected) { isSelected ->
|
||||||
|
when (isSelected) {
|
||||||
|
true -> Icon(
|
||||||
|
painterResource(R.drawable.check),
|
||||||
|
tint = Color.Black,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
|
||||||
|
else ->
|
||||||
|
if (color == Color.White) Icon(
|
||||||
|
painterResource(R.drawable.colors),
|
||||||
|
tint = Color.Black,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ColorSchemePickerDialog(
|
||||||
|
currentColor: Color,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
setShowDialog: (Boolean) -> Unit,
|
||||||
|
onColorChange: (Color) -> Unit,
|
||||||
|
) {
|
||||||
|
val colorSchemes = listOf(
|
||||||
|
Color(0xfffeb4a7), Color(0xffffb3c0), Color(0xfffcaaff), Color(0xffb9c3ff),
|
||||||
|
Color(0xff62d3ff), Color(0xff44d9f1), Color(0xff52dbc9), Color(0xff78dd77),
|
||||||
|
Color(0xff9fd75c), Color(0xffc1d02d), Color(0xfffabd00), Color(0xffffb86e),
|
||||||
|
Color.White
|
||||||
|
)
|
||||||
|
|
||||||
|
BasicAlertDialog(
|
||||||
|
onDismissRequest = { setShowDialog(false) },
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.wrapContentHeight(),
|
||||||
|
color = colorScheme.surfaceContainer,
|
||||||
|
shape = shapes.extraLarge,
|
||||||
|
tonalElevation = AlertDialogDefaults.TonalElevation
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(24.dp)) {
|
||||||
|
Text(
|
||||||
|
text = "Choose color scheme",
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Column(Modifier.align(Alignment.CenterHorizontally)) {
|
||||||
|
(0..11 step 4).forEach {
|
||||||
|
Row {
|
||||||
|
colorSchemes.slice(it..it + 3).fastForEach { color ->
|
||||||
|
ColorPickerButton(
|
||||||
|
color,
|
||||||
|
color == currentColor,
|
||||||
|
modifier = Modifier.padding(4.dp)
|
||||||
|
) {
|
||||||
|
onColorChange(color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ColorPickerButton(
|
||||||
|
colorSchemes.last(),
|
||||||
|
colorSchemes.last() == currentColor,
|
||||||
|
modifier = Modifier.padding(4.dp)
|
||||||
|
) {
|
||||||
|
onColorChange(colorSchemes.last())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
shapes = ButtonDefaults.shapes(),
|
||||||
|
onClick = { setShowDialog(false) },
|
||||||
|
modifier = Modifier.align(Alignment.End)
|
||||||
|
) {
|
||||||
|
Text("Ok")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun ColorPickerDialogPreview() {
|
||||||
|
var currentColor by remember { mutableStateOf(Color(0xfffeb4a7)) }
|
||||||
|
TomatoTheme(darkTheme = true) {
|
||||||
|
ColorSchemePickerDialog(
|
||||||
|
currentColor,
|
||||||
|
setShowDialog = {},
|
||||||
|
onColorChange = { currentColor = it }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* 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.settingsScreen
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import org.nsh07.pomodoro.R
|
||||||
|
import org.nsh07.pomodoro.ui.ClickableListItem
|
||||||
|
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ColorSchemePickerListItem(
|
||||||
|
color: Color,
|
||||||
|
items: Int,
|
||||||
|
index: Int,
|
||||||
|
onColorChange: (Color) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
var showDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (showDialog) {
|
||||||
|
ColorSchemePickerDialog(
|
||||||
|
currentColor = color,
|
||||||
|
setShowDialog = { showDialog = it },
|
||||||
|
onColorChange = onColorChange
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ClickableListItem(
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.palette),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = colorScheme.primary
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = { Text("Color scheme") },
|
||||||
|
supportingContent = {
|
||||||
|
Text(
|
||||||
|
if (color == Color.White) "Dynamic"
|
||||||
|
else "Color"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = listItemColors,
|
||||||
|
items = items,
|
||||||
|
index = index,
|
||||||
|
modifier = modifier.fillMaxWidth()
|
||||||
|
) { showDialog = true }
|
||||||
|
}
|
||||||
@@ -53,6 +53,8 @@ import androidx.compose.material3.TopAppBar
|
|||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberSliderState
|
import androidx.compose.material3.rememberSliderState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -61,6 +63,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
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
|
||||||
@@ -74,14 +77,17 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import org.nsh07.pomodoro.R
|
import org.nsh07.pomodoro.R
|
||||||
import org.nsh07.pomodoro.service.TimerService
|
import org.nsh07.pomodoro.service.TimerService
|
||||||
|
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState
|
||||||
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
|
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
|
||||||
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
||||||
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
|
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
|
||||||
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
|
||||||
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
||||||
|
import org.nsh07.pomodoro.utils.toColor
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -90,6 +96,12 @@ fun SettingsScreenRoot(
|
|||||||
viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory)
|
viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory)
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
viewModel.runTextFieldFlowCollection()
|
||||||
|
onDispose { viewModel.cancelTextFieldFlowCollection() }
|
||||||
|
}
|
||||||
|
|
||||||
val focusTimeInputFieldState = rememberSaveable(saver = TextFieldState.Saver) {
|
val focusTimeInputFieldState = rememberSaveable(saver = TextFieldState.Saver) {
|
||||||
viewModel.focusTimeTextFieldState
|
viewModel.focusTimeTextFieldState
|
||||||
}
|
}
|
||||||
@@ -104,6 +116,8 @@ fun SettingsScreenRoot(
|
|||||||
val vibrateEnabled by viewModel.vibrateEnabled.collectAsStateWithLifecycle(true)
|
val vibrateEnabled by viewModel.vibrateEnabled.collectAsStateWithLifecycle(true)
|
||||||
val alarmSound by viewModel.alarmSound.collectAsStateWithLifecycle(viewModel.currentAlarmSound)
|
val alarmSound by viewModel.alarmSound.collectAsStateWithLifecycle(viewModel.currentAlarmSound)
|
||||||
|
|
||||||
|
val preferencesState by viewModel.preferencesState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val sessionsSliderState = rememberSaveable(
|
val sessionsSliderState = rememberSaveable(
|
||||||
saver = SliderState.Saver(
|
saver = SliderState.Saver(
|
||||||
viewModel.sessionsSliderState.onValueChangeFinished,
|
viewModel.sessionsSliderState.onValueChangeFinished,
|
||||||
@@ -114,6 +128,7 @@ fun SettingsScreenRoot(
|
|||||||
}
|
}
|
||||||
|
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
|
preferencesState = preferencesState,
|
||||||
focusTimeInputFieldState = focusTimeInputFieldState,
|
focusTimeInputFieldState = focusTimeInputFieldState,
|
||||||
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
|
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
|
||||||
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
|
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
|
||||||
@@ -123,6 +138,7 @@ fun SettingsScreenRoot(
|
|||||||
alarmSound = alarmSound,
|
alarmSound = alarmSound,
|
||||||
onAlarmEnabledChange = viewModel::saveAlarmEnabled,
|
onAlarmEnabledChange = viewModel::saveAlarmEnabled,
|
||||||
onVibrateEnabledChange = viewModel::saveVibrateEnabled,
|
onVibrateEnabledChange = viewModel::saveVibrateEnabled,
|
||||||
|
onBlackThemeChange = viewModel::saveBlackTheme,
|
||||||
onAlarmSoundChanged = {
|
onAlarmSoundChanged = {
|
||||||
viewModel.saveAlarmSound(it)
|
viewModel.saveAlarmSound(it)
|
||||||
Intent(context, TimerService::class.java).apply {
|
Intent(context, TimerService::class.java).apply {
|
||||||
@@ -130,6 +146,8 @@ fun SettingsScreenRoot(
|
|||||||
context.startService(this)
|
context.startService(this)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onThemeChange = viewModel::saveTheme,
|
||||||
|
onColorSchemeChange = viewModel::saveColorScheme,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -137,6 +155,7 @@ fun SettingsScreenRoot(
|
|||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun SettingsScreen(
|
private fun SettingsScreen(
|
||||||
|
preferencesState: PreferencesState,
|
||||||
focusTimeInputFieldState: TextFieldState,
|
focusTimeInputFieldState: TextFieldState,
|
||||||
shortBreakTimeInputFieldState: TextFieldState,
|
shortBreakTimeInputFieldState: TextFieldState,
|
||||||
longBreakTimeInputFieldState: TextFieldState,
|
longBreakTimeInputFieldState: TextFieldState,
|
||||||
@@ -146,7 +165,10 @@ private fun SettingsScreen(
|
|||||||
alarmSound: String,
|
alarmSound: String,
|
||||||
onAlarmEnabledChange: (Boolean) -> Unit,
|
onAlarmEnabledChange: (Boolean) -> Unit,
|
||||||
onVibrateEnabledChange: (Boolean) -> Unit,
|
onVibrateEnabledChange: (Boolean) -> Unit,
|
||||||
|
onBlackThemeChange: (Boolean) -> Unit,
|
||||||
onAlarmSoundChanged: (Uri?) -> Unit,
|
onAlarmSoundChanged: (Uri?) -> Unit,
|
||||||
|
onThemeChange: (String) -> Unit,
|
||||||
|
onColorSchemeChange: (Color) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||||
@@ -154,7 +176,31 @@ private fun SettingsScreen(
|
|||||||
checkedIconColor = colorScheme.primary,
|
checkedIconColor = colorScheme.primary,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val themeMap: Map<String, Pair<Int, String>> = remember {
|
||||||
|
mapOf(
|
||||||
|
"auto" to Pair(
|
||||||
|
R.drawable.brightness_auto,
|
||||||
|
"System default"
|
||||||
|
),
|
||||||
|
"light" to Pair(R.drawable.light_mode, "Light"),
|
||||||
|
"dark" to Pair(R.drawable.dark_mode, "Dark")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val reverseThemeMap: Map<String, String> = remember {
|
||||||
|
mapOf(
|
||||||
|
"System default" to "auto",
|
||||||
|
"Light" to "light",
|
||||||
|
"Dark" to "dark"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
var alarmName by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
alarmName = RingtoneManager.getRingtone(context, alarmSound.toUri())
|
||||||
|
?.getTitle(context) ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
val ringtonePickerLauncher = rememberLauncherForActivityResult(
|
val ringtonePickerLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.StartActivityForResult()
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
@@ -180,8 +226,15 @@ private fun SettingsScreen(
|
|||||||
putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, alarmSound.toUri())
|
putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, alarmSound.toUri())
|
||||||
}
|
}
|
||||||
|
|
||||||
val switchItems = remember(alarmEnabled, vibrateEnabled) {
|
val switchItems = remember(preferencesState.blackTheme, alarmEnabled, vibrateEnabled) {
|
||||||
listOf(
|
listOf(
|
||||||
|
SettingsSwitchItem(
|
||||||
|
checked = preferencesState.blackTheme,
|
||||||
|
icon = R.drawable.contrast,
|
||||||
|
label = "Black theme",
|
||||||
|
description = "Use a pure black dark theme",
|
||||||
|
onClick = onBlackThemeChange
|
||||||
|
),
|
||||||
SettingsSwitchItem(
|
SettingsSwitchItem(
|
||||||
checked = alarmEnabled,
|
checked = alarmEnabled,
|
||||||
icon = R.drawable.alarm_on,
|
icon = R.drawable.alarm_on,
|
||||||
@@ -314,31 +367,83 @@ private fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = listItemColors,
|
colors = listItemColors,
|
||||||
modifier = Modifier.clip(topListItemShape)
|
modifier = Modifier.clip(cardShape)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item { Spacer(Modifier.height(12.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 = switchItems[0]
|
||||||
|
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(bottomListItemShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item { Spacer(Modifier.height(12.dp)) }
|
||||||
|
|
||||||
item {
|
item {
|
||||||
ListItem(
|
ListItem(
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
Icon(painterResource(R.drawable.alarm), null)
|
Icon(painterResource(R.drawable.alarm), null)
|
||||||
},
|
},
|
||||||
headlineContent = { Text("Alarm sound") },
|
headlineContent = { Text("Alarm sound") },
|
||||||
supportingContent = {
|
supportingContent = { Text(alarmName) },
|
||||||
Text(
|
|
||||||
remember(alarmSound) {
|
|
||||||
RingtoneManager.getRingtone(context, alarmSound.toUri())
|
|
||||||
.getTitle(context)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
colors = listItemColors,
|
colors = listItemColors,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(bottomListItemShape)
|
.clip(topListItemShape)
|
||||||
.clickable(onClick = { ringtonePickerLauncher.launch(intent) })
|
.clickable(onClick = { ringtonePickerLauncher.launch(intent) })
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item { Spacer(Modifier.height(12.dp)) }
|
itemsIndexed(switchItems.drop(1)) { index, item ->
|
||||||
itemsIndexed(switchItems) { index, item ->
|
|
||||||
ListItem(
|
ListItem(
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
Icon(painterResource(item.icon), contentDescription = null)
|
Icon(painterResource(item.icon), contentDescription = null)
|
||||||
@@ -371,8 +476,7 @@ private fun SettingsScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(
|
.clip(
|
||||||
when (index) {
|
when (index) {
|
||||||
0 -> topListItemShape
|
switchItems.lastIndex - 1 -> bottomListItemShape
|
||||||
switchItems.lastIndex -> bottomListItemShape
|
|
||||||
else -> middleListItemShape
|
else -> middleListItemShape
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -422,16 +526,20 @@ private fun SettingsScreen(
|
|||||||
fun SettingsScreenPreview() {
|
fun SettingsScreenPreview() {
|
||||||
TomatoTheme {
|
TomatoTheme {
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
focusTimeInputFieldState = rememberTextFieldState((25 * 60 * 1000).toString()),
|
preferencesState = PreferencesState(),
|
||||||
shortBreakTimeInputFieldState = rememberTextFieldState((5 * 60 * 1000).toString()),
|
focusTimeInputFieldState = rememberTextFieldState((25).toString()),
|
||||||
longBreakTimeInputFieldState = rememberTextFieldState((15 * 60 * 1000).toString()),
|
shortBreakTimeInputFieldState = rememberTextFieldState((5).toString()),
|
||||||
|
longBreakTimeInputFieldState = rememberTextFieldState((15).toString()),
|
||||||
sessionsSliderState = rememberSliderState(value = 3f, steps = 3, valueRange = 1f..5f),
|
sessionsSliderState = rememberSliderState(value = 3f, steps = 3, valueRange = 1f..5f),
|
||||||
alarmEnabled = true,
|
alarmEnabled = true,
|
||||||
vibrateEnabled = true,
|
vibrateEnabled = true,
|
||||||
alarmSound = "null",
|
alarmSound = "null",
|
||||||
onAlarmEnabledChange = {},
|
onAlarmEnabledChange = {},
|
||||||
onVibrateEnabledChange = {},
|
onVibrateEnabledChange = {},
|
||||||
|
onBlackThemeChange = {},
|
||||||
onAlarmSoundChanged = {},
|
onAlarmSoundChanged = {},
|
||||||
|
onThemeChange = {},
|
||||||
|
onColorSchemeChange = {},
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
* 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.settingsScreen
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
|
import androidx.compose.foundation.selection.selectable
|
||||||
|
import androidx.compose.foundation.selection.selectableGroup
|
||||||
|
import androidx.compose.material3.AlertDialogDefaults
|
||||||
|
import androidx.compose.material3.BasicAlertDialog
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||||
|
import androidx.compose.material3.MaterialTheme.shapes
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.nsh07.pomodoro.R
|
||||||
|
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
|
||||||
|
import org.nsh07.pomodoro.ui.theme.CustomColors.selectedListItemColors
|
||||||
|
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 ThemeDialog(
|
||||||
|
themeMap: Map<String, Pair<Int, String>>,
|
||||||
|
reverseThemeMap: Map<String, String>,
|
||||||
|
theme: String,
|
||||||
|
setShowThemeDialog: (Boolean) -> Unit,
|
||||||
|
onThemeChange: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val selectedOption =
|
||||||
|
remember { mutableStateOf(themeMap[theme]!!.second) }
|
||||||
|
|
||||||
|
BasicAlertDialog(
|
||||||
|
onDismissRequest = { setShowThemeDialog(false) }
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.wrapContentHeight(),
|
||||||
|
shape = shapes.extraLarge,
|
||||||
|
color = colorScheme.surfaceContainer,
|
||||||
|
tonalElevation = AlertDialogDefaults.TonalElevation
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(24.dp)) {
|
||||||
|
Text(
|
||||||
|
text = "Choose theme",
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
|
modifier = Modifier.selectableGroup()
|
||||||
|
) {
|
||||||
|
themeMap.entries.forEachIndexed { index: Int, pair: Map.Entry<String, Pair<Int, String>> ->
|
||||||
|
val text = pair.value.second
|
||||||
|
val selected = text == selectedOption.value
|
||||||
|
|
||||||
|
ListItem(
|
||||||
|
leadingContent = {
|
||||||
|
AnimatedContent(selected) {
|
||||||
|
if (it)
|
||||||
|
Icon(painterResource(R.drawable.check), null)
|
||||||
|
else
|
||||||
|
Icon(painterResource(pair.value.first), null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
headlineContent = {
|
||||||
|
Text(text = text, style = MaterialTheme.typography.bodyLarge)
|
||||||
|
},
|
||||||
|
colors = if (!selected) listItemColors else selectedListItemColors,
|
||||||
|
modifier = Modifier
|
||||||
|
.height(64.dp)
|
||||||
|
.clip(
|
||||||
|
when (index) {
|
||||||
|
0 -> topListItemShape
|
||||||
|
themeMap.size - 1 -> bottomListItemShape
|
||||||
|
else -> middleListItemShape
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.selectable(
|
||||||
|
selected = (text == selectedOption.value),
|
||||||
|
onClick = {
|
||||||
|
selectedOption.value = text
|
||||||
|
onThemeChange(reverseThemeMap[selectedOption.value]!!)
|
||||||
|
},
|
||||||
|
role = Role.RadioButton
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
TextButton(
|
||||||
|
shapes = ButtonDefaults.shapes(),
|
||||||
|
onClick = { setShowThemeDialog(false) },
|
||||||
|
modifier = Modifier.align(Alignment.End)
|
||||||
|
) {
|
||||||
|
Text("Ok")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* 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.settingsScreen
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import org.nsh07.pomodoro.ui.ClickableListItem
|
||||||
|
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ThemePickerListItem(
|
||||||
|
theme: String,
|
||||||
|
themeMap: Map<String, Pair<Int, String>>,
|
||||||
|
reverseThemeMap: Map<String, String>,
|
||||||
|
items: Int,
|
||||||
|
index: Int,
|
||||||
|
onThemeChange: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
var showDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (showDialog) {
|
||||||
|
ThemeDialog(
|
||||||
|
themeMap = themeMap,
|
||||||
|
reverseThemeMap = reverseThemeMap,
|
||||||
|
theme = theme,
|
||||||
|
setShowThemeDialog = { showDialog = it },
|
||||||
|
onThemeChange = onThemeChange
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ClickableListItem(
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(themeMap[theme]!!.first),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = { Text("Theme") },
|
||||||
|
supportingContent = {
|
||||||
|
Text(themeMap[theme]!!.second)
|
||||||
|
},
|
||||||
|
colors = listItemColors,
|
||||||
|
items = items,
|
||||||
|
index = index,
|
||||||
|
modifier = modifier.fillMaxWidth()
|
||||||
|
) { showDialog = true }
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* 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.settingsScreen.viewModel
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class PreferencesState(
|
||||||
|
val theme: String = "auto",
|
||||||
|
val colorScheme: String = Color.White.toString(),
|
||||||
|
val blackTheme: Boolean = false
|
||||||
|
)
|
||||||
@@ -12,6 +12,7 @@ import androidx.compose.foundation.text.input.TextFieldState
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.SliderState
|
import androidx.compose.material3.SliderState
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
|
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
|
||||||
@@ -20,8 +21,13 @@ import androidx.lifecycle.viewmodel.initializer
|
|||||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.nsh07.pomodoro.TomatoApplication
|
import org.nsh07.pomodoro.TomatoApplication
|
||||||
import org.nsh07.pomodoro.data.AppPreferenceRepository
|
import org.nsh07.pomodoro.data.AppPreferenceRepository
|
||||||
@@ -32,12 +38,18 @@ class SettingsViewModel(
|
|||||||
private val preferenceRepository: AppPreferenceRepository,
|
private val preferenceRepository: AppPreferenceRepository,
|
||||||
private val timerRepository: TimerRepository
|
private val timerRepository: TimerRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val focusTimeTextFieldState =
|
private val _preferencesState = MutableStateFlow(PreferencesState())
|
||||||
|
val preferencesState = _preferencesState.asStateFlow()
|
||||||
|
|
||||||
|
val focusTimeTextFieldState by lazy {
|
||||||
TextFieldState((timerRepository.focusTime / 60000).toString())
|
TextFieldState((timerRepository.focusTime / 60000).toString())
|
||||||
val shortBreakTimeTextFieldState =
|
}
|
||||||
|
val shortBreakTimeTextFieldState by lazy {
|
||||||
TextFieldState((timerRepository.shortBreakTime / 60000).toString())
|
TextFieldState((timerRepository.shortBreakTime / 60000).toString())
|
||||||
val longBreakTimeTextFieldState =
|
}
|
||||||
|
val longBreakTimeTextFieldState by lazy {
|
||||||
TextFieldState((timerRepository.longBreakTime / 60000).toString())
|
TextFieldState((timerRepository.longBreakTime / 60000).toString())
|
||||||
|
}
|
||||||
|
|
||||||
val sessionsSliderState = SliderState(
|
val sessionsSliderState = SliderState(
|
||||||
value = timerRepository.sessionLength.toFloat(),
|
value = timerRepository.sessionLength.toFloat(),
|
||||||
@@ -48,6 +60,11 @@ class SettingsViewModel(
|
|||||||
|
|
||||||
val currentAlarmSound = timerRepository.alarmSoundUri.toString()
|
val currentAlarmSound = timerRepository.alarmSoundUri.toString()
|
||||||
|
|
||||||
|
private val flowCollectionJob = SupervisorJob()
|
||||||
|
private val focusFlowCollectionJob = Job(flowCollectionJob)
|
||||||
|
private val shortBreakFlowCollectionJob = Job(flowCollectionJob)
|
||||||
|
private val longBreakFlowCollectionJob = Job(flowCollectionJob)
|
||||||
|
|
||||||
val alarmSound =
|
val alarmSound =
|
||||||
preferenceRepository.getStringPreferenceFlow("alarm_sound").distinctUntilChanged()
|
preferenceRepository.getStringPreferenceFlow("alarm_sound").distinctUntilChanged()
|
||||||
val alarmEnabled =
|
val alarmEnabled =
|
||||||
@@ -56,41 +73,21 @@ class SettingsViewModel(
|
|||||||
preferenceRepository.getBooleanPreferenceFlow("vibrate_enabled").distinctUntilChanged()
|
preferenceRepository.getBooleanPreferenceFlow("vibrate_enabled").distinctUntilChanged()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch {
|
||||||
snapshotFlow { focusTimeTextFieldState.text }
|
val theme = preferenceRepository.getStringPreference("theme")
|
||||||
.debounce(500)
|
?: preferenceRepository.saveStringPreference("theme", "auto")
|
||||||
.collect {
|
val colorScheme = preferenceRepository.getStringPreference("color_scheme")
|
||||||
if (it.isNotEmpty()) {
|
?: preferenceRepository.saveStringPreference("color_scheme", Color.White.toString())
|
||||||
timerRepository.focusTime = preferenceRepository.saveIntPreference(
|
val blackTheme = preferenceRepository.getBooleanPreference("black_theme")
|
||||||
"focus_time",
|
?: preferenceRepository.saveBooleanPreference("black_theme", false)
|
||||||
it.toString().toInt() * 60 * 1000
|
|
||||||
).toLong()
|
_preferencesState.update { currentState ->
|
||||||
}
|
currentState.copy(
|
||||||
}
|
theme = theme,
|
||||||
}
|
colorScheme = colorScheme,
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
blackTheme = blackTheme
|
||||||
snapshotFlow { shortBreakTimeTextFieldState.text }
|
)
|
||||||
.debounce(500)
|
}
|
||||||
.collect {
|
|
||||||
if (it.isNotEmpty()) {
|
|
||||||
timerRepository.shortBreakTime = preferenceRepository.saveIntPreference(
|
|
||||||
"short_break_time",
|
|
||||||
it.toString().toInt() * 60 * 1000
|
|
||||||
).toLong()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
snapshotFlow { longBreakTimeTextFieldState.text }
|
|
||||||
.debounce(500)
|
|
||||||
.collect {
|
|
||||||
if (it.isNotEmpty()) {
|
|
||||||
timerRepository.longBreakTime = preferenceRepository.saveIntPreference(
|
|
||||||
"long_break_time",
|
|
||||||
it.toString().toInt() * 60 * 1000
|
|
||||||
).toLong()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,25 +100,96 @@ class SettingsViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun runTextFieldFlowCollection() {
|
||||||
|
viewModelScope.launch(focusFlowCollectionJob + Dispatchers.IO) {
|
||||||
|
snapshotFlow { focusTimeTextFieldState.text }
|
||||||
|
.debounce(500)
|
||||||
|
.collect {
|
||||||
|
if (it.isNotEmpty()) {
|
||||||
|
timerRepository.focusTime = it.toString().toLong() * 60 * 1000
|
||||||
|
preferenceRepository.saveIntPreference(
|
||||||
|
"focus_time",
|
||||||
|
timerRepository.focusTime.toInt()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModelScope.launch(shortBreakFlowCollectionJob + Dispatchers.IO) {
|
||||||
|
snapshotFlow { shortBreakTimeTextFieldState.text }
|
||||||
|
.debounce(500)
|
||||||
|
.collect {
|
||||||
|
if (it.isNotEmpty()) {
|
||||||
|
timerRepository.shortBreakTime = it.toString().toLong() * 60 * 1000
|
||||||
|
preferenceRepository.saveIntPreference(
|
||||||
|
"short_break_time",
|
||||||
|
timerRepository.shortBreakTime.toInt()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModelScope.launch(longBreakFlowCollectionJob + Dispatchers.IO) {
|
||||||
|
snapshotFlow { longBreakTimeTextFieldState.text }
|
||||||
|
.debounce(500)
|
||||||
|
.collect {
|
||||||
|
if (it.isNotEmpty()) {
|
||||||
|
timerRepository.longBreakTime = it.toString().toLong() * 60 * 1000
|
||||||
|
preferenceRepository.saveIntPreference(
|
||||||
|
"long_break_time",
|
||||||
|
timerRepository.longBreakTime.toInt()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelTextFieldFlowCollection() = flowCollectionJob.cancel()
|
||||||
|
|
||||||
fun saveAlarmEnabled(enabled: Boolean) {
|
fun saveAlarmEnabled(enabled: Boolean) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
preferenceRepository.saveBooleanPreference("alarm_enabled", enabled)
|
|
||||||
timerRepository.alarmEnabled = enabled
|
timerRepository.alarmEnabled = enabled
|
||||||
|
preferenceRepository.saveBooleanPreference("alarm_enabled", enabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveVibrateEnabled(enabled: Boolean) {
|
fun saveVibrateEnabled(enabled: Boolean) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
preferenceRepository.saveBooleanPreference("vibrate_enabled", enabled)
|
|
||||||
timerRepository.vibrateEnabled = enabled
|
timerRepository.vibrateEnabled = enabled
|
||||||
|
preferenceRepository.saveBooleanPreference("vibrate_enabled", enabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveAlarmSound(uri: Uri?) {
|
fun saveAlarmSound(uri: Uri?) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
timerRepository.alarmSoundUri = uri
|
||||||
preferenceRepository.saveStringPreference("alarm_sound", uri.toString())
|
preferenceRepository.saveStringPreference("alarm_sound", uri.toString())
|
||||||
}
|
}
|
||||||
timerRepository.alarmSoundUri = uri
|
}
|
||||||
|
|
||||||
|
fun saveColorScheme(colorScheme: Color) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_preferencesState.update { currentState ->
|
||||||
|
currentState.copy(colorScheme = colorScheme.toString())
|
||||||
|
}
|
||||||
|
preferenceRepository.saveStringPreference("color_scheme", colorScheme.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveTheme(theme: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_preferencesState.update { currentState ->
|
||||||
|
currentState.copy(theme = theme)
|
||||||
|
}
|
||||||
|
preferenceRepository.saveStringPreference("theme", theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveBlackTheme(blackTheme: Boolean) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_preferencesState.update { currentState ->
|
||||||
|
currentState.copy(blackTheme = blackTheme)
|
||||||
|
}
|
||||||
|
preferenceRepository.saveBooleanPreference("black_theme", blackTheme)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -18,15 +18,27 @@ val PurpleGrey40 = Color(0xFF625b71)
|
|||||||
val Pink40 = Color(0xFF7D5260)
|
val Pink40 = Color(0xFF7D5260)
|
||||||
|
|
||||||
object CustomColors {
|
object CustomColors {
|
||||||
|
var black = false
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
val topBarColors: TopAppBarColors
|
val topBarColors: TopAppBarColors
|
||||||
@Composable get() {
|
@Composable get() =
|
||||||
return TopAppBarDefaults.topAppBarColors(
|
TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = colorScheme.surfaceContainer,
|
containerColor = if (!black) colorScheme.surfaceContainer else colorScheme.surface,
|
||||||
scrolledContainerColor = colorScheme.surfaceContainer
|
scrolledContainerColor = if (!black) colorScheme.surfaceContainer else colorScheme.surface
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
val listItemColors: ListItemColors
|
val listItemColors: ListItemColors
|
||||||
@Composable get() = ListItemDefaults.colors(containerColor = colorScheme.surfaceBright)
|
@Composable get() =
|
||||||
|
ListItemDefaults.colors(containerColor = if (!black) colorScheme.surfaceBright else colorScheme.surfaceContainerHigh)
|
||||||
|
|
||||||
|
val selectedListItemColors: ListItemColors
|
||||||
|
@Composable get() =
|
||||||
|
ListItemDefaults.colors(
|
||||||
|
containerColor = colorScheme.secondaryContainer,
|
||||||
|
headlineColor = colorScheme.secondary,
|
||||||
|
leadingIconColor = colorScheme.onSecondaryContainer,
|
||||||
|
supportingColor = colorScheme.onSecondaryFixedVariant,
|
||||||
|
trailingIconColor = colorScheme.onSecondaryFixedVariant
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,23 @@
|
|||||||
package org.nsh07.pomodoro.ui.theme
|
package org.nsh07.pomodoro.ui.theme
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.MaterialExpressiveTheme
|
||||||
|
import androidx.compose.material3.MotionScheme
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
import androidx.compose.material3.dynamicLightColorScheme
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
import androidx.compose.material3.lightColorScheme
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import com.materialkolor.dynamiccolor.ColorSpec
|
||||||
|
import com.materialkolor.rememberDynamicColorScheme
|
||||||
|
|
||||||
private val DarkColorScheme = darkColorScheme(
|
private val DarkColorScheme = darkColorScheme(
|
||||||
primary = Purple80,
|
primary = Purple80,
|
||||||
@@ -32,11 +41,13 @@ private val LightColorScheme = lightColorScheme(
|
|||||||
*/
|
*/
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun TomatoTheme(
|
fun TomatoTheme(
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
// Dynamic color is available on Android 12+
|
seedColor: Color = Color.White,
|
||||||
dynamicColor: Boolean = true,
|
dynamicColor: Boolean = true,
|
||||||
|
blackTheme: Boolean = false,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val colorScheme = when {
|
val colorScheme = when {
|
||||||
@@ -49,9 +60,33 @@ fun TomatoTheme(
|
|||||||
else -> LightColorScheme
|
else -> LightColorScheme
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialTheme(
|
val view = LocalView.current
|
||||||
colorScheme = colorScheme,
|
if (!view.isInEditMode) {
|
||||||
|
SideEffect {
|
||||||
|
val window = (view.context as Activity).window
|
||||||
|
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CustomColors.black = blackTheme && darkTheme
|
||||||
|
|
||||||
|
val dynamicColorScheme = rememberDynamicColorScheme(
|
||||||
|
seedColor = when (seedColor) {
|
||||||
|
Color.White -> colorScheme.primary
|
||||||
|
else -> seedColor
|
||||||
|
},
|
||||||
|
isDark = darkTheme,
|
||||||
|
specVersion = if (blackTheme && darkTheme) ColorSpec.SpecVersion.SPEC_2021 else ColorSpec.SpecVersion.SPEC_2025,
|
||||||
|
isAmoled = blackTheme && darkTheme
|
||||||
|
)
|
||||||
|
|
||||||
|
val scheme =
|
||||||
|
if (seedColor == Color.White && !(blackTheme && darkTheme)) colorScheme
|
||||||
|
else dynamicColorScheme
|
||||||
|
|
||||||
|
MaterialExpressiveTheme(
|
||||||
|
colorScheme = scheme,
|
||||||
typography = Typography,
|
typography = Typography,
|
||||||
|
motionScheme = MotionScheme.expressive(),
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -121,11 +121,11 @@ fun TimerScreen(
|
|||||||
if (!timerState.showBrandTitle) timerState.timerMode else TimerMode.BRAND,
|
if (!timerState.showBrandTitle) timerState.timerMode else TimerMode.BRAND,
|
||||||
transitionSpec = {
|
transitionSpec = {
|
||||||
slideInVertically(
|
slideInVertically(
|
||||||
animationSpec = motionScheme.slowSpatialSpec(),
|
animationSpec = motionScheme.defaultSpatialSpec(),
|
||||||
initialOffsetY = { (-it * 1.25).toInt() }
|
initialOffsetY = { (-it * 1.25).toInt() }
|
||||||
).togetherWith(
|
).togetherWith(
|
||||||
slideOutVertically(
|
slideOutVertically(
|
||||||
animationSpec = motionScheme.slowSpatialSpec(),
|
animationSpec = motionScheme.defaultSpatialSpec(),
|
||||||
targetOffsetY = { (it * 1.25).toInt() }
|
targetOffsetY = { (it * 1.25).toInt() }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -159,7 +159,7 @@ fun TimerScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
TimerMode.SHORT_BREAK -> Text(
|
TimerMode.SHORT_BREAK -> Text(
|
||||||
"Short Break",
|
"Short break",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontFamily = robotoFlexTopBar,
|
fontFamily = robotoFlexTopBar,
|
||||||
fontSize = 32.sp,
|
fontSize = 32.sp,
|
||||||
@@ -458,7 +458,7 @@ fun TimerScreen(
|
|||||||
Text(
|
Text(
|
||||||
when (timerState.nextTimerMode) {
|
when (timerState.nextTimerMode) {
|
||||||
TimerMode.FOCUS -> "Focus"
|
TimerMode.FOCUS -> "Focus"
|
||||||
TimerMode.SHORT_BREAK -> "Short Break"
|
TimerMode.SHORT_BREAK -> "Short break"
|
||||||
else -> "Long Break"
|
else -> "Long Break"
|
||||||
},
|
},
|
||||||
style = typography.titleMediumEmphasized
|
style = typography.titleMediumEmphasized
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ package org.nsh07.pomodoro.ui.timerScreen.viewModel
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import androidx.compose.material3.ColorScheme
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
@@ -52,105 +51,84 @@ class TimerViewModel(
|
|||||||
private var pauseTime = 0L
|
private var pauseTime = 0L
|
||||||
private var pauseDuration = 0L
|
private var pauseDuration = 0L
|
||||||
|
|
||||||
private lateinit var cs: ColorScheme
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
if (!timerRepository.serviceRunning)
|
||||||
timerRepository.focusTime =
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
preferenceRepository.getIntPreference("focus_time")?.toLong()
|
timerRepository.focusTime =
|
||||||
?: preferenceRepository.saveIntPreference(
|
preferenceRepository.getIntPreference("focus_time")?.toLong()
|
||||||
"focus_time",
|
?: preferenceRepository.saveIntPreference(
|
||||||
timerRepository.focusTime.toInt()
|
"focus_time",
|
||||||
).toLong()
|
timerRepository.focusTime.toInt()
|
||||||
timerRepository.shortBreakTime =
|
).toLong()
|
||||||
preferenceRepository.getIntPreference("short_break_time")?.toLong()
|
timerRepository.shortBreakTime =
|
||||||
?: preferenceRepository.saveIntPreference(
|
preferenceRepository.getIntPreference("short_break_time")?.toLong()
|
||||||
"short_break_time",
|
?: preferenceRepository.saveIntPreference(
|
||||||
timerRepository.shortBreakTime.toInt()
|
"short_break_time",
|
||||||
).toLong()
|
timerRepository.shortBreakTime.toInt()
|
||||||
timerRepository.longBreakTime =
|
).toLong()
|
||||||
preferenceRepository.getIntPreference("long_break_time")?.toLong()
|
timerRepository.longBreakTime =
|
||||||
?: preferenceRepository.saveIntPreference(
|
preferenceRepository.getIntPreference("long_break_time")?.toLong()
|
||||||
"long_break_time",
|
?: preferenceRepository.saveIntPreference(
|
||||||
timerRepository.longBreakTime.toInt()
|
"long_break_time",
|
||||||
).toLong()
|
timerRepository.longBreakTime.toInt()
|
||||||
timerRepository.sessionLength = preferenceRepository.getIntPreference("session_length")
|
).toLong()
|
||||||
?: preferenceRepository.saveIntPreference(
|
timerRepository.sessionLength =
|
||||||
"session_length",
|
preferenceRepository.getIntPreference("session_length")
|
||||||
timerRepository.sessionLength
|
?: preferenceRepository.saveIntPreference(
|
||||||
)
|
"session_length",
|
||||||
|
timerRepository.sessionLength
|
||||||
timerRepository.alarmEnabled =
|
|
||||||
preferenceRepository.getBooleanPreference("alarm_enabled")
|
|
||||||
?: preferenceRepository.saveBooleanPreference("alarm_enabled", true)
|
|
||||||
timerRepository.vibrateEnabled =
|
|
||||||
preferenceRepository.getBooleanPreference("vibrate_enabled")
|
|
||||||
?: preferenceRepository.saveBooleanPreference("vibrate_enabled", true)
|
|
||||||
|
|
||||||
timerRepository.alarmSoundUri = (
|
|
||||||
preferenceRepository.getStringPreference("alarm_sound")
|
|
||||||
?: preferenceRepository.saveStringPreference(
|
|
||||||
"alarm_sound",
|
|
||||||
(Settings.System.DEFAULT_ALARM_ALERT_URI
|
|
||||||
?: Settings.System.DEFAULT_RINGTONE_URI).toString()
|
|
||||||
)
|
)
|
||||||
).toUri()
|
|
||||||
|
|
||||||
resetTimer()
|
timerRepository.alarmEnabled =
|
||||||
|
preferenceRepository.getBooleanPreference("alarm_enabled")
|
||||||
|
?: preferenceRepository.saveBooleanPreference("alarm_enabled", true)
|
||||||
|
timerRepository.vibrateEnabled =
|
||||||
|
preferenceRepository.getBooleanPreference("vibrate_enabled")
|
||||||
|
?: preferenceRepository.saveBooleanPreference("vibrate_enabled", true)
|
||||||
|
|
||||||
var lastDate = statRepository.getLastDate()
|
timerRepository.alarmSoundUri = (
|
||||||
val today = LocalDate.now()
|
preferenceRepository.getStringPreference("alarm_sound")
|
||||||
|
?: preferenceRepository.saveStringPreference(
|
||||||
|
"alarm_sound",
|
||||||
|
(Settings.System.DEFAULT_ALARM_ALERT_URI
|
||||||
|
?: Settings.System.DEFAULT_RINGTONE_URI).toString()
|
||||||
|
)
|
||||||
|
).toUri()
|
||||||
|
|
||||||
// Fills dates between today and lastDate with 0s to ensure continuous history
|
_time.update { timerRepository.focusTime }
|
||||||
if (lastDate != null)
|
cycles = 0
|
||||||
while (ChronoUnit.DAYS.between(lastDate, today) > 0) {
|
startTime = 0L
|
||||||
lastDate = lastDate?.plusDays(1)
|
pauseTime = 0L
|
||||||
statRepository.insertStat(Stat(lastDate!!, 0, 0, 0, 0, 0))
|
pauseDuration = 0L
|
||||||
|
|
||||||
|
_timerState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
timerMode = TimerMode.FOCUS,
|
||||||
|
timeStr = millisecondsToStr(time.value),
|
||||||
|
totalTime = time.value,
|
||||||
|
nextTimerMode = if (timerRepository.sessionLength > 1) TimerMode.SHORT_BREAK else TimerMode.LONG_BREAK,
|
||||||
|
nextTimeStr = millisecondsToStr(if (timerRepository.sessionLength > 1) timerRepository.shortBreakTime else timerRepository.longBreakTime),
|
||||||
|
currentFocusCount = 1,
|
||||||
|
totalFocusCount = timerRepository.sessionLength
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
delay(1500)
|
var lastDate = statRepository.getLastDate()
|
||||||
|
val today = LocalDate.now()
|
||||||
|
|
||||||
_timerState.update { currentState ->
|
// Fills dates between today and lastDate with 0s to ensure continuous history
|
||||||
currentState.copy(showBrandTitle = false)
|
if (lastDate != null)
|
||||||
|
while (ChronoUnit.DAYS.between(lastDate, today) > 0) {
|
||||||
|
lastDate = lastDate?.plusDays(1)
|
||||||
|
statRepository.insertStat(Stat(lastDate!!, 0, 0, 0, 0, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(1500)
|
||||||
|
|
||||||
|
_timerState.update { currentState ->
|
||||||
|
currentState.copy(showBrandTitle = false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setCompositionLocals(colorScheme: ColorScheme) {
|
|
||||||
cs = colorScheme
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resetTimer() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
saveTimeToDb()
|
|
||||||
_time.update { timerRepository.focusTime }
|
|
||||||
cycles = 0
|
|
||||||
startTime = 0L
|
|
||||||
pauseTime = 0L
|
|
||||||
pauseDuration = 0L
|
|
||||||
|
|
||||||
_timerState.update { currentState ->
|
|
||||||
currentState.copy(
|
|
||||||
timerMode = TimerMode.FOCUS,
|
|
||||||
timeStr = millisecondsToStr(time.value),
|
|
||||||
totalTime = time.value,
|
|
||||||
nextTimerMode = if (timerRepository.sessionLength > 1) TimerMode.SHORT_BREAK else TimerMode.LONG_BREAK,
|
|
||||||
nextTimeStr = millisecondsToStr(if (timerRepository.sessionLength > 1) timerRepository.shortBreakTime else timerRepository.longBreakTime),
|
|
||||||
currentFocusCount = 1,
|
|
||||||
totalFocusCount = timerRepository.sessionLength
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun saveTimeToDb() {
|
|
||||||
when (timerState.value.timerMode) {
|
|
||||||
TimerMode.FOCUS -> statRepository
|
|
||||||
.addFocusTime((timerState.value.totalTime - time.value).coerceAtLeast(0L))
|
|
||||||
|
|
||||||
else -> statRepository
|
|
||||||
.addBreakTime((timerState.value.totalTime - time.value).coerceAtLeast(0L))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
package org.nsh07.pomodoro.utils
|
package org.nsh07.pomodoro.utils
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@@ -37,3 +38,25 @@ fun millisecondsToHoursMinutes(t: Long): String {
|
|||||||
TimeUnit.MILLISECONDS.toMinutes(t) % TimeUnit.HOURS.toMinutes(1)
|
TimeUnit.MILLISECONDS.toMinutes(t) % TimeUnit.HOURS.toMinutes(1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension function for [String] to convert it to a [androidx.compose.ui.graphics.Color]
|
||||||
|
*
|
||||||
|
* The base string must be of the format produced by [androidx.compose.ui.graphics.Color.toString],
|
||||||
|
* i.e, the color black with 100% opacity in sRGB would be represented by:
|
||||||
|
*
|
||||||
|
* Color(0.0, 0.0, 0.0, 1.0, sRGB IEC61966-2.1)
|
||||||
|
*/
|
||||||
|
fun String.toColor(): Color {
|
||||||
|
// Sample string: Color(0.0, 0.0, 0.0, 1.0, sRGB IEC61966-2.1)
|
||||||
|
val comma1 = this.indexOf(',')
|
||||||
|
val comma2 = this.indexOf(',', comma1 + 1)
|
||||||
|
val comma3 = this.indexOf(',', comma2 + 1)
|
||||||
|
val comma4 = this.indexOf(',', comma3 + 1)
|
||||||
|
|
||||||
|
val r = this.substringAfter('(').substringBefore(',').toFloat()
|
||||||
|
val g = this.slice(comma1 + 1..comma2 - 1).toFloat()
|
||||||
|
val b = this.slice(comma2 + 1..comma3 - 1).toFloat()
|
||||||
|
val a = this.slice(comma3 + 1..comma4 - 1).toFloat()
|
||||||
|
return Color(r, g, b, a)
|
||||||
|
}
|
||||||
|
|||||||
9
app/src/main/res/drawable/brightness_auto.xml
Normal file
9
app/src/main/res/drawable/brightness_auto.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<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="M312,640h64l16,-46q8,-21 31.5,-33.5T505,548q22,0 39.5,12.5T570,594l9,27q3,8 10.5,13.5T606,640q15,0 23.5,-12.5T633,601L519,299q-3,-9 -13.5,-14t-36.5,-5q-9,0 -17,5t-11,14L312,640ZM426,496 L478,346h4l52,150L426,496ZM346,800L240,800q-33,0 -56.5,-23.5T160,720v-106l-77,-78q-11,-12 -17,-26.5T60,480q0,-15 6,-29.5T83,424l77,-78v-106q0,-33 23.5,-56.5T240,160h106l78,-77q12,-11 26.5,-17t29.5,-6q15,0 29.5,6t26.5,17l78,77h106q33,0 56.5,23.5T800,240v106l77,78q11,12 17,26.5t6,29.5q0,15 -6,29.5T877,536l-77,78v106q0,33 -23.5,56.5T720,800L614,800l-78,77q-12,11 -26.5,17T480,900q-15,0 -29.5,-6T424,877l-78,-77Z" />
|
||||||
|
</vector>
|
||||||
16
app/src/main/res/drawable/colors.xml
Normal file
16
app/src/main/res/drawable/colors.xml
Normal 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="M346,820 L100,574q-10,-10 -15,-22t-5,-25q0,-13 5,-25t15,-22l230,-229 -75,-75q-13,-13 -13.5,-31t12.5,-32q13,-14 32,-14t33,14l367,367q10,10 14.5,22t4.5,25q0,13 -4.5,25T686,574L440,820q-10,10 -22,15t-25,5q-13,0 -25,-5t-22,-15ZM393,314L179,528h428L393,314ZM792,840q-36,0 -61,-25.5T706,752q0,-27 13.5,-51t30.5,-47l19,-24q9,-11 23.5,-11.5T816,629l20,25q16,23 30,47t14,51q0,37 -26,62.5T792,840Z" />
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/contrast.xml
Normal file
9
app/src/main/res/drawable/contrast.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<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,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880ZM520,797q119,-15 199.5,-104.5T800,480q0,-123 -80.5,-212.5T520,163v634Z" />
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/dark_mode.xml
Normal file
9
app/src/main/res/drawable/dark_mode.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<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,840q-151,0 -255.5,-104.5T120,480q0,-138 90,-239.5T440,122q13,-2 23,3.5t16,14.5q6,9 6.5,21t-7.5,23q-17,26 -25.5,55t-8.5,61q0,90 63,153t153,63q31,0 61.5,-9t54.5,-25q11,-7 22.5,-6.5T819,481q10,5 15.5,15t3.5,24q-14,138 -117.5,229T480,840Z" />
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/light_mode.xml
Normal file
9
app/src/main/res/drawable/light_mode.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<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,680q-83,0 -141.5,-58.5T280,480q0,-83 58.5,-141.5T480,280q83,0 141.5,58.5T680,480q0,83 -58.5,141.5T480,680ZM80,520q-17,0 -28.5,-11.5T40,480q0,-17 11.5,-28.5T80,440h80q17,0 28.5,11.5T200,480q0,17 -11.5,28.5T160,520L80,520ZM800,520q-17,0 -28.5,-11.5T760,480q0,-17 11.5,-28.5T800,440h80q17,0 28.5,11.5T920,480q0,17 -11.5,28.5T880,520h-80ZM480,200q-17,0 -28.5,-11.5T440,160v-80q0,-17 11.5,-28.5T480,40q17,0 28.5,11.5T520,80v80q0,17 -11.5,28.5T480,200ZM480,920q-17,0 -28.5,-11.5T440,880v-80q0,-17 11.5,-28.5T480,760q17,0 28.5,11.5T520,800v80q0,17 -11.5,28.5T480,920ZM226,282l-43,-42q-12,-11 -11.5,-28t11.5,-29q12,-12 29,-12t28,12l42,43q11,12 11,28t-11,28q-11,12 -27.5,11.5T226,282ZM720,777 L678,734q-11,-12 -11,-28.5t11,-27.5q11,-12 27.5,-11.5T734,678l43,42q12,11 11.5,28T777,777q-12,12 -29,12t-28,-12ZM678,282q-12,-11 -11.5,-27.5T678,226l42,-43q11,-12 28,-11.5t29,11.5q12,12 12,29t-12,28l-43,42q-12,11 -28,11t-28,-11ZM183,777q-12,-12 -12,-29t12,-28l43,-42q12,-11 28.5,-11t27.5,11q12,11 11.5,27.5T282,734l-42,43q-11,12 -28,11.5T183,777Z" />
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/palette.xml
Normal file
9
app/src/main/res/drawable/palette.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<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,880q-82,0 -155,-31.5t-127.5,-86Q143,708 111.5,635T80,480q0,-83 32.5,-156t88,-127Q256,143 330,111.5T488,80q80,0 151,27.5t124.5,76q53.5,48.5 85,115T880,442q0,115 -70,176.5T640,680h-74q-9,0 -12.5,5t-3.5,11q0,12 15,34.5t15,51.5q0,50 -27.5,74T480,880ZM260,520q26,0 43,-17t17,-43q0,-26 -17,-43t-43,-17q-26,0 -43,17t-17,43q0,26 17,43t43,17ZM380,360q26,0 43,-17t17,-43q0,-26 -17,-43t-43,-17q-26,0 -43,17t-17,43q0,26 17,43t43,17ZM580,360q26,0 43,-17t17,-43q0,-26 -17,-43t-43,-17q-26,0 -43,17t-17,43q0,26 17,43t43,17ZM700,520q26,0 43,-17t17,-43q0,-26 -17,-43t-43,-17q-26,0 -43,17t-17,43q0,26 17,43t43,17Z" />
|
||||||
|
</vector>
|
||||||
8
fastlane/metadata/android/en-US/changelogs/8.txt
Normal file
8
fastlane/metadata/android/en-US/changelogs/8.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
New features:
|
||||||
|
- You can now choose a custom theme and color scheme for the app's UI
|
||||||
|
- New pure black dark theme mode
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
- Average focus durations now do not include days with no activity
|
||||||
|
- Fix a critical bug that caused the app's timer state to reset to Focus whenever the app was closed from recents and then opened
|
||||||
|
- Replace the word "Reset" with "Exit" in the notification to make its purpose less ambiguous
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 348 KiB After Width: | Height: | Size: 386 KiB |
@@ -1,18 +1,19 @@
|
|||||||
[versions]
|
[versions]
|
||||||
activityCompose = "1.11.0"
|
activityCompose = "1.11.0"
|
||||||
adaptive = "1.1.0"
|
adaptive = "1.1.0"
|
||||||
agp = "8.11.1"
|
agp = "8.11.2"
|
||||||
composeBom = "2025.09.00"
|
composeBom = "2025.09.01"
|
||||||
coreKtx = "1.17.0"
|
coreKtx = "1.17.0"
|
||||||
espressoCore = "3.7.0"
|
espressoCore = "3.7.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.3.0"
|
junitVersion = "1.3.0"
|
||||||
kotlin = "2.2.20"
|
kotlin = "2.2.20"
|
||||||
ksp = "2.2.20-2.0.3"
|
ksp = "2.2.20-2.0.3"
|
||||||
lifecycleRuntimeKtx = "2.9.3"
|
lifecycleRuntimeKtx = "2.9.4"
|
||||||
navigation3Runtime = "1.0.0-alpha09"
|
materialKolor = "3.0.1"
|
||||||
room = "2.8.0"
|
navigation3 = "1.0.0-alpha10"
|
||||||
vico = "2.2.0-alpha.1"
|
room = "2.8.1"
|
||||||
|
vico = "2.2.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||||
@@ -25,8 +26,8 @@ androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecyc
|
|||||||
androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycleRuntimeKtx" }
|
androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycleRuntimeKtx" }
|
||||||
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
|
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
|
||||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "navigation3Runtime" }
|
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "navigation3" }
|
||||||
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "navigation3Runtime" }
|
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "navigation3" }
|
||||||
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
||||||
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
|
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
|
||||||
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
|
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
|
||||||
@@ -37,6 +38,7 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man
|
|||||||
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||||
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
|
material-kolor = { module = "com.materialkolor:material-kolor", version.ref = "materialKolor" }
|
||||||
vico-compose-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "vico" }
|
vico-compose-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "vico" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
|||||||
Reference in New Issue
Block a user