merge: 合并 K86.4 评论展开输入面板

This commit is contained in:
Zhou Weidong committed 2026-10-21 12:21:28 +08:00
commit 838beaaa3e
3 files changed
+225 -6

No files matched your search

@@ -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<DramaCommentsSheet> {
@override
void initState() {
super.initState();
_inputFocus.addListener(_onFocusChanged);
_input.addListener(_onInputChanged);
_comments = context.read<CommentRepository>();
_controller = CommentController(
comments: _comments,
@@ -66,12 +70,22 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
@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<void> _post() async {
if (_controller.submitting) return;
if (!await ensureSignedIn(context) || !mounted) return;
@@ -82,7 +96,7 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
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<DramaCommentsSheet> {
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<DramaCommentsSheet> {
);
}
Widget _expandedComposer(BuildContext context, double maxHeight) {
final l10n = AppLocalizations.of(context)!;
final signedIn = context.watch<AuthController>().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<DramaCommentsSheet> {
child: _list(l10n),
),
if (_replyTo != null) _replyBanner(l10n, _replyTo!),
const SizedBox(
height: _composerHeight + _composerTopGap,
),
const SizedBox(height: _composerHeight + _composerTopGap),
],
),
Positioned(
+94 -2
View File
@@ -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<TextField>(input).focusNode?.hasFocus, isTrue);
await tester.enterText(input, 'Nice demo');
await tester.pump();
expect(
tester
.widget<TextButton>(find.byKey(const Key('comment-post')))
.onPressed,
isNotNull,
);
await tester.tap(find.byKey(const Key('comment-post')));
await tester.pumpAndSettle();
expect(tester.widget<TextField>(input).focusNode?.hasFocus, isFalse);
expect(tester.widget<TextField>(input).controller?.text, isEmpty);
expect(tester.widget<TextField>(input).focusNode?.hasFocus, isTrue);
expect(find.byKey(const Key('comment-expanded-composer')), findsOneWidget);
expect(find.byKey(const Key('comment-list')), findsNothing);
expect(
tester
.widget<TextButton>(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<TextField>(
find.byKey(const Key('comment-input')),
);
expect(field.minLines, 1);
expect(field.maxLines, 4);
expect(field.style?.fontSize, 15);
expect(
tester
.widget<TextButton>(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<TextButton>(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);
});
+3
View File
@@ -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();