Feat: Android client Phase 0-3 full implementation
This commit is contained in:
@@ -1485,13 +1485,19 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
|
||||
if (TryMigrateClientMetadata(id, strPCName, path)) {
|
||||
modify = true;
|
||||
}
|
||||
CString loc = m_ClientMap->GetClientMapData(id, MAP_LOCATION);
|
||||
if (loc.IsEmpty()) {
|
||||
loc = v[RES_CLIENT_LOC].c_str();
|
||||
if (loc.IsEmpty()) {
|
||||
loc = m_IPConverter->GetGeoLocation(data[ONLINELIST_IP].GetString()).c_str();
|
||||
needConvert = !m_HasLocDB;
|
||||
}
|
||||
// 优先级:① 客户端刚发来的有效地理位置(非空非"?")
|
||||
// ② 服务端缓存(排除历史遗留的"?"无效值)
|
||||
// ③ 服务端按连接 IP 自行查询
|
||||
CString clientLoc = v[RES_CLIENT_LOC].c_str();
|
||||
CString cachedLoc = m_ClientMap->GetClientMapData(id, MAP_LOCATION);
|
||||
CString loc;
|
||||
if (!clientLoc.IsEmpty() && clientLoc != "?") {
|
||||
loc = clientLoc;
|
||||
} else if (!cachedLoc.IsEmpty() && cachedLoc != "?") {
|
||||
loc = cachedLoc;
|
||||
} else {
|
||||
loc = m_IPConverter->GetGeoLocation(data[ONLINELIST_IP].GetString()).c_str();
|
||||
needConvert = !m_HasLocDB;
|
||||
}
|
||||
// TODO: Remove SafeUtf8ToAnsi after migrating to UTF-8
|
||||
if (needConvert)
|
||||
@@ -3478,7 +3484,7 @@ void CMy2015RemoteDlg::CheckHeartbeat()
|
||||
{
|
||||
CLock lock(m_cs);
|
||||
auto now = time(0);
|
||||
int HEARTBEAT_TIMEOUT = max(60, m_settings.ReportInterval * 3);
|
||||
int HEARTBEAT_TIMEOUT = max(120, m_settings.ReportInterval * 3);
|
||||
|
||||
// 收集需要删除的 context(避免遍历时修改 vector)
|
||||
std::vector<context*> toRemove;
|
||||
@@ -10304,6 +10310,17 @@ context* CMy2015RemoteDlg::FindHostByIP(const std::string& ip)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
context* CMy2015RemoteDlg::FindHostByClientID(uint64_t clientID)
|
||||
{
|
||||
EnterCriticalSection(&m_cs);
|
||||
auto it = m_ClientIndex.find(clientID);
|
||||
context* ctx = nullptr;
|
||||
if (it != m_ClientIndex.end() && it->second < m_HostList.size())
|
||||
ctx = m_HostList[it->second];
|
||||
LeaveCriticalSection(&m_cs);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
uint64_t CMy2015RemoteDlg::FindClientIDByIP(const std::string& ip)
|
||||
{
|
||||
CString clientIP(ip.c_str());
|
||||
|
||||
@@ -368,6 +368,7 @@ public:
|
||||
CGridDialog * m_gridDlg = NULL;
|
||||
std::vector<DllInfo*> m_DllList;
|
||||
context* FindHostByIP(const std::string& ip);
|
||||
context* FindHostByClientID(uint64_t clientID); // 线程安全:通过 clientID 精确查主连接
|
||||
uint64_t FindClientIDByIP(const std::string& ip); // 线程安全:在锁内获取ID
|
||||
void InjectTinyRunDll(const std::string& ip, int pid);
|
||||
NOTIFYICONDATA m_Nid;
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
#include <md5.h>
|
||||
#include <cstdint> // for uint16_t
|
||||
|
||||
extern CMy2015RemoteDlg* g_2015RemoteDlg;
|
||||
|
||||
extern "C" uint32_t licenseGetBuildTag() { volatile uint32_t tag = 0xC0DE2026u; return tag; }
|
||||
#include <vector>
|
||||
#include <mutex> // for std::mutex, std::lock_guard
|
||||
@@ -225,7 +227,15 @@ CScreenSpyDlg::CScreenSpyDlg(CMy2015RemoteDlg* Parent, Server* IOCPServer, CONTE
|
||||
if (WebService().IsRunning() && !WebService().IsMfcTriggered(m_ClientID)) {
|
||||
int width = m_BitmapInfor_Full->bmiHeader.biWidth;
|
||||
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
|
||||
WebService().NotifyResolutionChange(m_ClientID, width, height);
|
||||
bool topDown = (m_BitmapInfor_Full->bmiHeader.biClrImportant == 1);
|
||||
// m_ContextObject is the screen sub-connection; client_type lives on the
|
||||
// main login connection. Look it up via the peer IP (same pattern as GetClientEncoding).
|
||||
std::string clientType;
|
||||
if (g_2015RemoteDlg) {
|
||||
context* main = g_2015RemoteDlg->FindHostByClientID(m_ClientID);
|
||||
if (main) clientType = main->GetAdditionalData(RES_CLIENT_TYPE).GetString();
|
||||
}
|
||||
WebService().NotifyResolutionChange(m_ClientID, width, height, topDown, clientType);
|
||||
// 透传客户端初始的音频开/关状态给 web,让前端按钮显示正确
|
||||
WebService().NotifyAudioState(m_ClientID, m_Settings.AudioEnabled != 0);
|
||||
}
|
||||
@@ -953,7 +963,6 @@ VOID CScreenSpyDlg::OnClose()
|
||||
CWnd* parent = GetParent();
|
||||
if (parent)
|
||||
parent->SendMessage(WM_CHILD_CLOSED, (WPARAM)this, 0);
|
||||
extern CMy2015RemoteDlg *g_2015RemoteDlg;
|
||||
if(g_2015RemoteDlg)
|
||||
g_2015RemoteDlg->RemoveRemoteWindow(GetSafeHwnd());
|
||||
|
||||
@@ -1126,7 +1135,13 @@ VOID CScreenSpyDlg::OnReceiveComplete()
|
||||
if (m_bIsWebSession && WebService().IsRunning()) {
|
||||
int width = m_BitmapInfor_Full->bmiHeader.biWidth;
|
||||
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
|
||||
WebService().NotifyResolutionChange(m_ClientID, width, height);
|
||||
bool topDown = (m_BitmapInfor_Full->bmiHeader.biClrImportant == 1);
|
||||
std::string clientType;
|
||||
if (g_2015RemoteDlg) {
|
||||
context* main = g_2015RemoteDlg->FindHostByClientID(m_ClientID);
|
||||
if (main) clientType = main->GetAdditionalData(RES_CLIENT_TYPE).GetString();
|
||||
}
|
||||
WebService().NotifyResolutionChange(m_ClientID, width, height, topDown, clientType);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -694,13 +694,17 @@ void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t
|
||||
// Get screen dimensions + audio state from device info cache (may not be ready)
|
||||
int width = 0, height = 0;
|
||||
int audio_enabled = -1; // -1 = unknown yet (前端走 audio_state 事件兜底)
|
||||
bool top_down = false;
|
||||
std::string client_type;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_DeviceCacheMutex);
|
||||
auto it = m_DeviceCache.find(device_id);
|
||||
if (it != m_DeviceCache.end()) {
|
||||
width = it->second->screen_width;
|
||||
height = it->second->screen_height;
|
||||
width = it->second->screen_width;
|
||||
height = it->second->screen_height;
|
||||
audio_enabled = it->second->audio_enabled;
|
||||
top_down = it->second->top_down;
|
||||
client_type = it->second->client_type;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -713,6 +717,8 @@ void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t
|
||||
if (width > 0 && height > 0) {
|
||||
res["width"] = width;
|
||||
res["height"] = height;
|
||||
res["top_down"] = top_down;
|
||||
if (!client_type.empty()) res["client_type"] = client_type;
|
||||
}
|
||||
if (audio_enabled >= 0) {
|
||||
res["audio_enabled"] = (audio_enabled != 0);
|
||||
@@ -1712,7 +1718,7 @@ void CWebService::BroadcastH264Frame(uint64_t device_id, const uint8_t* data, si
|
||||
}
|
||||
}
|
||||
|
||||
void CWebService::NotifyResolutionChange(uint64_t device_id, int width, int height) {
|
||||
void CWebService::NotifyResolutionChange(uint64_t device_id, int width, int height, bool top_down, const std::string& client_type) {
|
||||
if (m_bStopping) return;
|
||||
|
||||
// Update cache
|
||||
@@ -1723,8 +1729,10 @@ void CWebService::NotifyResolutionChange(uint64_t device_id, int width, int heig
|
||||
m_DeviceCache[device_id] = std::make_shared<WebDeviceInfo>();
|
||||
it = m_DeviceCache.find(device_id);
|
||||
}
|
||||
it->second->screen_width = width;
|
||||
it->second->screen_width = width;
|
||||
it->second->screen_height = height;
|
||||
it->second->top_down = top_down;
|
||||
if (!client_type.empty()) it->second->client_type = client_type;
|
||||
}
|
||||
|
||||
// Notify watching clients
|
||||
@@ -1733,6 +1741,8 @@ void CWebService::NotifyResolutionChange(uint64_t device_id, int width, int heig
|
||||
res["id"] = device_id;
|
||||
res["width"] = width;
|
||||
res["height"] = height;
|
||||
res["top_down"] = top_down;
|
||||
if (!client_type.empty()) res["client_type"] = client_type;
|
||||
|
||||
Json::StreamWriterBuilder builder;
|
||||
builder["indentation"] = "";
|
||||
|
||||
@@ -55,6 +55,8 @@ struct WebDeviceInfo {
|
||||
int screen_width;
|
||||
int screen_height;
|
||||
bool online;
|
||||
bool top_down = false; // true = Android/top-down H.264, no flip needed in browser
|
||||
std::string client_type; // RES_CLIENT_TYPE: "APK"=Android, "EXE"=Windows, "LNX"=Linux, "MAC"=macOS
|
||||
// 当前会话的音频开关。-1=未知(客户端 BITMAPINFO 还没回来),0=关,1=开
|
||||
int audio_enabled = -1;
|
||||
|
||||
@@ -98,7 +100,7 @@ public:
|
||||
void CacheKeyframe(uint64_t device_id, const uint8_t* data, size_t len);
|
||||
|
||||
// Resolution change notification
|
||||
void NotifyResolutionChange(uint64_t device_id, int width, int height);
|
||||
void NotifyResolutionChange(uint64_t device_id, int width, int height, bool top_down = false, const std::string& client_type = "");
|
||||
|
||||
// Audio enable/disable notification — pushes current state to all web
|
||||
// clients watching this device and caches it for newcomers.
|
||||
|
||||
@@ -783,6 +783,21 @@
|
||||
#screen-page:-webkit-full-screen .toolbar-toggle {
|
||||
display: flex;
|
||||
}
|
||||
/* Android portrait mode: full-viewport canvas, no top toolbar */
|
||||
#screen-page.android-portrait { overflow: hidden; }
|
||||
#screen-page.android-portrait .screen-toolbar { display: none !important; }
|
||||
#screen-page.android-portrait .canvas-container {
|
||||
height: 100dvh !important; height: 100vh !important;
|
||||
padding-top: 0 !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
#screen-page.android-portrait #screen-canvas {
|
||||
max-width: 100vw !important;
|
||||
max-height: 100dvh !important; max-height: 100vh !important;
|
||||
}
|
||||
#screen-page.android-portrait .toolbar-toggle { display: flex !important; }
|
||||
#screen-page.android-portrait .quick-controls { display: none !important; }
|
||||
|
||||
.compat-warning {
|
||||
background: linear-gradient(90deg, #ff9800, #f57c00);
|
||||
color: #000;
|
||||
@@ -1063,6 +1078,33 @@
|
||||
}
|
||||
.input-shortcuts .shortcut-btn:hover { background: rgba(128,128,128,0.5); }
|
||||
.input-shortcuts .shortcut-btn:active { transform: scale(0.95); background: rgba(128,128,128,0.6); }
|
||||
|
||||
/* Android virtual navigation bar - overlays bottom of canvas when controlling Android */
|
||||
.android-navbar {
|
||||
position: absolute;
|
||||
bottom: 0; left: 0; right: 0;
|
||||
height: 44px;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 56px;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
z-index: 102;
|
||||
user-select: none;
|
||||
}
|
||||
.android-navbar.visible { display: flex; }
|
||||
.android-navbar .nav-btn {
|
||||
width: 44px; height: 44px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
border: none; background: transparent;
|
||||
cursor: pointer; opacity: 0.85;
|
||||
transition: opacity 0.15s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
.android-navbar .nav-btn:active { opacity: 0.4; }
|
||||
/* Mobile responsive */
|
||||
@media (max-width: 768px) {
|
||||
.page {
|
||||
@@ -1195,6 +1237,18 @@
|
||||
<div class="canvas-container">
|
||||
<canvas id="screen-canvas"></canvas>
|
||||
<div class="cursor-overlay" id="cursor-overlay"></div>
|
||||
<!-- Android virtual navigation bar: Back / Home / Recents -->
|
||||
<div class="android-navbar" id="android-navbar">
|
||||
<button class="nav-btn" onclick="sendAndroidNav(8)" title="Back" tabindex="-1">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="white"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
|
||||
</button>
|
||||
<button class="nav-btn" onclick="sendAndroidNav(36)" title="Home" tabindex="-1">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><circle cx="12" cy="12" r="8"/></svg>
|
||||
</button>
|
||||
<button class="nav-btn" onclick="sendAndroidNav(93)" title="Recents" tabindex="-1">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><rect x="4" y="4" width="16" height="16" rx="3"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="quick-controls" id="quick-controls">
|
||||
<button class="qc-btn" id="qc-rdp" onclick="sendRdpReset()" title="RDP Reset" tabindex="-1">↻</button>
|
||||
<button class="qc-btn" id="qc-keyboard" onclick="toggleKeyboard()" title="Keyboard" tabindex="-1">⌨</button>
|
||||
@@ -1310,6 +1364,8 @@
|
||||
let bwBytesAccum = 0; // current-second byte accumulator
|
||||
let bwBytesPerSec = 0; // last second's throughput (bytes/sec)
|
||||
let currentWidth = 0, currentHeight = 0; // captured at frame decode time
|
||||
let isTopDown = false; // true = Android/MediaCodec top-down H.264, skip vertical flip
|
||||
let isAndroidRemote = false; // true = remote is Android (client_type==="APK") → direct-touch on mobile
|
||||
const canvas = document.getElementById('screen-canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
@@ -1564,6 +1620,10 @@
|
||||
// Resolution may not be available yet (first connection)
|
||||
if (msg.width && msg.height) {
|
||||
updateScreenStatus('connected');
|
||||
isTopDown = msg.top_down === true;
|
||||
isAndroidRemote = msg.client_type === 'APK';
|
||||
updateAndroidNavbar();
|
||||
if (isTouchDevice) updateUIForOrientation();
|
||||
initDecoder(msg.width, msg.height);
|
||||
} else {
|
||||
// Wait for resolution_changed message
|
||||
@@ -1609,6 +1669,15 @@
|
||||
break;
|
||||
case 'resolution_changed':
|
||||
updateScreenStatus('connected');
|
||||
isTopDown = msg.top_down === true;
|
||||
if (msg.client_type !== undefined) {
|
||||
isAndroidRemote = msg.client_type === 'APK';
|
||||
updateAndroidNavbar();
|
||||
// Refresh cursor overlay and immersive layout
|
||||
document.getElementById('cursor-overlay').classList.toggle(
|
||||
'active', controlEnabled && isTouchDevice && !isAndroidRemote);
|
||||
if (isTouchDevice) updateUIForOrientation();
|
||||
}
|
||||
initDecoder(msg.width, msg.height);
|
||||
break;
|
||||
case 'cursor':
|
||||
@@ -1694,8 +1763,13 @@
|
||||
// Update input shortcuts position after canvas resize
|
||||
requestAnimationFrame(updateInputShortcutsPosition);
|
||||
|
||||
// Set up vertical flip transform once (BMP is bottom-up)
|
||||
ctx.setTransform(1, 0, 0, -1, 0, height);
|
||||
// Windows/Linux/macOS clients send bottom-up H.264 (BMP convention) → flip.
|
||||
// Android MediaCodec sends top-down H.264 → no flip needed.
|
||||
if (isTopDown) {
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
} else {
|
||||
ctx.setTransform(1, 0, 0, -1, 0, height);
|
||||
}
|
||||
if (decoder) { try { decoder.close(); } catch(e) {} }
|
||||
// Reset FPS sliding window on decoder (re)init so a resolution
|
||||
// change or codec switch doesn't carry over stale counts.
|
||||
@@ -2084,7 +2158,7 @@
|
||||
}
|
||||
decoder.decode(new EncodedVideoChunk({
|
||||
type: isKeyframe ? 'key' : 'delta',
|
||||
timestamp: decodeTimestamp++,
|
||||
timestamp: decodeTimestamp++ * 33333, // µs,按 30fps 帧间隔递增
|
||||
data: videoData
|
||||
}));
|
||||
} catch (e) {
|
||||
@@ -2923,7 +2997,8 @@
|
||||
btnRdpResetBar, btnMouseBar, btnKeyboardBar, inputShortcuts } = uiElements;
|
||||
|
||||
if (isLandscape()) {
|
||||
// Landscape mode
|
||||
// Landscape mode: always clear android-portrait immersive class
|
||||
document.getElementById('screen-page').classList.remove('android-portrait');
|
||||
quickControls.classList.remove('visible');
|
||||
inputShortcuts.classList.remove('visible');
|
||||
|
||||
@@ -2942,8 +3017,21 @@
|
||||
btnMouseBar.classList.remove('ui-hidden');
|
||||
btnKeyboardBar.classList.remove('ui-hidden');
|
||||
}
|
||||
} else if (isAndroidRemote && isFullscreen()) {
|
||||
// Android portrait fullscreen: immersive mode — hide top toolbar, show three-dot toggle.
|
||||
// Canvas fills the full viewport via .android-portrait CSS class.
|
||||
document.getElementById('screen-page').classList.add('android-portrait');
|
||||
quickControls.classList.remove('visible');
|
||||
inputShortcuts.classList.remove('visible');
|
||||
toolbarToggle.classList.remove('ui-hidden');
|
||||
floatingToolbar.classList.remove('visible');
|
||||
toolbarVisible = false;
|
||||
btnRdpResetBar.classList.add('ui-hidden');
|
||||
btnMouseBar.classList.add('ui-hidden');
|
||||
btnKeyboardBar.classList.add('ui-hidden');
|
||||
} else {
|
||||
// Portrait mode: show quick controls
|
||||
// Portrait mode (non-Android): show quick controls
|
||||
document.getElementById('screen-page').classList.remove('android-portrait');
|
||||
quickControls.classList.add('visible');
|
||||
// Input shortcuts only visible when keyboard is open
|
||||
const keyboardOpen = document.activeElement === mobileKeyboard;
|
||||
@@ -3038,7 +3126,8 @@
|
||||
} else if (!controlEnabled) {
|
||||
canvas.style.cursor = 'default';
|
||||
}
|
||||
cursorOverlay.classList.toggle('active', controlEnabled && isTouchDevice);
|
||||
// Android direct mode: no cursor overlay (finger IS the pointer)
|
||||
cursorOverlay.classList.toggle('active', controlEnabled && isTouchDevice && !isAndroidRemote);
|
||||
if (controlEnabled) {
|
||||
applyRemoteCursor(currentCursorIndex);
|
||||
}
|
||||
@@ -3278,6 +3367,83 @@
|
||||
const touchIndicator = document.getElementById('touch-indicator');
|
||||
const mobileKeyboard = document.getElementById('mobile-keyboard');
|
||||
|
||||
// ── Android direct-touch state ────────────────────────────────────────
|
||||
// Active when isAndroidRemote && isTouchDevice.
|
||||
// Single finger maps directly to remote screen coordinates.
|
||||
let ad = {
|
||||
active: false,
|
||||
startX: 0, startY: 0,
|
||||
lastX: 0, lastY: 0,
|
||||
hasMoved: false,
|
||||
longPressTimer: null,
|
||||
lastTapX: 0, lastTapY: 0,
|
||||
lastTapTime: 0
|
||||
};
|
||||
|
||||
function adTouchStart(e) {
|
||||
if (e.touches.length !== 1) return;
|
||||
const touch = e.touches[0];
|
||||
const pos = getTouchPos(touch);
|
||||
const now = Date.now();
|
||||
|
||||
if (ad.longPressTimer) { clearTimeout(ad.longPressTimer); ad.longPressTimer = null; }
|
||||
|
||||
// Double-tap detection
|
||||
const dx = pos.x - ad.lastTapX, dy = pos.y - ad.lastTapY;
|
||||
if (now - ad.lastTapTime < 300 && dx*dx + dy*dy < 1600) {
|
||||
ad.lastTapTime = 0;
|
||||
ad.active = false;
|
||||
sendMouse('dblclick', pos.x, pos.y, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
ad.active = true;
|
||||
ad.startX = ad.lastX = pos.x;
|
||||
ad.startY = ad.lastY = pos.y;
|
||||
ad.hasMoved = false;
|
||||
|
||||
sendMouse('down', pos.x, pos.y, 0);
|
||||
|
||||
// Long press → right-click (Android long-press)
|
||||
ad.longPressTimer = setTimeout(function() {
|
||||
if (ad.active && !ad.hasMoved) {
|
||||
sendMouse('up', ad.lastX, ad.lastY, 0);
|
||||
sendMouse('down', ad.lastX, ad.lastY, 2);
|
||||
sendMouse('up', ad.lastX, ad.lastY, 2);
|
||||
ad.active = false;
|
||||
showTouchIndicator(touch.clientX, touch.clientY);
|
||||
}
|
||||
ad.longPressTimer = null;
|
||||
}, 600);
|
||||
}
|
||||
|
||||
function adTouchMove(e) {
|
||||
if (e.touches.length !== 1 || !ad.active) return;
|
||||
const pos = getTouchPos(e.touches[0]);
|
||||
const dx = pos.x - ad.startX, dy = pos.y - ad.startY;
|
||||
|
||||
if (!ad.hasMoved && dx*dx + dy*dy > 64) { // 8px threshold
|
||||
ad.hasMoved = true;
|
||||
if (ad.longPressTimer) { clearTimeout(ad.longPressTimer); ad.longPressTimer = null; }
|
||||
}
|
||||
if (ad.hasMoved) sendMouse('move', pos.x, pos.y, 0);
|
||||
ad.lastX = pos.x;
|
||||
ad.lastY = pos.y;
|
||||
}
|
||||
|
||||
function adTouchEnd() {
|
||||
if (!ad.active) return;
|
||||
if (ad.longPressTimer) { clearTimeout(ad.longPressTimer); ad.longPressTimer = null; }
|
||||
ad.active = false;
|
||||
if (!ad.hasMoved) {
|
||||
ad.lastTapTime = Date.now();
|
||||
ad.lastTapX = ad.lastX;
|
||||
ad.lastTapY = ad.lastY;
|
||||
}
|
||||
sendMouse('up', ad.lastX, ad.lastY, 0);
|
||||
}
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
// Initialize cursor to center of screen
|
||||
function initCursor() {
|
||||
if (!cursorState.initialized && canvas.width > 0) {
|
||||
@@ -3337,6 +3503,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
function sendAndroidNav(keyCode) {
|
||||
sendKey(keyCode, true);
|
||||
setTimeout(() => sendKey(keyCode, false), 50);
|
||||
}
|
||||
|
||||
function updateAndroidNavbar() {
|
||||
const nb = document.getElementById('android-navbar');
|
||||
if (!nb) return;
|
||||
nb.classList.toggle('visible', !!isAndroidRemote);
|
||||
}
|
||||
|
||||
function sendKey(keyCode, isDown, altKey) {
|
||||
if (!controlEnabled) return; // Control mode required
|
||||
// Filter Windows keys (handled locally, not sent to remote)
|
||||
@@ -3391,7 +3568,9 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Single finger touch
|
||||
// Single finger touch — Android direct mode bypasses the touchpad state machine
|
||||
if (isAndroidRemote && isTouchDevice) { adTouchStart(e); return; }
|
||||
|
||||
initCursor();
|
||||
const touch = e.touches[0];
|
||||
const oldStartX = touchState.startX;
|
||||
@@ -3560,6 +3739,9 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Single finger move — Android direct mode
|
||||
if (isAndroidRemote && isTouchDevice) { adTouchMove(e); return; }
|
||||
|
||||
// Single finger move - state machine based
|
||||
const touch = e.touches[0];
|
||||
const dx = touch.clientX - touchState.lastX;
|
||||
@@ -3646,6 +3828,9 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Android direct mode touchend
|
||||
if (isAndroidRemote && isTouchDevice) { adTouchEnd(); return; }
|
||||
|
||||
// State machine based touchend
|
||||
const overlay = document.getElementById('cursor-overlay');
|
||||
const x = Math.round(cursorState.x);
|
||||
@@ -3925,6 +4110,9 @@
|
||||
ws.send(JSON.stringify({ cmd: 'disconnect', token, id: String(currentDevice.id) }));
|
||||
}
|
||||
currentDevice = null; // Clear current device
|
||||
isAndroidRemote = false;
|
||||
updateAndroidNavbar();
|
||||
document.getElementById('screen-page').classList.remove('android-portrait');
|
||||
showPage('devices-page');
|
||||
getDevices();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user