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 _replyIndent = 52;
|
||||||
const double _composerTopGap = 8;
|
const double _composerTopGap = 8;
|
||||||
const double _replyBannerHeight = 46;
|
const double _replyBannerHeight = 46;
|
||||||
|
const double _expandedInputRadius = 12;
|
||||||
|
const double _postButtonHeight = 32;
|
||||||
|
|
||||||
/// 服务端评论底部弹窗:匿名列表、游标翻页、回复、举报和本人删除。
|
/// 服务端评论底部弹窗:匿名列表、游标翻页、回复、举报和本人删除。
|
||||||
class DramaCommentsSheet extends StatefulWidget {
|
class DramaCommentsSheet extends StatefulWidget {
|
||||||
@@ -54,6 +56,8 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_inputFocus.addListener(_onFocusChanged);
|
||||||
|
_input.addListener(_onInputChanged);
|
||||||
_comments = context.read<CommentRepository>();
|
_comments = context.read<CommentRepository>();
|
||||||
_controller = CommentController(
|
_controller = CommentController(
|
||||||
comments: _comments,
|
comments: _comments,
|
||||||
@@ -66,12 +70,22 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_inputFocus.removeListener(_onFocusChanged);
|
||||||
|
_input.removeListener(_onInputChanged);
|
||||||
_controller.dispose();
|
_controller.dispose();
|
||||||
_input.dispose();
|
_input.dispose();
|
||||||
_inputFocus.dispose();
|
_inputFocus.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onFocusChanged() {
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onInputChanged() {
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _post() async {
|
Future<void> _post() async {
|
||||||
if (_controller.submitting) return;
|
if (_controller.submitting) return;
|
||||||
if (!await ensureSignedIn(context) || !mounted) return;
|
if (!await ensureSignedIn(context) || !mounted) return;
|
||||||
@@ -82,7 +96,7 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
|
|||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
_input.clear();
|
_input.clear();
|
||||||
setState(() => _replyTo = null);
|
setState(() => _replyTo = null);
|
||||||
FocusScope.of(context).unfocus();
|
_inputFocus.requestFocus();
|
||||||
await _refreshCount();
|
await _refreshCount();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -184,6 +198,9 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
|
|||||||
final maxHeight = screenLimit < _maxSheetHeight
|
final maxHeight = screenLimit < _maxSheetHeight
|
||||||
? screenLimit
|
? screenLimit
|
||||||
: _maxSheetHeight;
|
: _maxSheetHeight;
|
||||||
|
if (_inputFocus.hasFocus) {
|
||||||
|
return _expandedComposer(context, maxHeight);
|
||||||
|
}
|
||||||
return Container(
|
return Container(
|
||||||
key: const Key('player-comments-sheet'),
|
key: const Key('player-comments-sheet'),
|
||||||
decoration: const BoxDecoration(
|
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) {
|
Widget _content(BuildContext context, double maxHeight) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final count = _serverCount ?? _controller.exactCount;
|
final count = _serverCount ?? _controller.exactCount;
|
||||||
@@ -277,9 +403,7 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
|
|||||||
child: _list(l10n),
|
child: _list(l10n),
|
||||||
),
|
),
|
||||||
if (_replyTo != null) _replyBanner(l10n, _replyTo!),
|
if (_replyTo != null) _replyBanner(l10n, _replyTo!),
|
||||||
const SizedBox(
|
const SizedBox(height: _composerHeight + _composerTopGap),
|
||||||
height: _composerHeight + _composerTopGap,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Positioned(
|
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(
|
final repository = FakeCommentRepository(
|
||||||
pages: [const CommentPage()],
|
pages: [const CommentPage()],
|
||||||
publishResult: commentFixture(id: '11', content: 'Nice demo', mine: true),
|
publishResult: commentFixture(id: '11', content: 'Nice demo', mine: true),
|
||||||
@@ -233,13 +235,98 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(tester.widget<TextField>(input).focusNode?.hasFocus, isTrue);
|
expect(tester.widget<TextField>(input).focusNode?.hasFocus, isTrue);
|
||||||
await tester.enterText(input, 'Nice demo');
|
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.tap(find.byKey(const Key('comment-post')));
|
||||||
await tester.pumpAndSettle();
|
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']);
|
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 {
|
testWidgets('should_use_polished_comment_sizes', (tester) async {
|
||||||
final repository = FakeCommentRepository(
|
final repository = FakeCommentRepository(
|
||||||
pages: [
|
pages: [
|
||||||
@@ -457,6 +544,8 @@ void main() {
|
|||||||
await tester.tap(find.byKey(const Key('comment-post')));
|
await tester.tap(find.byKey(const Key('comment-post')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('hello ***'), findsOneWidget);
|
expect(find.text('hello ***'), findsOneWidget);
|
||||||
expect(find.text('Under review'), findsOneWidget);
|
expect(find.text('Under review'), findsOneWidget);
|
||||||
expect(haptics.lightImpacts, 1);
|
expect(haptics.lightImpacts, 1);
|
||||||
@@ -488,10 +577,13 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.textContaining('Replying to'), findsOneWidget);
|
expect(find.textContaining('Replying to'), findsOneWidget);
|
||||||
await tester.enterText(find.byKey(const Key('comment-input')), 'My reply');
|
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.tap(find.byKey(const Key('comment-post')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(repository.publishCalls.single.parentId, '10');
|
expect(repository.publishCalls.single.parentId, '10');
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('My reply'), findsOneWidget);
|
expect(find.text('My reply'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -249,8 +249,11 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('No comments yet.'), findsOneWidget);
|
expect(find.text('No comments yet.'), findsOneWidget);
|
||||||
await tester.enterText(find.byKey(const Key('comment-input')), 'Nice demo');
|
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.tap(find.byKey(const Key('comment-post')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('Nice demo'), findsOneWidget);
|
expect(find.text('Nice demo'), findsOneWidget);
|
||||||
await tester.binding.handlePopRoute();
|
await tester.binding.handlePopRoute();
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|||||||
Reference in new issue
Block a user