From 519cab20fe1e241b7a69648bcc311560f0dd139f Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Wed, 21 Oct 2026 12:21:17 +0800 Subject: [PATCH] =?UTF-8?q?fix(player):=20=E8=AF=84=E8=AE=BA=E8=81=9A?= =?UTF-8?q?=E7=84=A6=E6=80=81=E6=94=B9=E4=B8=BA=E5=B1=95=E5=BC=80=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E9=9D=A2=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../player/widgets/drama_comments_sheet.dart | 132 +++++++++++++++++- test/comment/drama_comments_sheet_test.dart | 96 ++++++++++++- test/widget_test.dart | 3 + 3 files changed, 225 insertions(+), 6 deletions(-) diff --git a/lib/pages/player/widgets/drama_comments_sheet.dart b/lib/pages/player/widgets/drama_comments_sheet.dart index 33dc0bc..0d27c83 100644 --- a/lib/pages/player/widgets/drama_comments_sheet.dart +++ b/lib/pages/player/widgets/drama_comments_sheet.dart @@ -31,6 +31,8 @@ const double _headerHeight = 44; const double _replyIndent = 52; const double _composerTopGap = 8; const double _replyBannerHeight = 46; +const double _expandedInputRadius = 12; +const double _postButtonHeight = 32; /// 服务端评论底部弹窗:匿名列表、游标翻页、回复、举报和本人删除。 class DramaCommentsSheet extends StatefulWidget { @@ -54,6 +56,8 @@ class _DramaCommentsSheetState extends State { @override void initState() { super.initState(); + _inputFocus.addListener(_onFocusChanged); + _input.addListener(_onInputChanged); _comments = context.read(); _controller = CommentController( comments: _comments, @@ -66,12 +70,22 @@ class _DramaCommentsSheetState extends State { @override void dispose() { + _inputFocus.removeListener(_onFocusChanged); + _input.removeListener(_onInputChanged); _controller.dispose(); _input.dispose(); _inputFocus.dispose(); super.dispose(); } + void _onFocusChanged() { + if (mounted) setState(() {}); + } + + void _onInputChanged() { + if (mounted) setState(() {}); + } + Future _post() async { if (_controller.submitting) return; if (!await ensureSignedIn(context) || !mounted) return; @@ -82,7 +96,7 @@ class _DramaCommentsSheetState extends State { if (result.isSuccess) { _input.clear(); setState(() => _replyTo = null); - FocusScope.of(context).unfocus(); + _inputFocus.requestFocus(); await _refreshCount(); return; } @@ -184,6 +198,9 @@ class _DramaCommentsSheetState extends State { final maxHeight = screenLimit < _maxSheetHeight ? screenLimit : _maxSheetHeight; + if (_inputFocus.hasFocus) { + return _expandedComposer(context, maxHeight); + } return Container( key: const Key('player-comments-sheet'), decoration: const BoxDecoration( @@ -215,6 +232,115 @@ class _DramaCommentsSheetState extends State { ); } + Widget _expandedComposer(BuildContext context, double maxHeight) { + final l10n = AppLocalizations.of(context)!; + final signedIn = context.watch().isSignedIn; + final hasContent = _input.text.trim().isNotEmpty; + final canPost = signedIn && hasContent && !_controller.submitting; + return AnimatedPadding( + duration: const Duration(milliseconds: 150), + curve: Curves.easeOut, + padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom), + child: Container( + key: const Key('player-comments-sheet'), + decoration: const BoxDecoration( + color: AppColors.card, + borderRadius: BorderRadius.vertical( + top: Radius.circular(_sheetRadius), + ), + ), + child: SafeArea( + top: false, + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: maxHeight), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 12), + child: Column( + key: const Key('comment-expanded-composer'), + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (_replyTo != null) _replyBanner(l10n, _replyTo!), + Container( + decoration: BoxDecoration( + color: AppColors.commentInput, + borderRadius: BorderRadius.circular(_expandedInputRadius), + ), + child: TextField( + key: const Key('comment-input'), + controller: _input, + focusNode: _inputFocus, + autofocus: true, + readOnly: !signedIn, + canRequestFocus: signedIn, + minLines: 1, + maxLines: 4, + maxLength: _maxCommentLength, + textAlignVertical: TextAlignVertical.top, + style: const TextStyle( + color: AppColors.cream, + fontSize: 15, + ), + cursorColor: AppColors.blue, + decoration: InputDecoration( + hintText: l10n.writeComment, + 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 : _authenticateForComment, + ), + ), + const SizedBox(height: 12), + Align( + alignment: Alignment.centerRight, + child: SizedBox( + height: _postButtonHeight, + child: TextButton( + key: const Key('comment-post'), + onPressed: canPost ? _post : 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, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } + Widget _content(BuildContext context, double maxHeight) { final l10n = AppLocalizations.of(context)!; final count = _serverCount ?? _controller.exactCount; @@ -277,9 +403,7 @@ class _DramaCommentsSheetState extends State { child: _list(l10n), ), if (_replyTo != null) _replyBanner(l10n, _replyTo!), - const SizedBox( - height: _composerHeight + _composerTopGap, - ), + const SizedBox(height: _composerHeight + _composerTopGap), ], ), Positioned( diff --git a/test/comment/drama_comments_sheet_test.dart b/test/comment/drama_comments_sheet_test.dart index 360aeee..530f220 100644 --- a/test/comment/drama_comments_sheet_test.dart +++ b/test/comment/drama_comments_sheet_test.dart @@ -220,7 +220,9 @@ void main() { }, ); - testWidgets('should_unfocus_input_after_successful_post', (tester) async { + testWidgets('should_clear_and_keep_input_focused_after_successful_post', ( + tester, + ) async { final repository = FakeCommentRepository( pages: [const CommentPage()], publishResult: commentFixture(id: '11', content: 'Nice demo', mine: true), @@ -233,13 +235,98 @@ void main() { await tester.pump(); expect(tester.widget(input).focusNode?.hasFocus, isTrue); await tester.enterText(input, 'Nice demo'); + await tester.pump(); + expect( + tester + .widget(find.byKey(const Key('comment-post'))) + .onPressed, + isNotNull, + ); await tester.tap(find.byKey(const Key('comment-post'))); await tester.pumpAndSettle(); - expect(tester.widget(input).focusNode?.hasFocus, isFalse); + expect(tester.widget(input).controller?.text, isEmpty); + expect(tester.widget(input).focusNode?.hasFocus, isTrue); + expect(find.byKey(const Key('comment-expanded-composer')), findsOneWidget); + expect(find.byKey(const Key('comment-list')), findsNothing); + expect( + tester + .widget(find.byKey(const Key('comment-post'))) + .onPressed, + isNull, + ); expect(repository.countCalls, ['5', '5']); }); + testWidgets('should_expand_composer_only_while_input_is_focused', ( + tester, + ) async { + tester.view.physicalSize = const Size(390, 844); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + addTearDown(tester.view.resetViewInsets); + final repository = FakeCommentRepository( + pages: [ + CommentPage( + comments: [commentFixture(id: '10', content: 'A comment')], + ), + ], + ); + + await _pumpSheet(tester, repository: repository); + expect(find.byKey(const Key('comment-list')), findsOneWidget); + + await tester.tap(find.byKey(const Key('comment-input'))); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('comment-list')), findsNothing); + expect(find.byKey(const Key('comment-count-label')), findsNothing); + expect(find.byKey(const Key('comment-composer')), findsNothing); + expect(find.byKey(const Key('comment-expanded-composer')), findsOneWidget); + final field = tester.widget( + find.byKey(const Key('comment-input')), + ); + expect(field.minLines, 1); + expect(field.maxLines, 4); + expect(field.style?.fontSize, 15); + expect( + tester + .widget(find.byKey(const Key('comment-post'))) + .onPressed, + isNull, + ); + expect(tester.getSize(find.byKey(const Key('comment-post'))).height, 32); + expect(find.byType(IconButton), findsNothing); + expect(find.byIcon(Icons.image_outlined), findsNothing); + expect(find.byIcon(Icons.emoji_emotions_outlined), findsNothing); + expect(find.byIcon(Icons.alternate_email_rounded), findsNothing); + + await tester.enterText(find.byKey(const Key('comment-input')), 'Nice'); + await tester.pump(); + expect( + tester + .widget(find.byKey(const Key('comment-post'))) + .onPressed, + isNotNull, + ); + + tester.view.viewInsets = const FakeViewPadding(bottom: 180); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 160)); + expect( + tester.getBottomLeft(find.byKey(const Key('player-comments-sheet'))).dy, + closeTo(844 - 180, 0.1), + ); + + tester.view.viewInsets = const FakeViewPadding(); + FocusManager.instance.primaryFocus?.unfocus(); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('comment-expanded-composer')), findsNothing); + expect(find.byKey(const Key('comment-list')), findsOneWidget); + expect(find.byKey(const Key('comment-composer')), findsOneWidget); + }); + testWidgets('should_use_polished_comment_sizes', (tester) async { final repository = FakeCommentRepository( pages: [ @@ -457,6 +544,8 @@ void main() { await tester.tap(find.byKey(const Key('comment-post'))); await tester.pumpAndSettle(); + FocusManager.instance.primaryFocus?.unfocus(); + await tester.pumpAndSettle(); expect(find.text('hello ***'), findsOneWidget); expect(find.text('Under review'), findsOneWidget); expect(haptics.lightImpacts, 1); @@ -488,10 +577,13 @@ void main() { await tester.pump(); expect(find.textContaining('Replying to'), findsOneWidget); await tester.enterText(find.byKey(const Key('comment-input')), 'My reply'); + await tester.pump(); await tester.tap(find.byKey(const Key('comment-post'))); await tester.pumpAndSettle(); expect(repository.publishCalls.single.parentId, '10'); + FocusManager.instance.primaryFocus?.unfocus(); + await tester.pumpAndSettle(); expect(find.text('My reply'), findsOneWidget); }); diff --git a/test/widget_test.dart b/test/widget_test.dart index 27b9260..492e7a6 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -249,8 +249,11 @@ void main() { await tester.pumpAndSettle(); expect(find.text('No comments yet.'), findsOneWidget); await tester.enterText(find.byKey(const Key('comment-input')), 'Nice demo'); + await tester.pump(); await tester.tap(find.byKey(const Key('comment-post'))); await tester.pumpAndSettle(); + FocusManager.instance.primaryFocus?.unfocus(); + await tester.pumpAndSettle(); expect(find.text('Nice demo'), findsOneWidget); await tester.binding.handlePopRoute(); await tester.pumpAndSettle();