diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index e9f14c9..107cf83 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -31,7 +31,7 @@ android {
defaultConfig {
applicationId = "org.nsh07.pomodoro"
- minSdk = 26
+ minSdk = 27
targetSdk = 36
versionCode = 13
versionName = "1.5.0"
diff --git a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt
index baf7924..a98932a 100644
--- a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt
@@ -51,7 +51,10 @@ class MainActivity : ComponentActivity() {
appContainer.appTimerRepository.colorScheme = colorScheme
}
- AppScreen(timerViewModel = timerViewModel)
+ AppScreen(
+ timerViewModel = timerViewModel,
+ isAODEnabled = preferencesState.aodEnabled
+ )
}
}
}
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt
new file mode 100644
index 0000000..8225cc2
--- /dev/null
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt
@@ -0,0 +1,272 @@
+/*
+ * Copyright (c) 2025 Nishant Mishra
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nsh07.pomodoro.ui
+
+import android.app.Activity
+import android.view.WindowManager
+import androidx.activity.compose.LocalActivity
+import androidx.compose.animation.SharedTransitionLayout
+import androidx.compose.animation.SharedTransitionScope
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.animateIntAsState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.CircularWavyProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.MaterialTheme.colorScheme
+import androidx.compose.material3.MaterialTheme.motionScheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+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.graphics.StrokeCap
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.LocalWindowInfo
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
+import androidx.navigation3.ui.LocalNavAnimatedContentScope
+import kotlinx.coroutines.delay
+import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
+import org.nsh07.pomodoro.ui.theme.TomatoTheme
+import org.nsh07.pomodoro.ui.timerScreen.TimerScreen
+import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode
+import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
+import kotlin.random.Random
+
+/**
+ * Always On Display composable. Must be called within a [SharedTransitionScope] which allows
+ * animating the clock and progress indicator
+ *
+ * @param timerState [TimerState] instance. This must be the same instance as the one used on the
+ * root [TimerScreen] composable
+ * @param progress lambda that returns the current progress of the clock
+ * randomized offset for the clock to allow smooth motion with sharedBounds
+ */
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+fun SharedTransitionScope.AlwaysOnDisplay(
+ timerState: TimerState,
+ progress: () -> Float,
+ modifier: Modifier = Modifier
+) {
+ var sharedElementTransitionComplete by remember { mutableStateOf(false) }
+
+ val activity = LocalActivity.current
+ val density = LocalDensity.current
+ val windowInfo = LocalWindowInfo.current
+ val view = LocalView.current
+
+ val window = remember { (view.context as Activity).window }
+ val insetsController = remember { WindowCompat.getInsetsController(window, view) }
+
+ DisposableEffect(Unit) {
+ window.addFlags(
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
+ WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
+ )
+ activity?.setShowWhenLocked(true)
+ insetsController.apply {
+ hide(WindowInsetsCompat.Type.statusBars())
+ hide(WindowInsetsCompat.Type.navigationBars())
+ systemBarsBehavior =
+ WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ }
+
+ onDispose {
+ window.clearFlags(
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
+ WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
+ )
+ activity?.setShowWhenLocked(false)
+ insetsController.apply {
+ show(WindowInsetsCompat.Type.statusBars())
+ show(WindowInsetsCompat.Type.navigationBars())
+ systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
+ }
+ }
+ }
+
+ LaunchedEffect(Unit) {
+ delay(300)
+ sharedElementTransitionComplete = true
+ }
+
+ val primary by animateColorAsState(
+ if (sharedElementTransitionComplete) Color(0xFFA2A2A2)
+ else {
+ if (timerState.timerMode == TimerMode.FOCUS) colorScheme.primary
+ else colorScheme.tertiary
+ },
+ animationSpec = motionScheme.slowEffectsSpec()
+ )
+ val secondaryContainer by animateColorAsState(
+ if (sharedElementTransitionComplete) Color(0xFF1D1D1D)
+ else {
+ if (timerState.timerMode == TimerMode.FOCUS) colorScheme.secondaryContainer
+ else colorScheme.tertiaryContainer
+ },
+ animationSpec = motionScheme.slowEffectsSpec()
+ )
+ val surface by animateColorAsState(
+ if (sharedElementTransitionComplete) Color.Black
+ else colorScheme.surface,
+ animationSpec = motionScheme.slowEffectsSpec()
+ )
+ val onSurface by animateColorAsState(
+ if (sharedElementTransitionComplete) Color(0xFFE3E3E3)
+ else colorScheme.onSurface,
+ animationSpec = motionScheme.slowEffectsSpec()
+ )
+
+ var randomX by remember {
+ mutableIntStateOf(
+ Random.nextInt(
+ 16.dp.toIntPx(density),
+ windowInfo.containerSize.width - 266.dp.toIntPx(density)
+ )
+ )
+ }
+ var randomY by remember {
+ mutableIntStateOf(
+ Random.nextInt(
+ 16.dp.toIntPx(density),
+ windowInfo.containerSize.height - 266.dp.toIntPx(density)
+ )
+ )
+ }
+
+ LaunchedEffect(timerState.timeStr[1]) { // Randomize position every minute
+ if (sharedElementTransitionComplete) {
+ randomX = Random.nextInt(
+ 16.dp.toIntPx(density),
+ windowInfo.containerSize.width - 266.dp.toIntPx(density)
+ )
+ randomY = Random.nextInt(
+ 16.dp.toIntPx(density),
+ windowInfo.containerSize.height - 266.dp.toIntPx(density)
+ )
+ }
+ }
+
+ val x by animateIntAsState(randomX, motionScheme.slowSpatialSpec())
+ val y by animateIntAsState(randomY, motionScheme.slowSpatialSpec())
+
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ .background(surface)
+ ) {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.offset {
+ IntOffset(x, y)
+ }
+ ) {
+ if (timerState.timerMode == TimerMode.FOCUS) {
+ CircularProgressIndicator(
+ progress = progress,
+ modifier = Modifier
+ .sharedBounds(
+ sharedContentState = this@AlwaysOnDisplay.rememberSharedContentState("focus progress"),
+ animatedVisibilityScope = LocalNavAnimatedContentScope.current
+ )
+ .size(250.dp),
+ color = primary,
+ trackColor = secondaryContainer,
+ strokeWidth = 12.dp,
+ gapSize = 8.dp,
+ )
+ } else {
+ CircularWavyProgressIndicator(
+ progress = progress,
+ modifier = Modifier
+ .sharedBounds(
+ sharedContentState = this@AlwaysOnDisplay.rememberSharedContentState("break progress"),
+ animatedVisibilityScope = LocalNavAnimatedContentScope.current
+ )
+ .size(250.dp),
+ color = primary,
+ trackColor = secondaryContainer,
+ stroke = Stroke(
+ width = with(LocalDensity.current) {
+ 12.dp.toPx()
+ },
+ cap = StrokeCap.Round,
+ ),
+ trackStroke = Stroke(
+ width = with(LocalDensity.current) {
+ 12.dp.toPx()
+ },
+ cap = StrokeCap.Round,
+ ),
+ wavelength = 42.dp,
+ gapSize = 8.dp
+ )
+ }
+
+ Text(
+ text = timerState.timeStr,
+ style = TextStyle(
+ fontFamily = openRundeClock,
+ fontWeight = FontWeight.Bold,
+ fontSize = 56.sp,
+ letterSpacing = (-2).sp
+ ),
+ textAlign = TextAlign.Center,
+ color = onSurface,
+ maxLines = 1,
+ modifier = Modifier.sharedBounds(
+ sharedContentState = this@AlwaysOnDisplay.rememberSharedContentState("clock"),
+ animatedVisibilityScope = LocalNavAnimatedContentScope.current
+ )
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Preview
+@Composable
+private fun AlwaysOnDisplayPreview() {
+ val timerState = TimerState()
+ val progress = { 0.5f }
+ TomatoTheme {
+ SharedTransitionLayout {
+ AlwaysOnDisplay(
+ timerState = timerState,
+ progress = progress
+ )
+ }
+ }
+}
+
+fun Dp.toIntPx(density: Density) = with(density) { toPx().toInt() }
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt
index 59390fd..b26212e 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt
@@ -8,11 +8,14 @@
package org.nsh07.pomodoro.ui
import android.content.Intent
+import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.Crossfade
+import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleOut
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.padding
@@ -55,7 +58,8 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
@Composable
fun AppScreen(
modifier: Modifier = Modifier,
- timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory)
+ timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory),
+ isAODEnabled: Boolean
) {
val context = LocalContext.current
@@ -78,128 +82,159 @@ fun AppScreen(
}
}
+
Scaffold(
bottomBar = {
- val wide = remember {
- windowSizeClass.isWidthAtLeastBreakpoint(
- WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND
- )
- }
- ShortNavigationBar(
- arrangement =
- if (wide) ShortNavigationBarArrangement.Centered
- else ShortNavigationBarArrangement.EqualWeight
+ AnimatedVisibility(
+ backStack.last() !is Screen.AOD,
+ enter = fadeIn(),
+ exit = fadeOut()
) {
- screens.forEach {
- val selected = backStack.last() == it.route
- ShortNavigationBarItem(
- selected = selected,
- onClick = if (it.route != Screen.Timer) { // Ensure the backstack does not accumulate screens
- {
- if (backStack.size < 2) backStack.add(it.route)
- else backStack[1] = it.route
- }
- } else {
- { if (backStack.size > 1) backStack.removeAt(1) }
- },
- icon = {
- Crossfade(selected) { selected ->
- if (selected) Icon(painterResource(it.selectedIcon), null)
- else Icon(painterResource(it.unselectedIcon), null)
- }
- },
- iconPosition =
- if (wide) NavigationItemIconPosition.Start
- else NavigationItemIconPosition.Top,
- label = { Text(stringResource(it.label)) }
+ val wide = remember {
+ windowSizeClass.isWidthAtLeastBreakpoint(
+ WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND
)
}
+ ShortNavigationBar(
+ arrangement =
+ if (wide) ShortNavigationBarArrangement.Centered
+ else ShortNavigationBarArrangement.EqualWeight
+ ) {
+ screens.forEach {
+ val selected = backStack.last() == it.route
+ ShortNavigationBarItem(
+ selected = selected,
+ onClick = if (it.route != Screen.Timer) { // Ensure the backstack does not accumulate screens
+ {
+ if (backStack.size < 2) backStack.add(it.route)
+ else backStack[1] = it.route
+ }
+ } else {
+ { if (backStack.size > 1) backStack.removeAt(1) }
+ },
+ icon = {
+ Crossfade(selected) { selected ->
+ if (selected) Icon(painterResource(it.selectedIcon), null)
+ else Icon(painterResource(it.unselectedIcon), null)
+ }
+ },
+ iconPosition =
+ if (wide) NavigationItemIconPosition.Start
+ else NavigationItemIconPosition.Top,
+ label = { Text(stringResource(it.label)) }
+ )
+ }
+ }
}
}
) { contentPadding ->
- NavDisplay(
- backStack = backStack,
- onBack = { backStack.removeLastOrNull() },
- transitionSpec = {
- ContentTransform(
- fadeIn(motionScheme.defaultEffectsSpec()),
- fadeOut(motionScheme.defaultEffectsSpec())
- )
- },
- popTransitionSpec = {
- ContentTransform(
- fadeIn(motionScheme.defaultEffectsSpec()),
- fadeOut(motionScheme.defaultEffectsSpec())
- )
- },
- predictivePopTransitionSpec = {
- ContentTransform(
- fadeIn(motionScheme.defaultEffectsSpec()),
- fadeOut(motionScheme.defaultEffectsSpec()) +
- scaleOut(targetScale = 0.7f),
- )
- },
- entryProvider = entryProvider {
- entry {
- TimerScreen(
- timerState = uiState,
- progress = { progress },
- onAction = { action ->
- when (action) {
- TimerAction.ResetTimer ->
- Intent(context, TimerService::class.java).also {
- it.action = TimerService.Actions.RESET.toString()
- context.startService(it)
- }
-
- is TimerAction.SkipTimer ->
- Intent(context, TimerService::class.java).also {
- it.action = TimerService.Actions.SKIP.toString()
- context.startService(it)
- }
-
- TimerAction.StopAlarm ->
- Intent(context, TimerService::class.java).also {
- it.action = TimerService.Actions.STOP_ALARM.toString()
- context.startService(it)
- }
-
- TimerAction.ToggleTimer ->
- Intent(context, TimerService::class.java).also {
- it.action = TimerService.Actions.TOGGLE.toString()
- context.startService(it)
- }
- }
- },
- modifier = modifier.padding(
- start = contentPadding.calculateStartPadding(layoutDirection),
- end = contentPadding.calculateEndPadding(layoutDirection),
- bottom = contentPadding.calculateBottomPadding()
- )
+ SharedTransitionLayout {
+ NavDisplay(
+ backStack = backStack,
+ onBack = { backStack.removeLastOrNull() },
+ transitionSpec = {
+ ContentTransform(
+ fadeIn(motionScheme.defaultEffectsSpec()),
+ fadeOut(motionScheme.defaultEffectsSpec())
)
- }
-
- entry {
- SettingsScreenRoot(
- modifier = modifier.padding(
- start = contentPadding.calculateStartPadding(layoutDirection),
- end = contentPadding.calculateEndPadding(layoutDirection),
- bottom = contentPadding.calculateBottomPadding()
- )
+ },
+ popTransitionSpec = {
+ ContentTransform(
+ fadeIn(motionScheme.defaultEffectsSpec()),
+ fadeOut(motionScheme.defaultEffectsSpec())
)
- }
-
- entry {
- StatsScreenRoot(
- contentPadding = contentPadding,
- modifier = modifier.padding(
- start = contentPadding.calculateStartPadding(layoutDirection),
- end = contentPadding.calculateEndPadding(layoutDirection),
- bottom = contentPadding.calculateBottomPadding()
- )
+ },
+ predictivePopTransitionSpec = {
+ ContentTransform(
+ fadeIn(motionScheme.defaultEffectsSpec()),
+ fadeOut(motionScheme.defaultEffectsSpec()) +
+ scaleOut(targetScale = 0.7f),
)
+ },
+ entryProvider = entryProvider {
+ entry {
+ TimerScreen(
+ timerState = uiState,
+ progress = { progress },
+ onAction = { action ->
+ when (action) {
+ TimerAction.ResetTimer ->
+ Intent(context, TimerService::class.java).also {
+ it.action = TimerService.Actions.RESET.toString()
+ context.startService(it)
+ }
+
+ is TimerAction.SkipTimer ->
+ Intent(context, TimerService::class.java).also {
+ it.action = TimerService.Actions.SKIP.toString()
+ context.startService(it)
+ }
+
+ TimerAction.StopAlarm ->
+ Intent(context, TimerService::class.java).also {
+ it.action =
+ TimerService.Actions.STOP_ALARM.toString()
+ context.startService(it)
+ }
+
+ TimerAction.ToggleTimer ->
+ Intent(context, TimerService::class.java).also {
+ it.action = TimerService.Actions.TOGGLE.toString()
+ context.startService(it)
+ }
+ }
+ },
+ modifier = modifier
+ .padding(
+ start = contentPadding.calculateStartPadding(layoutDirection),
+ end = contentPadding.calculateEndPadding(layoutDirection),
+ bottom = contentPadding.calculateBottomPadding()
+ )
+ .then(
+ if (isAODEnabled) Modifier.clickable {
+ if (backStack.size < 2) backStack.add(Screen.AOD)
+ }
+ else Modifier
+ ),
+ )
+ }
+
+ entry {
+ AlwaysOnDisplay(
+ timerState = uiState,
+ progress = { progress },
+ modifier = Modifier
+ .then(
+ if (isAODEnabled) Modifier.clickable {
+ if (backStack.size > 1) backStack.removeLastOrNull()
+ }
+ else Modifier
+ )
+ )
+ }
+
+ entry {
+ SettingsScreenRoot(
+ modifier = modifier.padding(
+ start = contentPadding.calculateStartPadding(layoutDirection),
+ end = contentPadding.calculateEndPadding(layoutDirection),
+ bottom = contentPadding.calculateBottomPadding()
+ )
+ )
+ }
+
+ entry {
+ StatsScreenRoot(
+ contentPadding = contentPadding,
+ modifier = modifier.padding(
+ start = contentPadding.calculateStartPadding(layoutDirection),
+ end = contentPadding.calculateEndPadding(layoutDirection),
+ bottom = contentPadding.calculateBottomPadding()
+ )
+ )
+ }
}
- }
- )
+ )
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/Screen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/Screen.kt
index 85482cf..2a43655 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/Screen.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/Screen.kt
@@ -9,6 +9,9 @@ sealed class Screen : NavKey {
@Serializable
object Timer : Screen()
+ @Serializable
+ object AOD : Screen()
+
@Serializable
object Settings : Screen()
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt
index 67b00a0..b2bca48 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt
@@ -15,6 +15,7 @@ import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -143,6 +144,7 @@ fun SettingsScreenRoot(
onAlarmEnabledChange = viewModel::saveAlarmEnabled,
onVibrateEnabledChange = viewModel::saveVibrateEnabled,
onBlackThemeChange = viewModel::saveBlackTheme,
+ onAodEnabledChange = viewModel::saveAodEnabled,
onAlarmSoundChanged = {
viewModel.saveAlarmSound(it)
Intent(context, TimerService::class.java).apply {
@@ -170,6 +172,7 @@ private fun SettingsScreen(
onAlarmEnabledChange: (Boolean) -> Unit,
onVibrateEnabledChange: (Boolean) -> Unit,
onBlackThemeChange: (Boolean) -> Unit,
+ onAodEnabledChange: (Boolean) -> Unit,
onAlarmSoundChanged: (Uri?) -> Unit,
onThemeChange: (String) -> Unit,
onColorSchemeChange: (Color) -> Unit,
@@ -181,14 +184,14 @@ private fun SettingsScreen(
checkedIconColor = colorScheme.primary,
)
- val themeMap: Map> = remember {
+ val themeMap: Map> = remember {
mapOf(
"auto" to Pair(
R.drawable.brightness_auto,
- context.getString(R.string.system_default)
+ R.string.system_default
),
- "light" to Pair(R.drawable.light_mode, context.getString(R.string.light)),
- "dark" to Pair(R.drawable.dark_mode, context.getString(R.string.dark))
+ "light" to Pair(R.drawable.light_mode, R.string.light),
+ "dark" to Pair(R.drawable.dark_mode, R.string.dark)
)
}
val reverseThemeMap: Map = remember {
@@ -232,27 +235,39 @@ private fun SettingsScreen(
putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, alarmSound.toUri())
}
- val switchItems = remember(preferencesState.blackTheme, alarmEnabled, vibrateEnabled) {
+ val switchItems = remember(
+ preferencesState.blackTheme,
+ preferencesState.aodEnabled,
+ alarmEnabled,
+ vibrateEnabled
+ ) {
listOf(
SettingsSwitchItem(
checked = preferencesState.blackTheme,
icon = R.drawable.contrast,
- label = context.getString(R.string.black_theme),
- description = context.getString(R.string.black_theme_desc),
+ label = R.string.black_theme,
+ description = R.string.black_theme_desc,
onClick = onBlackThemeChange
),
+ SettingsSwitchItem(
+ checked = preferencesState.aodEnabled,
+ icon = R.drawable.aod,
+ label = R.string.always_on_display,
+ description = R.string.always_on_display_desc,
+ onClick = onAodEnabledChange
+ ),
SettingsSwitchItem(
checked = alarmEnabled,
icon = R.drawable.alarm_on,
- label = context.getString(R.string.alarm),
- description = context.getString(R.string.alarm_desc),
+ label = R.string.alarm,
+ description = R.string.alarm_desc,
onClick = onAlarmEnabledChange
),
SettingsSwitchItem(
checked = vibrateEnabled,
icon = R.drawable.mobile_vibrate,
- label = context.getString(R.string.vibrate),
- description = context.getString(R.string.vibrate_desc),
+ label = R.string.vibrate,
+ description = R.string.vibrate_desc,
onClick = onVibrateEnabledChange
)
)
@@ -404,14 +419,13 @@ private fun SettingsScreen(
.clip(middleListItemShape)
)
}
- item {
- val item = switchItems[0]
+ itemsIndexed(switchItems.take(2)) { index, item ->
ListItem(
leadingContent = {
Icon(painterResource(item.icon), contentDescription = null)
},
- headlineContent = { Text(item.label) },
- supportingContent = { Text(item.description) },
+ headlineContent = { Text(stringResource(item.label)) },
+ supportingContent = { Text(stringResource(item.description)) },
trailingContent = {
Switch(
checked = item.checked,
@@ -435,7 +449,9 @@ private fun SettingsScreen(
)
},
colors = listItemColors,
- modifier = Modifier.clip(bottomListItemShape)
+ modifier = Modifier
+ .padding(top = if (index != 0) 16.dp else 0.dp)
+ .clip(if (index == 0) bottomListItemShape else cardShape)
)
}
@@ -454,13 +470,13 @@ private fun SettingsScreen(
.clickable(onClick = { ringtonePickerLauncher.launch(intent) })
)
}
- itemsIndexed(switchItems.drop(1)) { index, item ->
+ itemsIndexed(switchItems.drop(2)) { index, item ->
ListItem(
leadingContent = {
Icon(painterResource(item.icon), contentDescription = null)
},
- headlineContent = { Text(item.label) },
- supportingContent = { Text(item.description) },
+ headlineContent = { Text(stringResource(item.label)) },
+ supportingContent = { Text(stringResource(item.description)) },
trailingContent = {
Switch(
checked = item.checked,
@@ -487,7 +503,7 @@ private fun SettingsScreen(
modifier = Modifier
.clip(
when (index) {
- switchItems.lastIndex - 1 -> bottomListItemShape
+ switchItems.lastIndex - 2 -> bottomListItemShape
else -> middleListItemShape
}
)
@@ -546,6 +562,7 @@ fun SettingsScreenPreview() {
onAlarmEnabledChange = {},
onVibrateEnabledChange = {},
onBlackThemeChange = {},
+ onAodEnabledChange = {},
onAlarmSoundChanged = {},
onThemeChange = {},
onColorSchemeChange = {},
@@ -557,7 +574,7 @@ fun SettingsScreenPreview() {
data class SettingsSwitchItem(
val checked: Boolean,
@param:DrawableRes val icon: Int,
- val label: String,
- val description: String,
+ @param:StringRes val label: Int,
+ @param:StringRes val description: Int,
val onClick: (Boolean) -> Unit
)
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemeDialog.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemeDialog.kt
index 67f4d7a..1de30cd 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemeDialog.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemeDialog.kt
@@ -31,11 +31,12 @@ 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.mutableIntStateOf
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.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
@@ -50,14 +51,16 @@ import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ThemeDialog(
- themeMap: Map>,
+ themeMap: Map>,
reverseThemeMap: Map,
theme: String,
setShowThemeDialog: (Boolean) -> Unit,
onThemeChange: (String) -> Unit
) {
val selectedOption =
- remember { mutableStateOf(themeMap[theme]!!.second) }
+ remember { mutableIntStateOf(themeMap[theme]!!.second) }
+
+ val context = LocalContext.current
BasicAlertDialog(
onDismissRequest = { setShowThemeDialog(false) }
@@ -80,7 +83,7 @@ fun ThemeDialog(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.selectableGroup()
) {
- themeMap.entries.forEachIndexed { index: Int, pair: Map.Entry> ->
+ themeMap.entries.forEachIndexed { index: Int, pair: Map.Entry> ->
val text = pair.value.second
val selected = text == selectedOption.value
@@ -94,7 +97,10 @@ fun ThemeDialog(
}
},
headlineContent = {
- Text(text = text, style = MaterialTheme.typography.bodyLarge)
+ Text(
+ text = stringResource(text),
+ style = MaterialTheme.typography.bodyLarge
+ )
},
colors = if (!selected) listItemColors else selectedListItemColors,
modifier = Modifier
@@ -110,7 +116,11 @@ fun ThemeDialog(
selected = (text == selectedOption.value),
onClick = {
selectedOption.value = text
- onThemeChange(reverseThemeMap[selectedOption.value]!!)
+ onThemeChange(
+ reverseThemeMap[context.getString(
+ selectedOption.intValue
+ )]!!
+ )
},
role = Role.RadioButton
)
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemePickerListItem.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemePickerListItem.kt
index 2bee9c5..0c66f00 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemePickerListItem.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemePickerListItem.kt
@@ -25,7 +25,7 @@ import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
@Composable
fun ThemePickerListItem(
theme: String,
- themeMap: Map>,
+ themeMap: Map>,
reverseThemeMap: Map,
items: Int,
index: Int,
@@ -53,7 +53,7 @@ fun ThemePickerListItem(
},
headlineContent = { Text(stringResource(R.string.theme)) },
supportingContent = {
- Text(themeMap[theme]!!.second)
+ Text(stringResource(themeMap[theme]!!.second))
},
colors = listItemColors,
items = items,
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/PreferencesState.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/PreferencesState.kt
index 0ae4849..7231852 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/PreferencesState.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/PreferencesState.kt
@@ -14,5 +14,6 @@ import androidx.compose.ui.graphics.Color
data class PreferencesState(
val theme: String = "auto",
val colorScheme: String = Color.White.toString(),
- val blackTheme: Boolean = false
+ val blackTheme: Boolean = false,
+ val aodEnabled: Boolean = false
)
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt
index 5f604c1..454bf82 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt
@@ -80,12 +80,15 @@ class SettingsViewModel(
?: preferenceRepository.saveStringPreference("color_scheme", Color.White.toString())
val blackTheme = preferenceRepository.getBooleanPreference("black_theme")
?: preferenceRepository.saveBooleanPreference("black_theme", false)
+ val aodEnabled = preferenceRepository.getBooleanPreference("aod_enabled")
+ ?: preferenceRepository.saveBooleanPreference("aod_enabled", false)
_preferencesState.update { currentState ->
currentState.copy(
theme = theme,
colorScheme = colorScheme,
- blackTheme = blackTheme
+ blackTheme = blackTheme,
+ aodEnabled = aodEnabled
)
}
}
@@ -196,6 +199,15 @@ class SettingsViewModel(
}
}
+ fun saveAodEnabled(aodEnabled: Boolean) {
+ viewModelScope.launch {
+ _preferencesState.update { currentState ->
+ currentState.copy(aodEnabled = aodEnabled)
+ }
+ preferenceRepository.saveBooleanPreference("aod_enabled", aodEnabled)
+ }
+ }
+
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt
index bdb563d..68b9094 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt
@@ -13,6 +13,8 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.SharedTransitionLayout
+import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
@@ -81,6 +83,7 @@ import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.navigation3.ui.LocalNavAnimatedContentScope
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
@@ -91,7 +94,7 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
-fun TimerScreen(
+fun SharedTransitionScope.TimerScreen(
timerState: TimerState,
progress: () -> Float,
onAction: (TimerAction) -> Unit,
@@ -209,6 +212,12 @@ fun TimerScreen(
CircularProgressIndicator(
progress = progress,
modifier = Modifier
+ .sharedBounds(
+ sharedContentState = this@TimerScreen.rememberSharedContentState(
+ "focus progress"
+ ),
+ animatedVisibilityScope = LocalNavAnimatedContentScope.current
+ )
.widthIn(max = 350.dp)
.fillMaxWidth(0.9f)
.aspectRatio(1f),
@@ -221,6 +230,12 @@ fun TimerScreen(
CircularWavyProgressIndicator(
progress = progress,
modifier = Modifier
+ .sharedBounds(
+ sharedContentState = this@TimerScreen.rememberSharedContentState(
+ "break progress"
+ ),
+ animatedVisibilityScope = LocalNavAnimatedContentScope.current
+ )
.widthIn(max = 350.dp)
.fillMaxWidth(0.9f)
.aspectRatio(1f),
@@ -261,7 +276,11 @@ fun TimerScreen(
letterSpacing = (-2).sp
),
textAlign = TextAlign.Center,
- maxLines = 1
+ maxLines = 1,
+ modifier = Modifier.sharedBounds(
+ sharedContentState = this@TimerScreen.rememberSharedContentState("clock"),
+ animatedVisibilityScope = LocalNavAnimatedContentScope.current
+ )
)
AnimatedVisibility(
expanded,
@@ -519,11 +538,13 @@ fun TimerScreenPreview() {
)
TomatoTheme {
Surface {
- TimerScreen(
- timerState,
- { 0.3f },
- {}
- )
+ SharedTransitionLayout {
+ TimerScreen(
+ timerState,
+ { 0.3f },
+ {}
+ )
+ }
}
}
}
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt
index 2202d42..53c23b5 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt
@@ -95,6 +95,9 @@ class TimerViewModel(
)
).toUri()
+ preferenceRepository.getBooleanPreference("aod_enabled")
+ ?: preferenceRepository.saveBooleanPreference("aod_enabled", false)
+
_time.update { timerRepository.focusTime }
cycles = 0
startTime = 0L
diff --git a/app/src/main/res/drawable/aod.xml b/app/src/main/res/drawable/aod.xml
new file mode 100644
index 0000000..ab821c8
--- /dev/null
+++ b/app/src/main/res/drawable/aod.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 067cff5..c185a08 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,60 +1,62 @@
- Tomato
- Start
- Stop
- Focus
- Short break
- Long break
- Exit
- Skip
- Stop alarm
- %1$s min remaining
- Paused
- Completed
- Up next: %1$s (%2$s)
- Start next
- Choose color scheme
- OK
- Color scheme
- Dynamic
- Color
- System default
Alarm
- Light
- Dark
- Choose theme
- Productivity analysis
- Focus durations at different times of the day
+ Ring alarm when a timer completes
Alarm sound
+ Always On Display
+ Tap anywhere when viewing the timer to switch to AOD mode
+ Tomato
Black theme
Use a pure black dark theme
- Ring alarm when a timer completes
- Vibrate
- Vibrate when a timer completes
- Theme
- Settings
+ Break
+ Choose color scheme
+ Choose theme
+ Color
+ Color scheme
+ Completed
+ Dark
+ Dynamic
+ Exit
+ Focus
+ focus per day (avg)
+ Last month
+ Last week
+ Last year
+ Light
+ Long break
+ %1$s min remaining
+ Monthly productivity analysis
+ More
+ More info
+ OK
+ Pause
+ Paused
+ Play
+ A \"session\" is a sequence of pomodoro intervals that contain focus intervals, short break intervals, and a long break interval. The last break of a session is always a long break.
+ Productivity analysis
+ Focus durations at different times of the day
+ Restart
Session length
Focus intervals in one session: %1$d
- A \"session\" is a sequence of pomodoro intervals that contain focus intervals, short break intervals, and a long break interval. The last break of a session is always a long break.
- Stats
- Today
- Break
- Last week
- focus per day (avg)
- More info
- Weekly productivity analysis
- Last month
- Monthly productivity analysis
- Stop Alarm?
- Current timer session is complete. Tap anywhere to stop the alarm.
- %1$d of %2$d
- More
- Pause
- Play
- Restart
+ Settings
+ Short break
+ Skip
Skip to next
- Up next
+ Start
+ Start next
+ Stats
+ Stop
+ Stop alarm
+ Current timer session is complete. Tap anywhere to stop the alarm.
+ Stop Alarm?
+ System default
+ Theme
Timer
Timer progress
- Last year
+ %1$d of %2$d
+ Today
+ Up next
+ Up next: %1$s (%2$s)
+ Vibrate
+ Vibrate when a timer completes
+ Weekly productivity analysis
\ No newline at end of file