feat: 多模块功能更新 - 文件传输/多语言/NFC/首页组件/进度美化等
- 文件传输: 设备发现、LAN发现服务优化 - NFC分享: provider和service增强 - 多语言: 16种语言翻译补全 - 首页: 句子详情面板、收藏页、离线页优化 - 我的: 成就、个人资料、签到、设置页面更新 - 新增: AR视图、进度美化页、Hive安全访问、鸿蒙兼容助手、共享组件 - iOS Widget: Intents扩展、XianyanWidget更新 - 鸿蒙: 6个卡片页面更新 - 其他: 路由注册、缓存配置、崩溃监控、TTS播放器等
This commit is contained in:
@@ -23,6 +23,7 @@ import 'package:xianyan/features/file_transfer/presentation/widgets/airdrop_disc
|
||||
import 'package:xianyan/features/file_transfer/presentation/widgets/recent_messages_section.dart';
|
||||
import 'package:xianyan/features/file_transfer/presentation/pages/transfer_chat_page.dart';
|
||||
import 'package:xianyan/features/file_transfer/presentation/pages/device_pairing_page.dart';
|
||||
import 'package:xianyan/features/file_transfer/presentation/pages/qr_code_tab.dart';
|
||||
import 'package:xianyan/shared/widgets/feedback/app_toast.dart';
|
||||
import 'package:xianyan/features/file_transfer/presentation/pages/file_transfer_device_actions.dart';
|
||||
|
||||
@@ -201,6 +202,22 @@ mixin FileTransferDiscoveryTab<T extends ConsumerStatefulWidget>
|
||||
size: 20,
|
||||
color: ext.accent.withValues(alpha: 0.6),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
GestureDetector(
|
||||
onTap: () => _navigateToQrScan(ext),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.accent.withValues(alpha: 0.1),
|
||||
borderRadius: AppRadius.smBorder,
|
||||
),
|
||||
child: Icon(
|
||||
CupertinoIcons.viewfinder,
|
||||
size: 18,
|
||||
color: ext.accent,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -594,5 +611,13 @@ mixin FileTransferDiscoveryTab<T extends ConsumerStatefulWidget>
|
||||
).push(CupertinoPageRoute<void>(builder: (_) => const DevicePairingPage()));
|
||||
}
|
||||
|
||||
void _navigateToQrScan(AppThemeExtension ext) {
|
||||
Navigator.of(context).push(
|
||||
CupertinoPageRoute<void>(
|
||||
builder: (_) => const QrCodeTab(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void showDeviceContextMenu(TransferDevice device, bool paired);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// ============================================================
|
||||
// 闲言APP — 设备发现状态管理
|
||||
// 创建时间: 2026-05-09
|
||||
/// 更新时间: 2026-05-30
|
||||
/// 更新时间: 2026-06-04
|
||||
/// 作用: 设备发现+配对方式选择+扫描状态+平台能力检测
|
||||
/// 上次更新: 使用SafeNotifierInit统一异常保护
|
||||
/// 上次更新: 增强设备去重( deviceId+IP ) + 心跳超时清理(30s)
|
||||
// ============================================================
|
||||
|
||||
import 'dart:async';
|
||||
@@ -105,7 +105,21 @@ class DeviceDiscoveryNotifier extends Notifier<DeviceDiscoveryState>
|
||||
StreamSubscription<TransferDevice?>? _nfcSub;
|
||||
StreamSubscription<List<TransferDevice>>? _usbSub;
|
||||
|
||||
/// 设备缓存: key = "deviceId|ip" 组合键,确保同一设备从不同IP广播时不重复
|
||||
final Map<String, TransferDevice> _allDevices = {};
|
||||
|
||||
// ============================================================
|
||||
// 心跳超时清理配置
|
||||
// ============================================================
|
||||
|
||||
/// 超时阈值: 30秒未收到广播的设备视为离线
|
||||
static const Duration _offlineTimeout = Duration(seconds: 30);
|
||||
|
||||
/// 清理检查间隔: 每10秒检查一次
|
||||
static const Duration _cleanupInterval = Duration(seconds: 10);
|
||||
|
||||
Timer? _heartbeatCleanupTimer;
|
||||
|
||||
bool _isDisposed = false;
|
||||
|
||||
Future<void> _init() async {
|
||||
@@ -181,6 +195,9 @@ class DeviceDiscoveryNotifier extends Notifier<DeviceDiscoveryState>
|
||||
state = state.copyWith(isScanning: true, activeMethods: methods);
|
||||
_allDevices.clear();
|
||||
|
||||
// 启动心跳超时清理定时器
|
||||
_startHeartbeatCleanup();
|
||||
|
||||
final futures = <Future<void>>[];
|
||||
|
||||
for (final method in methods) {
|
||||
@@ -234,6 +251,47 @@ class DeviceDiscoveryNotifier extends Notifier<DeviceDiscoveryState>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 心跳超时清理逻辑
|
||||
// ============================================================
|
||||
|
||||
void _startHeartbeatCleanup() {
|
||||
_heartbeatCleanupTimer?.cancel();
|
||||
_heartbeatCleanupTimer = Timer.periodic(_cleanupInterval, (_) {
|
||||
_cleanupStaleDevices();
|
||||
});
|
||||
Log.d('Discovery: 心跳超时清理已启动 (间隔=${_cleanupInterval.inSeconds}s, 超时=${_offlineTimeout.inSeconds}s)');
|
||||
}
|
||||
|
||||
void _cleanupStaleDevices() {
|
||||
if (_isDisposed || !state.isScanning) return;
|
||||
|
||||
final now = DateTime.now();
|
||||
final staleKeys = <String>[];
|
||||
var cleanedCount = 0;
|
||||
|
||||
for (final entry in _allDevices.entries) {
|
||||
final device = entry.value;
|
||||
final elapsed = now.difference(device.lastSeen);
|
||||
|
||||
if (elapsed > _offlineTimeout) {
|
||||
staleKeys.add(entry.key);
|
||||
cleanedCount++;
|
||||
Log.d('Discovery: 清理离线设备 [${device.alias}] (${device.ip}) 距上次活跃 ${elapsed.inSeconds}s');
|
||||
}
|
||||
}
|
||||
|
||||
// 移除超时设备
|
||||
for (final key in staleKeys) {
|
||||
_allDevices.remove(key);
|
||||
}
|
||||
|
||||
if (cleanedCount > 0) {
|
||||
_notifyDevicesChanged();
|
||||
Log.i('Discovery: 已清理 $cleanedCount 个离线设备, 剩余 ${_allDevices.length} 个');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stopScan() async {
|
||||
try {
|
||||
await _lanService.stopScan();
|
||||
@@ -265,26 +323,71 @@ class DeviceDiscoveryNotifier extends Notifier<DeviceDiscoveryState>
|
||||
await _usbSub?.cancel();
|
||||
_usbSub = null;
|
||||
|
||||
// 停止心跳清理定时器
|
||||
_heartbeatCleanupTimer?.cancel();
|
||||
_heartbeatCleanupTimer = null;
|
||||
|
||||
if (!_isDisposed) {
|
||||
state = state.copyWith(isScanning: false, activeMethods: {});
|
||||
}
|
||||
Log.i('Discovery: Scan stopped');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 设备合并(去重核心)
|
||||
// 基于 deviceId + IP 组合键去重,同一设备更新 lastSeenAt 而非新增
|
||||
// ============================================================
|
||||
|
||||
void _mergeDevices(List<TransferDevice> devices) {
|
||||
if (_isDisposed) return;
|
||||
for (final device in devices) {
|
||||
_allDevices[device.id] = device;
|
||||
// 构建组合键: "deviceId|ip" (IP为空则仅用deviceId)
|
||||
final compositeKey = _buildCompositeKey(device);
|
||||
|
||||
final existing = _allDevices[compositeKey];
|
||||
if (existing != null) {
|
||||
// 设备已存在,更新 lastSeen 时间戳和其他可变信息
|
||||
_allDevices[compositeKey] = existing.copyWith(
|
||||
lastSeen: DateTime.now(), // 更新最后活跃时间
|
||||
isOnline: true, // 标记为在线
|
||||
ip: device.ip ?? existing.ip,
|
||||
alias: device.alias.isNotEmpty ? device.alias : existing.alias,
|
||||
deviceModel: device.deviceModel ?? existing.deviceModel,
|
||||
);
|
||||
Log.d('Discovery: 更新设备活跃时间 [${device.alias}] ($compositeKey)');
|
||||
} else {
|
||||
// 新设备,添加到列表
|
||||
_allDevices[compositeKey] = device.copyWith(lastSeen: DateTime.now());
|
||||
Log.i('Discovery: 发现新设备 [${device.alias}] (${device.ip}) ($compositeKey)');
|
||||
}
|
||||
}
|
||||
_notifyDevicesChanged();
|
||||
}
|
||||
|
||||
void _addDevice(TransferDevice device) {
|
||||
if (_isDisposed) return;
|
||||
_allDevices[device.id] = device;
|
||||
final compositeKey = _buildCompositeKey(device);
|
||||
|
||||
final existing = _allDevices[compositeKey];
|
||||
if (existing != null) {
|
||||
// 设备已存在,更新 lastSeen 时间戳
|
||||
_allDevices[compositeKey] = existing.copyWith(
|
||||
lastSeen: DateTime.now(),
|
||||
isOnline: true,
|
||||
);
|
||||
} else {
|
||||
// 新设备
|
||||
_allDevices[compositeKey] = device.copyWith(lastSeen: DateTime.now());
|
||||
}
|
||||
_notifyDevicesChanged();
|
||||
}
|
||||
|
||||
/// 构建 deviceId + IP 组合键用于去重
|
||||
String _buildCompositeKey(TransferDevice device) {
|
||||
final ipPart = (device.ip != null && device.ip!.isNotEmpty) ? '|${device.ip}' : '';
|
||||
return '${device.id}$ipPart';
|
||||
}
|
||||
|
||||
void _notifyDevicesChanged() {
|
||||
if (_isDisposed) return;
|
||||
final devices = _allDevices.values.toList();
|
||||
@@ -312,7 +415,8 @@ class DeviceDiscoveryNotifier extends Notifier<DeviceDiscoveryState>
|
||||
void addSimulatedDevices(List<TransferDevice> devices) {
|
||||
if (_isDisposed) return;
|
||||
for (final device in devices) {
|
||||
_allDevices[device.id] = device;
|
||||
final compositeKey = _buildCompositeKey(device);
|
||||
_allDevices[compositeKey] = device.copyWith(lastSeen: DateTime.now());
|
||||
}
|
||||
_notifyDevicesChanged();
|
||||
}
|
||||
@@ -323,6 +427,7 @@ class DeviceDiscoveryNotifier extends Notifier<DeviceDiscoveryState>
|
||||
_bleSub?.cancel();
|
||||
_nfcSub?.cancel();
|
||||
_usbSub?.cancel();
|
||||
_heartbeatCleanupTimer?.cancel();
|
||||
_lanService.dispose();
|
||||
_bleService.dispose();
|
||||
_nfcService.dispose();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// ============================================================
|
||||
// 闲言APP — 局域网UDP多播发现服务
|
||||
// 创建时间: 2026-05-09
|
||||
// 更新时间: 2026-05-11
|
||||
// 更新时间: 2026-06-04
|
||||
// 作用: 基于LocalSend协议的局域网设备发现 — 渐进式发现策略
|
||||
// 上次更新: 修复StreamSubscription泄漏+异步安全防护
|
||||
// 上次更新: 增强设备去重( deviceId+IP ) + 心跳超时清理(30s)
|
||||
// ============================================================
|
||||
|
||||
import 'dart:async';
|
||||
@@ -32,11 +32,24 @@ class LanDiscoveryService {
|
||||
Stream<List<TransferDevice>> get onDevicesChanged =>
|
||||
_devicesController.stream;
|
||||
|
||||
/// 设备缓存: key = "deviceId|ip" 组合键,确保同一设备从不同IP广播时不重复
|
||||
final Map<String, TransferDevice> _mergedDevices = {};
|
||||
|
||||
List<TransferDevice> get discoveredDevices => _mergedDevices.values.toList();
|
||||
|
||||
StreamSubscription<List<TransferDevice>>? _localSendSub;
|
||||
|
||||
// ============================================================
|
||||
// 心跳超时清理配置
|
||||
// ============================================================
|
||||
|
||||
/// 超时阈值: 30秒未收到广播的设备视为离线
|
||||
static const Duration _offlineTimeout = Duration(seconds: 30);
|
||||
|
||||
/// 清理检查间隔: 每10秒检查一次
|
||||
static const Duration _cleanupInterval = Duration(seconds: 10);
|
||||
|
||||
Timer? _heartbeatCleanupTimer;
|
||||
|
||||
Future<void> startScan() async {
|
||||
if (_isScanning) return;
|
||||
@@ -51,9 +64,57 @@ class LanDiscoveryService {
|
||||
|
||||
await _localSendService.startDiscovery();
|
||||
|
||||
// 启动心跳超时清理定时器
|
||||
_startHeartbeatCleanup();
|
||||
|
||||
_startProgressiveFallback();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 心跳超时清理逻辑
|
||||
// ============================================================
|
||||
|
||||
void _startHeartbeatCleanup() {
|
||||
_heartbeatCleanupTimer?.cancel();
|
||||
_heartbeatCleanupTimer = Timer.periodic(_cleanupInterval, (_) {
|
||||
_cleanupStaleDevices();
|
||||
});
|
||||
Log.d('LAN Discovery: 心跳超时清理已启动 (间隔=${_cleanupInterval.inSeconds}s, 超时=${_offlineTimeout.inSeconds}s)');
|
||||
}
|
||||
|
||||
void _cleanupStaleDevices() {
|
||||
if (!_isScanning) return;
|
||||
|
||||
final now = DateTime.now();
|
||||
final staleKeys = <String>[];
|
||||
var cleanedCount = 0;
|
||||
|
||||
for (final entry in _mergedDevices.entries) {
|
||||
final device = entry.value;
|
||||
final elapsed = now.difference(device.lastSeen);
|
||||
|
||||
if (elapsed > _offlineTimeout) {
|
||||
staleKeys.add(entry.key);
|
||||
cleanedCount++;
|
||||
Log.d('LAN Discovery: 清理离线设备 [${device.alias}] (${device.ip}) 距上次活跃 ${elapsed.inSeconds}s');
|
||||
}
|
||||
}
|
||||
|
||||
// 移除超时设备
|
||||
for (final key in staleKeys) {
|
||||
_mergedDevices.remove(key);
|
||||
}
|
||||
|
||||
if (cleanedCount > 0 && !_devicesController.isClosed) {
|
||||
_devicesController.add(discoveredDevices);
|
||||
Log.i('LAN Discovery: 已清理 $cleanedCount 个离线设备, 剩余 ${_mergedDevices.length} 个');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 渐进式回退策略
|
||||
// ============================================================
|
||||
|
||||
void _startProgressiveFallback() async {
|
||||
await Future<void>.delayed(const Duration(seconds: 2));
|
||||
|
||||
@@ -76,18 +137,57 @@ class LanDiscoveryService {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 设备合并(去重核心)
|
||||
// 基于 deviceId + IP 组合键去重,同一设备更新 lastSeenAt 而非新增
|
||||
// ============================================================
|
||||
|
||||
void _mergeDevices(List<TransferDevice> devices) {
|
||||
for (final device in devices) {
|
||||
_mergedDevices[device.id] = device;
|
||||
// 构建组合键: "deviceId|ip" (IP为空则仅用deviceId)
|
||||
final compositeKey = _buildCompositeKey(device);
|
||||
|
||||
final existing = _mergedDevices[compositeKey];
|
||||
if (existing != null) {
|
||||
// 设备已存在,更新 lastSeen 时间戳和其他可变信息
|
||||
_mergedDevices[compositeKey] = existing.copyWith(
|
||||
lastSeen: DateTime.now(), // 更新最后活跃时间
|
||||
isOnline: true, // 标记为在线
|
||||
ip: device.ip ?? existing.ip,
|
||||
alias: device.alias.isNotEmpty ? device.alias : existing.alias,
|
||||
deviceModel: device.deviceModel ?? existing.deviceModel,
|
||||
);
|
||||
Log.d('LAN Discovery: 更新设备活跃时间 [${device.alias}] ($compositeKey)');
|
||||
} else {
|
||||
// 新设备,添加到列表
|
||||
_mergedDevices[compositeKey] = device.copyWith(lastSeen: DateTime.now());
|
||||
Log.i('LAN Discovery: 发现新设备 [${device.alias}] (${device.ip}) ($compositeKey)');
|
||||
}
|
||||
}
|
||||
|
||||
if (!_devicesController.isClosed) {
|
||||
_devicesController.add(discoveredDevices);
|
||||
}
|
||||
}
|
||||
|
||||
/// 构建 deviceId + IP 组合键用于去重
|
||||
String _buildCompositeKey(TransferDevice device) {
|
||||
final ipPart = (device.ip != null && device.ip!.isNotEmpty) ? '|${device.ip}' : '';
|
||||
return '${device.id}$ipPart';
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 生命周期管理
|
||||
// ============================================================
|
||||
|
||||
Future<void> stopScan() async {
|
||||
if (!_isScanning) return;
|
||||
_isScanning = false;
|
||||
|
||||
// 停止心跳清理定时器
|
||||
_heartbeatCleanupTimer?.cancel();
|
||||
_heartbeatCleanupTimer = null;
|
||||
|
||||
await _localSendSub?.cancel();
|
||||
_localSendSub = null;
|
||||
await _localSendService.stopDiscovery();
|
||||
@@ -106,6 +206,7 @@ class LanDiscoveryService {
|
||||
await stopScan();
|
||||
await stopHttpServer();
|
||||
await _httpScanService.dispose();
|
||||
_heartbeatCleanupTimer?.cancel();
|
||||
await _devicesController.close();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user