friends_controller.dart 10.5 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/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/services/thinking_data_service.dart';
import 'package:doublefeel_flutter/core/util/app_toast.dart';
import 'package:doublefeel_flutter/data/local/user_account_storage.dart';
import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
import 'package:get/get.dart';

import '../data/friends_repository.dart';
import '../dialog/edit_remark_dialog.dart';
import '../models/friend_health_data.dart';

class FriendsController extends GetxController {
  FriendsController({
    FriendsRepository? repository,
    Future<void> Function(WatchAppOtherInfo info)? updateWatchOtherUserInfo,
    UserPreferencesStorage? userPreferencesStorage,
    UserAccountStorage? userAccountStorage,
  })  : _repository = repository ??
            FriendsRepositoryImpl(
              Get.find<FriendApi>(),
              Get.find<UserPreferencesStorage>(),
              Get.find<UserAccountStorage>(),
            ),
        _userPreferencesStorage =
            userPreferencesStorage ?? Get.find<UserPreferencesStorage>(),
        _userAccountStorage =
            userAccountStorage ?? Get.find<UserAccountStorage>(),
        _updateWatchOtherUserInfo = updateWatchOtherUserInfo ??
            ((info) => PlatformHostApi().refreshWatchAppAndWidgets());

  static const maxFriends = 10;

  final FriendsRepository _repository;
  final UserPreferencesStorage _userPreferencesStorage;
  final UserAccountStorage _userAccountStorage;
  final Future<void> Function(WatchAppOtherInfo info) _updateWatchOtherUserInfo;
  final friends = <FriendHealthData>[].obs;
  final isLoading = false.obs;
  final selfHealthData = Rxn<SelfFriendHealthData>();
  Future<void>? _friendsRequest;
  bool _isPageVisible = false;
  late final Worker _preferencesWorker;
  late final Worker _realtimeStressSettingsWorker;
  bool _showRealtimeStress = false;

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

  @override
  void onInit() {
    super.onInit();
    _showRealtimeStress = _currentShowRealtimeStress;
    _preferencesWorker = ever<UserPreferences>(
      _userPreferencesStorage.preferences,
      _onPreferencesChanged,
    );
    _realtimeStressSettingsWorker = ever<int>(
      _userAccountStorage.realtimeStressSettingsVersion,
      (_) => _onPreferencesChanged(_userPreferencesStorage.preferences.value),
    );
    unawaited(refreshData());
  }

  @override
  void onClose() {
    _preferencesWorker.dispose();
    _realtimeStressSettingsWorker.dispose();
    super.onClose();
  }

  bool get _currentShowRealtimeStress {
    final preferences = _userPreferencesStorage.preferences.value;
    final userId = preferences.meUserInfo?.id;
    return preferences.vipInfo?.isVip == true &&
        (userId == null ||
            (_userAccountStorage.showRealtimeStress(userId) ?? true));
  }

  void _onPreferencesChanged(UserPreferences _) {
    final showRealtimeStress = _currentShowRealtimeStress;
    if (showRealtimeStress == _showRealtimeStress) return;
    _showRealtimeStress = showRealtimeStress;
    unawaited(refreshData());
  }

  void setPageVisible(bool visible) {
    if (visible) {
      _markPageVisible();
    } else {
      _markPageHidden();
    }
  }

  void _markPageVisible() {
    if (_isPageVisible) return;
    _isPageVisible = true;
    unawaited(refreshData());
    ta.track('enter_doublefeel_friend_page');
  }

  void _markPageHidden() {
    _isPageVisible = false;
  }

  Future<void> refreshData() => loadFriends();

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

  Future<void> _loadFriends() async {
    isLoading.value = true;
    try {
      final data = await _repository.getFriendList(withSelfHealthData: true);
      final sortedFriends = _sortFriends(data.friends);
      await _syncFriendOrder(sortedFriends);
      friends.assignAll(sortedFriends);
      selfHealthData.value = data.selfHealthData;
    } catch (error, stackTrace) {
      AppLogger.e('FriendsController.loadFriends failed', error, stackTrace);
    } finally {
      isLoading.value = false;
      _friendsRequest = null;
    }
  }

