// Author: fruit.com.cn import 'dart:async'; import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../../core/data/models/comment.dart'; import '../../../core/data/services/auth_controller.dart'; import '../../../core/data/services/comment_controller.dart'; import '../../../core/platform/haptics.dart'; import '../../../core/platform/keyboard_inset.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/widgets/auth_gate.dart'; import '../../../l10n/app_localizations.dart'; const double _sheetRadius = 20; const double _inputRadius = 12; const double _postButtonHeight = 32; const int _maxCommentLength = 300; /// 独立评论输入弹窗,复用原评论控制器的发布与列表更新能力。 class CommentComposerSheet extends StatefulWidget { const CommentComposerSheet({ super.key, required this.controller, this.replyTo, }); final CommentController controller; final DramaComment? replyTo; @override State createState() => _CommentComposerSheetState(); } class _CommentComposerSheetState extends State with WidgetsBindingObserver { final _input = TextEditingController(); final _inputFocus = FocusNode(); final _keyboardInset = KeyboardInsetNotifier.instance; bool _keyboardWasVisible = false; bool _hadFocus = false; bool _closing = false; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _input.addListener(_onInputChanged); _inputFocus.addListener(_onFocusChanged); _keyboardInset.addListener(_onKeyboardInsetChanged); widget.controller.addListener(_onControllerChanged); unawaited(_keyboardInset.start()); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _input.removeListener(_onInputChanged); _inputFocus.removeListener(_onFocusChanged); _keyboardInset.removeListener(_onKeyboardInsetChanged); widget.controller.removeListener(_onControllerChanged); _input.dispose(); _inputFocus.dispose(); super.dispose(); } void _onInputChanged() { if (mounted) setState(() {}); } void _onControllerChanged() { if (mounted) setState(() {}); } void _onFocusChanged() { if (!mounted) return; final hasFocus = _inputFocus.hasFocus; if (_hadFocus && !hasFocus) { _closeSheet(); } _hadFocus = hasFocus; setState(() {}); } void _onKeyboardInsetChanged() { if (!mounted) return; setState(() {}); _syncKeyboardVisibility(_keyboardHeight(context)); } @override void didChangeMetrics() { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; _syncKeyboardVisibility(_keyboardHeight(context)); }); } void _syncKeyboardVisibility(double keyboardHeight) { if (keyboardHeight > 0) { _keyboardWasVisible = true; return; } if (_keyboardWasVisible) _closeSheet(); } double _keyboardHeight(BuildContext context) => math.max( MediaQuery.viewInsetsOf(context).bottom, _keyboardInset.value / MediaQuery.devicePixelRatioOf(context), ); void _closeSheet() { if (_closing || !mounted) return; _closing = true; Navigator.pop(context); } Future _submit() async { if (widget.controller.submitting) return; final text = _input.text.trim(); if (text.isEmpty) return; unawaited(context.read().lightImpact()); final result = await widget.controller.publish( text, replyTo: widget.replyTo, ); if (!mounted) return; if (result.isSuccess) { _closing = true; Navigator.pop(context, true); return; } final l10n = AppLocalizations.of(context)!; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(result.errorMessage ?? l10n.commentPostFailed)), ); } Future _authenticate() async { await ensureSignedIn(context); if (!mounted || !context.read().isSignedIn) return; _inputFocus.requestFocus(); } @override Widget build(BuildContext context) { final screenHeight = MediaQuery.sizeOf(context).height; final halfHeight = (screenHeight * 0.5).roundToDouble(); final maxHeight = math.max( 0.0, screenHeight - MediaQuery.paddingOf(context).top, ); final sheetHeight = math.min(halfHeight, maxHeight); final keyboardHeight = _keyboardHeight(context); final l10n = AppLocalizations.of(context)!; final signedIn = context.watch().isSignedIn; final hasContent = _input.text.trim().isNotEmpty; final canPost = signedIn && hasContent && !widget.controller.submitting; final multiline = _input.text.contains('\n'); return AnimatedPadding( duration: const Duration(milliseconds: 200), curve: Curves.easeOut, padding: EdgeInsets.only(bottom: keyboardHeight), child: SizedBox( key: const Key('comment-composer-sheet'), height: sheetHeight, child: Material( color: AppColors.card, clipBehavior: Clip.antiAlias, borderRadius: const BorderRadius.vertical( top: Radius.circular(_sheetRadius), ), child: SafeArea( top: false, child: Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 12), child: TextFieldTapRegion( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (widget.replyTo != null) Padding( padding: const EdgeInsets.only(bottom: 8), child: Text( l10n.replyingTo( widget.replyTo!.nickname.isEmpty ? l10n.anonymousUser : widget.replyTo!.nickname, ), style: const TextStyle( color: AppColors.muted, fontSize: 12, ), ), ), Expanded( child: Align( alignment: Alignment.topCenter, child: Container( decoration: BoxDecoration( color: AppColors.commentInput, borderRadius: BorderRadius.circular(_inputRadius), ), child: TextField( key: const Key('comment-composer-input'), autofocus: true, controller: _input, focusNode: _inputFocus, readOnly: !signedIn, canRequestFocus: signedIn, minLines: 1, maxLines: 4, maxLength: _maxCommentLength, textAlignVertical: multiline ? TextAlignVertical.top : TextAlignVertical.center, style: TextStyle( color: AppColors.cream, fontSize: 15, height: multiline ? 1.4 : null, ), cursorColor: AppColors.blue, decoration: InputDecoration( hintText: signedIn ? l10n.writeComment : l10n.commentLoginHint, hintStyle: const TextStyle( color: AppColors.muted, fontSize: 14, ), counterText: '', contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, ), onTap: signedIn ? null : _authenticate, onTapOutside: (_) { _inputFocus.unfocus(); _closeSheet(); }, ), ), ), ), const SizedBox(height: 12), Align( alignment: Alignment.centerRight, child: SizedBox( height: _postButtonHeight, child: TextButton( key: const Key('comment-composer-post'), onPressed: canPost ? _submit : null, style: TextButton.styleFrom( minimumSize: const Size(0, _postButtonHeight), padding: const EdgeInsets.symmetric(horizontal: 16), tapTargetSize: MaterialTapTargetSize.shrinkWrap, backgroundColor: hasContent ? AppColors.blue.withValues(alpha: 0.12) : AppColors.raised, foregroundColor: AppColors.blue, disabledForegroundColor: AppColors.muted, disabledBackgroundColor: AppColors.raised, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: Text( l10n.postComment, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, ), ), ), ), ), ], ), ), ), ), ), ), ); } }