@@ -60,6 +60,7 @@ class AppController extends ChangeNotifier {
6060 late final SecurityService securityService;
6161 late final TransportService transportService;
6262 late final DiscoveryService discoveryService;
63+
6364 /// 设置子控制器:语言/外观/自动复制/托盘/开机自启。对外字段与方法通过下方 getter
6465 /// 与同名方法委托,UI 无需感知拆分(计划 P1 控制器拆分)。
6566 late final SettingsController settings;
@@ -69,10 +70,13 @@ class AppController extends ChangeNotifier {
6970 List <Conversation > conversations = [];
7071 List <ChatMessage > messages = [];
7172 Map <String , Transfer > transfersById = {};
73+
7274 /// 各会话未读数(conversationId -> count)。
7375 Map <String , int > unreadCounts = {};
76+
7477 /// 各会话最后一条消息预览(conversationId -> message)。
7578 Map <String , ChatMessage > lastMessages = {};
79+
7680 /// 会话列表名称过滤关键字。
7781 String conversationFilter = '' ;
7882
@@ -81,15 +85,18 @@ class AppController extends ChangeNotifier {
8185 conversationFilter = value;
8286 notifyListeners ();
8387 }
88+
8489 /// 当前会话分页游标(用于判断是否还有更早的页 + 向前加载)。
8590 ChatPageCursor ? messageCursor;
91+
8692 /// 当前会话是否还有更早的消息可加载。
8793 bool hasMoreMessages = false ;
8894 Device ? selectedDevice;
8995 Conversation ? selectedConversation;
9096 PendingPairRequest ? pendingPairRequest;
9197 bool initialized = false ;
9298 bool busy = false ;
99+
93100 /// 细粒度操作状态:记录正在进行的特定操作(配对、重试等),替代仅靠全局 busy
94101 /// 禁用输入的做法。文件传输已入队异步执行,不再阻塞输入框(计划 P1)。
95102 final Set <String > activeOperations = {};
@@ -205,12 +212,22 @@ class AppController extends ChangeNotifier {
205212 selectedDevice;
206213 selectedConversation = await db.ensureConversation (selectedDevice! );
207214 // 分页:刷新时只重载最新一页,避免大历史全量加载。
208- final page = await db.listMessagesPage (
215+ final latestPage = await db.listMessagesPage (
209216 conversationId: selectedConversation! .id,
210217 limit: 50 ,
211218 );
212- messages = page;
213- _updateMessageCursor (page);
219+ // 保留用户已经加载的旧页或搜索定位页,只合并最新页的状态与新增消息。
220+ // 否则传输进度/在线状态触发的 refresh 会每 200ms 把视图重置回最新 50 条。
221+ final merged =
222+ < String , ChatMessage > {
223+ for (final message in messages) message.id: message,
224+ for (final message in latestPage) message.id: message,
225+ }.values.toList ()..sort ((a, b) {
226+ final byTime = a.createdAt.compareTo (b.createdAt);
227+ return byTime != 0 ? byTime : a.id.compareTo (b.id);
228+ });
229+ messages = merged;
230+ _updateMessageCursor (messages);
214231 final transferIds = messages
215232 .map ((message) => message.transferId)
216233 .whereType <String >();
@@ -287,6 +304,26 @@ class AppController extends ChangeNotifier {
287304 return db.searchMessages (conversation.id, query.trim ());
288305 }
289306
307+ /// 将搜索命中的历史消息所在页载入当前会话,避免只搜索到结果却无法定位。
308+ Future <bool > loadSearchResult (ChatMessage target) async {
309+ final conversation = selectedConversation;
310+ if (conversation == null || target.conversationId != conversation.id) {
311+ return false ;
312+ }
313+ if (messages.any ((message) => message.id == target.id)) return true ;
314+ final page = await db.listMessagesPageEndingAt (
315+ conversationId: conversation.id,
316+ createdAt: target.createdAt,
317+ id: target.id,
318+ limit: 50 ,
319+ );
320+ messages = page;
321+ _updateMessageCursor (page);
322+ await _ensureTransfersForCurrentMessages ();
323+ notifyListeners ();
324+ return page.any ((message) => message.id == target.id);
325+ }
326+
290327 void _updateMessageCursor (List <ChatMessage > page) {
291328 if (page.isEmpty) {
292329 messageCursor = null ;
@@ -424,9 +461,7 @@ class AppController extends ChangeNotifier {
424461 Future <void > setAutostartEnabled (bool value) async {
425462 await settings.setAutostartEnabled (value);
426463 status = value
427- ? (settings.languageCode == 'en'
428- ? 'Start on boot enabled'
429- : '已开启开机自启' )
464+ ? (settings.languageCode == 'en' ? 'Start on boot enabled' : '已开启开机自启' )
430465 : (settings.languageCode == 'en'
431466 ? 'Start on boot disabled'
432467 : '已关闭开机自启' );
@@ -759,10 +794,12 @@ class AppController extends ChangeNotifier {
759794
760795 /// 打开消息中的链接:Windows 用默认浏览器,其他平台用 OpenFilex。
761796 Future <void > openUrl (String url) async {
762- if (url.isEmpty) return ;
797+ final uri = Uri .tryParse (url);
798+ if (uri == null || (uri.scheme != 'http' && uri.scheme != 'https' )) return ;
763799 if (Platform .isWindows) {
764800 try {
765- await Process .run ('cmd' , ['/c' , 'start' , '' , url]);
801+ // 不经过 cmd.exe,避免消息中的 &、| 等字符被解释为 shell 命令。
802+ await Process .start ('explorer.exe' , [url]);
766803 return ;
767804 } catch (_) {}
768805 }
@@ -955,11 +992,17 @@ class AppController extends ChangeNotifier {
955992 /// 取消一个出站传输。优先本地取消(排队或活动任务);若该任务已发送到对端且
956993 /// 对端支持取消能力,则同时请求对端中断接收。返回是否成功取消。
957994 Future <bool > cancelTransfer (Transfer transfer) async {
995+ final wasActive = transportService.isOutboundActive (transfer.id);
996+ if (wasActive) {
997+ final peer = await db.getDevice (transfer.peerDeviceId);
998+ if (peer != null && transportService.deviceSupportsCancel (peer)) {
999+ // 先让接收端关闭并清理临时文件,再中止本地发送流。
1000+ await transportService.requestRemoteCancel (peer, transfer.id);
1001+ }
1002+ }
9581003 final localCanceled = await transportService.cancelOutbound (transfer.id);
9591004 if (localCanceled) {
960- status = languageCode == 'en'
961- ? 'Transfer canceled'
962- : '已取消传输' ;
1005+ status = languageCode == 'en' ? 'Transfer canceled' : '已取消传输' ;
9631006 notifyListeners ();
9641007 return true ;
9651008 }
@@ -976,15 +1019,26 @@ class AppController extends ChangeNotifier {
9761019 final ok = await transportService.requestRemoteCancel (peer, transfer.id);
9771020 status = ok
9781021 ? (languageCode == 'en' ? 'Transfer canceled' : '已取消传输' )
979- : (languageCode == 'en'
980- ? 'Transfer could not be canceled'
981- : '无法取消该传输' );
1022+ : (languageCode == 'en' ? 'Transfer could not be canceled' : '无法取消该传输' );
9821023 notifyListeners ();
9831024 return ok;
9841025 }
9851026
9861027 /// 取消整组出站传输(文件夹或批量附件)。
9871028 Future <int > cancelTransferGroup (String groupId) async {
1029+ final activeTransferId = transportService.activeOutboundTransferIdForGroup (
1030+ groupId,
1031+ );
1032+ if (activeTransferId != null ) {
1033+ final transfers = await db.listTransfersByIds ([activeTransferId]);
1034+ final active = transfers.firstOrNull;
1035+ if (active != null ) {
1036+ final peer = await db.getDevice (active.peerDeviceId);
1037+ if (peer != null && transportService.deviceSupportsCancel (peer)) {
1038+ await transportService.requestRemoteCancel (peer, active.id);
1039+ }
1040+ }
1041+ }
9881042 final count = await transportService.cancelOutboundGroup (groupId);
9891043 status = languageCode == 'en'
9901044 ? 'Canceled $count transfer(s)'
0 commit comments