feat(remote): 修复键盘输入映射并重构连接流程

- Android端注入按键时显式设置SOURCE_KEYBOARD,修复IME丢弃事件导致输入框无法输入文字的问题
- Flutter端新增Flutter逻辑键到Android keyCode的转换映射,避免功能键误触发
- 重构RemoteController,分离信令连接与远程控制逻辑,支持设备列表选择与配对码兑换
- AuthRepository接口新增getBindings、getOnlineDevices、redeemPairingCode方法
- DioAuthRepository实现401自动刷新token重试机制
- ConnectionSessionState扩展设备列表、在线状态、配对码等状态字段
This commit is contained in:
TongTongStudio
2026-08-03 20:22:29 +08:00
parent 79cffcb041
commit e1de4c8170
21 changed files with 1529 additions and 703 deletions

View File

@@ -6,6 +6,7 @@ import '../../../app/constants/app_constants.dart';
import '../../../core/network/api_exception.dart';
import '../../../core/storage/token_storage.dart';
import '../domain/auth_repository.dart';
import '../../connection/domain/binding_device.dart';
/// 基于 Dio 的 [AuthRepository] 实现。
class DioAuthRepository implements AuthRepository {
@@ -101,13 +102,41 @@ class DioAuthRepository implements AuthRepository {
Future<Map<String, dynamic>> verify() => _get('/api/client/verify');
@override
Future<Map<String, dynamic>> bindings() => _get('/api/client/bindings');
Future<List<BindingDevice>> getBindings() async {
final data = await _get('/api/client/bindings');
final list = (data['bindings'] as List?) ?? [];
return [
for (final e in list)
if (e is Map)
BindingDevice.fromJson(e.cast<String, dynamic>()),
];
}
@override
Future<List<BindingDevice>> getOnlineDevices() async {
final data = await _get('/api/client/devices/online');
final list = (data['devices'] as List?) ?? [];
return [
for (final e in list)
if (e is Map)
BindingDevice.fromJson(e.cast<String, dynamic>()),
];
}
@override
Future<void> redeemPairingCode(String code) async {
await _post(
'/api/client/pairing/redeem',
body: {'code': code},
auth: true,
);
}
@override
Future<Map<String, dynamic>?> turnCredentials() async {
try {
return await _get('/api/client/turn-credentials');
} on ApiException {
} on Object {
return null;
}
}
@@ -121,24 +150,61 @@ class DioAuthRepository implements AuthRepository {
Future<Map<String, dynamic>> _post(
String path, {
required Map<String, dynamic> body,
bool auth = false,
}) async {
final res = await _dio.post<dynamic>(
path,
data: jsonEncode(body),
return _handle(
await _send(
() => _dio.post<dynamic>(
path,
data: jsonEncode(body),
options: Options(
headers: {
if (auth && _accessToken != null)
'Authorization': 'Bearer $_accessToken',
},
),
),
),
);
return _handle(res);
}
Future<Map<String, dynamic>> _get(String path) async {
final res = await _dio.get<dynamic>(
path,
options: Options(
headers: {
if (_accessToken != null) 'Authorization': 'Bearer $_accessToken',
},
return _handle(
await _send(
() => _dio.get<dynamic>(
path,
options: Options(
headers: {
if (_accessToken != null)
'Authorization': 'Bearer $_accessToken',
},
),
),
),
);
return _handle(res);
}
/// 发送请求,遇 401 时尝试用 refreshToken 续期后重试一次。
///
/// Dio 默认对非 2xx 直接抛出 [DioException]bad response不会走到
/// [_handle],因此在异常层拦截 401 并触发刷新重试,避免已登录用户因
/// accessToken 过期而直接失败。
Future<Response<dynamic>> _send(
Future<Response<dynamic>> Function() request,
) async {
try {
return await request();
} on DioException catch (e) {
if (e.response?.statusCode == 401 && await hasRefreshToken()) {
try {
await refresh();
} on Object {
rethrow;
}
return await request();
}
rethrow;
}
}
Map<String, dynamic> _handle(Response<dynamic> res) {

View File

@@ -1,3 +1,5 @@
import '../../connection/domain/binding_device.dart';
/// 账号体系与自助接口抽象(对应服务端 /api/auth/* 与 /api/client/*)。
abstract interface class AuthRepository {
/// 内存中的 accessToken掉线即失
@@ -19,7 +21,14 @@ abstract interface class AuthRepository {
Future<Map<String, dynamic>> verify();
Future<Map<String, dynamic>> bindings();
/// 获取主控端已绑定的被控设备列表(含名称、鉴权方式等)。
Future<List<BindingDevice>> getBindings();
/// 获取已绑定且当前在线(信令网络活跃)的设备列表。
Future<List<BindingDevice>> getOnlineDevices();
/// 使用被控端展示的一次性配对码建立绑定关系。
Future<void> redeemPairingCode(String code);
/// 拉取 TURN 短期凭证(服务端开启时返回 iceServers关闭时返回 null。
Future<Map<String, dynamic>?> turnCredentials();