friends_controller.dart 10.2 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/core/platform/pigeon_api_facade.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) => AppPlatformHostApi().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;
  bool _isUploadingFriendOrder = false;
  _PendingFriendOrder? _pendingFriendOrder;
  int _friendListVersion = 0;
  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);
      _friendListVersion++;
      friends.assignAll(data.friends);
      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 uploads the complete order to the server.
  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 listVersion = ++_friendListVersion;

    _pendingFriendOrder = _PendingFriendOrder(
      friends: List<FriendHealthData>.from(friends),
      listVersion: listVersion,
    );
    await _flushPendingFriendOrder();
  }

  /// Uploads the latest pending order. New drags replace a queued order.
  Future<void> _flushPendingFriendOrder() async {
    if (_isUploadingFriendOrder) return;

    _isUploadingFriendOrder = true;
    try {
      while (_pendingFriendOrder != null) {
        final pendingOrder = _pendingFriendOrder!;
        _pendingFriendOrder = null;
        try {
          await _uploadFriendOrder(pendingOrder.friends);
          if (_pendingFriendOrder == null &&
              _friendListVersion != pendingOrder.listVersion) {
            unawaited(refreshData());
          }
          ta.track('ita_doublefeel_friend_page',
              properties: {'ita_content': '长按拖动卡片'});
        } catch (error, stackTrace) {
          AppLogger.e(
            'FriendsController.reorderFriends failed',
            error,
            stackTrace,
          );
          if (_pendingFriendOrder == null &&
              _friendListVersion == pendingOrder.listVersion) {
            unawaited(refreshData());
          }
        }
      }
    } finally {
      _isUploadingFriendOrder = false;
      if (_pendingFriendOrder != null) {
        unawaited(_flushPendingFriendOrder());
      }
    }
  }

  Future<void> _uploadFriendOrder(List<FriendHealthData> orderedFriends) {
    final friendUserIds =
        orderedFriends.map((friend) => friend.userId).whereType<int>().toList();
    if (friendUserIds.length != orderedFriends.length) {
      return Future.error(
        StateError('Cannot upload a friend order containing a null user ID.'),
      );
    }
    return _repository.sortFriends(friendUserIds);
  }

  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,
        ),
      );
      ta.track('success_set_doublefeel_friend_on_watch');
    } 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 AppPlatformHostApi().refreshWatchAppAndWidgets();
    } catch (error, stackTrace) {
      AppLogger.e(
        'FriendsController.refreshWatchAppAndWidgetsAfterFriendDeleted failed',
        error,
        stackTrace,
      );
    }
  }
}

class _PendingFriendOrder {
  const _PendingFriendOrder({
    required this.friends,
    required this.listVersion,
  });

  final List<FriendHealthData> friends;
  final int listVersion;
}