friends_controller.dart 7.19 KB
import 'dart:async';

import 'package:doublefeel_flutter/app/actions/dialog_action.dart';
import 'package:doublefeel_flutter/app/models/dialog_meta_data.dart';
import 'package:doublefeel_flutter/app/models/input_dialog_meta_data.dart';
import 'package:doublefeel_flutter/app/utils/dialog_utils.dart';
import 'package:doublefeel_flutter/core/error/app_error.dart';
import 'package:doublefeel_flutter/core/logging/app_logger.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/util/app_toast.dart';
import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
import 'package:doublefeel_flutter/pigeon/wear_engine_api.g.dart';
import 'package:get/get.dart';

import '../data/friends_repository.dart';
import '../data/self_health_repository.dart';
import '../models/friend_health_data.dart';

class FriendsController extends GetxController {
  FriendsController({
    FriendsRepository? repository,
    SelfHealthRepository? selfHealthRepository,
    Future<void> Function(WatchAppOtherInfo info)? updateWatchOtherUserInfo,
  })  : _repository =
            repository ?? FriendsRepositoryImpl(Get.find<FriendApi>()),
        _selfHealthRepository = selfHealthRepository ??
            SelfHealthRepositoryImpl(Get.find<HealthApi>()),
        _updateWatchOtherUserInfo = updateWatchOtherUserInfo ??
            ((info) => PlatformHostApi().refreshWatchAppAndWidgets());

  static const maxFriends = 10;

  final FriendsRepository _repository;
  final SelfHealthRepository _selfHealthRepository;
  final Future<void> Function(WatchAppOtherInfo info) _updateWatchOtherUserInfo;
  final friends = <FriendHealthData>[].obs;
  final isLoading = false.obs;
  final selfHealthData = Rxn<V2HealthData>();
  final selfStressScore = Rxn<V2StressScore>();
  final selfHealthUpdatedAt = Rxn<DateTime>();
  final isSelfHealthLoading = false.obs;
  Future<void>? _friendsRequest;
  Future<void>? _selfHealthRequest;

  bool get isFull => friends.length >= maxFriends;

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

  Future<void> refreshData() {
    return Future.wait([loadFriends(), loadSelfHealth()]);
  }

  Future<void> loadFriends() {
    return _friendsRequest ??= _loadFriends();
  }

  Future<void> _loadFriends() async {
    isLoading.value = true;
    try {
      friends.assignAll(await _repository.getFriends());
    } catch (error, stackTrace) {
      AppLogger.e('FriendsController.loadFriends failed', error, stackTrace);
    } finally {
      isLoading.value = false;
      _friendsRequest = null;
    }
  }

  Future<void> loadSelfHealth() {
    return _selfHealthRequest ??= _loadSelfHealth();
  }

  Future<void> _loadSelfHealth() async {
    isSelfHealthLoading.value = true;
    try {
      final today = DateTime.now();
      await Future.wait([
        _loadSelfHealthData(today),
        _loadSelfStressScore(today),
      ]);
    } finally {
      isSelfHealthLoading.value = false;
      _selfHealthRequest = null;
    }
  }

  Future<void> _loadSelfHealthData(DateTime date) async {
    try {
      selfHealthData.value = await _selfHealthRepository.getHealthData(date);
      selfHealthUpdatedAt.value = DateTime.now();
    } catch (error, stackTrace) {
      AppLogger.e(
        'FriendsController.loadSelfHealthData failed',
        error,
        stackTrace,
      );
    }
  }

  Future<void> _loadSelfStressScore(DateTime date) async {
    try {
      selfStressScore.value = await _selfHealthRepository.getStressScore(date);
      selfHealthUpdatedAt.value = DateTime.now();
    } catch (error, stackTrace) {
      AppLogger.e(
        'FriendsController.loadSelfStressScore failed',
        error,
        stackTrace,
      );
    }
  }

  Future<void> showEditRemarkDialog(FriendHealthData friend) async {
    final result = await DialogUtils.showInputDialog(
      InputDialogMetaData(
        title: l10n.friendsEditRemarkTitle,
        initialValue: friend.remark ?? '',
        hintText: l10n.friendsEditRemarkHint,
        confirmText: l10n.friendsSave,
        maxLength: 6,
      ),
    );

    final remark = result.data?.trim();
    if (result.action != DialogAction.confirm ||
        remark == null ||
        remark.isEmpty) {
      return;
    }

    final userId = friend.userId;
    if (userId == null) return;
    try {
      await _repository.updateRemark(userId, remark);
      final index = friends.indexOf(friend);
      if (index >= 0) friends[index] = friend.copyWith(remark: remark);
    } catch (error, stackTrace) {
      if (error is AppError) {
        AppToast.show(error.displayMessage);
      } else {
        AppLogger.e(
          'FriendsController.showEditRemarkDialog failed',
          error,
          stackTrace,
        );
      }
    }
  }

  Future<void> applyWatchFaceSelection(FriendHealthData selectedFriend) async {
    final friendUserId = selectedFriend.userId;
    if (friendUserId == null) {
      return;
    }
    try {
      await _repository.selectWatchFaceFriend(friendUserId);
      final updatedFriends = friends
          .map(
            (friend) => friend.userId == friendUserId
                ? friend.copyWith(isOnWatchFace: true)
                : friend,
          )
          .toList(growable: false);
      friends.assignAll(updatedFriends);
      await _updateWatchOtherUserInfo(
        WatchAppOtherInfo(
          userId: friendUserId,
          nickname: selectedFriend.name,
          markName: selectedFriend.remark,
        ),
      );
    } catch (error, stackTrace) {
      await loadFriends();
      if (error is AppError) {
        AppToast.show(error.displayMessage);
      } else {
        AppLogger.e(
          'FriendsController.applyWatchFaceSelection failed',
          error,
          stackTrace,
        );
      }
    }
  }

  Future<void> showDeleteConfirmDialog(FriendHealthData friend) async {
    try {
      final dialogMetaData = DialogMetaData(
        iconAsset: null,
        title: l10n.friendsDeleteConfirmTitle(friend.displayName),
        message: l10n.friendsDeleteConfirmMessage,
        confirmText: l10n.friendsDeleteConfirmAction,
        cancelText: l10n.cancel,
      );
      final result = await DialogUtils.showCommonDialog(dialogMetaData);
      if (result.action != DialogAction.confirm) return;

      final userId = friend.userId;
      if (userId == null) return;
      await _repository.deleteFriend(userId);
      await _refreshWatchAppAndWidgetsAfterFriendDeleted();
      await loadFriends();
    } catch (error, stackTrace) {
      if (error is AppError) {
        AppToast.show(error.displayMessage);
      } else {
        AppLogger.e(
          'FriendsController.showDeleteConfirmDialog failed',
          error,
          stackTrace,
        );
      }
    }
  }

  Future<void> _refreshWatchAppAndWidgetsAfterFriendDeleted() async {
    try {
      await PlatformHostApi().refreshWatchAppAndWidgets();
    } catch (error, stackTrace) {
      AppLogger.e(
        'FriendsController.refreshWatchAppAndWidgetsAfterFriendDeleted failed',
        error,
        stackTrace,
      );
    }
  }
}