Skip to content

Commit 4a98bbb

Browse files
committed
feat: Enhance Pomodoro mode with auto-start phases and improved notifications
This commit introduces significant improvements to the Pomodoro timer logic, specifically handling phase transitions more robustly and adding user-requested settings for automation and feedback. Key changes include: * **Pomodoro Automation**: Added support for "Auto-start Pomodoros" and "Auto-start Breaks" settings. The `FocusModeService` now automatically transitions between phases and starts the countdown based on user preferences. * **Focus Completion Notifications**: Introduced a new high-priority notification channel for session completion, including support for custom sounds and a dedicated "complete" message. * **Vibration Controls**: Added a toggle for "Transition Vibration" in Pomodoro settings, allowing users to enable or disable haptic feedback on phase changes. * **UI Refinement**: * Updated `TimerScreen` to use `DMSerif` typography for the timer display and replaced the text-based reset button with a standard `Replay` icon. * Deleted `TimerControls.kt` as part of the timer UI consolidation. * Simplified navigation transitions in `MainActivity` with a unified fade and slide animation. * Removed `UnifiedTopBar` from `MainActivity` to streamline the interface. * **Permission Handling**: `PermissionsCheckActivity` now automatically returns to the previous screen once all required permissions are granted. * **Accessibility & Strict Mode**: * Modified the persistent notification to hide the "Pause" button when Strict Mode is enabled. * DND (Do Not Disturb) is now dynamically enabled/disabled based on whether the current active phase is a focus phase. * **Internationalization**: Updated French translations and added new string resources for Pomodoro completion and reset actions. Signed-off-by: invokevirtual <purwarpranav80@gmail.com>
1 parent a11b719 commit 4a98bbb

File tree

9 files changed

+185
-243
lines changed

9 files changed

+185
-243
lines changed

Reef/src/main/java/dev/pranav/reef/MainActivity.kt

