Skip to content

feat: pomodoro timer added #764

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added assets/ringtones/break_complete.mp3
Binary file not shown.
Binary file added assets/ringtones/work_complete.mp3
Binary file not shown.
241 changes: 241 additions & 0 deletions lib/app/modules/timer/controllers/pomodoro_controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:ultimate_alarm_clock/app/modules/timer/views/pomodoro_completion_view.dart';
import 'package:ultimate_alarm_clock/app/utils/constants.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

enum TimerType { work, Break }

class PomodoroController extends GetxController {
RxInt selectedWorkTime = 45.obs; // Default work time in minutes
RxInt selectedBreakTime = 15.obs; // Default break time in minutes
RxString selectedLabel = "Work".obs;
RxBool isRunning = false.obs;
RxBool isBreakTime = false.obs;
RxInt remainingSeconds = 0.obs;
Timer? timer;

// Added number of intervals feature
RxInt selectedIntervals = 4.obs; // Default intervals
RxInt currentInterval = 0.obs; // Current interval tracker

final AudioPlayer audioPlayer = AudioPlayer();
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();

// For custom labels
RxList<String> labelOptions = ["Work", "Study", "Sleep", "Focus", "Create"].obs;
TextEditingController newLabelController = TextEditingController();

@override
void onInit() {
super.onInit();
remainingSeconds.value = selectedWorkTime.value * 60;
_initAudioPlayer();
_initNotifications();
}

@override
void onClose() {
timer?.cancel();
newLabelController.dispose();
audioPlayer.dispose();
super.onClose();
}

// Initialize the audio player
void _initAudioPlayer() async {
// Pre-load sounds for faster playback when needed
await audioPlayer.setSource(AssetSource('ringtones/work_complete.mp3'));
}

// Play notification sound
void playNotificationSound(bool isBreakFinished) async {
try {
// Different sounds for work end and break end
if (isBreakFinished) {
await audioPlayer.setSource(AssetSource('ringtones/break_complete.mp3'));
} else {
await audioPlayer.setSource(AssetSource('ringtones/work_complete.mp3'));
}

await audioPlayer.resume();
} catch (e) {
print('Error playing notification sound: $e');
}
}

// Initialize notifications
Future<void> _initNotifications() async {
// Initialize settings
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher');

const DarwinInitializationSettings initializationSettingsIOS =
DarwinInitializationSettings();

const InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS,
);

await flutterLocalNotificationsPlugin.initialize(initializationSettings);
}

// Method to show notification
Future<void> _showLocalNotification(String title, String body) async {
const AndroidNotificationDetails androidPlatformChannelSpecifics =
AndroidNotificationDetails(
'pomodoro_timer_channel',
'Pomodoro Timer Notifications',
channelDescription: 'Notifications for pomodoro timer events',
importance: Importance.high,
priority: Priority.high,
sound: RawResourceAndroidNotificationSound('notification_sound'),
);

const NotificationDetails platformChannelSpecifics =
NotificationDetails(android: androidPlatformChannelSpecifics);

await flutterLocalNotificationsPlugin.show(
0,
title,
body,
platformChannelSpecifics,
);
}

void startTimer() {
isRunning.value = true;
if (currentInterval.value == 0) {
// First start - reset interval counter
currentInterval.value = 1;
}

timer = Timer.periodic(Duration(seconds: 1), (timer) {
if (remainingSeconds.value > 0) {
remainingSeconds.value--;
} else {
// Timer completed
if (isBreakTime.value) {
// Break time finished - play break end sound
playNotificationSound(true);
_showLocalNotification(
'Break Time Complete',
'Time to focus for ${selectedWorkTime.value} minutes!'
);

// Break time finished
currentInterval.value++;

// Check if we've completed all intervals
if (currentInterval.value > selectedIntervals.value) {
// All intervals completed
timer.cancel();
isRunning.value = false;
isBreakTime.value = false;
currentInterval.value = 0;
remainingSeconds.value = selectedWorkTime.value * 60;
Get.off(() => CompletionScreen(type: 'all', duration: selectedWorkTime.value * selectedIntervals.value));
return;
}

// Still have intervals to go, switch to work time
isBreakTime.value = false;
remainingSeconds.value = selectedWorkTime.value * 60;
Get.off(() => CompletionScreen(type: 'break', duration: selectedBreakTime.value, currentInterval: currentInterval.value, totalIntervals: selectedIntervals.value));
} else {
// Work time finished - play work end sound
playNotificationSound(false);
_showLocalNotification(
'Work Time Complete',
'Time for a ${selectedBreakTime.value} minute break!'
);

// Check if it's the last interval, if so, no break needed
if (currentInterval.value >= selectedIntervals.value) {
// All intervals completed
timer.cancel();
isRunning.value = false;
isBreakTime.value = false;
currentInterval.value = 0;
remainingSeconds.value = selectedWorkTime.value * 60;
Get.off(() => CompletionScreen(type: 'all', duration: selectedWorkTime.value * selectedIntervals.value));
return;
}

// Not the last interval, switch to break time
isBreakTime.value = true;
remainingSeconds.value = selectedBreakTime.value * 60;
Get.off(() => CompletionScreen(type: 'work', duration: selectedWorkTime.value, currentInterval: currentInterval.value, totalIntervals: selectedIntervals.value));
}
}
});
}

