fix(player): 评论输入改为独立弹窗
This commit is contained in:
1 parent
aa14d0a835
commit
282f51c24b
4 files changed
+418
-495
No files matched your search
@@ -0,0 +1,294 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../core/data/models/comment.dart';
|
||||
import '../../../core/data/services/auth_controller.dart';
|
||||
import '../../../core/data/services/comment_controller.dart';
|
||||
import '../../../core/platform/haptics.dart';
|
||||
import '../../../core/platform/keyboard_inset.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/widgets/auth_gate.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
|
||||
const double _sheetRadius = 20;
|
||||
const double _inputRadius = 12;
|
||||
const double _postButtonHeight = 32;
|
||||
const int _maxCommentLength = 300;
|
||||
|
||||
/// 独立评论输入弹窗,复用原评论控制器的发布与列表更新能力。
|
||||
class CommentComposerSheet extends StatefulWidget {
|
||||
const CommentComposerSheet({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.replyTo,
|
||||
});
|
||||
|
||||
final CommentController controller;
|
||||
final DramaComment? replyTo;
|
||||
|
||||
@override
|
||||
State<CommentComposerSheet> createState() => _CommentComposerSheetState();
|
||||
}
|
||||
|
||||
class _CommentComposerSheetState extends State<CommentComposerSheet>
|
||||
with WidgetsBindingObserver {
|
||||
final _input = TextEditingController();
|
||||
final _inputFocus = FocusNode();
|
||||
final _keyboardInset = KeyboardInsetNotifier.instance;
|
||||
bool _keyboardWasVisible = false;
|
||||
bool _hadFocus = false;
|
||||
bool _closing = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_input.addListener(_onInputChanged);
|
||||
_inputFocus.addListener(_onFocusChanged);
|
||||
_keyboardInset.addListener(_onKeyboardInsetChanged);
|
||||
widget.controller.addListener(_onControllerChanged);
|
||||
unawaited(_keyboardInset.start());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_input.removeListener(_onInputChanged);
|
||||
_inputFocus.removeListener(_onFocusChanged);
|
||||
_keyboardInset.removeListener(_onKeyboardInsetChanged);
|
||||
widget.controller.removeListener(_onControllerChanged);
|
||||
_input.dispose();
|
||||
_inputFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onInputChanged() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
void _onControllerChanged() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
void _onFocusChanged() {
|
||||
if (!mounted) return;
|
||||
final hasFocus = _inputFocus.hasFocus;
|
||||
if (_hadFocus && !hasFocus) {
|
||||
_closeSheet();
|
||||
}
|
||||
_hadFocus = hasFocus;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _onKeyboardInsetChanged() {
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
_syncKeyboardVisibility(_keyboardHeight(context));
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeMetrics() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
_syncKeyboardVisibility(_keyboardHeight(context));
|
||||
});
|
||||
}
|
||||
|
||||
void _syncKeyboardVisibility(double keyboardHeight) {
|
||||
if (keyboardHeight > 0) {
|
||||
_keyboardWasVisible = true;
|
||||
return;
|
||||
}
|
||||
if (_keyboardWasVisible) _closeSheet();
|
||||
}
|
||||
|
||||
double _keyboardHeight(BuildContext context) => math.max(
|
||||
MediaQuery.viewInsetsOf(context).bottom,
|
||||
_keyboardInset.value / MediaQuery.devicePixelRatioOf(context),
|
||||
);
|
||||
|
||||
void _closeSheet() {
|
||||
if (_closing || !mounted) return;
|
||||
_closing = true;
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (widget.controller.submitting) return;
|
||||
final text = _input.text.trim();
|
||||
if (text.isEmpty) return;
|
||||
unawaited(context.read<Haptics>().lightImpact());
|
||||
final result = await widget.controller.publish(
|
||||
text,
|
||||
replyTo: widget.replyTo,
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (result.isSuccess) {
|
||||
_closing = true;
|
||||
Navigator.pop(context, true);
|
||||
return;
|
||||
}
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(result.errorMessage ?? l10n.commentPostFailed)),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _authenticate() async {
|
||||
await ensureSignedIn(context);
|
||||
if (!mounted || !context.read<AuthController>().isSignedIn) return;
|
||||
_inputFocus.requestFocus();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenHeight = MediaQuery.sizeOf(context).height;
|
||||
final halfHeight = (screenHeight * 0.5).roundToDouble();
|
||||
final maxHeight = math.max(
|
||||
0.0,
|
||||
screenHeight - MediaQuery.paddingOf(context).top,
|
||||
);
|
||||
final sheetHeight = math.min(halfHeight, maxHeight);
|
||||
final keyboardHeight = _keyboardHeight(context);
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final signedIn = context.watch<AuthController>().isSignedIn;
|
||||
final hasContent = _input.text.trim().isNotEmpty;
|
||||
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'),
|
||||
height: sheetHeight,
|
||||
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, 16, 16, 12),
|
||||
child: TextFieldTapRegion(
|
||||
child: Column(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: 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),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
l10n.postComment,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user