From 4908dc9d7639dbe8c031c5315b3feec7bbbe0482 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Wed, 21 Oct 2026 14:24:24 +0800 Subject: [PATCH] =?UTF-8?q?fix(player):=20=E8=AF=84=E8=AE=BA=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E6=94=B9=E4=B8=BA=E9=80=8F=E6=98=8E=E6=B5=AE=E5=B1=82?= =?UTF-8?q?=E5=B9=B6=E6=B6=88=E9=99=A4=E6=94=B6=E8=B5=B7=E9=94=AE=E7=9B=98?= =?UTF-8?q?=E6=8A=96=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../widgets/comment_composer_sheet.dart | 249 +++++++++--------- .../player/widgets/drama_comments_sheet.dart | 53 +++- test/comment/drama_comments_sheet_test.dart | 43 ++- 3 files changed, 198 insertions(+), 147 deletions(-) diff --git a/lib/pages/player/widgets/comment_composer_sheet.dart b/lib/pages/player/widgets/comment_composer_sheet.dart index 420891e..a302c45 100644 --- a/lib/pages/player/widgets/comment_composer_sheet.dart +++ b/lib/pages/player/widgets/comment_composer_sheet.dart @@ -25,10 +25,14 @@ class CommentComposerSheet extends StatefulWidget { const CommentComposerSheet({ super.key, required this.controller, + required this.onDismiss, + required this.onPosted, this.replyTo, }); final CommentController controller; + final VoidCallback onDismiss; + final VoidCallback onPosted; final DramaComment? replyTo; @override @@ -40,7 +44,6 @@ class _CommentComposerSheetState extends State { final _inputFocus = FocusNode(); final _keyboardInset = KeyboardInsetNotifier.instance; double _pinnedKeyboardInset = 0; - bool _closing = false; @override void initState() { @@ -49,6 +52,9 @@ class _CommentComposerSheetState extends State { _keyboardInset.addListener(_onKeyboardInsetChanged); widget.controller.addListener(_onControllerChanged); unawaited(_keyboardInset.start()); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) _inputFocus.requestFocus(); + }); } @override @@ -56,6 +62,7 @@ class _CommentComposerSheetState extends State { _input.removeListener(_onInputChanged); _keyboardInset.removeListener(_onKeyboardInsetChanged); widget.controller.removeListener(_onControllerChanged); + _inputFocus.unfocus(); _input.dispose(); _inputFocus.dispose(); super.dispose(); @@ -79,12 +86,6 @@ class _CommentComposerSheetState extends State { _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(); @@ -97,8 +98,7 @@ class _CommentComposerSheetState extends State { if (!mounted) return; if (result.isSuccess) { _input.clear(); - _closing = true; - Navigator.pop(context, true); + widget.onPosted(); return; } final l10n = AppLocalizations.of(context)!; @@ -124,146 +124,135 @@ class _CommentComposerSheetState extends State { final multiline = _input.text.contains('\n'); return Stack( + key: const Key('comment-composer-overlay'), fit: StackFit.expand, children: [ Positioned.fill( child: GestureDetector( key: const Key('comment-composer-dismiss-layer'), behavior: HitTestBehavior.opaque, - onTap: () { - _inputFocus.unfocus(); - _closeSheet(); - }, + onTap: widget.onDismiss, ), ), - Align( - alignment: Alignment.bottomCenter, - child: AnimatedPadding( - duration: const Duration(milliseconds: 200), - curve: Curves.easeOut, - padding: EdgeInsets.only(bottom: _pinnedKeyboardInset), - 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, - ), + Positioned( + left: 0, + right: 0, + bottom: _pinnedKeyboardInset, + 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, - ), + ), + Container( + decoration: BoxDecoration( + color: AppColors.commentInput, + borderRadius: BorderRadius.circular(_inputRadius), + ), + child: TextField( + key: const Key('comment-composer-input'), + 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, ), - 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, ), - cursorColor: AppColors.blue, - decoration: InputDecoration( - hintText: signedIn - ? l10n.writeComment - : l10n.commentLoginHint, - hintStyle: const TextStyle( - color: AppColors.muted, - fontSize: 14, - ), - counterText: '', - contentPadding: const EdgeInsets.symmetric( + 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, - vertical: 12, ), - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, + 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), + ), ), - 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, - ), + child: Text( + l10n.postComment, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, ), ), ), ), - ], - ), + ), + ], ), ), ), diff --git a/lib/pages/player/widgets/drama_comments_sheet.dart b/lib/pages/player/widgets/drama_comments_sheet.dart index 3daa972..72abbf5 100644 --- a/lib/pages/player/widgets/drama_comments_sheet.dart +++ b/lib/pages/player/widgets/drama_comments_sheet.dart @@ -41,6 +41,7 @@ class DramaCommentsSheet extends StatefulWidget { class _DramaCommentsSheetState extends State { late final CommentRepository _comments; late final CommentController _controller; + OverlayEntry? _composerOverlay; OverlayEntry? _successOverlay; Timer? _successOverlayTimer; int? _serverCount; @@ -61,6 +62,7 @@ class _DramaCommentsSheetState extends State { @override void dispose() { + _removeComposerOverlay(); _successOverlayTimer?.cancel(); _successOverlay?.remove(); _controller.dispose(); @@ -86,20 +88,38 @@ class _DramaCommentsSheetState extends State { Future _openComposer({DramaComment? replyTo}) async { await ensureSignedIn(context); if (!mounted || !context.read().isSignedIn) return; - final published = await showModalBottomSheet( - context: context, - isScrollControlled: true, - isDismissible: true, - backgroundColor: Colors.transparent, - barrierColor: Colors.transparent, - showDragHandle: false, - builder: (_) => - CommentComposerSheet(controller: _controller, replyTo: replyTo), + _removeComposerOverlay(); + final entry = OverlayEntry( + builder: (_) => CommentComposerSheet( + controller: _controller, + replyTo: replyTo, + onDismiss: _closeComposer, + onPosted: _handleComposerPosted, + ), ); - if (published == true && mounted) { - _showPostSuccess(); - await _refreshCount(); + setState(() => _composerOverlay = entry); + Overlay.of(context, rootOverlay: true).insert(entry); + } + + void _handleComposerPosted() { + _closeComposer(); + _showPostSuccess(); + unawaited(_refreshCount()); + } + + void _closeComposer() { + if (!mounted) { + _removeComposerOverlay(); + return; } + setState(_removeComposerOverlay); + } + + void _removeComposerOverlay() { + final entry = _composerOverlay; + _composerOverlay = null; + if (entry?.mounted ?? false) entry!.remove(); + FocusManager.instance.primaryFocus?.unfocus(); } void _handleDismissLayerTap() { @@ -133,7 +153,7 @@ class _DramaCommentsSheetState extends State { @override Widget build(BuildContext context) { final sheetHeight = _sheetHeight(context); - return Stack( + final content = Stack( fit: StackFit.expand, children: [ Positioned.fill( @@ -179,6 +199,13 @@ class _DramaCommentsSheetState extends State { ), ], ); + return PopScope( + canPop: _composerOverlay == null, + onPopInvokedWithResult: (didPop, _) { + if (!didPop && _composerOverlay != null) _closeComposer(); + }, + child: content, + ); } Widget _content(BuildContext context) { diff --git a/test/comment/drama_comments_sheet_test.dart b/test/comment/drama_comments_sheet_test.dart index 758888e..cd0c676 100644 --- a/test/comment/drama_comments_sheet_test.dart +++ b/test/comment/drama_comments_sheet_test.dart @@ -352,10 +352,18 @@ void main() { final composer = find.byKey(const Key('comment-composer-sheet')); final composerInput = find.byKey(const Key('comment-composer-input')); expect(composer, findsOneWidget); - expect( - ModalRoute.of(tester.element(composer))?.barrierColor, - Colors.transparent, - ); + final overlayRoot = find.byKey(const Key('comment-composer-overlay')); + expect(overlayRoot, findsOneWidget); + final overlayStack = tester.widget(overlayRoot); + expect(overlayStack.children, hasLength(2)); + final dismissLayer = overlayStack.children.first as Positioned; + expect(dismissLayer.left, 0); + expect(dismissLayer.top, 0); + expect(dismissLayer.right, 0); + expect(dismissLayer.bottom, 0); + final dismissGesture = dismissLayer.child as GestureDetector; + expect(dismissGesture.child, isNull); + expect(dismissGesture.behavior, HitTestBehavior.opaque); final compactHeight = tester.getSize(composer).height; expect(compactHeight, inInclusiveRange(100, 150)); expect(tester.widget(composerInput).focusNode?.hasFocus, isTrue); @@ -486,6 +494,33 @@ void main() { expect(sheet, findsNothing); }); + testWidgets( + 'should_close_composer_overlay_on_back_without_closing_comments', + (tester) async { + final repository = FakeCommentRepository( + pages: [ + CommentPage( + comments: [commentFixture(id: '10', content: 'A comment')], + ), + ], + ); + + await _pumpSheet(tester, repository: repository); + await tester.tap(find.byKey(const Key('comment-input-placeholder'))); + await tester.pumpAndSettle(); + + final sheet = find.byKey(const Key('player-comments-sheet')); + final composer = find.byKey(const Key('comment-composer-sheet')); + expect(sheet, findsOneWidget); + expect(composer, findsOneWidget); + + await tester.binding.handlePopRoute(); + await tester.pumpAndSettle(); + expect(sheet, findsOneWidget); + expect(composer, findsNothing); + }, + ); + testWidgets('should_use_polished_comment_sizes', (tester) async { final repository = FakeCommentRepository( pages: [