void stopTimer() {
Get.defaultDialog(
title: "Give Up?",
middleText: "Are you sure you want to give up this session?",
textConfirm: "Yes",
textCancel: "No",
confirmTextColor: Colors.white,
buttonColor: kprimaryColor,
backgroundColor: Colors.white,
titleStyle: TextStyle(color: Colors.black),
middleTextStyle: TextStyle(color: Colors.black54),
onConfirm: () {
timer?.cancel();
isRunning.value = false;
isBreakTime.value = false;
currentInterval.value = 0;
remainingSeconds.value = selectedWorkTime.value * 60;
Get.back(); // Close Dialog
},
onCancel: () {
Get.back(); // Close Dialog
},
);
}

void setWorkTime(int minutes) {
selectedWorkTime.value = minutes;
if (!isRunning.value && !isBreakTime.value) {
remainingSeconds.value = minutes * 60;
}
}

void setBreakTime(int minutes) {
selectedBreakTime.value = minutes;
}

void setIntervals(int intervals) {
selectedIntervals.value = intervals;
}

void setLabel(String label) {
selectedLabel.value = label;
}

void addCustomLabel(String label) {
if (label.isNotEmpty && !labelOptions.contains(label)) {
labelOptions.add(label);
selectedLabel.value = label;
}
newLabelController.clear();
}

String get formattedTime {
int minutes = remainingSeconds.value ~/ 60;
int seconds = remainingSeconds.value % 60;
return "${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}";
}

String get progressText {
return isBreakTime.value
? "Break ${currentInterval.value}/${selectedIntervals.value}"
: "Session ${currentInterval.value}/${selectedIntervals.value}";
}
}
121 changes: 121 additions & 0 deletions lib/app/modules/timer/views/pomodoro_completion_view.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:ultimate_alarm_clock/app/modules/timer/views/pomodoro_timer_view.dart';


class CompletionScreen extends StatelessWidget {
final String type; // 'work' or 'break'
final int duration;
final int? currentInterval; // Optional parameter for current interval
final int? totalIntervals; // Optional parameter for total intervals

CompletionScreen({
required this.type,
required this.duration,
this.currentInterval,
this.totalIntervals,
});

@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Success Icon
Container(
height: 120,
width: 120,
decoration: BoxDecoration(
color: type == 'work' ? Colors.green.withOpacity(0.2) : Colors.orange.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Center(
child: Icon(
type == 'work' ? Icons.emoji_emotions : Icons.coffee,
color: type == 'work' ? Colors.green : Colors.orange,
size: 70,
),
),
),

SizedBox(height: 30),

// Great! text
Text(
"Great!",
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),

SizedBox(height: 20),

// Message
Text(
type == 'work'
? "You've completed a $duration-minute work session!"
: "Break time complete! Ready to get back to work?",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Colors.black54,
),
),

SizedBox(height: 40),

// Continue Button
GestureDetector(
onTap: () => Get.to(PomodoroPage()),
child: Container(
width: 180,
padding: EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: type == 'work' ? Colors.green : Colors.orange,
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: (type == 'work' ? Colors.green : Colors.orange).withOpacity(0.3),
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
child: Center(
child: Text(
"Continue",
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
),

SizedBox(height: 30),

// Small text at bottom
Text(
"Taking regular breaks helps reduce eye strain and mental fatigue",
style: TextStyle(
fontSize: 12,
color: Colors.black38,
),
),
],
),
),
),
);
}
}
Loading