|
...
|
...
|
@@ -11,16 +11,21 @@ import 'package:doublefeel_flutter/app/routes/app_pages.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/network/api/pay_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/health_kit_upload_service.dart';
|
|
|
|
import 'package:doublefeel_flutter/core/services/user_state_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/friend/friend_models.dart';
|
|
|
|
import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart';
|
|
|
|
import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
|
|
|
|
import 'package:doublefeel_flutter/data/models/pay/pay_models.dart';
|
|
|
|
import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart';
|
|
|
|
import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
|
|
|
|
import 'package:doublefeel_flutter/pigeon/wear_engine_api.g.dart';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:get/get.dart';
|
|
|
|
import 'package:intl/intl.dart';
|
|
...
|
...
|
@@ -48,8 +53,9 @@ class HrvAnnotation { |
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
class TodayController extends GetxController {
|
|
|
|
class TodayController extends GetMaterialController {
|
|
|
|
TodayController(
|
|
|
|
this._payApi,
|
|
|
|
this._vipApi,
|
|
|
|
this._healthApi,
|
|
|
|
this._friendApi,
|
|
...
|
...
|
@@ -59,6 +65,7 @@ class TodayController extends GetxController { |
|
|
|
FriendItem? friendInfo, // null = 自己,非 null = 好友
|
|
|
|
}) : _initialFriendInfo = friendInfo;
|
|
|
|
|
|
|
|
final PayApi _payApi;
|
|
|
|
final VipApi _vipApi;
|
|
|
|
final HealthApi _healthApi;
|
|
|
|
final FriendApi _friendApi;
|
|
...
|
...
|
@@ -84,7 +91,7 @@ class TodayController extends GetxController { |
|
|
|
final scrollOffset = 0.0.obs;
|
|
|
|
|
|
|
|
final isLoadingToday = false.obs;
|
|
|
|
final showHealthDataAuthCardStatus = (-1).obs;
|
|
|
|
final showHealthDataAuthCardStatus = 1.obs;
|
|
|
|
final stressSubtitle = 'Hi, 你今日的综合压力状态'.obs;
|
|
|
|
|
|
|
|
// ── HRV 表盘引导 Banner ───────────────────
|
|
...
|
...
|
@@ -93,7 +100,14 @@ class TodayController extends GetxController { |
|
|
|
|
|
|
|
void dismissHrvAdBanner() => showHrvAdBanner.value = false;
|
|
|
|
|
|
|
|
void dismissPartnerAdBanner() => showPartnerAdBanner.value = false;
|
|
|
|
Future<void> dismissPartnerAdBanner() async {
|
|
|
|
final userId =
|
|
|
|
Get.find<UserPreferencesStorage>().preferences.value.meUserInfo?.id ??
|
|
|
|
0;
|
|
|
|
await Get.find<UserAccountStorage>()
|
|
|
|
.saveLastAddFriendBannerShowTime(userId);
|
|
|
|
showPartnerAdBanner.value = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// ── HRV 数字 + 心率 ───────────────────────
|
|
|
|
final avgHrv = '--'.obs;
|
|
...
|
...
|
@@ -106,8 +120,6 @@ class TodayController extends GetxController { |
|
|
|
final activityMoveProgress = 0.0.obs;
|
|
|
|
final activityExerciseProgress = 0.0.obs;
|
|
|
|
final activityStandProgress = 0.0.obs;
|
|
|
|
int? _activityMoveTarget;
|
|
|
|
int? _activityStandTarget;
|
|
|
|
|
|
|
|
// ── HRV 趋势图 ────────────────────────────
|
|
|
|
final hrvChartData = <V2HrvTrendItem>[].obs;
|
|
...
|
...
|
@@ -138,32 +150,50 @@ class TodayController extends GetxController { |
|
|
|
});
|
|
|
|
|
|
|
|
unawaited(loadDataForDate(today));
|
|
|
|
|
|
|
|
checkAddFriendVisible();
|
|
|
|
checkHrvAdBannerVisible();
|
|
|
|
checkHealthDataAuthCardVisible();
|
|
|
|
if (!Get.find<UserStateService>().isVip) {
|
|
|
|
getProductList();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void checkHrvAdBannerVisible() {
|
|
|
|
Future<void> checkHrvAdBannerVisible() async {
|
|
|
|
if (isFriend) {
|
|
|
|
showHrvAdBanner.value = false;
|
|
|
|
} else {
|
|
|
|
showHrvAdBanner.value = true;
|
|
|
|
try {
|
|
|
|
bool hasInstalledWatchSurface =
|
|
|
|
await WearEngineHostApi().hasInstalledWatchSurface();
|
|
|
|
showHrvAdBanner.value = !hasInstalledWatchSurface;
|
|
|
|
} on Exception catch (e) {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void checkAddFriendVisible() {
|
|
|
|
if (isFriend) {
|
|
|
|
showHrvAdBanner.value = false;
|
|
|
|
showPartnerAdBanner.value = false;
|
|
|
|
} else {
|
|
|
|
_refreshFriendList(onFriendListUpdated: () {
|
|
|
|
if (friendsList.isNotEmpty || friendsList.value.length >= 10) {
|
|
|
|
_refreshFriendList(onFriendListUpdated: () {
|
|
|
|
if (friendsList.isNotEmpty ||
|
|
|
|
friendsList.value.length >= friendsListLimit) {
|
|
|
|
showPartnerAdBanner.value = false;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// 关闭后 3 天内不再显示
|
|
|
|
final userId =
|
|
|
|
Get.find<UserPreferencesStorage>().preferences.value.meUserInfo?.id ??
|
|
|
|
0;
|
|
|
|
final lastShowTime =
|
|
|
|
Get.find<UserAccountStorage>().getLastAddFriendBannerShowTime(userId);
|
|
|
|
if (lastShowTime != null) {
|
|
|
|
final diff = DateTime.now()
|
|
|
|
.difference(DateTime.fromMillisecondsSinceEpoch(lastShowTime));
|
|
|
|
if (diff.inDays < 3) {
|
|
|
|
showPartnerAdBanner.value = false;
|
|
|
|
} else {
|
|
|
|
showPartnerAdBanner.value = true;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
showPartnerAdBanner.value = true;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
void changeDate(DateTime date, {DateTime? focused}) {
|
|
...
|
...
|
@@ -251,8 +281,17 @@ class TodayController extends GetxController { |
|
|
|
|
|
|
|
Future<void> checkHealthDataAuthCardVisible() async {
|
|
|
|
try {
|
|
|
|
final hasUploaded =
|
|
|
|
switch (await _healthApi.getHealthDataEverUploaded()) {
|
|
|
|
AppSuccess(:final data) => data,
|
|
|
|
AppFailure() => false,
|
|
|
|
};
|
|
|
|
final result = await _hostApi.checkHealthAppAuthorization();
|
|
|
|
|
|
|
|
if (!hasUploaded) {
|
|
|
|
showHealthDataAuthCardStatus.value = 0;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
showHealthDataAuthCardStatus.value = result.status;
|
|
|
|
// TODO:若用户从始至终没有任何数据,也显示该引导
|
|
|
|
if (result.status == 1) {
|
|
...
|
...
|
@@ -276,13 +315,13 @@ class TodayController extends GetxController { |
|
|
|
|
|
|
|
switch (await _healthApi.getV2StressScore(friendUserId, intDate)) {
|
|
|
|
case AppSuccess(:final data):
|
|
|
|
// v2StressScore.value = data;
|
|
|
|
v2StressScore.value = data;
|
|
|
|
|
|
|
|
var nextInt = Random().nextInt(4);
|
|
|
|
v2StressScore.value = V2StressScore(
|
|
|
|
state: nextInt,
|
|
|
|
comprehensiveScore: 100 - (nextInt * 25),
|
|
|
|
);
|
|
|
|
// var nextInt = Random().nextInt(4);
|
|
|
|
// v2StressScore.value = V2StressScore(
|
|
|
|
// state: nextInt,
|
|
|
|
// comprehensiveScore: 100 - (nextInt * 25),
|
|
|
|
// );
|
|
|
|
case AppFailure():
|
|
|
|
v2StressScore.value = null;
|
|
|
|
}
|
|
...
|
...
|
@@ -296,66 +335,67 @@ class TodayController extends GetxController { |
|
|
|
|
|
|
|
switch (await _healthApi.getV2HrvTrend(friendUserId, intDate)) {
|
|
|
|
case AppSuccess(:final data):
|
|
|
|
// hrvChartData.assignAll(data.list ?? []);
|
|
|
|
hrvChartData.assignAll([
|
|
|
|
// 每小时一条,00:00 ~ 15:00(CST 午夜 = 1782057600)
|
|
|
|
V2HrvTrendItem(time: 1782057600, trendHrv: 45, state: 3), // 00:00
|
|
|
|
V2HrvTrendItem(time: 1782061200, trendHrv: 42, state: 3), // 01:00
|
|
|
|
V2HrvTrendItem(time: 1782064800, trendHrv: 38, state: 3), // 02:00
|
|
|
|
V2HrvTrendItem(time: 1782068400, trendHrv: 35, state: 2), // 03:00
|
|
|
|
V2HrvTrendItem(time: 1782072000, trendHrv: 40, state: 3), // 04:00
|
|
|
|
V2HrvTrendItem(time: 1782075600, trendHrv: 55, state: 4), // 05:00
|
|
|
|
V2HrvTrendItem(time: 1782079200, trendHrv: 65, state: 4), // 06:00
|
|
|
|
V2HrvTrendItem(time: 1782082800, trendHrv: 72, state: 4), // 07:00
|
|
|
|
V2HrvTrendItem(time: 1782086400, trendHrv: 68, state: 4), // 08:00
|
|
|
|
V2HrvTrendItem(time: 1782090000, trendHrv: 58, state: 4), // 09:00
|
|
|
|
V2HrvTrendItem(time: 1782093600, trendHrv: 52, state: 3), // 10:00
|
|
|
|
V2HrvTrendItem(time: 1782097200, trendHrv: 48, state: 3), // 11:00
|
|
|
|
V2HrvTrendItem(time: 1782100800, trendHrv: 44, state: 3), // 12:00
|
|
|
|
V2HrvTrendItem(time: 1782104400, trendHrv: 38, state: 2), // 13:00
|
|
|
|
V2HrvTrendItem(time: 1782108000, trendHrv: 42, state: 3), // 14:00
|
|
|
|
V2HrvTrendItem(time: 1782111600, trendHrv: 45, state: 3), // 15:00
|
|
|
|
]);
|
|
|
|
hrvChartData.assignAll(data.list ?? []);
|
|
|
|
// hrvChartData.assignAll([
|
|
|
|
// // 每小时一条,00:00 ~ 15:00(CST 午夜 = 1782057600)
|
|
|
|
// V2HrvTrendItem(time: 1782057600, trendHrv: 45, state: 3), // 00:00
|
|
|
|
// V2HrvTrendItem(time: 1782061200, trendHrv: 42, state: 3), // 01:00
|
|
|
|
// V2HrvTrendItem(time: 1782064800, trendHrv: 38, state: 3), // 02:00
|
|
|
|
// V2HrvTrendItem(time: 1782068400, trendHrv: 35, state: 2), // 03:00
|
|
|
|
// V2HrvTrendItem(time: 1782072000, trendHrv: 40, state: 3), // 04:00
|
|
|
|
// V2HrvTrendItem(time: 1782075600, trendHrv: 55, state: 4), // 05:00
|
|
|
|
// V2HrvTrendItem(time: 1782079200, trendHrv: 65, state: 4), // 06:00
|
|
|
|
// V2HrvTrendItem(time: 1782082800, trendHrv: 72, state: 4), // 07:00
|
|
|
|
// V2HrvTrendItem(time: 1782086400, trendHrv: 68, state: 4), // 08:00
|
|
|
|
// V2HrvTrendItem(time: 1782090000, trendHrv: 58, state: 4), // 09:00
|
|
|
|
// V2HrvTrendItem(time: 1782093600, trendHrv: 52, state: 3), // 10:00
|
|
|
|
// V2HrvTrendItem(time: 1782097200, trendHrv: 48, state: 3), // 11:00
|
|
|
|
// V2HrvTrendItem(time: 1782100800, trendHrv: 44, state: 3), // 12:00
|
|
|
|
// V2HrvTrendItem(time: 1782104400, trendHrv: 38, state: 2), // 13:00
|
|
|
|
// V2HrvTrendItem(time: 1782108000, trendHrv: 42, state: 3), // 14:00
|
|
|
|
// V2HrvTrendItem(time: 1782111600, trendHrv: 45, state: 3), // 15:00
|
|
|
|
// ]);
|
|
|
|
case AppFailure():
|
|
|
|
hrvChartData.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (await _healthApi.getV2RealtimeStress(friendUserId, intDate)) {
|
|
|
|
case AppSuccess(:final data):
|
|
|
|
// stressChartData.assignAll(data.list ?? []);
|
|
|
|
stressChartData.assignAll(
|
|
|
|
// 每 6 分钟一条,00:00 ~ 15:00(共 150 条)
|
|
|
|
// midnight CST = 1782057600,步长 360s
|
|
|
|
List.generate(150, (i) {
|
|
|
|
const midnight = 1782057600;
|
|
|
|
final ts = midnight + i * 360;
|
|
|
|
final minutesFromMidnight = i * 6;
|
|
|
|
final hour = minutesFromMidnight ~/ 60;
|
|
|
|
final minInHour = minutesFromMidnight % 60;
|
|
|
|
// 用正弦波模拟真实波动,早高峰(7-9点)压力最高
|
|
|
|
final base = hour < 6
|
|
|
|
? 25.0 // 深夜低压力
|
|
|
|
: hour < 9
|
|
|
|
? 55.0 + (hour - 6) * 10.0 // 早高峰爬升
|
|
|
|
: hour < 12
|
|
|
|
? 75.0 - (hour - 9) * 8.0 // 上午下降
|
|
|
|
: 50.0 - (hour - 12) * 3.0; // 下午缓降
|
|
|
|
// 叠加微小波动(用 i 模拟随机)
|
|
|
|
final jitter = (i % 7 - 3) * 2.0 + (minInHour % 3) * 1.5;
|
|
|
|
final value = (base + jitter).clamp(10.0, 100.0).round();
|
|
|
|
final int state;
|
|
|
|
if (value >= 75) {
|
|
|
|
state = 1;
|
|
|
|
} else if (value >= 55) {
|
|
|
|
state = 2;
|
|
|
|
} else if (value >= 35) {
|
|
|
|
state = 3;
|
|
|
|
} else {
|
|
|
|
state = 4;
|
|
|
|
}
|
|
|
|
return V2RealtimeStressItem(time: ts, value: value, state: state);
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
stressChartData.assignAll(data.list ?? []);
|
|
|
|
// stressChartData.assignAll(
|
|
|
|
// // 每 6 分钟一条,00:00 ~ 15:00(共 150 条)
|
|
|
|
// // midnight CST = 1782057600,步长 360s
|
|
|
|
// List.generate(150, (i) {
|
|
|
|
// const midnight = 1782057600;
|
|
|
|
// final ts = midnight + i * 360;
|
|
|
|
// final minutesFromMidnight = i * 6;
|
|
|
|
// final hour = minutesFromMidnight ~/ 60;
|
|
|
|
// final minInHour = minutesFromMidnight % 60;
|
|
|
|
// // 用正弦波模拟真实波动,早高峰(7-9点)压力最高
|
|
|
|
// final base = hour < 6
|
|
|
|
// ? 25.0 // 深夜低压力
|
|
|
|
// : hour < 9
|
|
|
|
// ? 55.0 + (hour - 6) * 10.0 // 早高峰爬升
|
|
|
|
// : hour < 12
|
|
|
|
// ? 75.0 - (hour - 9) * 8.0 // 上午下降
|
|
|
|
// : 50.0 - (hour - 12) * 3.0; // 下午缓降
|
|
|
|
// // 叠加微小波动(用 i 模拟随机)
|
|
|
|
// final jitter = (i % 7 - 3) * 2.0 + (minInHour % 3) * 1.5;
|
|
|
|
// final value = (base + jitter).clamp(10.0, 100.0).round();
|
|
|
|
// final int state;
|
|
|
|
// if (value >= 75) {
|
|
|
|
// state = 1;
|
|
|
|
// } else if (value >= 55) {
|
|
|
|
// state = 2;
|
|
|
|
// } else if (value >= 35) {
|
|
|
|
// state = 3;
|
|
|
|
// } else {
|
|
|
|
// state = 4;
|
|
|
|
// }
|
|
|
|
// return V2RealtimeStressItem(time: ts, value: value, state: state);
|
|
|
|
// }),
|
|
|
|
// );
|
|
|
|
|
|
|
|
case AppFailure():
|
|
|
|
stressChartData.clear();
|
|
|
|
}
|
|
...
|
...
|
@@ -459,6 +499,7 @@ class TodayController extends GetxController { |
|
|
|
switch (res) {
|
|
|
|
case AppSuccess(:final data):
|
|
|
|
friendsList.assignAll(data.list);
|
|
|
|
friendsListLimit = data.limit ?? 10;
|
|
|
|
onFriendListUpdated?.call();
|
|
|
|
case AppFailure():
|
|
|
|
}
|
|
...
|
...
|
@@ -467,6 +508,7 @@ class TodayController extends GetxController { |
|
|
|
}
|
|
|
|
|
|
|
|
RxList<FriendItem> friendsList = RxList.empty();
|
|
|
|
int friendsListLimit = 10;
|
|
|
|
|
|
|
|
Future<void> toPremiumPage() async {
|
|
|
|
await Get.toNamed(Routes.PURCHASE);
|
|
...
|
...
|
@@ -478,4 +520,88 @@ class TodayController extends GetxController { |
|
|
|
} on Exception catch (e) {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> toPremiumDiscoutPage() async {
|
|
|
|
await Get.toNamed(Routes.MEMBERSHIP_OFFER);
|
|
|
|
final vipResult = await _vipApi.getVipInfo();
|
|
|
|
if (vipResult case AppSuccess(data: final vip)) {
|
|
|
|
try {
|
|
|
|
final vipPrefs = UserPreferencesVipInfo.fromVipInfo(vip);
|
|
|
|
await Get.find<UserPreferencesStorage>().updateVipInfo(vipPrefs);
|
|
|
|
} on Exception catch (e) {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
final yearlyProduct = Rxn<PayProduct>();
|
|
|
|
final yearlyProductAppleInfo = Rxn<AppleProductInfo>();
|
|
|
|
final PlatformHostApi _platformHostApi = PlatformHostApi();
|
|
|
|
|
|
|
|
Future<void> getProductList() async {
|
|
|
|
final result = await _payApi.getProductList(10);
|
|
|
|
if (result is! AppSuccess<PayProductListResponse>) return;
|
|
|
|
|
|
|
|
final products = (result.data.productList ?? const <PayProduct>[])
|
|
|
|
.where((product) => _nonEmpty(product.appleId) != null)
|
|
|
|
.toList();
|
|
|
|
yearlyProduct.value = products
|
|
|
|
// .firstWhereOrNull((p) => p.appleId == 'com.doublefeel.yearly_v1');
|
|
|
|
.firstWhereOrNull((p) => p.appleId == 'com.doublefeel.yearly');
|
|
|
|
AppLogger.e(yearlyProduct);
|
|
|
|
if (yearlyProduct.value != null) {
|
|
|
|
final appleProductId = _nonEmpty(yearlyProduct.value!.appleId);
|
|
|
|
if (appleProductId == null) return;
|
|
|
|
|
|
|
|
yearlyProductAppleInfo.value = await _requestAppleProductInfo(
|
|
|
|
productId: appleProductId,
|
|
|
|
baseUnit: yearlyProduct.value!.content?.baseUnit() ?? 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<AppleProductInfo?> _requestAppleProductInfo({
|
|
|
|
required String productId,
|
|
|
|
required int baseUnit,
|
|
|
|
}) async {
|
|
|
|
try {
|
|
|
|
return await _platformHostApi.requestAppleProductInfo(
|
|
|
|
productId,
|
|
|
|
baseUnit,
|
|
|
|
);
|
|
|
|
} catch (_) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
String? _nonEmpty(String? value) {
|
|
|
|
final trimmed = value?.trim();
|
|
|
|
if (trimmed == null || trimmed.isEmpty) return null;
|
|
|
|
return trimmed;
|
|
|
|
}
|
|
|
|
|
|
|
|
String originDisplayPrice() {
|
|
|
|
final appleInfo = yearlyProductAppleInfo.value;
|
|
|
|
if (appleInfo == null) return '';
|
|
|
|
final actual =
|
|
|
|
appleInfo.price > 0 ? appleInfo.price : appleInfo.originPrice;
|
|
|
|
final inflated = actual / 100.0 * 1.2;
|
|
|
|
// 保留两位小数,单位与 currencyCode 一致
|
|
|
|
return '${appleInfo.currencyCode}${inflated.toStringAsFixed(2)}';
|
|
|
|
}
|
|
|
|
|
|
|
|
String displayPrice() {
|
|
|
|
final appleInfo = yearlyProductAppleInfo.value;
|
|
|
|
if (appleInfo == null) return '';
|
|
|
|
final price = appleInfo.price;
|
|
|
|
if (price > 0) {
|
|
|
|
final priceDescription = appleInfo.priceDescription.trim();
|
|
|
|
if (priceDescription.isNotEmpty) {
|
|
|
|
return priceDescription;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return appleInfo.originPriceDescription;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
|
|
super.didChangeAppLifecycleState(state);
|
|
|
|
}
|
|
|
|
} |
...
|
...
|
|