  /// Moves a friend immediately and persists the order for the current account.
  Future<void> reorderFriends(int oldIndex, int newIndex) async {
    if (oldIndex < 0 || oldIndex >= friends.length || newIndex < 0) return;

    var targetIndex = newIndex;
    if (targetIndex > oldIndex) targetIndex--;
    if (targetIndex < 0 || targetIndex >= friends.length) return;

    final friend = friends.removeAt(oldIndex);
    friends.insert(targetIndex, friend);

    final userId = _userPreferencesStorage.preferences.value.meUserInfo?.id;
    if (userId == null) return;
    try {
      await _userAccountStorage.saveFriendOrder(
        userId,
        friends.map((friend) => friend.userId).whereType<int>().toList(),
      );
    } catch (error, stackTrace) {
      AppLogger.e(
        'FriendsController.reorderFriends failed',
        error,
        stackTrace,
      );
    }
  }

  List<FriendHealthData> _sortFriends(List<FriendHealthData> items) {
    final userId = _userPreferencesStorage.preferences.value.meUserInfo?.id;
    final savedOrder =
        userId == null ? null : _userAccountStorage.friendOrder(userId);
    if (savedOrder == null || savedOrder.isEmpty) return items;

    final positions = <int, int>{
      for (final (index, friendId) in savedOrder.indexed)
        if (int.tryParse(friendId) case final id?) id: index,
    };
    final originalPositions = <FriendHealthData, int>{
      for (final (index, friend) in items.indexed) friend: index,
    };
    final sorted = [...items];
    sorted.sort((a, b) {
      final aPosition = a.userId == null ? null : positions[a.userId!];
      final bPosition = b.userId == null ? null : positions[b.userId!];
      if (aPosition == null && bPosition == null) {
        return originalPositions[a]!.compareTo(originalPositions[b]!);
      }
      if (aPosition == null) return 1;
      if (bPosition == null) return -1;
      return aPosition.compareTo(bPosition);
    });
    return sorted;
  }

  /// Removes deleted friends from the saved order and appends newly added ones.
  Future<void> _syncFriendOrder(List<FriendHealthData> sortedFriends) async {
    final userId = _userPreferencesStorage.preferences.value.meUserInfo?.id;
    if (userId == null) return;

    final currentOrder =
        sortedFriends.map((friend) => friend.userId).whereType<int>().toList();
    final savedOrder = _userAccountStorage.friendOrder(userId);
    final matchesSavedOrder = savedOrder != null &&
        savedOrder.length == currentOrder.length &&
        savedOrder.indexed.every(
          (entry) => entry.$2 == currentOrder[entry.$1].toString(),
        );
    if (matchesSavedOrder) return;

    try {
      await _userAccountStorage.saveFriendOrder(userId, currentOrder);
    } catch (error, stackTrace) {
      AppLogger.e(
        'FriendsController.syncFriendOrder failed',
        error,
        stackTrace,
      );
    }
  }

  Future<void> showEditRemarkDialog(FriendHealthData friend) async {
    await Get.dialog<String>(
      FriendEditRemarkDialog(
        title: l10n.friendsEditRemarkTitle,
        initialValue: friend.remark ?? '',
        hintText: l10n.friendsEditRemarkHint,
        confirmText: l10n.friendsSave,
        maxLength: 6,
        onConfirm: (remark) => _updateRemark(friend, remark),
      ),
      barrierDismissible: true,
    );
  }

  Future<bool> _updateRemark(FriendHealthData friend, String remark) async {
    final userId = friend.userId;
    if (userId == null) return false;
    try {
      await _repository.updateRemark(userId, remark);
      await loadFriends();
      return true;
    } catch (error, stackTrace) {
      if (error is AppError) {
        AppToast.show(error.displayMessage);
      } else {
        AppLogger.e(
          'FriendsController.showEditRemarkDialog failed',
          error,
          stackTrace,
        );
      }
      return false;
    }
  }

  Future<void> applyWatchFaceSelection(FriendHealthData selectedFriend) async {
    final friendUserId = selectedFriend.userId;
    if (friendUserId == null) {
      return;
    }
    try {
      await _repository.selectWatchFaceFriend(friendUserId);
      await loadFriends();
      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,
      );
    }
  }
}