common_input_dialog_view.dart 8.77 KB
import 'dart:math' as math;

import 'package:doublefeel_flutter/app/actions/dialog_action.dart';
import 'package:doublefeel_flutter/app/models/action_result.dart';
import 'package:doublefeel_flutter/app/models/input_dialog_meta_data.dart';
import 'package:doublefeel_flutter/core/theme/app_colors.dart';
import 'package:doublefeel_flutter/core/util/size_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class CommonInputDialogView extends StatefulWidget {
  const CommonInputDialogView({
    super.key,
    required this.dialogMetaData,
    this.onAction,
  });

  final InputDialogMetaData dialogMetaData;
  final ValueChanged<ActionResult<DialogAction, String>>? onAction;

  @override
  State<CommonInputDialogView> createState() => _CommonInputDialogViewState();
}

class _CommonInputDialogViewState extends State<CommonInputDialogView> {
  late final TextEditingController _textController;
  late final FocusNode _focusNode;

  String get _input => _textController.text.trim();

  bool get _canSave => _input.isNotEmpty;

  @override
  void initState() {
    super.initState();
    _textController =
        TextEditingController(text: widget.dialogMetaData.initialValue);
    _focusNode = FocusNode();
    _textController.addListener(_handleTextChanged);
  }

  @override
  void dispose() {
    _textController.removeListener(_handleTextChanged);
    _textController.dispose();
    _focusNode.dispose();
    super.dispose();
  }

  void _handleTextChanged() {
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    final mediaQuery = MediaQuery.of(context);
    final screenWidth = mediaQuery.size.width;
    final dialogWidth = math.min(300.dp, screenWidth - 48.dp);
    final keyboardHeight = mediaQuery.viewInsets.bottom;

    return GestureDetector(
      behavior: HitTestBehavior.opaque,
      onTap: widget.dialogMetaData.isCancelable ? _dismiss : null,
      child: LayoutBuilder(
        builder: (context, constraints) {
          final verticalPadding = 24.dp;
          final availableHeight = math.max(
            0.0,
            constraints.maxHeight - keyboardHeight,
          );

          return AnimatedPadding(
            duration: const Duration(milliseconds: 180),
            curve: Curves.easeOut,
            padding: EdgeInsets.fromLTRB(
              24.dp,
              verticalPadding,
              24.dp,
              keyboardHeight + verticalPadding,
            ),
            child: SingleChildScrollView(
              physics: const ClampingScrollPhysics(),
              child: ConstrainedBox(
                constraints: BoxConstraints(
                  minHeight:
                      math.max(0.0, availableHeight - verticalPadding * 2),
                ),
                child: Center(
                  child: GestureDetector(
                    behavior: HitTestBehavior.opaque,
                    onTap: () {},
                    child: Material(
                      color: Colors.transparent,
                      child: Container(
                        width: dialogWidth,
                        padding:
                            EdgeInsets.fromLTRB(24.dp, 24.dp, 24.dp, 20.dp),
                        decoration: BoxDecoration(
                          color: Colors.white,
                          borderRadius: BorderRadius.circular(24.dp),
                        ),
                        child: Column(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            SizedBox(
                              width: 220.dp,
                              child: Text(
                                widget.dialogMetaData.title,
                                maxLines: 1,
                                overflow: TextOverflow.ellipsis,
                                textAlign: TextAlign.center,
                                style: const TextStyle(
                                  color: Color(0xFF141414),
                                  fontSize: 18,
                                  fontWeight: FontWeight.w600,
                                  height: 1.25,
                                ),
                              ),
                            ),
                            SizedBox(height: 16.dp),
                            _buildInput(),
                            SizedBox(height: 24.dp),
                            Opacity(
                              opacity: _canSave ? 1 : 0.4,
                              child: _InputDialogButton(
                                text: widget.dialogMetaData.confirmText,
                                onTap: _canSave ? _confirm : null,
                              ),
                            ),
                          ],
                        ),
                      ),
                    ),
                  ),
                ),
              ),
            ),
          );
        },
      ),
    );
  }

  Widget _buildInput() {
    return Container(
      width: double.infinity,
      height: 52.dp,
      decoration: BoxDecoration(
        color: const Color(0xFFF3F3F3),
        borderRadius: BorderRadius.circular(27.dp),
      ),
      child: Stack(
        children: [
          Positioned.fill(
            right: 58.dp,
            child: Padding(
              padding: EdgeInsets.only(left: 22.dp),
              child: Align(
                alignment: Alignment.centerLeft,
                child: TextField(
                  controller: _textController,
                  focusNode: _focusNode,
                  autofocus: true,
                  maxLines: 1,
                  contextMenuBuilder: (BuildContext context,
                      EditableTextState editableTextState) {
                    // 获取默认的菜单项列表
                    List<ContextMenuButtonItem> buttonItems =
                        editableTextState.contextMenuButtonItems;

                    // 例如:只保留“粘贴” (Paste)
                    buttonItems.removeWhere(
                        (item) => item.type != ContextMenuButtonType.paste);
                    return AdaptiveTextSelectionToolbar.buttonItems(
                      anchors: editableTextState.contextMenuAnchors,
                      buttonItems: buttonItems,
                    );
                  },
                  inputFormatters: [
                    LengthLimitingTextInputFormatter(
                      widget.dialogMetaData.maxLength,
                    ),
                  ],
                  style: const TextStyle(
                    color: Color(0xFF141414),
                    fontSize: 16,
                    fontWeight: FontWeight.w400,
                  ),
                  decoration: InputDecoration(
                    border: InputBorder.none,
                    hintText: widget.dialogMetaData.hintText,
                    hintStyle: const TextStyle(
                      color: AppColors.textTertiary,
                      fontSize: 16,
                      fontWeight: FontWeight.w400,
                    ),
                  ),
                ),
              ),
            ),
          ),
          Positioned(
            right: 22.dp,
            top: 0,
            bottom: 0,
            child: Center(
              child: Text(
                '${_textController.text.length}/${widget.dialogMetaData.maxLength}',
                style: const TextStyle(
                  color: Color(0xFFD9D9D9),
                  fontSize: 16,
                  fontWeight: FontWeight.w400,
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

  void _confirm() {
    widget.onAction?.call(
      ActionResult(
        action: DialogAction.confirm,
        data: _input,
      ),
    );
  }

  void _dismiss() {
    widget.onAction?.call(
      const ActionResult(
        action: DialogAction.dismiss,
      ),
    );
  }
}

class _InputDialogButton extends StatelessWidget {
  const _InputDialogButton({
    required this.text,
    required this.onTap,
  });

  final String text;
  final VoidCallback? onTap;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      behavior: HitTestBehavior.opaque,
      child: Container(
        width: 220.dp,
        height: 48.dp,
        alignment: Alignment.center,
        decoration: BoxDecoration(
          color: AppColors.primary,
          borderRadius: BorderRadius.circular(24.dp),
        ),
        padding: EdgeInsets.symmetric(horizontal: 16.dp),
        child: Text(
          text,
          maxLines: 1,
          overflow: TextOverflow.ellipsis,
          textAlign: TextAlign.center,
          style: const TextStyle(
            color: Colors.white,
            fontSize: 14,
            fontWeight: FontWeight.w500,
            height: 1.2,
          ),
        ),
      ),
    );
  }
}