Files
ttstd_family_care/lib/features/auth/presentation/auth_controller.dart
TongTongStudio bfb106f1fb feat(connection): 增加远程视频录制、分辨率/帧率切换及高刷适配
- 新增 VideoRecorder 基于 flutter_webrtc MediaRecorder 录制远程视频为 MP4
- 新增 ControlMessageDecoder 解析被控端上报的 REPORT_* protobuf 消息
- 连接控制器支持分辨率/帧率切换、录制状态管理与远程上报处理
- 远程控制页新增更多菜单(静音/分辨率/帧率/录制),并统一退出时断开连接
- WebRTC 编排器在 DataChannel 打开及媒体就绪后主动请求默认分辨率,解决黑屏问题
- 新增 DisplayModeUtil 在 Android 上启用最高刷新率
- 登出时失效设备相关 Provider,避免切换账号后读到旧缓存
2026-08-27 11:39:42 +08:00

189 lines
5.6 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/network/api_exception.dart';
import '../../../core/network/error_message.dart';
import '../../../core/storage/token_storage.dart';
import '../../device/data/device_providers.dart';
import '../data/auth_providers.dart';
import '../domain/auth_models.dart';
import '../domain/auth_state.dart';
/// 认证控制器。
///
/// 承载登录 / 注册 / 发送验证码 / 倒计时等全部业务逻辑UI 仅触发动作并消费状态。
/// 使用 keepAlive避免登录流程中页面切换导致状态丢失。
class AuthController extends Notifier<AuthState> {
AuthController();
Timer? _countdownTimer;
@override
AuthState build() {
ref.keepAlive();
ref.onDispose(() => _countdownTimer?.cancel());
return const AuthState();
}
String get _deviceId => TokenStorage.deviceId;
/// 密码登录。
Future<bool> loginByPassword({
required String phone,
required String password,
}) async {
return _run(() => ref.read(authRepositoryProvider).loginByPassword(
phone: phone,
password: password,
deviceId: _deviceId,
));
}
/// 短信登录。
Future<bool> loginBySms({
required String phone,
required String code,
}) async {
return _run(() => ref.read(authRepositoryProvider).loginBySms(
phone: phone,
code: code,
deviceId: _deviceId,
));
}
/// 注册。
Future<bool> register({
required String phone,
required String code,
required String password,
}) async {
return _run(() => ref.read(authRepositoryProvider).register(
phone: phone,
code: code,
password: password,
deviceId: _deviceId,
));
}
/// 发送短信验证码并启动 60s 倒计时。
///
/// 发送前先做本地校验:手机号为空或格式不正确时直接提示,不发起请求、
/// 不启动倒计时,避免出现「空号也能点但弹报错」的体验问题。
Future<void> sendSmsCode({
required String phone,
required SmsScene scene,
}) async {
if (state.isSendingCode || state.countdownSeconds > 0) return;
final trimmed = phone.trim();
final phoneError = _validatePhone(trimmed);
if (phoneError != null) {
state = state.copyWith(alert: phoneError);
return;
}
state = state.copyWith(isSendingCode: true, clearAlert: true);
try {
await ref.read(authRepositoryProvider).sendSmsCode(
phone: trimmed,
scene: scene,
);
_startCountdown();
} catch (e) {
state = state.copyWith(
isSendingCode: false,
alert: userMessageOf(e),
);
}
}
/// 校验手机号:空或不符合 11 位中国大陆手机号规则时返回提示文案。
String? _validatePhone(String phone) {
if (phone.isEmpty) return '请输入手机号';
// 1 开头的 11 位手机号。
if (!RegExp(r'^1\d{10}$').hasMatch(phone)) return '手机号格式不正确';
return null;
}
/// 重置密码(忘记密码流程)。
Future<bool> resetPassword({
required String phone,
required String code,
required String password,
}) async {
state = state.copyWith(isLoading: true, clearAlert: true);
try {
await ref.read(authRepositoryProvider).resetPassword(
phone: phone,
code: code,
password: password,
);
state = state.copyWith(isLoading: false);
return true;
} catch (e) {
state = state.copyWith(isLoading: false, alert: userMessageOf(e));
return false;
}
}
/// 消费一次性提示。
void consumeAlert() {
if (state.alert != null) {
state = state.copyWith(clearAlert: true);
}
}
/// 登出。
///
/// 除清除令牌与登录态外,还需失效当前账号的已绑定设备列表及由其推导的
/// 选中设备 SN 等 Provider否则切换账号登录后仍会读到上一账号缓存的设备/
/// SN 数据(`myDevicesProvider` / `selectedDeviceSnProvider` 等持有旧值)。
void logout() {
_countdownTimer?.cancel();
TokenStorage.clear();
state = const AuthState();
// 失效当前账号相关的设备与选中 SN 缓存,避免下一账号登录后沿用旧 SN。
ref.invalidate(myDevicesProvider);
}
Future<bool> _run(Future<LoginResult> Function() action) async {
state = state.copyWith(isLoading: true, clearAlert: true);
try {
await action();
state = state.copyWith(isLoading: false);
return true;
} catch (e) {
state = state.copyWith(isLoading: false, alert: userMessageOf(e));
// 鉴权失效:本地令牌可能已作废,主动登出清理。
if (_isUnauthorized(e)) {
logout();
}
return false;
}
}
/// 判断异常是否为鉴权失效401
bool _isUnauthorized(Object e) {
if (e is ApiException) return e.code == '401';
return false;
}
void _startCountdown() {
state = state.copyWith(isSendingCode: false, countdownSeconds: 60);
_countdownTimer?.cancel();
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
final left = state.countdownSeconds - 1;
if (left <= 0) {
timer.cancel();
state = state.copyWith(countdownSeconds: 0);
} else {
state = state.copyWith(countdownSeconds: left);
}
});
}
}
/// 认证控制器 Provider会话级 keepAlive 在 build 内通过 ref.keepAlive 实现)。
final authControllerProvider =
NotifierProvider<AuthController, AuthState>(AuthController.new);