bb39b36d84
- account_providers:新增 meProvider(GET /v1/me,登录态变化自动重取)、 plansProvider、usageProvider(family by days)。 - isFreePlanProvider 改为派生自 meProvider.plan(去掉可写演示开关)。 - quotaProvider:总额取 plans free.daily_minutes,今日剩余取 me.quota_today_min (后端已算好);adUnlocked 保留本地态(ad SDK 未接)。 - account_page:邮箱/套餐/到期接 me;删「演示:免费版视角」开关 + kDemoEmail; 协议标签 WireGuard→REALITY/Hysteria2。 - 重写 quota_controller_test 为 provider 驱动;account/quota golden 重生成。 flutter analyze 0 error;113 tests passed。统计页(weekly/月流量/延迟)随 P4 一并接(其均延迟依赖 P4 实测探针)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
73 lines
2.5 KiB
Dart
73 lines
2.5 KiB
Dart
// quota_provider.dart — 免费版每日额度状态(Riverpod)
|
||
//
|
||
// 设计约定(design/CLAUDE.md §7 / §2):免费额度权威在服务端。
|
||
// 总额度取自 plans 的 free.daily_minutes,今日剩余取自 me.quota_today_min
|
||
// (后端已算好 = 上限 − 今日已用)。adUnlocked 为本地会话态(看广告需 ad SDK,
|
||
// 尚未接入,保留本地乐观置位)。
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
|
||
import 'account_providers.dart';
|
||
|
||
/// 免费额度快照。
|
||
class FreeQuotaState {
|
||
const FreeQuotaState({
|
||
this.totalMinutes = 10,
|
||
this.remainingMinutes = 10,
|
||
this.adUnlocked = false,
|
||
});
|
||
|
||
/// 每日总额度(分钟)。§7:免费版每日 10 分钟。
|
||
final int totalMinutes;
|
||
|
||
/// 今日剩余分钟(展示值,权威以服务端为准)。
|
||
final int remainingMinutes;
|
||
|
||
/// 今日是否已观看激励视频解锁。
|
||
final bool adUnlocked;
|
||
|
||
/// 进度(0–1),用于进度条宽度。
|
||
double get progress =>
|
||
totalMinutes == 0 ? 0 : (remainingMinutes / totalMinutes).clamp(0.0, 1.0);
|
||
|
||
/// 是否进入低额度警示(≤3 分钟切 warning 色)。
|
||
bool get isLow => remainingMinutes <= 3;
|
||
|
||
FreeQuotaState copyWith({int? totalMinutes, int? remainingMinutes, bool? adUnlocked}) =>
|
||
FreeQuotaState(
|
||
totalMinutes: totalMinutes ?? this.totalMinutes,
|
||
remainingMinutes: remainingMinutes ?? this.remainingMinutes,
|
||
adUnlocked: adUnlocked ?? this.adUnlocked,
|
||
);
|
||
}
|
||
|
||
class QuotaController extends StateNotifier<FreeQuotaState> {
|
||
QuotaController(this._ref) : super(const FreeQuotaState()) {
|
||
_sync();
|
||
// me / plans 任一就绪或变化都重算。
|
||
_ref.listen(meProvider, (_, __) => _sync());
|
||
_ref.listen(plansProvider, (_, __) => _sync());
|
||
}
|
||
|
||
final Ref _ref;
|
||
|
||
void _sync() {
|
||
final me = _ref.read(meProvider).valueOrNull;
|
||
final plans = _ref.read(plansProvider).valueOrNull;
|
||
var total = 10; // §7 默认免费 10 分钟,plans 就绪后以其为准
|
||
if (plans != null) {
|
||
for (final p in plans) {
|
||
if (p.code == 'free' && p.dailyMinutes != null) total = p.dailyMinutes!;
|
||
}
|
||
}
|
||
final remaining = (me?.quotaTodayMin ?? total).clamp(0, total);
|
||
state = state.copyWith(totalMinutes: total, remainingMinutes: remaining);
|
||
}
|
||
|
||
/// 观看激励视频后解锁今日使用(本地乐观;真实 ad 校验待 ad SDK 接入)。
|
||
void watchAd() => state = state.copyWith(adUnlocked: true);
|
||
}
|
||
|
||
final quotaProvider = StateNotifierProvider<QuotaController, FreeQuotaState>(
|
||
(ref) => QuotaController(ref),
|
||
);
|