import 'dart:ui'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import '../../constant/colors.dart'; import '../../constant/style.dart'; import '../../controller/functions/tts.dart'; // ───────────────────────────────────────────────────────────────────────────── // Config // ───────────────────────────────────────────────────────────────────────────── class _DC { static const Duration animDuration = Duration(milliseconds: 280); static const double blur = 20.0; static const double radius = 24.0; static const Color barrierColor = Color(0x66000000); } // ───────────────────────────────────────────────────────────────────────────── // Shared animated wrapper — every dialog uses this // ───────────────────────────────────────────────────────────────────────────── class _DialogShell extends StatelessWidget { final Widget child; const _DialogShell({required this.child}); @override Widget build(BuildContext context) { return TweenAnimationBuilder( duration: _DC.animDuration, tween: Tween(begin: 0.0, end: 1.0), curve: Curves.easeOutBack, builder: (_, v, c) => Transform.scale( scale: 0.88 + (0.12 * v), child: Opacity(opacity: v.clamp(0.0, 1.0), child: c), ), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: _DC.blur, sigmaY: _DC.blur), child: Dialog( backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.symmetric(horizontal: 28, vertical: 40), child: child, ), ), ); } } // ───────────────────────────────────────────────────────────────────────────── // Shared glass card // ───────────────────────────────────────────────────────────────────────────── class _GlassCard extends StatelessWidget { final Widget child; const _GlassCard({required this.child}); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(_DC.radius), gradient: LinearGradient( colors: [ AppColor.secondaryColor.withOpacity(0.95), AppColor.secondaryColor.withOpacity(0.88), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.18), blurRadius: 40, spreadRadius: -4, offset: const Offset(0, 16), ), BoxShadow( color: AppColor.primaryColor.withOpacity(0.08), blurRadius: 20, offset: const Offset(0, 4), ), ], border: Border.all( color: AppColor.secondaryColor.withOpacity(0.6), width: 1.2, ), ), child: child, ); } } // ───────────────────────────────────────────────────────────────────────────── // Shared bottom action row // ───────────────────────────────────────────────────────────────────────────── class _ActionRow extends StatelessWidget { final VoidCallback onCancel; final VoidCallback onConfirm; final String confirmLabel; final bool isDestructive; const _ActionRow({ required this.onCancel, required this.onConfirm, this.confirmLabel = 'OK', this.isDestructive = false, }); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( border: Border( top: BorderSide(color: Colors.grey.withOpacity(0.15), width: 1), ), ), child: Row( children: [ // Cancel Expanded( child: _ActionButton( label: 'Cancel'.tr, color: Colors.grey[600]!, backgroundColor: Colors.grey.withOpacity(0.07), onPressed: () { HapticFeedback.lightImpact(); onCancel(); }, isLeft: true, ), ), Container(width: 1, height: 52, color: Colors.grey.withOpacity(0.15)), // Confirm Expanded( child: _ActionButton( label: confirmLabel, color: isDestructive ? AppColor.redColor : AppColor.primaryColor, backgroundColor: isDestructive ? AppColor.redColor.withOpacity(0.07) : AppColor.primaryColor.withOpacity(0.07), onPressed: () { HapticFeedback.mediumImpact(); onConfirm(); }, isLeft: false, isBold: true, ), ), ], ), ); } } class _ActionButton extends StatelessWidget { final String label; final Color color; final Color backgroundColor; final VoidCallback onPressed; final bool isLeft; final bool isBold; const _ActionButton({ required this.label, required this.color, required this.backgroundColor, required this.onPressed, required this.isLeft, this.isBold = false, }); @override Widget build(BuildContext context) { return Material( color: Colors.transparent, child: InkWell( onTap: onPressed, borderRadius: BorderRadius.only( bottomLeft: isLeft ? const Radius.circular(_DC.radius) : Radius.zero, bottomRight: !isLeft ? const Radius.circular(_DC.radius) : Radius.zero, ), child: Container( height: 52, decoration: BoxDecoration( color: backgroundColor, borderRadius: BorderRadius.only( bottomLeft: isLeft ? const Radius.circular(_DC.radius) : Radius.zero, bottomRight: !isLeft ? const Radius.circular(_DC.radius) : Radius.zero, ), ), alignment: Alignment.center, child: Text( label, style: TextStyle( color: color, fontSize: 15, fontWeight: isBold ? FontWeight.w700 : FontWeight.w500, letterSpacing: -0.2, ), ), ), ), ); } } // ───────────────────────────────────────────────────────────────────────────── // TTS speak button // ───────────────────────────────────────────────────────────────────────────── class _SpeakButton extends StatefulWidget { final List texts; const _SpeakButton({required this.texts}); @override State<_SpeakButton> createState() => _SpeakButtonState(); } class _SpeakButtonState extends State<_SpeakButton> with SingleTickerProviderStateMixin { bool _speaking = false; late AnimationController _pulse; @override void initState() { super.initState(); _pulse = AnimationController( vsync: this, duration: const Duration(milliseconds: 900), lowerBound: 0.92, upperBound: 1.0, ); } @override void dispose() { _pulse.dispose(); super.dispose(); } Future _onTap() async { if (_speaking) return; HapticFeedback.selectionClick(); setState(() => _speaking = true); _pulse.repeat(reverse: true); final tts = Get.find(); for (final t in widget.texts) { await tts.speakText(t); } _pulse.stop(); _pulse.reset(); if (mounted) setState(() => _speaking = false); } @override Widget build(BuildContext context) { return GestureDetector( onTap: _onTap, child: AnimatedBuilder( animation: _pulse, builder: (_, child) => Transform.scale(scale: _pulse.value, child: child), child: AnimatedContainer( duration: const Duration(milliseconds: 250), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( color: _speaking ? AppColor.primaryColor.withOpacity(0.15) : AppColor.primaryColor.withOpacity(0.08), borderRadius: BorderRadius.circular(30), border: Border.all( color: AppColor.primaryColor.withOpacity(_speaking ? 0.4 : 0.15), width: 1, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( _speaking ? CupertinoIcons.speaker_3_fill : CupertinoIcons.speaker_2_fill, color: AppColor.primaryColor, size: 16, ), const SizedBox(width: 6), Text( _speaking ? 'Speaking...'.tr : 'Listen'.tr, style: TextStyle( color: AppColor.primaryColor, fontSize: 12, fontWeight: FontWeight.w600, ), ), ], ), ), ), ); } } // ───────────────────────────────────────────────────────────────────────────── // MyDialog — title + text content // ───────────────────────────────────────────────────────────────────────────── class MyDialog extends GetxController { void getDialog( String title, String? midTitle, VoidCallback onPressed, { IconData? icon, bool isDestructive = false, }) { HapticFeedback.mediumImpact(); Get.dialog( _DialogShell( child: _GlassCard( child: Column( mainAxisSize: MainAxisSize.min, children: [ // ── Body ────────────────────────────────────────────── Padding( padding: const EdgeInsets.fromLTRB(24, 28, 24, 20), child: Column( children: [ // Icon badge Container( width: 56, height: 56, decoration: BoxDecoration( shape: BoxShape.circle, color: (isDestructive ? AppColor.redColor : AppColor.primaryColor) .withOpacity(0.1), border: Border.all( color: (isDestructive ? AppColor.redColor : AppColor.primaryColor) .withOpacity(0.2), ), ), child: Icon( icon ?? (isDestructive ? Icons.warning_amber_rounded : Icons.info_outline_rounded), color: isDestructive ? AppColor.redColor : AppColor.primaryColor, size: 26, ), ), const SizedBox(height: 16), // Title Text( title, textAlign: TextAlign.center, style: AppStyle.title.copyWith( fontSize: 18, fontWeight: FontWeight.w700, letterSpacing: -0.4, color: AppColor.writeColor, ), ), if (midTitle != null && midTitle.isNotEmpty) ...[ const SizedBox(height: 10), Text( midTitle, textAlign: TextAlign.center, style: AppStyle.subtitle.copyWith( fontSize: 14.5, height: 1.5, color: Colors.grey[600], ), ), const SizedBox(height: 16), // TTS button _SpeakButton( texts: [title, if (midTitle.isNotEmpty) midTitle]), ], ], ), ), // ── Actions ─────────────────────────────────────────── _ActionRow( onCancel: () => Get.back(), onConfirm: onPressed, confirmLabel: 'OK'.tr, isDestructive: isDestructive, ), ], ), ), ), barrierDismissible: true, barrierColor: _DC.barrierColor, ); } } // ───────────────────────────────────────────────────────────────────────────── // MyDialogContent — title + arbitrary widget content // ───────────────────────────────────────────────────────────────────────────── class MyDialogContent extends GetxController { void getDialog( String title, Widget? content, VoidCallback onPressed, { IconData? icon, bool isDestructive = false, String confirmLabel = 'OK', }) { HapticFeedback.mediumImpact(); Get.dialog( _DialogShell( child: _GlassCard( child: Column( mainAxisSize: MainAxisSize.min, children: [ // ── Body ────────────────────────────────────────────── Padding( padding: const EdgeInsets.fromLTRB(24, 28, 24, 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header row: icon + title + TTS Row( children: [ Container( width: 44, height: 44, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: (isDestructive ? AppColor.redColor : AppColor.primaryColor) .withOpacity(0.1), ), child: Icon( icon ?? (isDestructive ? Icons.warning_amber_rounded : Icons.tune_rounded), color: isDestructive ? AppColor.redColor : AppColor.primaryColor, size: 22, ), ), const SizedBox(width: 12), Expanded( child: Text( title, style: AppStyle.title.copyWith( fontSize: 17, fontWeight: FontWeight.w700, letterSpacing: -0.3, color: AppColor.writeColor, ), ), ), _SpeakButton(texts: [title]), ], ), // Divider Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: Divider( height: 1, color: Colors.grey.withOpacity(0.15), ), ), // Content if (content != null) content, ], ), ), // ── Actions ─────────────────────────────────────────── _ActionRow( onCancel: () => Get.back(), onConfirm: onPressed, confirmLabel: confirmLabel.tr, isDestructive: isDestructive, ), ], ), ), ), barrierDismissible: true, barrierColor: _DC.barrierColor, ); } }