app_home_controller.dart 7.25 KB
import 'dart:async';

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/health_api.dart';
import 'package:doublefeel_flutter/core/network/api/pay_api.dart';
import 'package:doublefeel_flutter/core/network/api/user_api.dart';
import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_local_datasource.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_remote_datasource.dart';
import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
import 'package:doublefeel_flutter/data/models/friend/friend_models.dart'
    show FriendItem;
import 'package:doublefeel_flutter/data/models/user/user_models.dart'
    show UserInfoResponse;
import 'package:flutter/material.dart';
import 'package:get/get.dart';

import 'today_controller.dart';

/// Owns the Today controllers embedded in the first app tab.
///
/// The self and watched-friend controllers deliberately have different tags so
/// their date, scroll position, and loaded health data remain isolated.
class AppHomeController extends GetxController {
  AppHomeController({
    FriendsRepository? friendsRepository,
    UserPreferencesStorage? userPreferencesStorage,
    UserApi? userApi,
  }) : _friendsRepository =
           friendsRepository ?? FriendsRepositoryImpl(Get.find<FriendApi>()),
       _userPreferencesStorage =
           userPreferencesStorage ?? Get.find<UserPreferencesStorage>(),
       _userApi = userApi ?? Get.find<UserApi>();

  static const selfTodayTag = 'app_home_self';
  static const _friendTodayTagPrefix = 'app_home_friend_';

  final FriendsRepository _friendsRepository;
  final UserPreferencesStorage _userPreferencesStorage;
  final UserApi _userApi;
  final selectedTargetId = selfTodayTag.obs;
  final watchedFriend = Rxn<FriendHealthData>();
  final friends = <FriendHealthData>[].obs;

  String? _friendTodayTag;

  TodayController get selfTodayController =>
      Get.find<TodayController>(tag: selfTodayTag);

  TodayController get selectedTodayController {
    final tag = selectedTargetId.value;
    if (tag == selfTodayTag || !Get.isRegistered<TodayController>(tag: tag)) {
      return selfTodayController;
    }
    return Get.find<TodayController>(tag: tag);
  }

  TodayController? get friendTodayController {
    final tag = _friendTodayTag;
    if (tag == null || !Get.isRegistered<TodayController>(tag: tag)) {
      return null;
    }
    return Get.find<TodayController>(tag: tag);
  }

  bool get hasWatchedFriend => watchedFriend.value != null;
  bool get hasAnyFriend => friends.isNotEmpty;
  bool get isShowingSelf => selectedTargetId.value == selfTodayTag;
  String get selfName {
    final info = _userPreferencesStorage.preferences.value.meUserInfo;
    return info?.nickname?.trim().isNotEmpty == true
        ? info!.nickname!.trim()
        : '我';
  }

  String? get selfAvatar =>
      _userPreferencesStorage.preferences.value.meUserInfo?.avatar;

  @override
  void onInit() {
    super.onInit();
    _registerSelfController();
    unawaited(refreshOnShow());
  }

  void selectSelf() => selectedTargetId.value = selfTodayTag;

  void selectFriend() {
    final tag = _friendTodayTag;
    if (tag != null && Get.isRegistered<TodayController>(tag: tag)) {
      selectedTargetId.value = tag;
    }
  }

  /// Refreshes the data shown by the app-home header whenever this tab returns
  /// to the foreground. The page itself is kept alive by the outer
  /// [IndexedStack], so this cannot rely on [onInit].
  Future<void> refreshOnShow() async {
    await Future.wait<void>([_refreshMyProfile(), _loadFriends()]);
    await refreshSelectedToday();
  }

  Future<void> _refreshMyProfile() async {
    final result = await _userApi.getUserInfo();
    if (result case AppSuccess<UserInfoResponse>(data: final user)) {
      await _userPreferencesStorage.updateMeUserInfo(user);
    }
  }

  /// The app home owns its friend snapshot and reads it directly from the
  /// friend-list API. It intentionally does not depend on FriendsController,
  /// whose list is scoped to the separate Friends tab.
  Future<void> _loadFriends() async {
    try {
      final data = await _friendsRepository.getFriendList();
      friends.assignAll(data.friends);
      _syncWatchedFriend(data.friends);
    } catch (_) {
      // Keep the current home selection visible if refreshing friends fails.
    }
  }

  Future<void> refreshSelectedToday() => selectedTodayController.refreshTab();

  void scrollSelectedToLatestHrv() {
    final controller = selectedTodayController;
    if (!controller.scrollController.hasClients) return;
    controller.scrollController.animateTo(
      850,
      duration: const Duration(milliseconds: 400),
      curve: Curves.easeInOut,
    );
  }

  void _syncWatchedFriend(List<FriendHealthData> friends) {
    final next = friends
        .where(
          (friend) =>
              friend.isOnWatchFace && friend.friendItem.friendUserId != null,
        )
        .firstOrNull;
    final previousId = watchedFriend.value?.friendItem.friendUserId;
    final nextId = next?.friendItem.friendUserId;
    if (previousId == nextId) {
      watchedFriend.value = next;
      // The watched person did not change, but their nickname, remark, or
      // avatar may have. Keep the friend Today controller in sync as well.
      friendTodayController?.targetFriendInfo.value = next?.friendItem;
      return;
    }

    final oldTag = _friendTodayTag;
    if (oldTag != null && Get.isRegistered<TodayController>(tag: oldTag)) {
      Get.delete<TodayController>(tag: oldTag, force: true);
    }
    _friendTodayTag = null;
    watchedFriend.value = next;
    selectSelf();

    if (next == null || nextId == null) return;
    final tag = '$_friendTodayTagPrefix$nextId';
    Get.put<TodayController>(_createTodayController(next.friendItem), tag: tag);
    _friendTodayTag = tag;
  }

  void _registerSelfController() {
    if (Get.isRegistered<TodayController>(tag: selfTodayTag)) return;
    Get.put<TodayController>(_createTodayController(null), tag: selfTodayTag);
  }

  TodayController _createTodayController(FriendItem? friendInfo) =>
      TodayController(
        Get.find<PayApi>(),
        Get.find<VipApi>(),
        HealthDataSourceWrapper(
          remoteDataSource: RemoteHealthDataSource(Get.find<HealthApi>()),
          localDataSource: LocalHealthDataSource(
            coreService: Get.find<HealthRawDataCoreService>(),
          ),
        ),
        Get.find<FriendApi>(),
        Get.find<UserStateService>(),
        friendInfo: friendInfo,
      );

  @override
  void onClose() {
    Get.delete<TodayController>(tag: selfTodayTag, force: true);
    final tag = _friendTodayTag;
    if (tag != null) Get.delete<TodayController>(tag: tag, force: true);
    super.onClose();
  }
}