tutorial_overlay.dart 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. import 'package:flutter/material.dart';
  2. import '../../utils/colors.dart';
  3. import '../../utils/haptic_utils.dart';
  4. import '../../utils/settings_manager.dart';
  5. import '../../l10n/app_localizations.dart';
  6. class TutorialOverlay extends StatefulWidget {
  7. final VoidCallback onComplete;
  8. const TutorialOverlay({super.key, required this.onComplete});
  9. @override
  10. State<TutorialOverlay> createState() => _TutorialOverlayState();
  11. }
  12. class _TutorialOverlayState extends State<TutorialOverlay>
  13. with TickerProviderStateMixin {
  14. late AnimationController _pulseController;
  15. late AnimationController _fadeController;
  16. late Animation<double> _pulseAnimation;
  17. late Animation<double> _fadeAnimation;
  18. int _currentStep = 0;
  19. final int _totalSteps = 3;
  20. @override
  21. void initState() {
  22. super.initState();
  23. _pulseController = AnimationController(
  24. duration: const Duration(milliseconds: 1500),
  25. vsync: this,
  26. )..repeat(reverse: true);
  27. _fadeController = AnimationController(
  28. duration: const Duration(milliseconds: 500),
  29. vsync: this,
  30. );
  31. _pulseAnimation = Tween<double>(begin: 0.8, end: 1.2).animate(
  32. CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
  33. );
  34. _fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
  35. CurvedAnimation(parent: _fadeController, curve: Curves.easeInOut),
  36. );
  37. _fadeController.forward();
  38. }
  39. @override
  40. void dispose() {
  41. _pulseController.dispose();
  42. _fadeController.dispose();
  43. super.dispose();
  44. }
  45. List<TutorialStep> _getSteps(BuildContext context) {
  46. final l10n = AppLocalizations.of(context)!;
  47. return [
  48. TutorialStep(
  49. title: l10n.tutorialWelcomeTitle,
  50. description: l10n.tutorialWelcomeDescription,
  51. icon: Icons.touch_app,
  52. position: TutorialPosition.center,
  53. ),
  54. TutorialStep(
  55. title: l10n.tutorialRelaxationTitle,
  56. description: l10n.tutorialRelaxationDescription,
  57. icon: Icons.stars,
  58. position: TutorialPosition.center,
  59. ),
  60. TutorialStep(
  61. title: l10n.tutorialZenTitle,
  62. description: l10n.tutorialZenDescription,
  63. icon: Icons.self_improvement,
  64. position: TutorialPosition.center,
  65. ),
  66. ];
  67. }
  68. @override
  69. Widget build(BuildContext context) {
  70. final l10n = AppLocalizations.of(context)!;
  71. final steps = _getSteps(context);
  72. final step = steps[_currentStep];
  73. return FadeTransition(
  74. opacity: _fadeAnimation,
  75. child: Container(
  76. color: ZenColors.black.withValues(alpha: 0.85),
  77. child: SafeArea(
  78. child: Stack(
  79. children: [
  80. // Tutorial content
  81. Positioned.fill(
  82. child: Column(
  83. mainAxisAlignment: MainAxisAlignment.center,
  84. children: [
  85. // Animated icon
  86. AnimatedBuilder(
  87. animation: _pulseAnimation,
  88. builder: (context, child) {
  89. return Transform.scale(
  90. scale: _pulseAnimation.value,
  91. child: Container(
  92. padding: const EdgeInsets.all(30),
  93. decoration: BoxDecoration(
  94. shape: BoxShape.circle,
  95. color: ZenColors.buttonBackground.withValues(
  96. alpha: 0.2,
  97. ),
  98. border: Border.all(
  99. color: ZenColors.buttonBackground,
  100. width: 2,
  101. ),
  102. ),
  103. child: Icon(
  104. step.icon,
  105. size: 60,
  106. color: ZenColors.buttonBackground,
  107. ),
  108. ),
  109. );
  110. },
  111. ),
  112. const SizedBox(height: 40),
  113. // Title
  114. Padding(
  115. padding: const EdgeInsets.symmetric(horizontal: 40),
  116. child: Text(
  117. step.title,
  118. style: const TextStyle(
  119. color: ZenColors.primaryText,
  120. fontSize: 28,
  121. fontWeight: FontWeight.bold,
  122. letterSpacing: 0.5,
  123. ),
  124. textAlign: TextAlign.center,
  125. ),
  126. ),
  127. const SizedBox(height: 20),
  128. // Description
  129. Padding(
  130. padding: const EdgeInsets.symmetric(horizontal: 40),
  131. child: Text(
  132. step.description,
  133. style: TextStyle(
  134. color: ZenColors.secondaryText,
  135. fontSize: 16,
  136. height: 1.5,
  137. ),
  138. textAlign: TextAlign.center,
  139. ),
  140. ),
  141. const SizedBox(height: 60),
  142. // Progress indicator
  143. Row(
  144. mainAxisAlignment: MainAxisAlignment.center,
  145. children: List.generate(_totalSteps, (index) {
  146. return Container(
  147. margin: const EdgeInsets.symmetric(horizontal: 4),
  148. width: 12,
  149. height: 12,
  150. decoration: BoxDecoration(
  151. shape: BoxShape.circle,
  152. color:
  153. index <= _currentStep
  154. ? ZenColors.buttonBackground
  155. : ZenColors.mutedText.withValues(
  156. alpha: 0.3,
  157. ),
  158. ),
  159. );
  160. }),
  161. ),
  162. const SizedBox(height: 40),
  163. // Action buttons
  164. Padding(
  165. padding: const EdgeInsets.symmetric(horizontal: 40),
  166. child: Row(
  167. children: [
  168. if (_currentStep > 0)
  169. Expanded(
  170. child: OutlinedButton(
  171. onPressed: _previousStep,
  172. style: OutlinedButton.styleFrom(
  173. foregroundColor: ZenColors.primaryText,
  174. side: const BorderSide(
  175. color: ZenColors.mutedText,
  176. ),
  177. shape: RoundedRectangleBorder(
  178. borderRadius: BorderRadius.circular(12),
  179. ),
  180. padding: const EdgeInsets.symmetric(
  181. vertical: 16,
  182. ),
  183. ),
  184. child: Text(
  185. l10n.previous,
  186. style: const TextStyle(fontSize: 16),
  187. ),
  188. ),
  189. ),
  190. if (_currentStep > 0) const SizedBox(width: 16),
  191. Expanded(
  192. flex: 2,
  193. child: ElevatedButton(
  194. onPressed: _nextStep,
  195. style: ElevatedButton.styleFrom(
  196. backgroundColor: ZenColors.buttonBackground,
  197. foregroundColor: ZenColors.buttonText,
  198. shape: RoundedRectangleBorder(
  199. borderRadius: BorderRadius.circular(12),
  200. ),
  201. padding: const EdgeInsets.symmetric(
  202. vertical: 16,
  203. ),
  204. elevation: 4,
  205. ),
  206. child: Text(
  207. _currentStep == _totalSteps - 1
  208. ? l10n.startRelaxing
  209. : l10n.continueButton,
  210. style: const TextStyle(
  211. fontSize: 16,
  212. fontWeight: FontWeight.w600,
  213. ),
  214. ),
  215. ),
  216. ),
  217. ],
  218. ),
  219. ),
  220. ],
  221. ),
  222. ),
  223. // Skip button
  224. Positioned(
  225. top: 20,
  226. right: 20,
  227. child: TextButton(
  228. onPressed: _skipTutorial,
  229. style: TextButton.styleFrom(
  230. foregroundColor: ZenColors.mutedText,
  231. ),
  232. child: Text(l10n.skip, style: const TextStyle(fontSize: 16)),
  233. ),
  234. ),
  235. ],
  236. ),
  237. ),
  238. ),
  239. );
  240. }
  241. void _nextStep() async {
  242. if (SettingsManager.isHapticsEnabled) {
  243. await HapticUtils.vibrate(duration: 50);
  244. }
  245. if (_currentStep < _totalSteps - 1) {
  246. setState(() {
  247. _currentStep++;
  248. });
  249. } else {
  250. await _completeTutorial();
  251. }
  252. }
  253. void _previousStep() async {
  254. if (SettingsManager.isHapticsEnabled) {
  255. await HapticUtils.vibrate(duration: 50);
  256. }
  257. if (_currentStep > 0) {
  258. setState(() {
  259. _currentStep--;
  260. });
  261. }
  262. }
  263. void _skipTutorial() async {
  264. if (SettingsManager.isHapticsEnabled) {
  265. await HapticUtils.vibrate(duration: 50);
  266. }
  267. await _completeTutorial();
  268. }
  269. Future<void> _completeTutorial() async {
  270. await SettingsManager.setTutorialShown(true);
  271. await _fadeController.reverse();
  272. widget.onComplete();
  273. }
  274. }
  275. class TutorialStep {
  276. final String title;
  277. final String description;
  278. final IconData icon;
  279. final TutorialPosition position;
  280. TutorialStep({
  281. required this.title,
  282. required this.description,
  283. required this.icon,
  284. required this.position,
  285. });
  286. }
  287. enum TutorialPosition { center, top, bottom }