TempoFlow/lib/pages/home_page.dart

597 lines
19 KiB
Dart

import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../theme/app_theme.dart';
import '../models/app_settings.dart';
import '../models/focus_session.dart';
import '../models/history_repository.dart';
import '../widgets/ambient_mesh_background.dart';
import '../widgets/rotary_knob.dart';
import '../widgets/bottom_sheets.dart';
import 'breathe_page.dart';
import 'stats_page.dart';
import 'settings_page.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
double _setDuration = 25.0;
double _currentDuration = 0;
bool _isTiming = false;
Timer? _timer;
final List<String> _tags = ["Deep Work", "Reading", "Meditation", "Creativity"];
int _selectedTagIndex = 0;
String? _currentIntent;
late AnimationController _breathingController;
late AnimationController _scaleController;
@override
void initState() {
super.initState();
_currentDuration = _setDuration * 60;
_breathingController = AnimationController(
vsync: this,
duration: const Duration(seconds: 4),
)..repeat(reverse: true);
_scaleController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
lowerBound: 0.95,
upperBound: 1.0,
);
}
@override
void dispose() {
_timer?.cancel();
_breathingController.dispose();
_scaleController.dispose();
super.dispose();
}
void _triggerHaptic() {
if (AppSettings.enableHaptics) {
HapticFeedback.lightImpact();
}
}
void _handleStartRequest() {
if (_isTiming) {
_stopTimer();
} else {
_showIntentDialog();
}
}
void _showIntentDialog() {
final TextEditingController intentController = TextEditingController();
showDialog(
context: context,
builder: (context) {
return AlertDialog(
backgroundColor: Colors.white.withOpacity(0.95),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
title: const Text(
"Focus Intent",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w800),
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"What is your goal?",
style: TextStyle(color: AppTheme.textSub, fontSize: 16),
),
const SizedBox(height: 16),
TextField(
controller: intentController,
autofocus: true,
style: const TextStyle(
color: AppTheme.textMain,
fontWeight: FontWeight.bold,
fontSize: 18,
),
decoration: InputDecoration(
hintText: "e.g. Design System",
hintStyle: TextStyle(color: Colors.grey[400]),
filled: true,
fillColor: AppTheme.bgLight,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
),
),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
_startTimer(intent: null);
},
child: const Text(
"Skip",
style: TextStyle(color: AppTheme.textSub),
),
),
TextButton(
onPressed: () {
Navigator.pop(context);
_startTimer(intent: intentController.text.trim());
},
child: const Text(
"START",
style: TextStyle(
fontWeight: FontWeight.w900,
color: AppTheme.primary,
letterSpacing: 1,
),
),
),
],
);
},
);
}
void _startTimer({String? intent}) {
if (AppSettings.enableHaptics) HapticFeedback.heavyImpact();
setState(() {
_isTiming = true;
_currentIntent = (intent != null && intent.isNotEmpty) ? intent : null;
_currentDuration = _setDuration * 60;
});
_scaleController.forward();
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_currentDuration > 0) {
setState(() => _currentDuration--);
} else {
_stopTimer(completed: true);
}
});
}
void _stopTimer({bool completed = false}) {
_timer?.cancel();
_scaleController.reverse();
setState(() => _isTiming = false);
if (completed) {
_saveSession(5.0);
_showCompletionDialog();
} else {
_showRatingSheet();
}
}
void _saveSession(double rating) {
HistoryRepository.addSession(FocusSession(
startTime: DateTime.now(),
durationMinutes: (_setDuration - _currentDuration / 60).round(),
tag: _tags[_selectedTagIndex],
intent: _currentIntent,
rating: rating,
));
}
void _showRatingSheet() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => FocusRatingSheet(
onSave: (rating) {
_saveSession(rating);
},
),
);
}
void _showCompletionDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white.withOpacity(0.95),
title: const Text(
"Session Complete",
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.w800),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.check_circle_outline,
size: 80,
color: AppTheme.accent,
),
if (_currentIntent != null) ...[
const SizedBox(height: 16),
Text(
'"${_currentIntent!}"',
textAlign: TextAlign.center,
style: const TextStyle(
fontWeight: FontWeight.w800,
fontSize: 18,
color: AppTheme.textMain,
),
),
const SizedBox(height: 4),
const Text(
"completed!",
style: TextStyle(color: AppTheme.textSub),
),
]
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text(
"Done",
style: TextStyle(fontWeight: FontWeight.bold),
),
)
],
),
);
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final isSmallScreen = size.height < 700;
return Scaffold(
extendBodyBehindAppBar: true,
body: Stack(
children: [
AmbientMeshBackground(isDark: _isTiming),
SafeArea(
child: Column(
children: [
_buildAppBar(),
const Spacer(),
if (_isTiming && _currentIntent != null)
AnimatedOpacity(
opacity: 1.0,
duration: const Duration(milliseconds: 500),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: Column(
children: [
const Icon(
Icons.center_focus_strong,
color: AppTheme.accent,
size: 28,
),
const SizedBox(height: 16),
Text(
_currentIntent!,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.w800,
letterSpacing: 0.5,
height: 1.2,
shadows: [
Shadow(blurRadius: 15, color: Colors.black)
],
),
),
],
),
),
)
else
AnimatedOpacity(
opacity: _isTiming ? 0.0 : 1.0,
duration: const Duration(milliseconds: 500),
child: SizedBox(
height: 50,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 30),
itemCount: _tags.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (context, index) {
final isSelected = _selectedTagIndex == index;
return GestureDetector(
onTap: () {
_triggerHaptic();
setState(() => _selectedTagIndex = index);
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
decoration: BoxDecoration(
color: isSelected
? AppTheme.primary
: Colors.white.withOpacity(0.6),
borderRadius: BorderRadius.circular(30),
boxShadow: isSelected ? AppTheme.softShadow : [],
),
child: Text(
_tags[index],
style: TextStyle(
color: isSelected
? Colors.white
: AppTheme.textMain,
fontWeight: FontWeight.w700,
fontSize: 15,
),
),
),
);
},
),
),
),
const Spacer(),
_buildKnobSection(isSmallScreen),
if (AppSettings.strictMode && !_isTiming)
Padding(
padding: const EdgeInsets.only(top: 30),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(
Icons.screen_rotation,
size: 20,
color: AppTheme.textSub,
),
SizedBox(width: 10),
Text(
"Flip phone to start",
style: TextStyle(
color: AppTheme.textSub,
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
],
),
),
const Spacer(),
_buildBottomControls(),
const SizedBox(height: 40),
],
),
),
],
),
);
}
Widget _buildAppBar() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: Icon(
Icons.air,
color: _isTiming ? Colors.white70 : AppTheme.primary,
size: 28,
),
onPressed: () => Navigator.push(
context,
CupertinoPageRoute(builder: (_) => const BreathePage()),
),
),
Text(
_isTiming ? "F O C U S" : "T E M P O",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w900,
letterSpacing: 5,
color: _isTiming ? Colors.white : AppTheme.primary,
shadows: _isTiming
? [const Shadow(blurRadius: 5, color: Colors.black)]
: null,
),
),
Row(
children: [
IconButton(
icon: Icon(
Icons.bar_chart,
color: _isTiming ? Colors.white70 : AppTheme.primary,
size: 28,
),
onPressed: () => Navigator.push(
context,
CupertinoPageRoute(builder: (_) => const StatsPage()),
).then((_) => setState(() {})),
),
IconButton(
icon: Icon(
Icons.settings_outlined,
color: _isTiming ? Colors.white70 : AppTheme.primary,
size: 28,
),
onPressed: () => Navigator.push(
context,
CupertinoPageRoute(builder: (_) => const SettingsPage()),
).then((_) => setState(() {})),
),
],
),
],
),
);
}
Widget _buildKnobSection(bool isSmallScreen) {
final double size = isSmallScreen ? 240 : 300;
final double knobSize = isSmallScreen ? 200 : 260;
return Stack(
alignment: Alignment.center,
children: [
AnimatedBuilder(
animation: _breathingController,
builder: (context, child) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
(_isTiming ? AppTheme.flowStart : AppTheme.accent)
.withOpacity(0.2 * _breathingController.value),
Colors.transparent,
],
),
),
);
},
),
RotaryKnob(
size: knobSize,
value: _setDuration,
min: 1,
max: 120,
isTiming: _isTiming,
currentSeconds: _currentDuration,
onChanged: (val) {
setState(() {
_setDuration = val;
_currentDuration = val * 60;
});
},
),
],
);
}
Widget _buildBottomControls() {
final double progress =
(HistoryRepository.todayMinutes / AppSettings.dailyGoalMinutes)
.clamp(0.0, 1.0);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Column(
children: [
AnimatedOpacity(
opacity: _isTiming ? 0.0 : 1.0,
duration: const Duration(milliseconds: 300),
child: GestureDetector(
onTap: () {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => const SoundMixerSheet(),
);
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 12,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.8),
borderRadius: BorderRadius.circular(24),
border: Border.all(color: Colors.white),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
)
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(Icons.graphic_eq, size: 18, color: AppTheme.primary),
SizedBox(width: 8),
Text(
"Soundscape Mixer",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w800,
color: AppTheme.textMain,
),
),
],
),
),
),
),
const SizedBox(height: 30),
Stack(
alignment: Alignment.center,
children: [
if (!_isTiming)
SizedBox(
width: 92,
height: 92,
child: CircularProgressIndicator(
value: progress,
strokeWidth: 4,
backgroundColor: Colors.grey.withOpacity(0.2),
valueColor: const AlwaysStoppedAnimation<Color>(
AppTheme.accent,
),
strokeCap: StrokeCap.round,
),
),
GestureDetector(
onTap: _handleStartRequest,
child: AnimatedContainer(
duration: const Duration(milliseconds: 400),
curve: Curves.easeOutCubic,
height: 80,
width: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _isTiming ? Colors.red[400] : AppTheme.primary,
boxShadow: _isTiming
? [
BoxShadow(
color: Colors.red.withOpacity(0.4),
blurRadius: 20,
spreadRadius: 5,
)
]
: AppTheme.softShadow,
),
child: Icon(
_isTiming ? Icons.stop_rounded : Icons.play_arrow_rounded,
color: Colors.white,
size: 44,
),
),
),
],
),
],
),
);
}
}