merge: 合并 K86.16 评论提示与输入弹层修复

This commit is contained in:
Zhou Weidong committed 2026-10-21 13:59:16 +08:00
commit f9f77090d9
3 files changed
+274 -200

No files matched your search

@@ -155,130 +155,160 @@ class _CommentComposerSheetState extends State<CommentComposerSheet>
final canPost = signedIn && hasContent && !widget.controller.submitting;
final multiline = _input.text.contains('\n');
return AnimatedPadding(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
padding: EdgeInsets.only(bottom: keyboardHeight),
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),
return Stack(
fit: StackFit.expand,
children: [
Positioned.fill(
child: GestureDetector(
key: const Key('comment-composer-dismiss-layer'),
behavior: HitTestBehavior.opaque,
onTap: () {
_inputFocus.unfocus();
_closeSheet();
},
),
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),
),
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,
),
counterText: '',
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
),
onTap: signedIn ? null : _authenticate,
onTapOutside: (_) {
_inputFocus.unfocus();
_closeSheet();
},
),
),
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),
),
Align(
alignment: Alignment.bottomCenter,
child: AnimatedPadding(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
padding: EdgeInsets.only(bottom: keyboardHeight),
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,
),
),
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,
),
counterText: '',
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
),
onTap: signedIn ? null : _authenticate,
onTapOutside: (_) {
_inputFocus.unfocus();
_closeSheet();
},
),
),
),
child: Text(
l10n.postComment,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
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,
),
),
),
),
),
),
],
),
),
),
],
),
),
),
),
),
),
),
],
);
}
}
@@ -41,7 +41,8 @@ class DramaCommentsSheet extends StatefulWidget {
class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
late final CommentRepository _comments;
late final CommentController _controller;
final _messengerKey = GlobalKey<ScaffoldMessengerState>();
OverlayEntry? _successOverlay;
Timer? _successOverlayTimer;
int? _serverCount;
int _countRequest = 0;
@@ -60,6 +61,8 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
@override
void dispose() {
_successOverlayTimer?.cancel();
_successOverlay?.remove();
_controller.dispose();
super.dispose();
}
@@ -88,6 +91,7 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
isScrollControlled: true,
isDismissible: true,
backgroundColor: Colors.transparent,
barrierColor: Colors.transparent,
showDragHandle: false,
builder: (_) =>
CommentComposerSheet(controller: _controller, replyTo: replyTo),
@@ -103,32 +107,19 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
}
void _showPostSuccess() {
final messenger = _messengerKey.currentState;
if (messenger == null) return;
_successOverlayTimer?.cancel();
_successOverlay?.remove();
final l10n = AppLocalizations.of(context)!;
messenger
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
key: const Key('comment-post-success-snackbar'),
content: Text(
l10n.commentPostSuccess,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
behavior: SnackBarBehavior.floating,
duration: const Duration(milliseconds: 1500),
backgroundColor: AppColors.ink,
elevation: 6,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
margin: EdgeInsets.fromLTRB(16, 0, 16, _sheetHeight(context) + 12),
),
);
final entry = OverlayEntry(
builder: (_) =>
_CommentPostSuccessOverlay(message: l10n.commentPostSuccess),
);
_successOverlay = entry;
Overlay.of(context, rootOverlay: true).insert(entry);
_successOverlayTimer = Timer(const Duration(milliseconds: 1200), () {
if (entry.mounted) entry.remove();
if (identical(_successOverlay, entry)) _successOverlay = null;
});
}
double _sheetHeight(BuildContext context) {
@@ -142,60 +133,51 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
@override
Widget build(BuildContext context) {
final sheetHeight = _sheetHeight(context);
return ScaffoldMessenger(
key: _messengerKey,
child: Scaffold(
backgroundColor: Colors.transparent,
resizeToAvoidBottomInset: false,
body: Stack(
fit: StackFit.expand,
children: [
Positioned.fill(
child: GestureDetector(
key: const Key('player-comments-dismiss-layer'),
behavior: HitTestBehavior.opaque,
onTap: _handleDismissLayerTap,
),
),
Align(
alignment: Alignment.bottomCenter,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {},
child: KeyedSubtree(
key: const Key('player-comments-sheet'),
child: SizedBox(
key: const Key('comment-sheet-constraints'),
height: sheetHeight,
child: Container(
decoration: const BoxDecoration(
color: AppColors.card,
borderRadius: BorderRadius.vertical(
top: Radius.circular(_sheetRadius),
),
),
child: SafeArea(
top: false,
child: ListenableBuilder(
listenable: _controller,
builder: (context, _) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
),
child: _content(context),
);
},
),
),
return Stack(
fit: StackFit.expand,
children: [
Positioned.fill(
child: GestureDetector(
key: const Key('player-comments-dismiss-layer'),
behavior: HitTestBehavior.opaque,
onTap: _handleDismissLayerTap,
),
),
Align(
alignment: Alignment.bottomCenter,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {},
child: KeyedSubtree(
key: const Key('player-comments-sheet'),
child: SizedBox(
key: const Key('comment-sheet-constraints'),
height: sheetHeight,
child: Container(
decoration: const BoxDecoration(
color: AppColors.card,
borderRadius: BorderRadius.vertical(
top: Radius.circular(_sheetRadius),
),
),
child: SafeArea(
top: false,
child: ListenableBuilder(
listenable: _controller,
builder: (context, _) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: _content(context),
);
},
),
),
),
),
),
],
),
),
),
],
);
}
@@ -372,6 +354,44 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
}
}
class _CommentPostSuccessOverlay extends StatelessWidget {
const _CommentPostSuccessOverlay({required this.message});
final String message;
@override
Widget build(BuildContext context) {
return Positioned.fill(
child: IgnorePointer(
child: SafeArea(
child: Align(
alignment: const Alignment(0, 0.55),
child: Semantics(
liveRegion: true,
child: Container(
key: const Key('comment-post-success-snackbar'),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.82),
borderRadius: BorderRadius.circular(8),
),
child: Text(
message,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white, fontSize: 14),
),
),
),
),
),
),
);
}
}
class _CommentThread extends StatelessWidget {
const _CommentThread({
required this.comment,
+37 -13
View File
@@ -291,21 +291,32 @@ void main() {
await tester.pump(const Duration(milliseconds: 500));
expect(find.byKey(const Key('comment-composer-sheet')), findsNothing);
final successSnackBar = find.byKey(
final successOverlay = find.byKey(
const Key('comment-post-success-snackbar'),
);
expect(successSnackBar, findsOneWidget);
expect(successOverlay, findsOneWidget);
expect(find.text('发表成功'), findsOneWidget);
final snackBar = tester.widget<SnackBar>(successSnackBar);
expect(snackBar.action, isNull);
expect(snackBar.behavior, SnackBarBehavior.floating);
expect(snackBar.duration, const Duration(milliseconds: 1500));
expect(find.byType(SnackBar), findsNothing);
final capsule = tester.widget<Container>(successOverlay);
final decoration = capsule.decoration! as BoxDecoration;
expect(decoration.color, Colors.black.withValues(alpha: 0.82));
expect(decoration.borderRadius, BorderRadius.circular(8));
expect(
tester.getRect(find.text('发表成功')).bottom,
lessThanOrEqualTo(
tester.getRect(find.byKey(const Key('player-comments-sheet'))).top,
),
capsule.padding,
const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
);
final successText = tester.widget<Text>(find.text('发表成功'));
expect(successText.style?.color, Colors.white);
expect(successText.style?.fontSize, 14);
final placeholderRect = tester.getRect(
find.byKey(const Key('comment-input-placeholder')),
);
expect(
tester.getRect(successOverlay).bottom,
lessThan(placeholderRect.top),
);
await tester.pump(const Duration(milliseconds: 1200));
expect(successOverlay, findsNothing);
expect(find.byKey(const Key('comment-list')), findsOneWidget);
expect(find.byKey(const Key('comment-count-label')), findsOneWidget);
expect(find.byKey(const Key('comment-input-placeholder')), findsOneWidget);
@@ -341,6 +352,10 @@ 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 compactHeight = tester.getSize(composer).height;
expect(compactHeight, inInclusiveRange(100, 150));
expect(tester.widget<TextField>(composerInput).focusNode?.hasFocus, isTrue);
@@ -406,12 +421,21 @@ void main() {
await tester.tap(find.byKey(const Key('comment-input-placeholder')));
await tester.pumpAndSettle();
expect(sheet, findsOneWidget);
expect(find.byKey(const Key('comment-composer-sheet')), findsOneWidget);
final composer = find.byKey(const Key('comment-composer-sheet'));
expect(composer, findsOneWidget);
expect(
find.byKey(const Key('comment-composer-dismiss-layer')),
findsOneWidget,
);
await tester.tapAt(const Offset(20, 20));
final sheetRect = tester.getRect(sheet);
final composerRect = tester.getRect(composer);
await tester.tapAt(
Offset(sheetRect.center.dx, (sheetRect.top + composerRect.top) / 2),
);
await tester.pumpAndSettle();
expect(sheet, findsOneWidget);
expect(find.byKey(const Key('comment-composer-sheet')), findsNothing);
expect(composer, findsNothing);
await tester.tapAt(const Offset(20, 20));
await tester.pumpAndSettle();