import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; import '../../core/i18n/app_localizations.dart'; import '../../core/theme/app_colors_extension.dart'; enum AppInputType { text, integer, decimal } class AppInputDialog { static Future show({ required BuildContext context, required String title, String? hintText, String initialText = '', AppInputType inputType = AppInputType.text, int decimalPlaces = 2, num? min, num? max, bool barrierDismissible = true, }) { final l10n = AppLocalizations.of(context); final ctrl = TextEditingController(text: initialText); final colors = Theme.of(context).extension()!; TextInputType keyboardType; List? inputFormatters; switch (inputType) { case AppInputType.text: keyboardType = TextInputType.text; inputFormatters = null; case AppInputType.integer: keyboardType = const TextInputType.numberWithOptions(decimal: false); inputFormatters = [FilteringTextInputFormatter.digitsOnly]; case AppInputType.decimal: keyboardType = const TextInputType.numberWithOptions(decimal: true); inputFormatters = [ FilteringTextInputFormatter.allow( RegExp(r'^-?\d*\.?\d{0,' + decimalPlaces.toString() + '}'), ), ]; } void onConfirm() { final value = ctrl.text.trim(); if (value.isEmpty) { Navigator.pop(context); return; } if (inputType != AppInputType.text) { final num? parsed = double.tryParse(value); if (parsed == null) { TDToast.showText(l10n.get('enterValidNumber'), context: context); return; } if (min != null && parsed < min) { TDToast.showText('${l10n.get('minValue')}: $min', context: context); return; } if (max != null && parsed > max) { TDToast.showText('${l10n.get('maxValue')}: $max', context: context); return; } } Navigator.pop(context, value); } Widget buildCard(Color _, BuildContext ctx) { final bottomInset = MediaQuery.of(ctx).viewInsets.bottom; return Material( color: Colors.transparent, child: GestureDetector( onTap: () => FocusScope.of(ctx).unfocus(), child: Padding( padding: EdgeInsets.only(bottom: bottomInset), child: Center( child: GestureDetector( onTap: () {}, child: Container( margin: const EdgeInsets.symmetric(horizontal: 32), padding: const EdgeInsets.fromLTRB(20, 24, 20, 8), decoration: BoxDecoration( color: colors.bgCard, borderRadius: BorderRadius.circular(12), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(title, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: colors.textPrimary)), const SizedBox(height: 20), _InputField( controller: ctrl, keyboardType: keyboardType, inputFormatters: inputFormatters, hintText: hintText ?? l10n.get('pleaseEnter'), ), if (inputType != AppInputType.text && (min != null || max != null)) ...[ const SizedBox(height: 8), Align( alignment: Alignment.centerRight, child: Text(_rangeHint(min, max), style: TextStyle( fontSize: 12, color: colors.textPlaceholder)), ), ], const SizedBox(height: 20), Row( children: [ Expanded( child: TextButton( onPressed: () => Navigator.pop(ctx), child: Text(l10n.get('cancel'), style: TextStyle( fontSize: 16, color: colors.textSecondary)), ), ), const SizedBox(width: 12), Expanded( child: TextButton( onPressed: onConfirm, child: Text(l10n.get('confirm'), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: colors.primary)), ), ), ], ), ], ), ), ), ), ), ), ); } return showGeneralDialog( context: context, barrierDismissible: barrierDismissible, barrierLabel: barrierDismissible ? 'Dismiss' : null, barrierColor: Colors.black38, transitionBuilder: (ctx, animation, secondaryAnimation, child) { return FadeTransition( opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut), child: child, ); }, pageBuilder: (ctx, animation, secondaryAnimation) { return buildCard(Colors.transparent, ctx); }, ); } static String _rangeHint(num? min, num? max) { if (min != null && max != null) return '$min ~ $max'; if (min != null) return '≥ $min'; if (max != null) return '≤ $max'; return ''; } } class _InputField extends StatefulWidget { final TextEditingController controller; final TextInputType keyboardType; final List? inputFormatters; final String hintText; const _InputField({ required this.controller, required this.keyboardType, this.inputFormatters, required this.hintText, }); @override State<_InputField> createState() => _InputFieldState(); } class _InputFieldState extends State<_InputField> { bool _hasText = false; @override void initState() { super.initState(); _hasText = widget.controller.text.isNotEmpty; widget.controller.addListener(_onChange); } void _onChange() { final has = widget.controller.text.isNotEmpty; if (has != _hasText) setState(() => _hasText = has); } @override void dispose() { widget.controller.removeListener(_onChange); super.dispose(); } @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; return Container( decoration: BoxDecoration( color: colors.bgPage, borderRadius: BorderRadius.circular(4), ), child: TextField( controller: widget.controller, autofocus: true, keyboardType: widget.keyboardType, inputFormatters: widget.inputFormatters, style: TextStyle(fontSize: 16, color: colors.textPrimary), decoration: InputDecoration( hintText: widget.hintText, hintStyle: TextStyle(fontSize: 16, color: colors.textPlaceholder), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), border: InputBorder.none, isCollapsed: true, suffixIcon: _hasText ? GestureDetector( onTap: () { widget.controller.clear(); setState(() => _hasText = false); }, child: const Padding( padding: EdgeInsets.only(right: 8), child: Icon(Icons.close, size: 18, color: Color(0xFFBBBBBB)), ), ) : null, ), ), ); } }