277 lines
10 KiB
Dart
277 lines
10 KiB
Dart
// 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> {
|
|
final _input = TextEditingController();
|
|
final _inputFocus = FocusNode();
|
|
final _keyboardInset = KeyboardInsetNotifier.instance;
|
|
bool _closing = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_input.addListener(_onInputChanged);
|
|
_keyboardInset.addListener(_onKeyboardInsetChanged);
|
|
widget.controller.addListener(_onControllerChanged);
|
|
unawaited(_keyboardInset.start());
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_input.removeListener(_onInputChanged);
|
|
_keyboardInset.removeListener(_onKeyboardInsetChanged);
|
|
widget.controller.removeListener(_onControllerChanged);
|
|
_input.dispose();
|
|
_inputFocus.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _onInputChanged() {
|
|
if (mounted) setState(() {});
|
|
}
|
|
|
|
void _onControllerChanged() {
|
|
if (mounted) setState(() {});
|
|
}
|
|
|
|
void _onKeyboardInsetChanged() {
|
|
if (!mounted) return;
|
|
setState(() {});
|
|
}
|
|
|
|
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) {
|
|
_input.clear();
|
|
_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 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 Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
Positioned.fill(
|
|
child: GestureDetector(
|
|
key: const Key('comment-composer-dismiss-layer'),
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () {
|
|
_inputFocus.unfocus();
|
|
_closeSheet();
|
|
},
|
|
),
|
|
),
|
|
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,
|
|
),
|
|
),
|
|
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,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|