interact_controller.dart 11.6 KB
import 'dart:async';

import 'package:doublefeel_flutter/app/routes/app_pages.dart';
import 'package:doublefeel_flutter/app/modules/interact/models/interact_route_arguments.dart';
import 'package:doublefeel_flutter/app/modules/friends/data/friends_repository.dart';
import 'package:doublefeel_flutter/app/modules/friends/models/friend_health_data.dart';
import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
import 'package:doublefeel_flutter/core/network/api/interaction_api.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
import 'package:doublefeel_flutter/data/models/interaction/interaction_models.dart';
import 'package:doublefeel_flutter/data/models/user/user_models.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';

enum InteractAction { stick, miss, punch }

extension InteractActionUi on InteractAction {
  int get apiValue => switch (this) {
    InteractAction.stick => 0,
    InteractAction.miss => 1,
    InteractAction.punch => 2,
  };

  String get title => switch (this) {
    InteractAction.stick => '戳一戳',
    InteractAction.miss => '想Ta',
    InteractAction.punch => '打一拳',
  };

  bool get needsVip => this != InteractAction.stick;

  String get iconAsset => switch (this) {
    InteractAction.stick => 'assets/images/interact/icon_stick.png',
    InteractAction.miss => 'assets/images/interact/icon_miss.png',
    InteractAction.punch => 'assets/images/interact/icon_punch.png',
  };

  String get titleAsset => switch (this) {
    InteractAction.stick => 'assets/images/interact/title_stick.png',
    InteractAction.miss => 'assets/images/interact/title_miss.png',
    InteractAction.punch => 'assets/images/interact/title_punch.png',
  };

  String get lockedTitleAsset => switch (this) {
    InteractAction.stick => 'assets/images/interact/title_stick_locked.png',
    InteractAction.miss => 'assets/images/interact/title_miss_locked.png',
    InteractAction.punch => 'assets/images/interact/title_punch_locked.png',
  };
}

class InteractController extends GetxController {
  InteractController(
    this._interactionApi,
    this._userPreferencesStorage, {
    FriendsRepository? friendsRepository,
  }) : _friendsRepository =
           friendsRepository ?? FriendsRepositoryImpl(Get.find<FriendApi>());

  final InteractionApi _interactionApi;
  final UserPreferencesStorage _userPreferencesStorage;
  final FriendsRepository _friendsRepository;

  final records = <InteractionRecord>[].obs;
  final isLoading = false.obs;
  final sendingAction = Rxn<InteractAction>();
  final activeAction = Rxn<InteractAction>();
  final selectedFriend = Rxn<FriendHealthData>();
  final targetFriendUserId = RxnInt();
  final friends = <FriendHealthData>[].obs;
  InteractRouteArguments? entryArguments;

  UserInfoResponse? get me =>
      _userPreferencesStorage.preferences.value.meUserInfo;
  UserInfoResponse? get partner => selectedFriend.value == null
      ? _userPreferencesStorage.preferences.value.partnerUserInfo
      : null;
  bool get isPaired => targetFriendUserId.value != null || partner?.id != null;
  bool get isVip =>
      _userPreferencesStorage.preferences.value.vipInfo?.isVip ?? false;
  List<FriendHealthData> get availableFriends =>
      friends.toList(growable: false);

  String get friendName =>
      selectedFriend.value?.name ?? partner?.nickname ?? '好友';
  String? get friendRemark => selectedFriend.value?.remark;
  String? get friendAvatarUrl =>
      selectedFriend.value?.avatarUrl ?? partner?.avatar;
  String get friendLabel {
    final remark = friendRemark?.trim();
    return remark == null || remark.isEmpty
        ? friendName
        : '$remark$friendName)';
  }

  String get selfName => me?.nickname ?? '我';
  String? get selfAvatarUrl => me?.avatar;

