feat: 多模块功能更新 - 文件传输/多语言/NFC/首页组件/进度美化等

- 文件传输: 设备发现、LAN发现服务优化
- NFC分享: provider和service增强
- 多语言: 16种语言翻译补全
- 首页: 句子详情面板、收藏页、离线页优化
- 我的: 成就、个人资料、签到、设置页面更新
- 新增: AR视图、进度美化页、Hive安全访问、鸿蒙兼容助手、共享组件
- iOS Widget: Intents扩展、XianyanWidget更新
- 鸿蒙: 6个卡片页面更新
- 其他: 路由注册、缓存配置、崩溃监控、TTS播放器等
This commit is contained in:
Developer
2026-06-04 04:08:31 +08:00
parent 74b615afc4
commit 67f26ff166
64 changed files with 6122 additions and 408 deletions

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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();
}
}