// 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 { final _input = TextEditingController(); final _inputFocus = FocusNode(); final _keyboardInset = KeyboardInsetNotifier.instance; bool _closing = false; @override void initState() { super.initState(); _input.addListener(_onInputChanged); _keyboardInset.addListener(_onKeyboardInsetChanged); widget.controller.addListener(_onControllerChanged); unawaited(_keyboardInset.start()); } @override void dispose() { _input.removeListener(_onInputChanged); _keyboardInset.removeListener(_onKeyboardInsetChanged); widget.controller.removeListener(_onControllerChanged); _input.dispose(); _inputFocus.dispose(); super.dispose(); } void _onInputChanged() { if (mounted) setState(() {}); } void _onControllerChanged() { if (mounted) setState(() {}); } void _onKeyboardInsetChanged() { if (!mounted) return; setState(() {}); } 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) { _input.clear(); _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 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 Stack( fit: StackFit.expand, children: [ Positioned.fill( child: GestureDetector( key: const Key('comment-composer-dismiss-layer'), behavior: HitTestBehavior.opaque, onTap: () { _inputFocus.unfocus(); _closeSheet(); }, ), ), Align( alignment: Alignment.bottomCenter, child: AnimatedPadding( duration: const Duration(milliseconds: 200), curve: Curves.easeOut, padding: EdgeInsets.only(bottom: keyboardHeight), child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () {}, child: SizedBox( key: const Key('comment-composer-sheet'), width: double.infinity, 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, 12, 16, 12), child: TextFieldTapRegion( child: Column( mainAxisSize: MainAxisSize.min, 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, ), ), ), 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, ), ), 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, ), ), ), ), ), ], ), ), ), ), ), ), ), ), ), ], ); } }