  /// V2 interaction types for the currently implemented home-page entries.
  int get interactionType {
    final values = entryArguments?.values;
    if (values == null || values.isEmpty) return 100;
    return switch (values.first.type) {
      InteractValueType.averageHrv => 101,
      InteractValueType.restingHeartRate => 102,
      InteractValueType.sleepDuration ||
      InteractValueType.sleepQualityScore ||
      InteractValueType.sleepQualityState ||
      InteractValueType.averageSleepHeartRate => 103,
      InteractValueType.activeCalories ||
      InteractValueType.exerciseDuration ||
      InteractValueType.standDuration => 104,
    };
  }

  @override
  void onInit() {
    super.onInit();
    final arguments = Get.arguments;
    if (arguments is InteractRouteArguments) {
      entryArguments = arguments;
      targetFriendUserId.value = arguments.friendUserId;
    }
    refreshPage();
  }

  /// Reload target profile and records together after switching friends.
  Future<void> refreshPage() async {
    await _loadSelectedFriend();
    await loadRecords();
  }

  Future<void> loadFriendCandidates() async {
    await _loadFriends();
    await _loadSelectedFriend();
  }

  Future<void> selectFriend(FriendHealthData friend) async {
    final userId = friend.userId;
    if (userId == null || userId == targetFriendUserId.value) return;
    targetFriendUserId.value = userId;
    selectedFriend.value = friend;
    await refreshPage();
  }

  Future<void> _loadSelectedFriend() async {
    await _loadFriends();
    var targetId = targetFriendUserId.value;
    if (targetId == null) {
      // Read directly from the friend source rather than the app-home state.
      final watchedFriend = availableFriends
          .where(
            (friend) =>
                friend.isOnWatchFace && friend.friendItem.friendUserId != null,
          )
          .firstOrNull;
      targetId =
          watchedFriend?.userId ?? watchedFriend?.friendItem.friendUserId;
      targetId ??= partner?.id;
      targetFriendUserId.value = targetId;
    }
    if (targetId == null) {
      selectedFriend.value = null;
      return;
    }
    final friend = availableFriends
        .where((friend) => friend.userId == targetId)
        .firstOrNull;
    if (friend != null) {
      selectedFriend.value = friend;
      return;
    }
    selectedFriend.value = null;
  }

  Future<void> _loadFriends() async {
    try {
      final data = await _friendsRepository.getFriendList();
      friends.assignAll(data.friends);
    } catch (_) {
      // Keep the existing snapshot during a transient list-request failure.
    }
  }

  Future<void> loadRecords() async {
    isLoading.value = true;
    final friendUserId = targetFriendUserId.value;
    if (friendUserId == null) {
      records.clear();
      isLoading.value = false;
      return;
    }
    final result = await _interactionApi.getInteractionRecordList(friendUserId);
    isLoading.value = false;
    switch (result) {
      case AppSuccess(:final data):
        final responseRecords = data.records ?? const <InteractionRecord>[];
        records.assignAll(
          kDebugMode && responseRecords.isEmpty
              ? _mockRecords()
              : responseRecords,
        );
      case AppFailure():
        if (kDebugMode) {
          records.assignAll(_mockRecords());
        }
      // Keep the last successful list visible in release when a refresh fails.
    }
  }

  Future<void> sendAction(
    InteractAction action, {
    InteractionDataAction? actionVariables,
  }) async {
    if (!isPaired) {
      _showToast('请先绑定另一半');
      Get.toNamed(AppRoutes.bindPartner);
      return;
    }
    if (action.needsVip && !isVip) {
      _showToast('${action.title}为会员互动,请先开通会员');
      Get.toNamed(Routes.PURCHASE);
      return;
    }
    if (sendingAction.value != null) return;
    final friendUserId = targetFriendUserId.value;
    if (friendUserId == null) {
      _showToast('未找到互动好友');
      return;
    }

    sendingAction.value = action;
    activeAction.value = action;
    final result = await _interactionApi.sendInteraction(
      InteractionData(
        friendUserId: friendUserId,
        interactionType: interactionType,
        actionType: action.apiValue,
        action: actionVariables,
      ),
    );
    sendingAction.value = null;

    switch (result) {
      case AppSuccess():
        _showToast('${action.title}成功!');
        unawaited(loadRecords());
      case AppFailure(:final error):
        _showToast('互动发送失败:$error');
    }

    Future<void>.delayed(const Duration(seconds: 4), () {
      if (activeAction.value == action) activeAction.value = null;
    });
  }

