Merge pull request #82 from nsh07/always-on-display
feat(ui): Always On Display (AOD) mode
This commit is contained in:
@@ -31,7 +31,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "org.nsh07.pomodoro"
|
applicationId = "org.nsh07.pomodoro"
|
||||||
minSdk = 26
|
minSdk = 27
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 13
|
versionCode = 13
|
||||||
versionName = "1.5.0"
|
versionName = "1.5.0"
|
||||||
|
|||||||
@@ -51,7 +51,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
appContainer.appTimerRepository.colorScheme = colorScheme
|
appContainer.appTimerRepository.colorScheme = colorScheme
|
||||||
}
|
}
|
||||||
|
|
||||||
AppScreen(timerViewModel = timerViewModel)
|
AppScreen(
|
||||||
|
timerViewModel = timerViewModel,
|
||||||
|
isAODEnabled = preferencesState.aodEnabled
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
272
app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt
Normal file
272
app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt
Normal file
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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() }
|
||||||
@@ -8,11 +8,14 @@
|
|||||||
package org.nsh07.pomodoro.ui
|
package org.nsh07.pomodoro.ui
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.ContentTransform
|
import androidx.compose.animation.ContentTransform
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.animation.SharedTransitionLayout
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.scaleOut
|
import androidx.compose.animation.scaleOut
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.calculateEndPadding
|
import androidx.compose.foundation.layout.calculateEndPadding
|
||||||
import androidx.compose.foundation.layout.calculateStartPadding
|
import androidx.compose.foundation.layout.calculateStartPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -55,7 +58,8 @@ 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),
|
||||||
|
isAODEnabled: Boolean
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
@@ -78,128 +82,159 @@ fun AppScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
val wide = remember {
|
AnimatedVisibility(
|
||||||
windowSizeClass.isWidthAtLeastBreakpoint(
|
backStack.last() !is Screen.AOD,
|
||||||
WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND
|
enter = fadeIn(),
|
||||||
)
|
exit = fadeOut()
|
||||||
}
|
|
||||||
ShortNavigationBar(
|
|
||||||
arrangement =
|
|
||||||
if (wide) ShortNavigationBarArrangement.Centered
|
|
||||||
else ShortNavigationBarArrangement.EqualWeight
|
|
||||||
) {
|
) {
|
||||||
screens.forEach {
|
val wide = remember {
|
||||||
val selected = backStack.last() == it.route
|
windowSizeClass.isWidthAtLeastBreakpoint(
|
||||||
ShortNavigationBarItem(
|
WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND
|
||||||
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)) }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
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 ->
|
) { contentPadding ->
|
||||||
NavDisplay(
|
SharedTransitionLayout {
|
||||||
backStack = backStack,
|
NavDisplay(
|
||||||
onBack = { backStack.removeLastOrNull() },
|
backStack = backStack,
|
||||||
transitionSpec = {
|
onBack = { backStack.removeLastOrNull() },
|
||||||
ContentTransform(
|
transitionSpec = {
|
||||||
fadeIn(motionScheme.defaultEffectsSpec()),
|
ContentTransform(
|
||||||
fadeOut(motionScheme.defaultEffectsSpec())
|
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<Screen.Timer> {
|
|
||||||
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()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
popTransitionSpec = {
|
||||||
entry<Screen.Settings> {
|
ContentTransform(
|
||||||
SettingsScreenRoot(
|
fadeIn(motionScheme.defaultEffectsSpec()),
|
||||||
modifier = modifier.padding(
|
fadeOut(motionScheme.defaultEffectsSpec())
|
||||||
start = contentPadding.calculateStartPadding(layoutDirection),
|
|
||||||
end = contentPadding.calculateEndPadding(layoutDirection),
|
|
||||||
bottom = contentPadding.calculateBottomPadding()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
predictivePopTransitionSpec = {
|
||||||
entry<Screen.Stats> {
|
ContentTransform(
|
||||||
StatsScreenRoot(
|
fadeIn(motionScheme.defaultEffectsSpec()),
|
||||||
contentPadding = contentPadding,
|
fadeOut(motionScheme.defaultEffectsSpec()) +
|
||||||
modifier = modifier.padding(
|
scaleOut(targetScale = 0.7f),
|
||||||
start = contentPadding.calculateStartPadding(layoutDirection),
|
|
||||||
end = contentPadding.calculateEndPadding(layoutDirection),
|
|
||||||
bottom = contentPadding.calculateBottomPadding()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
entryProvider = entryProvider {
|
||||||
|
entry<Screen.Timer> {
|
||||||
|
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<Screen.AOD> {
|
||||||
|
AlwaysOnDisplay(
|
||||||
|
timerState = uiState,
|
||||||
|
progress = { progress },
|
||||||
|
modifier = Modifier
|
||||||
|
.then(
|
||||||
|
if (isAODEnabled) Modifier.clickable {
|
||||||
|
if (backStack.size > 1) backStack.removeLastOrNull()
|
||||||
|
}
|
||||||
|
else Modifier
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry<Screen.Settings> {
|
||||||
|
SettingsScreenRoot(
|
||||||
|
modifier = modifier.padding(
|
||||||
|
start = contentPadding.calculateStartPadding(layoutDirection),
|
||||||
|
end = contentPadding.calculateEndPadding(layoutDirection),
|
||||||
|
bottom = contentPadding.calculateBottomPadding()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry<Screen.Stats> {
|
||||||
|
StatsScreenRoot(
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
modifier = modifier.padding(
|
||||||
|
start = contentPadding.calculateStartPadding(layoutDirection),
|
||||||
|
end = contentPadding.calculateEndPadding(layoutDirection),
|
||||||
|
bottom = contentPadding.calculateBottomPadding()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,9 @@ sealed class Screen : NavKey {
|
|||||||
@Serializable
|
@Serializable
|
||||||
object Timer : Screen()
|
object Timer : Screen()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
object AOD : Screen()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
object Settings : Screen()
|
object Settings : Screen()
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import android.os.Build
|
|||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@@ -143,6 +144,7 @@ fun SettingsScreenRoot(
|
|||||||
onAlarmEnabledChange = viewModel::saveAlarmEnabled,
|
onAlarmEnabledChange = viewModel::saveAlarmEnabled,
|
||||||
onVibrateEnabledChange = viewModel::saveVibrateEnabled,
|
onVibrateEnabledChange = viewModel::saveVibrateEnabled,
|
||||||
onBlackThemeChange = viewModel::saveBlackTheme,
|
onBlackThemeChange = viewModel::saveBlackTheme,
|
||||||
|
onAodEnabledChange = viewModel::saveAodEnabled,
|
||||||
onAlarmSoundChanged = {
|
onAlarmSoundChanged = {
|
||||||
viewModel.saveAlarmSound(it)
|
viewModel.saveAlarmSound(it)
|
||||||
Intent(context, TimerService::class.java).apply {
|
Intent(context, TimerService::class.java).apply {
|
||||||
@@ -170,6 +172,7 @@ private fun SettingsScreen(
|
|||||||
onAlarmEnabledChange: (Boolean) -> Unit,
|
onAlarmEnabledChange: (Boolean) -> Unit,
|
||||||
onVibrateEnabledChange: (Boolean) -> Unit,
|
onVibrateEnabledChange: (Boolean) -> Unit,
|
||||||
onBlackThemeChange: (Boolean) -> Unit,
|
onBlackThemeChange: (Boolean) -> Unit,
|
||||||
|
onAodEnabledChange: (Boolean) -> Unit,
|
||||||
onAlarmSoundChanged: (Uri?) -> Unit,
|
onAlarmSoundChanged: (Uri?) -> Unit,
|
||||||
onThemeChange: (String) -> Unit,
|
onThemeChange: (String) -> Unit,
|
||||||
onColorSchemeChange: (Color) -> Unit,
|
onColorSchemeChange: (Color) -> Unit,
|
||||||
@@ -181,14 +184,14 @@ private fun SettingsScreen(
|
|||||||
checkedIconColor = colorScheme.primary,
|
checkedIconColor = colorScheme.primary,
|
||||||
)
|
)
|
||||||
|
|
||||||
val themeMap: Map<String, Pair<Int, String>> = remember {
|
val themeMap: Map<String, Pair<Int, Int>> = remember {
|
||||||
mapOf(
|
mapOf(
|
||||||
"auto" to Pair(
|
"auto" to Pair(
|
||||||
R.drawable.brightness_auto,
|
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)),
|
"light" to Pair(R.drawable.light_mode, R.string.light),
|
||||||
"dark" to Pair(R.drawable.dark_mode, context.getString(R.string.dark))
|
"dark" to Pair(R.drawable.dark_mode, R.string.dark)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val reverseThemeMap: Map<String, String> = remember {
|
val reverseThemeMap: Map<String, String> = remember {
|
||||||
@@ -232,27 +235,39 @@ private fun SettingsScreen(
|
|||||||
putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, alarmSound.toUri())
|
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(
|
listOf(
|
||||||
SettingsSwitchItem(
|
SettingsSwitchItem(
|
||||||
checked = preferencesState.blackTheme,
|
checked = preferencesState.blackTheme,
|
||||||
icon = R.drawable.contrast,
|
icon = R.drawable.contrast,
|
||||||
label = context.getString(R.string.black_theme),
|
label = R.string.black_theme,
|
||||||
description = context.getString(R.string.black_theme_desc),
|
description = R.string.black_theme_desc,
|
||||||
onClick = onBlackThemeChange
|
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(
|
SettingsSwitchItem(
|
||||||
checked = alarmEnabled,
|
checked = alarmEnabled,
|
||||||
icon = R.drawable.alarm_on,
|
icon = R.drawable.alarm_on,
|
||||||
label = context.getString(R.string.alarm),
|
label = R.string.alarm,
|
||||||
description = context.getString(R.string.alarm_desc),
|
description = R.string.alarm_desc,
|
||||||
onClick = onAlarmEnabledChange
|
onClick = onAlarmEnabledChange
|
||||||
),
|
),
|
||||||
SettingsSwitchItem(
|
SettingsSwitchItem(
|
||||||
checked = vibrateEnabled,
|
checked = vibrateEnabled,
|
||||||
icon = R.drawable.mobile_vibrate,
|
icon = R.drawable.mobile_vibrate,
|
||||||
label = context.getString(R.string.vibrate),
|
label = R.string.vibrate,
|
||||||
description = context.getString(R.string.vibrate_desc),
|
description = R.string.vibrate_desc,
|
||||||
onClick = onVibrateEnabledChange
|
onClick = onVibrateEnabledChange
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -404,14 +419,13 @@ private fun SettingsScreen(
|
|||||||
.clip(middleListItemShape)
|
.clip(middleListItemShape)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
itemsIndexed(switchItems.take(2)) { index, item ->
|
||||||
val item = switchItems[0]
|
|
||||||
ListItem(
|
ListItem(
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
Icon(painterResource(item.icon), contentDescription = null)
|
Icon(painterResource(item.icon), contentDescription = null)
|
||||||
},
|
},
|
||||||
headlineContent = { Text(item.label) },
|
headlineContent = { Text(stringResource(item.label)) },
|
||||||
supportingContent = { Text(item.description) },
|
supportingContent = { Text(stringResource(item.description)) },
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
Switch(
|
Switch(
|
||||||
checked = item.checked,
|
checked = item.checked,
|
||||||
@@ -435,7 +449,9 @@ private fun SettingsScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
colors = listItemColors,
|
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) })
|
.clickable(onClick = { ringtonePickerLauncher.launch(intent) })
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
itemsIndexed(switchItems.drop(1)) { index, item ->
|
itemsIndexed(switchItems.drop(2)) { index, item ->
|
||||||
ListItem(
|
ListItem(
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
Icon(painterResource(item.icon), contentDescription = null)
|
Icon(painterResource(item.icon), contentDescription = null)
|
||||||
},
|
},
|
||||||
headlineContent = { Text(item.label) },
|
headlineContent = { Text(stringResource(item.label)) },
|
||||||
supportingContent = { Text(item.description) },
|
supportingContent = { Text(stringResource(item.description)) },
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
Switch(
|
Switch(
|
||||||
checked = item.checked,
|
checked = item.checked,
|
||||||
@@ -487,7 +503,7 @@ private fun SettingsScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(
|
.clip(
|
||||||
when (index) {
|
when (index) {
|
||||||
switchItems.lastIndex - 1 -> bottomListItemShape
|
switchItems.lastIndex - 2 -> bottomListItemShape
|
||||||
else -> middleListItemShape
|
else -> middleListItemShape
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -546,6 +562,7 @@ fun SettingsScreenPreview() {
|
|||||||
onAlarmEnabledChange = {},
|
onAlarmEnabledChange = {},
|
||||||
onVibrateEnabledChange = {},
|
onVibrateEnabledChange = {},
|
||||||
onBlackThemeChange = {},
|
onBlackThemeChange = {},
|
||||||
|
onAodEnabledChange = {},
|
||||||
onAlarmSoundChanged = {},
|
onAlarmSoundChanged = {},
|
||||||
onThemeChange = {},
|
onThemeChange = {},
|
||||||
onColorSchemeChange = {},
|
onColorSchemeChange = {},
|
||||||
@@ -557,7 +574,7 @@ fun SettingsScreenPreview() {
|
|||||||
data class SettingsSwitchItem(
|
data class SettingsSwitchItem(
|
||||||
val checked: Boolean,
|
val checked: Boolean,
|
||||||
@param:DrawableRes val icon: Int,
|
@param:DrawableRes val icon: Int,
|
||||||
val label: String,
|
@param:StringRes val label: Int,
|
||||||
val description: String,
|
@param:StringRes val description: Int,
|
||||||
val onClick: (Boolean) -> Unit
|
val onClick: (Boolean) -> Unit
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,11 +31,12 @@ import androidx.compose.material3.Surface
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
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.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
@@ -50,14 +51,16 @@ import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
|
|||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ThemeDialog(
|
fun ThemeDialog(
|
||||||
themeMap: Map<String, Pair<Int, String>>,
|
themeMap: Map<String, Pair<Int, Int>>,
|
||||||
reverseThemeMap: Map<String, String>,
|
reverseThemeMap: Map<String, String>,
|
||||||
theme: String,
|
theme: String,
|
||||||
setShowThemeDialog: (Boolean) -> Unit,
|
setShowThemeDialog: (Boolean) -> Unit,
|
||||||
onThemeChange: (String) -> Unit
|
onThemeChange: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
val selectedOption =
|
val selectedOption =
|
||||||
remember { mutableStateOf(themeMap[theme]!!.second) }
|
remember { mutableIntStateOf(themeMap[theme]!!.second) }
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
BasicAlertDialog(
|
BasicAlertDialog(
|
||||||
onDismissRequest = { setShowThemeDialog(false) }
|
onDismissRequest = { setShowThemeDialog(false) }
|
||||||
@@ -80,7 +83,7 @@ fun ThemeDialog(
|
|||||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
modifier = Modifier.selectableGroup()
|
modifier = Modifier.selectableGroup()
|
||||||
) {
|
) {
|
||||||
themeMap.entries.forEachIndexed { index: Int, pair: Map.Entry<String, Pair<Int, String>> ->
|
themeMap.entries.forEachIndexed { index: Int, pair: Map.Entry<String, Pair<Int, Int>> ->
|
||||||
val text = pair.value.second
|
val text = pair.value.second
|
||||||
val selected = text == selectedOption.value
|
val selected = text == selectedOption.value
|
||||||
|
|
||||||
@@ -94,7 +97,10 @@ fun ThemeDialog(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(text = text, style = MaterialTheme.typography.bodyLarge)
|
Text(
|
||||||
|
text = stringResource(text),
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
},
|
},
|
||||||
colors = if (!selected) listItemColors else selectedListItemColors,
|
colors = if (!selected) listItemColors else selectedListItemColors,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -110,7 +116,11 @@ fun ThemeDialog(
|
|||||||
selected = (text == selectedOption.value),
|
selected = (text == selectedOption.value),
|
||||||
onClick = {
|
onClick = {
|
||||||
selectedOption.value = text
|
selectedOption.value = text
|
||||||
onThemeChange(reverseThemeMap[selectedOption.value]!!)
|
onThemeChange(
|
||||||
|
reverseThemeMap[context.getString(
|
||||||
|
selectedOption.intValue
|
||||||
|
)]!!
|
||||||
|
)
|
||||||
},
|
},
|
||||||
role = Role.RadioButton
|
role = Role.RadioButton
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
|
|||||||
@Composable
|
@Composable
|
||||||
fun ThemePickerListItem(
|
fun ThemePickerListItem(
|
||||||
theme: String,
|
theme: String,
|
||||||
themeMap: Map<String, Pair<Int, String>>,
|
themeMap: Map<String, Pair<Int, Int>>,
|
||||||
reverseThemeMap: Map<String, String>,
|
reverseThemeMap: Map<String, String>,
|
||||||
items: Int,
|
items: Int,
|
||||||
index: Int,
|
index: Int,
|
||||||
@@ -53,7 +53,7 @@ fun ThemePickerListItem(
|
|||||||
},
|
},
|
||||||
headlineContent = { Text(stringResource(R.string.theme)) },
|
headlineContent = { Text(stringResource(R.string.theme)) },
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
Text(themeMap[theme]!!.second)
|
Text(stringResource(themeMap[theme]!!.second))
|
||||||
},
|
},
|
||||||
colors = listItemColors,
|
colors = listItemColors,
|
||||||
items = items,
|
items = items,
|
||||||
|
|||||||
@@ -14,5 +14,6 @@ import androidx.compose.ui.graphics.Color
|
|||||||
data class PreferencesState(
|
data class PreferencesState(
|
||||||
val theme: String = "auto",
|
val theme: String = "auto",
|
||||||
val colorScheme: String = Color.White.toString(),
|
val colorScheme: String = Color.White.toString(),
|
||||||
val blackTheme: Boolean = false
|
val blackTheme: Boolean = false,
|
||||||
|
val aodEnabled: Boolean = false
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -80,12 +80,15 @@ class SettingsViewModel(
|
|||||||
?: preferenceRepository.saveStringPreference("color_scheme", Color.White.toString())
|
?: preferenceRepository.saveStringPreference("color_scheme", Color.White.toString())
|
||||||
val blackTheme = preferenceRepository.getBooleanPreference("black_theme")
|
val blackTheme = preferenceRepository.getBooleanPreference("black_theme")
|
||||||
?: preferenceRepository.saveBooleanPreference("black_theme", false)
|
?: preferenceRepository.saveBooleanPreference("black_theme", false)
|
||||||
|
val aodEnabled = preferenceRepository.getBooleanPreference("aod_enabled")
|
||||||
|
?: preferenceRepository.saveBooleanPreference("aod_enabled", false)
|
||||||
|
|
||||||
_preferencesState.update { currentState ->
|
_preferencesState.update { currentState ->
|
||||||
currentState.copy(
|
currentState.copy(
|
||||||
theme = theme,
|
theme = theme,
|
||||||
colorScheme = colorScheme,
|
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 {
|
companion object {
|
||||||
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
||||||
initializer {
|
initializer {
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.SharedTransitionLayout
|
||||||
|
import androidx.compose.animation.SharedTransitionScope
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.expandVertically
|
import androidx.compose.animation.expandVertically
|
||||||
import androidx.compose.animation.fadeIn
|
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.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation3.ui.LocalNavAnimatedContentScope
|
||||||
import org.nsh07.pomodoro.R
|
import org.nsh07.pomodoro.R
|
||||||
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
|
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
|
||||||
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
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)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun TimerScreen(
|
fun SharedTransitionScope.TimerScreen(
|
||||||
timerState: TimerState,
|
timerState: TimerState,
|
||||||
progress: () -> Float,
|
progress: () -> Float,
|
||||||
onAction: (TimerAction) -> Unit,
|
onAction: (TimerAction) -> Unit,
|
||||||
@@ -209,6 +212,12 @@ fun TimerScreen(
|
|||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
progress = progress,
|
progress = progress,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.sharedBounds(
|
||||||
|
sharedContentState = this@TimerScreen.rememberSharedContentState(
|
||||||
|
"focus progress"
|
||||||
|
),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||||
|
)
|
||||||
.widthIn(max = 350.dp)
|
.widthIn(max = 350.dp)
|
||||||
.fillMaxWidth(0.9f)
|
.fillMaxWidth(0.9f)
|
||||||
.aspectRatio(1f),
|
.aspectRatio(1f),
|
||||||
@@ -221,6 +230,12 @@ fun TimerScreen(
|
|||||||
CircularWavyProgressIndicator(
|
CircularWavyProgressIndicator(
|
||||||
progress = progress,
|
progress = progress,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.sharedBounds(
|
||||||
|
sharedContentState = this@TimerScreen.rememberSharedContentState(
|
||||||
|
"break progress"
|
||||||
|
),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||||
|
)
|
||||||
.widthIn(max = 350.dp)
|
.widthIn(max = 350.dp)
|
||||||
.fillMaxWidth(0.9f)
|
.fillMaxWidth(0.9f)
|
||||||
.aspectRatio(1f),
|
.aspectRatio(1f),
|
||||||
@@ -261,7 +276,11 @@ fun TimerScreen(
|
|||||||
letterSpacing = (-2).sp
|
letterSpacing = (-2).sp
|
||||||
),
|
),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
maxLines = 1
|
maxLines = 1,
|
||||||
|
modifier = Modifier.sharedBounds(
|
||||||
|
sharedContentState = this@TimerScreen.rememberSharedContentState("clock"),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||||
|
)
|
||||||
)
|
)
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
expanded,
|
expanded,
|
||||||
@@ -519,11 +538,13 @@ fun TimerScreenPreview() {
|
|||||||
)
|
)
|
||||||
TomatoTheme {
|
TomatoTheme {
|
||||||
Surface {
|
Surface {
|
||||||
TimerScreen(
|
SharedTransitionLayout {
|
||||||
timerState,
|
TimerScreen(
|
||||||
{ 0.3f },
|
timerState,
|
||||||
{}
|
{ 0.3f },
|
||||||
)
|
{}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,9 @@ class TimerViewModel(
|
|||||||
)
|
)
|
||||||
).toUri()
|
).toUri()
|
||||||
|
|
||||||
|
preferenceRepository.getBooleanPreference("aod_enabled")
|
||||||
|
?: preferenceRepository.saveBooleanPreference("aod_enabled", false)
|
||||||
|
|
||||||
_time.update { timerRepository.focusTime }
|
_time.update { timerRepository.focusTime }
|
||||||
cycles = 0
|
cycles = 0
|
||||||
startTime = 0L
|
startTime = 0L
|
||||||
|
|||||||
16
app/src/main/res/drawable/aod.xml
Normal file
16
app/src/main/res/drawable/aod.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="M360,480h240q17,0 28.5,-11.5T640,440q0,-17 -11.5,-28.5T600,400L360,400q-17,0 -28.5,11.5T320,440q0,17 11.5,28.5T360,480ZM400,620h160q17,0 28.5,-11.5T600,580q0,-17 -11.5,-28.5T560,540L400,540q-17,0 -28.5,11.5T360,580q0,17 11.5,28.5T400,620ZM280,920q-33,0 -56.5,-23.5T200,840v-720q0,-33 23.5,-56.5T280,40h400q33,0 56.5,23.5T760,120v124q18,7 29,22t11,34v80q0,19 -11,34t-29,22v404q0,33 -23.5,56.5T680,920L280,920Z" />
|
||||||
|
</vector>
|
||||||
@@ -1,60 +1,62 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Tomato</string>
|
|
||||||
<string name="start">Start</string>
|
|
||||||
<string name="stop">Stop</string>
|
|
||||||
<string name="focus">Focus</string>
|
|
||||||
<string name="short_break">Short break</string>
|
|
||||||
<string name="long_break">Long break</string>
|
|
||||||
<string name="exit">Exit</string>
|
|
||||||
<string name="skip">Skip</string>
|
|
||||||
<string name="stop_alarm">Stop alarm</string>
|
|
||||||
<string name="min_remaining_notification">%1$s min remaining</string>
|
|
||||||
<string name="paused">Paused</string>
|
|
||||||
<string name="completed">Completed</string>
|
|
||||||
<string name="up_next_notification">Up next: %1$s (%2$s)</string>
|
|
||||||
<string name="start_next">Start next</string>
|
|
||||||
<string name="choose_color_scheme">Choose color scheme</string>
|
|
||||||
<string name="ok">OK</string>
|
|
||||||
<string name="color_scheme">Color scheme</string>
|
|
||||||
<string name="dynamic">Dynamic</string>
|
|
||||||
<string name="color">Color</string>
|
|
||||||
<string name="system_default">System default</string>
|
|
||||||
<string name="alarm">Alarm</string>
|
<string name="alarm">Alarm</string>
|
||||||
<string name="light">Light</string>
|
<string name="alarm_desc">Ring alarm when a timer completes</string>
|
||||||
<string name="dark">Dark</string>
|
|
||||||
<string name="choose_theme">Choose theme</string>
|
|
||||||
<string name="productivity_analysis">Productivity analysis</string>
|
|
||||||
<string name="productivity_analysis_desc">Focus durations at different times of the day</string>
|
|
||||||
<string name="alarm_sound">Alarm sound</string>
|
<string name="alarm_sound">Alarm sound</string>
|
||||||
|
<string name="always_on_display">Always On Display</string>
|
||||||
|
<string name="always_on_display_desc">Tap anywhere when viewing the timer to switch to AOD mode</string>
|
||||||
|
<string name="app_name">Tomato</string>
|
||||||
<string name="black_theme">Black theme</string>
|
<string name="black_theme">Black theme</string>
|
||||||
<string name="black_theme_desc">Use a pure black dark theme</string>
|
<string name="black_theme_desc">Use a pure black dark theme</string>
|
||||||
<string name="alarm_desc">Ring alarm when a timer completes</string>
|
<string name="break_">Break</string>
|
||||||
<string name="vibrate">Vibrate</string>
|
<string name="choose_color_scheme">Choose color scheme</string>
|
||||||
<string name="vibrate_desc">Vibrate when a timer completes</string>
|
<string name="choose_theme">Choose theme</string>
|
||||||
<string name="theme">Theme</string>
|
<string name="color">Color</string>
|
||||||
<string name="settings">Settings</string>
|
<string name="color_scheme">Color scheme</string>
|
||||||
|
<string name="completed">Completed</string>
|
||||||
|
<string name="dark">Dark</string>
|
||||||
|
<string name="dynamic">Dynamic</string>
|
||||||
|
<string name="exit">Exit</string>
|
||||||
|
<string name="focus">Focus</string>
|
||||||
|
<string name="focus_per_day_avg">focus per day (avg)</string>
|
||||||
|
<string name="last_month">Last month</string>
|
||||||
|
<string name="last_week">Last week</string>
|
||||||
|
<string name="last_year">Last year</string>
|
||||||
|
<string name="light">Light</string>
|
||||||
|
<string name="long_break">Long break</string>
|
||||||
|
<string name="min_remaining_notification">%1$s min remaining</string>
|
||||||
|
<string name="monthly_productivity_analysis">Monthly productivity analysis</string>
|
||||||
|
<string name="more">More</string>
|
||||||
|
<string name="more_info">More info</string>
|
||||||
|
<string name="ok">OK</string>
|
||||||
|
<string name="pause">Pause</string>
|
||||||
|
<string name="paused">Paused</string>
|
||||||
|
<string name="play">Play</string>
|
||||||
|
<string name="pomodoro_info">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.</string>
|
||||||
|
<string name="productivity_analysis">Productivity analysis</string>
|
||||||
|
<string name="productivity_analysis_desc">Focus durations at different times of the day</string>
|
||||||
|
<string name="restart">Restart</string>
|
||||||
<string name="session_length">Session length</string>
|
<string name="session_length">Session length</string>
|
||||||
<string name="session_length_desc">Focus intervals in one session: %1$d</string>
|
<string name="session_length_desc">Focus intervals in one session: %1$d</string>
|
||||||
<string name="pomodoro_info">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.</string>
|
<string name="settings">Settings</string>
|
||||||
<string name="stats">Stats</string>
|
<string name="short_break">Short break</string>
|
||||||
<string name="today">Today</string>
|
<string name="skip">Skip</string>
|
||||||
<string name="break_">Break</string>
|
|
||||||
<string name="last_week">Last week</string>
|
|
||||||
<string name="focus_per_day_avg">focus per day (avg)</string>
|
|
||||||
<string name="more_info">More info</string>
|
|
||||||
<string name="weekly_productivity_analysis">Weekly productivity analysis</string>
|
|
||||||
<string name="last_month">Last month</string>
|
|
||||||
<string name="monthly_productivity_analysis">Monthly productivity analysis</string>
|
|
||||||
<string name="stop_alarm_question">Stop Alarm?</string>
|
|
||||||
<string name="stop_alarm_dialog_text">Current timer session is complete. Tap anywhere to stop the alarm.</string>
|
|
||||||
<string name="timer_session_count">%1$d of %2$d</string>
|
|
||||||
<string name="more">More</string>
|
|
||||||
<string name="pause">Pause</string>
|
|
||||||
<string name="play">Play</string>
|
|
||||||
<string name="restart">Restart</string>
|
|
||||||
<string name="skip_to_next">Skip to next</string>
|
<string name="skip_to_next">Skip to next</string>
|
||||||
<string name="up_next">Up next</string>
|
<string name="start">Start</string>
|
||||||
|
<string name="start_next">Start next</string>
|
||||||
|
<string name="stats">Stats</string>
|
||||||
|
<string name="stop">Stop</string>
|
||||||
|
<string name="stop_alarm">Stop alarm</string>
|
||||||
|
<string name="stop_alarm_dialog_text">Current timer session is complete. Tap anywhere to stop the alarm.</string>
|
||||||
|
<string name="stop_alarm_question">Stop Alarm?</string>
|
||||||
|
<string name="system_default">System default</string>
|
||||||
|
<string name="theme">Theme</string>
|
||||||
<string name="timer">Timer</string>
|
<string name="timer">Timer</string>
|
||||||
<string name="timer_progress">Timer progress</string>
|
<string name="timer_progress">Timer progress</string>
|
||||||
<string name="last_year">Last year</string>
|
<string name="timer_session_count">%1$d of %2$d</string>
|
||||||
|
<string name="today">Today</string>
|
||||||
|
<string name="up_next">Up next</string>
|
||||||
|
<string name="up_next_notification">Up next: %1$s (%2$s)</string>
|
||||||
|
<string name="vibrate">Vibrate</string>
|
||||||
|
<string name="vibrate_desc">Vibrate when a timer completes</string>
|
||||||
|
<string name="weekly_productivity_analysis">Weekly productivity analysis</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user