app_home_tab.dart 10.4 KB
import 'package:cached_network_image/cached_network_image.dart';
import 'package:doublefeel_flutter/app/modules/home/controllers/app_home_controller.dart';
import 'package:doublefeel_flutter/app/modules/home/controllers/today_controller.dart';
import 'package:doublefeel_flutter/app/modules/home/views/tabs/today_tab.dart';
import 'package:doublefeel_flutter/core/theme/app_theme.dart';
import 'package:doublefeel_flutter/app/modules/home/widgets/today/app_home_stress_card.dart';
import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart'
    show V2StressScore;
import 'package:flutter/material.dart';
import 'package:get/get.dart';

class AppHomeTab extends GetView<AppHomeController> {
  const AppHomeTab({super.key});

  @override
  Widget build(BuildContext context) => Obx(() {
    final friend = controller.watchedFriend.value;
    final selfTodayController = controller.selfTodayController;
    final friendTodayController = controller.friendTodayController;
    final children = <Widget>[
      _todayBody(selfTodayController, showPeopleSwitcher: friend != null),
      if (friend != null && friendTodayController != null)
        _todayBody(friendTodayController, showPeopleSwitcher: true),
    ];
    final index = controller.isShowingSelf || friendTodayController == null
        ? 0
        : 1;
    return IndexedStack(index: index, children: children);
  });

  Widget _todayBody(
    TodayController todayController, {
    required bool showPeopleSwitcher,
  }) => TodayTabBody(
    controller: todayController,
    useHomeHeader: true,
    headerFooter: showPeopleSwitcher
        ? _PeopleSwitcher(controller: controller)
        : null,
    stressCard: showPeopleSwitcher
        ? AppHomeStressCard(controller: todayController)
        : null,
    showInteractionEntry: controller.hasAnyFriend,
  );
}

class _PeopleSwitcher extends StatelessWidget {
  const _PeopleSwitcher({required this.controller});

  final AppHomeController controller;

  @override
  Widget build(BuildContext context) => Obx(() {
    final friend = controller.watchedFriend.value;
    final selfTodayController = controller.selfTodayController;
    final friendTodayController = controller.friendTodayController;
    // Subscribe to both controllers explicitly. Their date changes update the
    // score independently while the outer home tab remains mounted.
    final selfStressScore = selfTodayController.v2StressScore.value;
    final friendStressScore = friendTodayController?.v2StressScore.value;
    if (friend == null) return const SizedBox.shrink();
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16),
      child: LayoutBuilder(
        builder: (context, constraints) {
          final height = constraints.maxWidth * 252 / 1029;
          final scale = height / 84;
          return SizedBox(
            height: height,
            child: DecoratedBox(
              decoration: BoxDecoration(
                image: DecorationImage(
                  image: AssetImage(
                    controller.isShowingSelf
                        ? 'assets/images/today/today_switch_right@3x.png'
                        : 'assets/images/today/today_switch_left@3x.png',
                  ),
                  fit: BoxFit.fill,
                ),
              ),
              child: Row(
                children: [
                  Expanded(
                    child: _PersonTab(
                      selected: !controller.isShowingSelf,
                      name: _friendName(friend.remark, friend.name),
                      avatarUrl: friend.avatarUrl,
                      isMe: false,
                      status: _statusText(
                        isSelected: !controller.isShowingSelf,
                        stressScore: friendStressScore,
                      ),
                      scale: scale,
                      onTap: controller.selectFriend,
                    ),
                  ),
                  Expanded(
                    child: _PersonTab(
                      selected: controller.isShowingSelf,
                      name: _selfName(controller.selfName),
                      avatarUrl: controller.selfAvatar,
                      isMe: true,
                      status: _statusText(
                        isSelected: controller.isShowingSelf,
                        stressScore: selfStressScore,
                      ),
                      scale: scale,
                      onTap: controller.selectSelf,
                    ),
                  ),
                ],
              ),
            ),
          );
        },
      ),
    );
  });

  String _friendName(String? remark, String nickname) {
    final value = remark?.trim();
    return value == null || value.isEmpty ? nickname : '$value$nickname)';
  }

  String _selfName(String nickname) => '$nickname(我)';

  _StatusText? _statusText({
    required bool isSelected,
    required V2StressScore? stressScore,
  }) {
    if (!isSelected || stressScore == null) return null;
    final state = stressScore.state;
    final text = stressScore.stateString();
    if (text.isEmpty) return null;
    return _StatusText(text, _statusColor(state));
  }

  Color _statusColor(int? state) => switch (state) {
    1 => const Color(0xFFFF5279),
    2 => const Color(0xFFFF9A6E),
    3 => const Color(0xFF7B9BFB),
    4 => const Color(0xFF3BD49D),
    _ => const Color(0xFF78787D),
  };
}

