引导、通知栏计时
This commit is contained in:
@@ -28,7 +28,9 @@
|
|||||||
"Bash(gem install:*)",
|
"Bash(gem install:*)",
|
||||||
"Bash(export PATH=\"$PATH:$HOME/sdk/flutter/bin\")",
|
"Bash(export PATH=\"$PATH:$HOME/sdk/flutter/bin\")",
|
||||||
"Bash(flutter doctor:*)",
|
"Bash(flutter doctor:*)",
|
||||||
"Bash(flutter analyze:*)"
|
"Bash(flutter analyze:*)",
|
||||||
|
"Bash(start \"\" \"f:\\cursor-auto\\focusBuddy\\onboarding-preview.html\")",
|
||||||
|
"Bash(flutter emulators:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'services/storage_service.dart';
|
|||||||
import 'services/encouragement_service.dart';
|
import 'services/encouragement_service.dart';
|
||||||
import 'services/notification_service.dart';
|
import 'services/notification_service.dart';
|
||||||
import 'screens/home_screen.dart';
|
import 'screens/home_screen.dart';
|
||||||
|
import 'screens/onboarding_screen.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
@@ -23,7 +24,7 @@ void main() async {
|
|||||||
runApp(MyApp(encouragementService: encouragementService));
|
runApp(MyApp(encouragementService: encouragementService));
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatefulWidget {
|
||||||
final EncouragementService encouragementService;
|
final EncouragementService encouragementService;
|
||||||
|
|
||||||
const MyApp({
|
const MyApp({
|
||||||
@@ -31,13 +32,45 @@ class MyApp extends StatelessWidget {
|
|||||||
required this.encouragementService,
|
required this.encouragementService,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MyApp> createState() => _MyAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MyAppState extends State<MyApp> {
|
||||||
|
bool _hasCompletedOnboarding = false;
|
||||||
|
bool _isLoading = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_checkOnboardingStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkOnboardingStatus() async {
|
||||||
|
final completed = await OnboardingScreen.hasCompletedOnboarding();
|
||||||
|
setState(() {
|
||||||
|
_hasCompletedOnboarding = completed;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'FocusBuddy',
|
title: 'FocusBuddy',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: AppTheme.lightTheme,
|
theme: AppTheme.lightTheme,
|
||||||
home: HomeScreen(encouragementService: encouragementService),
|
home: _isLoading
|
||||||
|
? const Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: _hasCompletedOnboarding
|
||||||
|
? HomeScreen(encouragementService: widget.encouragementService)
|
||||||
|
: OnboardingScreen(
|
||||||
|
encouragementService: widget.encouragementService,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,21 +24,66 @@ class FocusScreen extends StatefulWidget {
|
|||||||
State<FocusScreen> createState() => _FocusScreenState();
|
State<FocusScreen> createState() => _FocusScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FocusScreenState extends State<FocusScreen> {
|
class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||||
late Timer _timer;
|
late Timer _timer;
|
||||||
late int _remainingSeconds;
|
late int _remainingSeconds;
|
||||||
late DateTime _startTime;
|
late DateTime _startTime;
|
||||||
final List<String> _distractions = [];
|
final List<String> _distractions = [];
|
||||||
bool _isPaused = false;
|
bool _isPaused = false;
|
||||||
|
bool _isInBackground = false;
|
||||||
|
final NotificationService _notificationService = NotificationService();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_remainingSeconds = widget.durationMinutes * 60;
|
_remainingSeconds = widget.durationMinutes * 60;
|
||||||
_startTime = DateTime.now();
|
_startTime = DateTime.now();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
_startTimer();
|
_startTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
_timer.cancel();
|
||||||
|
// Cancel ongoing notification when leaving the screen
|
||||||
|
_notificationService.cancelOngoingFocusNotification();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
super.didChangeAppLifecycleState(state);
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case AppLifecycleState.paused:
|
||||||
|
case AppLifecycleState.inactive:
|
||||||
|
case AppLifecycleState.detached:
|
||||||
|
// App went to background
|
||||||
|
_isInBackground = true;
|
||||||
|
if (!_isPaused && _remainingSeconds > 0) {
|
||||||
|
_showBackgroundNotification();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case AppLifecycleState.resumed:
|
||||||
|
// App came back to foreground
|
||||||
|
_isInBackground = false;
|
||||||
|
_notificationService.cancelOngoingFocusNotification();
|
||||||
|
break;
|
||||||
|
case AppLifecycleState.hidden:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showBackgroundNotification() {
|
||||||
|
final minutes = _remainingSeconds ~/ 60;
|
||||||
|
final seconds = _remainingSeconds % 60;
|
||||||
|
_notificationService.showOngoingFocusNotification(
|
||||||
|
remainingMinutes: minutes,
|
||||||
|
remainingSeconds: seconds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _startTimer() {
|
void _startTimer() {
|
||||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
if (!_isPaused && _remainingSeconds > 0) {
|
if (!_isPaused && _remainingSeconds > 0) {
|
||||||
@@ -46,6 +91,18 @@ class _FocusScreenState extends State<FocusScreen> {
|
|||||||
_remainingSeconds--;
|
_remainingSeconds--;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update background notification every 30 seconds when in background
|
||||||
|
if (_isInBackground && _remainingSeconds > 0) {
|
||||||
|
if (_remainingSeconds % 30 == 0) {
|
||||||
|
final minutes = _remainingSeconds ~/ 60;
|
||||||
|
final seconds = _remainingSeconds % 60;
|
||||||
|
_notificationService.updateOngoingFocusNotification(
|
||||||
|
remainingMinutes: minutes,
|
||||||
|
remainingSeconds: seconds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (_remainingSeconds == 0) {
|
if (_remainingSeconds == 0) {
|
||||||
_onTimerComplete();
|
_onTimerComplete();
|
||||||
}
|
}
|
||||||
@@ -55,11 +112,14 @@ class _FocusScreenState extends State<FocusScreen> {
|
|||||||
|
|
||||||
void _onTimerComplete() async {
|
void _onTimerComplete() async {
|
||||||
_timer.cancel();
|
_timer.cancel();
|
||||||
|
|
||||||
|
// Cancel ongoing notification and show completion notification
|
||||||
|
await _notificationService.cancelOngoingFocusNotification();
|
||||||
|
|
||||||
_saveFocusSession(completed: true);
|
_saveFocusSession(completed: true);
|
||||||
|
|
||||||
// Send notification
|
// Send completion notification
|
||||||
final notificationService = NotificationService();
|
await _notificationService.showFocusCompletedNotification(
|
||||||
await notificationService.showFocusCompletedNotification(
|
|
||||||
minutes: widget.durationMinutes,
|
minutes: widget.durationMinutes,
|
||||||
distractionCount: _distractions.length,
|
distractionCount: _distractions.length,
|
||||||
);
|
);
|
||||||
@@ -82,6 +142,13 @@ class _FocusScreenState extends State<FocusScreen> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_isPaused = !_isPaused;
|
_isPaused = !_isPaused;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update notification when paused
|
||||||
|
if (_isPaused && _isInBackground) {
|
||||||
|
_notificationService.cancelOngoingFocusNotification();
|
||||||
|
} else if (!_isPaused && _isInBackground) {
|
||||||
|
_showBackgroundNotification();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _stopEarly() {
|
void _stopEarly() {
|
||||||
@@ -257,12 +324,6 @@ class _FocusScreenState extends State<FocusScreen> {
|
|||||||
return '${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}';
|
return '${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_timer.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|||||||
243
lib/screens/onboarding_screen.dart
Normal file
243
lib/screens/onboarding_screen.dart
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import '../theme/app_colors.dart';
|
||||||
|
import '../theme/app_text_styles.dart';
|
||||||
|
import 'home_screen.dart';
|
||||||
|
import '../services/encouragement_service.dart';
|
||||||
|
|
||||||
|
/// Onboarding Screen - Shows on first launch
|
||||||
|
class OnboardingScreen extends StatefulWidget {
|
||||||
|
final EncouragementService encouragementService;
|
||||||
|
|
||||||
|
const OnboardingScreen({
|
||||||
|
super.key,
|
||||||
|
required this.encouragementService,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Check if user has completed onboarding
|
||||||
|
static Future<bool> hasCompletedOnboarding() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
return prefs.getBool(_onboardingKey) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark onboarding as completed
|
||||||
|
static Future<void> setOnboardingCompleted() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setBool(_onboardingKey, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const String _onboardingKey = 'onboarding_completed';
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<OnboardingScreen> createState() => _OnboardingScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OnboardingScreenState extends State<OnboardingScreen> {
|
||||||
|
final PageController _pageController = PageController();
|
||||||
|
int _currentPage = 0;
|
||||||
|
|
||||||
|
final List<OnboardingPage> _pages = [
|
||||||
|
OnboardingPage(
|
||||||
|
emoji: '💚',
|
||||||
|
title: 'Focus without guilt',
|
||||||
|
description:
|
||||||
|
"This app is different — it won't punish you for losing focus.\n\nPerfect for ADHD, anxiety, or anyone who finds traditional timers too harsh.",
|
||||||
|
),
|
||||||
|
OnboardingPage(
|
||||||
|
emoji: '🤚',
|
||||||
|
title: 'Tap when you get distracted',
|
||||||
|
description:
|
||||||
|
"We'll gently remind you to come back.\n\nNo shame. No stress. Just a friendly nudge.",
|
||||||
|
),
|
||||||
|
OnboardingPage(
|
||||||
|
emoji: '📊',
|
||||||
|
title: 'Track your progress',
|
||||||
|
description:
|
||||||
|
"See how you're improving, one session at a time.\n\nEvery distraction is just data — not failure.",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
void _onPageChanged(int page) {
|
||||||
|
setState(() {
|
||||||
|
_currentPage = page;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _nextPage() {
|
||||||
|
if (_currentPage < _pages.length - 1) {
|
||||||
|
_pageController.animateToPage(
|
||||||
|
_currentPage + 1,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_completeOnboarding();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _skipOnboarding() {
|
||||||
|
_completeOnboarding();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _completeOnboarding() async {
|
||||||
|
await OnboardingScreen.setOnboardingCompleted();
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => HomeScreen(
|
||||||
|
encouragementService: widget.encouragementService,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_pageController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: AppColors.background,
|
||||||
|
body: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Skip button
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.topRight,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: _skipOnboarding,
|
||||||
|
child: const Text(
|
||||||
|
'Skip',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Nunito',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// PageView
|
||||||
|
Expanded(
|
||||||
|
child: PageView.builder(
|
||||||
|
controller: _pageController,
|
||||||
|
onPageChanged: _onPageChanged,
|
||||||
|
itemCount: _pages.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return _buildPage(_pages[index]);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Page indicators
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 24.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: List.generate(
|
||||||
|
_pages.length,
|
||||||
|
(index) => _buildIndicator(index == _currentPage),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Next/Get Started button
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _nextPage,
|
||||||
|
child: Text(
|
||||||
|
_currentPage == _pages.length - 1
|
||||||
|
? 'Get Started'
|
||||||
|
: 'Next',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPage(OnboardingPage page) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// Emoji
|
||||||
|
Text(
|
||||||
|
page.emoji,
|
||||||
|
style: const TextStyle(fontSize: 80),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 48),
|
||||||
|
|
||||||
|
// Title
|
||||||
|
Text(
|
||||||
|
page.title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'Nunito',
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Description
|
||||||
|
Text(
|
||||||
|
page.description,
|
||||||
|
style: AppTextStyles.bodyText.copyWith(
|
||||||
|
fontSize: 18,
|
||||||
|
height: 1.6,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildIndicator(bool isActive) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
width: isActive ? 24 : 8,
|
||||||
|
height: 8,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isActive ? AppColors.primary : AppColors.divider,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Data class for onboarding page
|
||||||
|
class OnboardingPage {
|
||||||
|
final String emoji;
|
||||||
|
final String title;
|
||||||
|
final String description;
|
||||||
|
|
||||||
|
OnboardingPage({
|
||||||
|
required this.emoji,
|
||||||
|
required this.title,
|
||||||
|
required this.description,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -110,6 +110,24 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
_showAboutDialog();
|
_showAboutDialog();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
const Divider(color: AppColors.divider),
|
||||||
|
ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: Text(
|
||||||
|
'Reset Onboarding',
|
||||||
|
style: AppTextStyles.bodyText.copyWith(
|
||||||
|
color: AppColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: const Icon(
|
||||||
|
Icons.refresh,
|
||||||
|
size: 16,
|
||||||
|
color: AppColors.textSecondary,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_resetOnboarding();
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -318,4 +336,40 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _resetOnboarding() async {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Reset Onboarding?'),
|
||||||
|
content: Text(
|
||||||
|
'This will show the onboarding screens again when you restart the app.',
|
||||||
|
style: AppTextStyles.bodyText,
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.remove('onboarding_completed');
|
||||||
|
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
Navigator.pop(context);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Onboarding reset. Restart the app to see it again.'),
|
||||||
|
duration: Duration(seconds: 3),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text('Reset'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,6 +189,88 @@ class NotificationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cancel a specific notification by ID
|
||||||
|
Future<void> cancel(int id) async {
|
||||||
|
if (kIsWeb || !_initialized) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _notifications.cancel(id);
|
||||||
|
} catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print('Failed to cancel notification $id: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show ongoing focus session notification (for background)
|
||||||
|
/// This notification stays visible while the timer is running
|
||||||
|
Future<void> showOngoingFocusNotification({
|
||||||
|
required int remainingMinutes,
|
||||||
|
required int remainingSeconds,
|
||||||
|
}) async {
|
||||||
|
if (kIsWeb || !_initialized) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Format time display
|
||||||
|
final timeStr = '${remainingMinutes.toString().padLeft(2, '0')}:${(remainingSeconds % 60).toString().padLeft(2, '0')}';
|
||||||
|
|
||||||
|
const androidDetails = AndroidNotificationDetails(
|
||||||
|
'focus_timer',
|
||||||
|
'Focus Timer',
|
||||||
|
channelDescription: 'Shows ongoing focus session timer',
|
||||||
|
importance: Importance.low,
|
||||||
|
priority: Priority.low,
|
||||||
|
ongoing: true, // Makes notification persistent
|
||||||
|
autoCancel: false,
|
||||||
|
showWhen: false,
|
||||||
|
enableVibration: false,
|
||||||
|
playSound: false,
|
||||||
|
// Show in status bar
|
||||||
|
showProgress: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const iosDetails = DarwinNotificationDetails(
|
||||||
|
presentAlert: true,
|
||||||
|
presentBadge: false,
|
||||||
|
presentSound: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const notificationDetails = NotificationDetails(
|
||||||
|
android: androidDetails,
|
||||||
|
iOS: iosDetails,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _notifications.show(
|
||||||
|
2, // Use ID 2 for ongoing notifications
|
||||||
|
'⏱️ Focus session in progress',
|
||||||
|
'$timeStr remaining',
|
||||||
|
notificationDetails,
|
||||||
|
payload: 'focus_ongoing',
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print('Failed to show ongoing notification: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update ongoing notification with new time
|
||||||
|
Future<void> updateOngoingFocusNotification({
|
||||||
|
required int remainingMinutes,
|
||||||
|
required int remainingSeconds,
|
||||||
|
}) async {
|
||||||
|
// On Android, showing the same notification ID updates it
|
||||||
|
await showOngoingFocusNotification(
|
||||||
|
remainingMinutes: remainingMinutes,
|
||||||
|
remainingSeconds: remainingSeconds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cancel ongoing focus notification
|
||||||
|
Future<void> cancelOngoingFocusNotification() async {
|
||||||
|
await cancel(2); // Cancel notification with ID 2
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if notifications are supported on this platform
|
/// Check if notifications are supported on this platform
|
||||||
bool get isSupported => !kIsWeb;
|
bool get isSupported => !kIsWeb;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,26 +5,19 @@
|
|||||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||||
// tree, read text, and verify that the values of widget properties are correct.
|
// tree, read text, and verify that the values of widget properties are correct.
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:focus_buddy/services/encouragement_service.dart';
|
||||||
import 'package:focus_buddy/main.dart';
|
import 'package:focus_buddy/main.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
testWidgets('App loads successfully', (WidgetTester tester) async {
|
||||||
|
// Create a mock encouragement service
|
||||||
|
final encouragementService = EncouragementService();
|
||||||
|
|
||||||
// Build our app and trigger a frame.
|
// Build our app and trigger a frame.
|
||||||
await tester.pumpWidget(const MyApp());
|
await tester.pumpWidget(MyApp(encouragementService: encouragementService));
|
||||||
|
|
||||||
// Verify that our counter starts at 0.
|
// Verify that the app loads
|
||||||
expect(find.text('0'), findsOneWidget);
|
expect(find.byType(MyApp), findsOneWidget);
|
||||||
expect(find.text('1'), findsNothing);
|
|
||||||
|
|
||||||
// Tap the '+' icon and trigger a frame.
|
|
||||||
await tester.tap(find.byIcon(Icons.add));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
// Verify that our counter has incremented.
|
|
||||||
expect(find.text('0'), findsNothing);
|
|
||||||
expect(find.text('1'), findsOneWidget);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user