merge: 合并 K86.4 评论展开输入面板
This commit is contained in:
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(
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user