Lines changed: 20 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -281,60 +281,32 @@ class MainActivity: ComponentActivity() {
281281
startDestination = Screen.Home,
282282
modifier = Modifier.padding(paddingValues),
283283
enterTransition = {
284-
val fromNavBar = initialState.destination.hasRoute<Screen.Home>() ||
285-
initialState.destination.hasRoute<Screen.Usage>() ||
286-
initialState.destination.hasRoute<Screen.Timer>() ||
287-
initialState.destination.hasRoute<Screen.Settings>()
288-
val toNavBar = targetState.destination.hasRoute<Screen.Home>() ||
289-
targetState.destination.hasRoute<Screen.Usage>() ||
290-
targetState.destination.hasRoute<Screen.Timer>() ||
291-
targetState.destination.hasRoute<Screen.Settings>()
292-
293-
if (fromNavBar && toNavBar) {
294-
slideInHorizontally(
295-
initialOffsetX = { it / 4 },
296-
animationSpec = tween(200)
297-
)
298-
} else {
299-
slideInHorizontally(
300-
initialOffsetX = { it },
301-
animationSpec = tween(300)
302-
)
303-
}
284+
fadeIn(animationSpec = tween(250)) +
285+
slideInHorizontally(
286+
initialOffsetX = { it / 8 },
287+
animationSpec = tween(250)
288+
)
304289
},
305290
exitTransition = {
306-
val fromNavBar = initialState.destination.hasRoute<Screen.Home>() ||
307-
initialState.destination.hasRoute<Screen.Usage>() ||
308-
initialState.destination.hasRoute<Screen.Timer>() ||
309-
initialState.destination.hasRoute<Screen.Settings>()
310-
val toNavBar = targetState.destination.hasRoute<Screen.Home>() ||
311-
targetState.destination.hasRoute<Screen.Usage>() ||
312-
targetState.destination.hasRoute<Screen.Timer>() ||
313-
targetState.destination.hasRoute<Screen.Settings>()
314-
315-
if (fromNavBar && toNavBar) {
316-
slideOutHorizontally(
317-
targetOffsetX = { -it / 4 },
318-
animationSpec = tween(200)
319-
)
320-
} else {
321-
slideOutHorizontally(
322-
targetOffsetX = { -it },
323-
animationSpec = tween(300)
324-
)
325-
}
291+
fadeOut(animationSpec = tween(250)) +
292+
slideOutHorizontally(
293+
targetOffsetX = { -it / 8 },
294+
animationSpec = tween(250)
295+
)
326296
},
327297
popEnterTransition = {
328-
slideInHorizontally(
329-
initialOffsetX = { -it / 3 },
330-
animationSpec = tween(300)
331-
) + fadeIn(animationSpec = tween(300))
298+
fadeIn(animationSpec = tween(250)) +
299+
slideInHorizontally(
300+
initialOffsetX = { -it / 8 },
301+
animationSpec = tween(250)
302+
)
332303
},
333304
popExitTransition = {
334-
slideOutHorizontally(
335-
targetOffsetX = { it },
336-
animationSpec = tween(300)
337-
) + fadeOut(animationSpec = tween(300))
305+
fadeOut(animationSpec = tween(250)) +
306+
slideOutHorizontally(
307+
targetOffsetX = { it / 8 },
308+
animationSpec = tween(250)
309+
)
338310
}
339311
) {
340312
composable<Screen.Home> {
@@ -769,70 +741,6 @@ class MainActivity: ComponentActivity() {
769741
}
770742
}
771743

772-
@OptIn(ExperimentalMaterial3Api::class)
773-
@Composable
774-
private fun UnifiedTopBar(
775-
currentDestination: androidx.navigation.NavDestination?,
776-
) {
777-
val isHome = currentDestination?.hasRoute<Screen.Home>() == true
778-
val isTimer = currentDestination?.hasRoute<Screen.Timer>() == true
779-
val isUsage = currentDestination?.hasRoute<Screen.Usage>() == true
780-
val isSettings = currentDestination?.hasRoute<Screen.Settings>() == true
781-
val isWhitelist = currentDestination?.hasRoute<Screen.Whitelist>() == true
782-
val isRoutines = currentDestination?.hasRoute<Screen.Routines>() == true
783-
784-
val title = when {
785-
isHome -> stringResource(R.string.app_name)
786-
isTimer -> stringResource(R.string.focus_mode_title)
787-
isUsage -> stringResource(R.string.app_usage)
788-
isWhitelist -> stringResource(R.string.whitelist_apps_title)
789-
isSettings -> stringResource(R.string.settings)
790-
isRoutines -> stringResource(R.string.routines)
791-
else -> ""
792-
}
793-
794-
MediumTopAppBar(
795-
title = {
796-
Row(
797-
verticalAlignment = Alignment.CenterVertically,
798-
horizontalArrangement = Arrangement.spacedBy(12.dp)
799-
) {
800-
if (isHome) {
801-
Surface(
802-
modifier = Modifier.size(44.dp),
803-
shape = CircleShape,
804-
color = MaterialTheme.colorScheme.primaryContainer
805-
) {
806-
Box(
807-
contentAlignment = Alignment.Center,
808-
modifier = Modifier.fillMaxSize()
809-
) {
810-
Icon(
811-
Icons.Filled.Waves,
812-
contentDescription = null,
813-
modifier = Modifier.size(26.dp),
814-
tint = MaterialTheme.colorScheme.onPrimaryContainer
815-
)
816-
}
817-
}
818-
}
819-
Text(
820-
title,
821-
style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.Medium),
822-
color = MaterialTheme.colorScheme.onSurface
823-
)
824-
}
825-
},
826-
colors = TopAppBarDefaults.topAppBarColors(
827-
containerColor = Color.Transparent
828-
)
829-
)
830-
831-
if (isHome) {
832-
Spacer(modifier = Modifier.height(32.dp))
833-
}
834-
}
835-
836744
@Composable
837745
private fun ReefBottomNavBar(
838746
selectedItem: Int,

Reef/src/main/java/dev/pranav/reef/PermissionsCheckActivity.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ fun PermissionsScreen(onBackClick: () -> Unit) {
6363
val observer = LifecycleEventObserver { _, event ->
6464
if (event == Lifecycle.Event.ON_RESUME) {
6565
permissions = context.checkAllPermissions()
66+
67+
if (permissions.all { it.isGranted }) {
68+
onBackClick()
69+
}
6670
}
6771
}
6872
lifecycleOwner.lifecycle.addObserver(observer)

Reef/src/main/java/dev/pranav/reef/accessibility/FocusModeService.kt

Lines changed: 76 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class FocusModeService: Service() {
3131
companion object {
3232
private const val NOTIFICATION_ID = 1
3333
private const val BREAK_ALERT_NOTIFICATION_ID = 2
34+
private const val COMPLETE_NOTIFICATION_ID = 3
3435
const val ACTION_TIMER_UPDATED = "dev.pranav.reef.TIMER_UPDATED"
3536
const val ACTION_PAUSE = "dev.pranav.reef.PAUSE_TIMER"
3637
const val ACTION_RESUME = "dev.pranav.reef.RESUME_TIMER"
@@ -181,12 +182,17 @@ class FocusModeService: Service() {
181182
copy(isRunning = true, isPaused = false)
182183
}
183184

184-
prefs.edit { putBoolean("focus_mode", true) }
185+
val isFocusPhase = state.isPomodoroMode && state.pomodoroPhase == PomodoroPhase.FOCUS
186+
prefs.edit { putBoolean("focus_mode", isFocusPhase || !state.isPomodoroMode) }
187+
188+
if (isFocusPhase || !state.isPomodoroMode) {
189+
enableDNDIfNeeded()
190+
}
185191

186192
updateNotification(
187193
title = getNotificationTitle(),
188194
text = getString(R.string.time_remaining, formatTime(state.timeRemaining)),
189-
showPauseButton = true,
195+
showPauseButton = !state.isStrictMode,
190196
timeLeft = state.timeRemaining
191197
)
192198
startCountdown(state.timeRemaining)
@@ -249,6 +255,7 @@ class FocusModeService: Service() {
249255
broadcastTimerUpdate("00:00")
250256
TimerStateManager.reset()
251257
restoreDND()
258+
showFocusCompleteNotification()
252259
stopSelf()
253260
}
254261

@@ -263,47 +270,71 @@ class FocusModeService: Service() {
263270
return
264271
}
265272

273+
val shouldAutoStart = when (nextPhase.phase) {
274+
PomodoroPhase.FOCUS -> prefs.getBoolean("auto_start_pomodoros", false)
275+
PomodoroPhase.SHORT_BREAK, PomodoroPhase.LONG_BREAK -> prefs.getBoolean(
276+
"auto_start_breaks",
277+
true
278+
)
279+
280+
else -> false
281+
}
282+
266283
TimerStateManager.updateState {
267284
copy(
268285
pomodoroPhase = nextPhase.phase,
269286
currentCycle = nextPhase.currentCycle,
270-
timeRemaining = nextPhase.duration
287+
timeRemaining = nextPhase.duration,
288+
isRunning = shouldAutoStart,
289+
isPaused = !shouldAutoStart
271290
)
272291
}
273292

274293
// Store current cycle for persistence
275294
prefs.edit {
276295
putInt("pomodoro_current_cycle", nextPhase.currentCycle)
277-
putBoolean("focus_mode", nextPhase.phase == PomodoroPhase.FOCUS)
296+
putBoolean("focus_mode", shouldAutoStart && nextPhase.phase == PomodoroPhase.FOCUS)
278297
}
279298

280299
initialDuration = nextPhase.duration
281300

282301
if (nextPhase.phase == PomodoroPhase.FOCUS) {
283-
enableDNDIfNeeded()
302+
if (shouldAutoStart) {
303+
enableDNDIfNeeded()
304+
}
284305
if (prefs.getBoolean("break_alerts", true)) {
285306
showBreakEndedNotification()
286307
}
287308
} else {
288-
restoreDND()
309+
if (!shouldAutoStart) {
310+
restoreDND()
311+
}
289312
}
290313

291-
if (prefs.getBoolean("enable_pomodoro_vibration", true)) {
292-
AndroidUtilities.vibrate(this, 1000)
314+
if (prefs.getBoolean("pomodoro_sound_enabled", true)) {
315+
if (prefs.getBoolean("pomodoro_vibration_enabled", true)) {
316+
AndroidUtilities.vibrate(this, 1000)
317+
}
318+
playTransitionSound()
293319
}
294320

295-
if (prefs.getBoolean("enable_pomodoro_sound", true)) {
296-
playTransitionSound()
321+
val notificationText = if (shouldAutoStart) {
322+
getString(R.string.time_remaining, formatTime(nextPhase.duration))
323+
} else {
324+
getString(R.string.tap_to_start_next_phase)
297325
}
298326

299327
updateNotification(
300328
title = getNotificationTitle(),
301-
text = getString(R.string.time_remaining, formatTime(nextPhase.duration)),
302-
showPauseButton = !state.isStrictMode,
329+
text = notificationText,
330+
showPauseButton = shouldAutoStart && !state.isStrictMode,
303331
timeLeft = nextPhase.duration
304332
)
305333
broadcastTimerUpdate(formatTime(nextPhase.duration))
306-
startCountdown(nextPhase.duration)
334+
335+
if (shouldAutoStart) {
336+
startCountdown(nextPhase.duration)
337+
}
307338
}
308339

309340
private data class NextPhaseResult(
@@ -500,6 +531,38 @@ class FocusModeService: Service() {
500531
notificationManager.notify(BREAK_ALERT_NOTIFICATION_ID, notification)
501532
}
502533

534+
private fun showFocusCompleteNotification() {
535+
val intent = Intent(this, MainActivity::class.java).apply {
536+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
537+
}
538+
val pendingIntent = PendingIntent.getActivity(
539+
this, 0, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
540+
)
541+
542+
val soundUri = try {
543+
val soundUriString = prefs.getString("pomodoro_sound", null)
544+
if (soundUriString.isNullOrEmpty()) {
545+
android.media.RingtoneManager.getDefaultUri(android.media.RingtoneManager.TYPE_NOTIFICATION)
546+
} else {
547+
soundUriString.toUri()
548+
}
549+
} catch (e: Exception) {
550+
android.media.RingtoneManager.getDefaultUri(android.media.RingtoneManager.TYPE_NOTIFICATION)
551+
}
552+
553+
val notification = NotificationCompat.Builder(this, FOCUS_MODE_CHANNEL_ID)
554+
.setSmallIcon(R.drawable.ic_launcher_monochrome)
555+
.setContentTitle(getString(R.string.focus_session_complete))
556+
.setContentText(getString(R.string.focus_session_complete_message))
557+
.setPriority(NotificationCompat.PRIORITY_HIGH)
558+
.setSound(soundUri)
559+
.setAutoCancel(true)
560+
.setContentIntent(pendingIntent)
561+
.build()
562+
563+
notificationManager.notify(COMPLETE_NOTIFICATION_ID, notification)
564+
}
565+
503566
override fun onDestroy() {
504567
super.onDestroy()
505568
countDownTimer?.cancel()

Reef/src/main/java/dev/pranav/reef/screens/PomodoroSettingsScreen.kt

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ fun PomodoroSettingsContent(
6262
)
6363
)
6464
}
65+
var vibrationEnabled by remember {
66+
mutableStateOf(
67+
prefs.getBoolean(
68+
"pomodoro_vibration_enabled",
69+
true
70+
)
71+
)
72+
}
6573

6674
val numberSettings = listOf(
6775
NumberSetting(
@@ -323,6 +331,60 @@ fun PomodoroSettingsContent(
323331
)
324332
}
325333
}
334+
335+
item {
336+
HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp))
337+
}
338+
339+
item {
340+
Text(
341+
text = stringResource(R.string.vibration),
342+
style = MaterialTheme.typography.titleMedium,
343+
fontWeight = FontWeight.Bold,
344+
color = MaterialTheme.colorScheme.primary,
345+
modifier = Modifier.padding(vertical = 8.dp)
346+
)
347+
}
348+
349+
item {
350+
SettingsCard(index = 0, listSize = 1) {
351+
ListItem(
352+
modifier = Modifier
353+
.clickable {
354+
vibrationEnabled = !vibrationEnabled
355+
prefs.edit {
356+
putBoolean(
357+
"pomodoro_vibration_enabled",
358+
vibrationEnabled
359+
)
360+
}
361+
}
362+
.padding(4.dp),
363+
headlineContent = {
364+
Text(
365+
text = stringResource(R.string.transition_vibration),
366+
style = MaterialTheme.typography.titleMedium
367+
)
368+
},
369+
supportingContent = {
370+
Text(
371+
text = stringResource(R.string.transition_vibration_description),
372+
style = MaterialTheme.typography.bodySmall
373+
)
374+
},
375+
trailingContent = {
376+
Switch(
377+
checked = vibrationEnabled,
378+
onCheckedChange = {
379+
vibrationEnabled = it
380+
prefs.edit { putBoolean("pomodoro_vibration_enabled", it) }
381+
}
382+
)
383+
},
384+
colors = ListItemDefaults.colors(containerColor = Color.Transparent)
385+
)
386+
}
387+
}
326388
}
327389
}
328390
}

0 commit comments

Comments
 (0)