  String recordText(InteractionRecord record) {
    final text = record.text?.trim();
    if (text != null && text.isNotEmpty) return text;
    final actor = record.userId == me?.id ? '我' : '对方';
    final target = record.action?.objectTarget == 1 ? '自己' : 'Ta';
    return switch (record.actionType) {
      0 => '$actor 戳了戳$target~',
      1 => '$actor$target 了一下~',
      2 => '$actor 打了$target 一拳~',
      _ => '$actor 发起了一次互动~',
    };
  }

  String recordTime(InteractionRecord record) {
    final timestamp = record.createTime;
    if (timestamp == null || timestamp <= 0) return '';
    final date = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
    return '${date.month.toString().padLeft(2, '0')}-'
        '${date.day.toString().padLeft(2, '0')} '
        '${date.hour.toString().padLeft(2, '0')}:'
        '${date.minute.toString().padLeft(2, '0')}';
  }

  String? partnerActionAsset() {
    final action = activeAction.value;
    if (action == null || !isPaired) return null;
    final character = switch (partner?.persona) {
      2 => 'Dog',
      3 => 'Rabbit',
      4 => 'Elephant',
      _ => 'Cat',
    };
    final actionName = switch (action) {
      InteractAction.stick => 'Stick',
      InteractAction.miss => 'Miss',
      InteractAction.punch => 'Punch',
    };
    return 'assets/images/interact/animations/'
        'character${character}Interaction${actionName}Animation.png';
  }

  String? actionMaskAsset() => switch (activeAction.value) {
    InteractAction.stick =>
      'assets/images/interact/animations/interactionStickMaskAnimation.png',
    InteractAction.miss =>
      'assets/images/interact/animations/interactionMissMaskAnimation.png',
    InteractAction.punch =>
      'assets/images/interact/animations/interactionPunchMaskAnimation.png',
    null => null,
  };

  void _showToast(String message) {
    Get.snackbar('互动', message, snackPosition: SnackPosition.BOTTOM);
  }

  List<InteractionRecord> _mockRecords() {
    final now = DateTime.now();
    final meId = me?.id ?? 1;
    final friendId = targetFriendUserId.value ?? 2;
    const texts = <String>[
      '我戳了戳Ta一下~',
      'Ta打了你一拳~',
      '我想Ta了,今天也要好好照顾自己呀~',
      'Ta戳了戳你,提醒你起来活动一下。',
      '我送给Ta一份今天的好心情~',
      'Ta想你了,记得给Ta一个回应。',
      '我打了一拳,今天也要元气满满!',
      'Ta戳了戳你,别忘了喝水。',
      '我想念Ta,晚点一起聊聊天吧。',
      'Ta送来一份鼓励:今天辛苦了!',
      '我戳了戳Ta,继续加油~',
      'Ta打了一拳,是轻轻的一拳。',
      '我想Ta了。',
      'Ta戳了戳你。',
      '我送给Ta一个拥抱。',
    ];
    return List<InteractionRecord>.generate(
      texts.length,
      (index) => InteractionRecord(
        id: index + 1,
        userId: index.isEven ? meId : friendId,
        actionType: index % 3,
        text: texts[index],
        createTime:
            now
                .subtract(Duration(minutes: index * 17 + 3))
                .millisecondsSinceEpoch ~/
            Duration.millisecondsPerSecond,
      ),
    );
  }
}