class _StatusText {
  const _StatusText(this.value, this.color);

  final String value;
  final Color color;
}

class _PersonTab extends StatelessWidget {
  const _PersonTab({
    required this.isMe,
    required this.selected,
    required this.name,
    required this.avatarUrl,
    required this.status,
    required this.scale,
    required this.onTap,
  });

  final bool isMe;
  final bool selected;
  final String name;
  final String? avatarUrl;
  final _StatusText? status;
  final double scale;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) => GestureDetector(
    behavior: HitTestBehavior.opaque,
    onTap: onTap,
    child: AnimatedContainer(
      duration: const Duration(milliseconds: 160), // 透明背景,确保点击区域覆盖整个卡片
      padding: EdgeInsets.only(
        left: (isMe && !selected ? 16 : 10) * scale,
        right: (isMe || (!isMe && selected) ? 10 : 16) * scale,
      ),
      margin: EdgeInsets.only(
        top: (selected ? 0 : 10) * scale,
        bottom: (selected ? 24 : 26) * scale,
      ),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          SizedBox(
            width: (selected ? 36 : 32) * scale,
            height: (selected ? 36 : 32) * scale,
            child: _Avatar(url: avatarUrl, name: name),
          ),
          SizedBox(width: 8),
          Expanded(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                _PersonName(name: name, selected: selected),
                if (status != null)
                  Text(
                    status!.value,
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                    style: TextStyle(
                      color: status!.color,
                      fontSize: 9,
                      fontWeight: FontWeight.w500,
                    ),
                  ),
              ],
            ),
          ),
        ],
      ),
    ),
  );
}

// class _PersonalTabCard extends StatelessWidget {
//   final bool selected;
//   final String name;
//   final String? avatarUrl;
//   final _StatusText? status;

// _PersonalTabCard({
//     required this.selected,
//     required this.name,
//     required this.avatarUrl,
//     required this.status,
// })

//   @override
//   Widget build(BuildContext context) {
//     return Row(
//       children:[
//         SizedBox(
//             width: selected ? 36 : 32,
//             height: selected ? 36 : 32,
//             child: _Avatar(url: avatarUrl, name: name),
//           ),
//           const SizedBox(width: 8),
//           Expanded(
//             child: Column(
//               mainAxisAlignment: MainAxisAlignment.center,
//               crossAxisAlignment: CrossAxisAlignment.start,
//               children: [
//                 _PersonName(name: name, selected: selected),
//                 if (status != null)
//                   Text(
//                     status!.value,
//                     maxLines: 1,
//                     overflow: TextOverflow.ellipsis,
//                     style: TextStyle(
//                       color: status!.color,
//                       fontSize: 9,
//                       fontWeight: FontWeight.w500,
//                     ),
//                   ),
//               ],
//             ),
//           )
//       ]
//     );
//   }

// }

class _PersonName extends StatelessWidget {
  const _PersonName({required this.name, required this.selected});

  final String name;
  final bool selected;

  @override
  Widget build(BuildContext context) {
    final bracketIndex = name.indexOf('(');
    final primary = bracketIndex < 0 ? name : name.substring(0, bracketIndex);
    final suffix = bracketIndex < 0 ? '' : name.substring(bracketIndex);
    return RichText(
      maxLines: 1,
      overflow: TextOverflow.ellipsis,
      text: TextSpan(
        children: [
          TextSpan(
            text: primary,
            style: TextStyle(
              color: context.colors.textPrimary,
              fontSize: selected ? 14 : 13,
              fontWeight: FontWeight.w600,
            ),
          ),
          TextSpan(
            text: suffix,
            style: const TextStyle(
              color: Color(0xFF78787D),
              fontSize: 13,
              fontWeight: FontWeight.w500,
            ),
          ),
        ],
      ),
    );
  }
}

class _Avatar extends StatelessWidget {
  const _Avatar({required this.url, required this.name});

  final String? url;
  final String name;

  @override
  Widget build(BuildContext context) {
    final avatarUrl = url?.trim();
    return CircleAvatar(
      backgroundColor: const Color(0xFFF0EEF8),
      backgroundImage: avatarUrl == null || avatarUrl.isEmpty
          ? null
          : CachedNetworkImageProvider(avatarUrl),
      child: avatarUrl == null || avatarUrl.isEmpty
          ? Text(
              name.isEmpty ? '?' : name.characters.first,
              style: TextStyle(color: context.colors.textPrimary),
            )
          : null,
    );
  }
}