Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ Double-click `ModAVD.exe` in the package root: it starts a local server that lis
- Several instances on one key is not a conflict: a press hides them all when one of them shows its window, and shows them all when all are hidden. It looks at whether the windows show now, so windows hidden by AUTO-MAS or `modavd window` come back too.
- Only instances running with their window are affected; headless ones have no window. Hidden windows keep rendering and the screenshot channel keeps working, they have no taskbar button, and they come back at the same position and size.
- The manager registers the keys, so they work while the manager runs (left in the tray with its window closed counts). A key is registered only while an instance using it runs with its window, and released when none does. When another program holds a key, the settings page shows a warning icon next to it, saying on hover that another program holds the key and to pick another one; no dialog pops up.
- Tray (Windows only): the manager puts an icon in the notification area, and closing the window leaves the manager there. A left click on the icon brings every hidden window back; the right-click menu has "打开管理器" (open the manager) and "退出" (exit). Exiting shows the hidden windows first and waits for running tasks to finish.
- Tray (Windows only): closing the window leaves the manager in the tray. A single left click restores hidden instance windows after the system double-click interval. A double click hides or shows all windows on the manager's Edge profile, preserving page state, or opens one if none exists. The right-click menu's "打开管理器" (open the manager) restores existing windows first. "退出" (exit) waits for running tasks to finish. Exiting also restores hidden manager and instance windows. With the default browser, a double click only opens the manager.
- Security: each launch uses a random token, which is exchanged for an HttpOnly cookie (its name includes the port, so managers for two root folders running at the same time do not overwrite each other); it validates Host (against DNS rebinding) and Origin; strict CSP.
- One root folder gets one server; double-clicking again just reopens the window. The state file is in `%LOCALAPPDATA%\ModAVD\`, and the log is at `<root>\logs\modavd-ui.log`.
- After the window is closed: on Windows it stays in the tray (see above); on macOS it waits for the running tasks to finish and exits. The latter also applies when the tray icon cannot be put up.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ Apple Silicon macOS 可运行 `ModAVD` 管理器;`ModAVD.app` 放在包根目
- 几台设成同一个键不算冲突:按下时,这几台里有一台的窗口显示着就全部藏起来,全都藏着就全部显示。看的是窗口现在显不显示,所以 AUTO-MAS、`modavd window` 藏起来的也能叫回来。
- 只管带窗口运行的实例,无头的没有窗口,不受影响。藏着的时候照旧渲染、照旧能截图,任务栏上没有它的按钮;叫回来以后位置、大小和藏之前一样。
- 由管理器注册,所以管理器开着(窗口关了、留在托盘也算)才管用。只在用这个键的实例有带窗口运行着的时候才注册,都关了就注销。被别的程序占了注册不上时,设置页那一项旁边标一个警告图标,悬停说明「这个键被别的程序占了,换一个」,不弹窗。
- 托盘(只有 Windows):管理器在托盘里放一个图标,关掉窗口后管理器留在托盘。左键点图标把藏着的窗口都叫回来;右键菜单有「打开管理器」「退出」。退出前先把藏着的窗口都显示出来,等正在跑的任务做完再退出。
- 托盘(只有 Windows):关掉窗口后管理器留在托盘。左键单击图标,等系统双击间隔过去后,把藏着的实例窗口都叫回来。双击切换管理器窗口的隐藏和显示,保留页面状态;同一管理器 Edge 配置的多个窗口一起切换,没有窗口时新开一个。右键「打开管理器」优先显示已有窗口,「退出」等正在跑的任务做完再退出。退出前也会恢复藏着的管理器和实例窗口。用默认浏览器打开时,双击只打开管理器。
- 安全:每次启动随机令牌,换成 HttpOnly cookie(名字带端口,两个根目录的管理器同时开不会互相顶掉);校验 Host(防 DNS 重绑定)和 Origin;严格的 CSP。
- 一个根目录只开一个服务,再双击只是把窗口重新打开;状态文件在 `%LOCALAPPDATA%\ModAVD\`,日志在 `<根>\logs\modavd-ui.log`。
- 关掉窗口后:Windows 上留在托盘(见上);macOS 上等正在跑的任务做完再退出。托盘图标放不成时也按后者。
Expand Down
2 changes: 1 addition & 1 deletion internal/ui/API.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ ModAVD/ModAVD#101, Windows only.
- Each instance has one boss key: `InstanceInfo.boss_key` (`"Ctrl+Alt+Shift+1"`, `""` for none), stored as `instances.<i>.bossKey` in `mas-avd.json`. Never set, instances 0–9 use Ctrl+Alt+Shift+number (the digit row of the main keyboard) and instances above 9 have none. It needs at least one of Ctrl, Alt and Shift and never takes Win; the key can be a digit, a letter, F1–F24, Num0–Num9, Space, Insert, Delete, Home, End, PageUp, PageDown or an arrow key.
- The manager registers them all with `RegisterHotKey`: each key once, and only while an instance using it runs with its window; when they are all off or headless, it is unregistered. It checks every 3 seconds from the processes and instance files (no adb), and at once after a setting changed. A key that cannot be registered goes into `taken` and is tried again each round, so it is registered once the other program lets go.
- A press looks at the instances using that key that run with a window: if one of them shows its window, all are hidden; if all are hidden, all are shown (as `modavd window <i> show|hide`, by whether the window shows now, whoever hid it). Hidden windows keep rendering and the screenshot channel keeps working; they have no taskbar button and keep their position and size.
- The tray icon: a left click shows every hidden window; the right-click menu has "打开管理器" (open the manager) and "退出" (exit). Before exiting (any way it exits), the hidden windows are shown first, then all boss keys are unregistered and the icon removed.
- The tray icon: a single left click restores hidden instance windows after the system double-click interval. A double click hides or shows manager windows, preserving page state, without restoring instance windows. All manager windows on the dedicated Edge profile toggle together. If none exists, a double click opens one. The right-click menu's "打开管理器" (open the manager) restores existing windows first. With the default browser fallback, a double click only opens the manager. Every orderly exit path, including the tray's "退出" (exit), cancellation, and CLI update handoff, restores hidden manager and instance windows before unregistering boss keys and removing the icon.
- The boss key field of the settings page: click the field and press the combination. A change is tried at once with `POST /api/boss-keys/check`, and once more when `PATCH` saves it; opening the settings marks a key in `taken` from `GET /api/boss-keys` with a warning icon. The field is hidden when `supported` is `false`.

`resolution` accepts only 1080 and 720. A new instance defaults to 1080; they correspond to 1920×1080 / DPI 280 and 1280×720 / DPI 240 respectively; a change keeps the orientation and takes effect at the next boot. `InstanceInfo.config.resolution` is the tier recognized from the current configured width and height; after a change while running, it means the setting for the next boot.
Expand Down
2 changes: 1 addition & 1 deletion internal/ui/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ ModAVD/ModAVD#101,只有 Windows。
- 每台实例一个老板键:`InstanceInfo.boss_key`(`"Ctrl+Alt+Shift+1"`,`""` 是不要),存在 `mas-avd.json` 的 `instances.<i>.bossKey`;没写过时编号 0–9 是 Ctrl+Alt+Shift+编号(主键盘那排数字),超过 9 没有。至少带 Ctrl、Alt、Shift 中的一个,不收 Win;按键可以是数字、字母、F1–F24、Num0–Num9、Space、Insert、Delete、Home、End、PageUp、PageDown、方向键。
- 由管理器统一 `RegisterHotKey`:同一个键只注册一次,只在用它的实例里有带窗口运行着的时候注册,都关了或都是无头的就注销。每 3 秒按进程和实例文件对一遍(不走 adb),改了设置马上对一遍。注册不上的键进 `taken`,之后每轮再试,别的程序放了就注册上。
- 按下时,这个键管的实例里带窗口运行着的:有一台的窗口显示着就全部藏起来,全都藏着就全部显示(同 `modavd window <i> show|hide`,按窗口现在显不显示,不管是谁藏的)。藏着照旧渲染、照旧能截图,任务栏上没有按钮,位置大小不变。
- 托盘图标:左键点一下把藏着的窗口都显示出来;右键菜单「打开管理器」「退出」。退出前(任何方式退出都一样)先把藏着的窗口都显示出来,再注销全部老板键、去掉图标。
- 托盘图标:左键单击等系统双击间隔过去后恢复藏着的实例窗口。双击切换管理器窗口的隐藏和显示,保留页面状态;双击不会恢复实例窗口。同一独立 Edge 配置的所有管理器窗口一起切换,没有窗口时打开一个。右键「打开管理器」优先显示已有窗口,默认浏览器回退时双击只打开管理器。任何正常退出路径(托盘「退出」、取消运行、交给命令行更新)都先恢复藏着的管理器和实例窗口,再注销老板键、去掉图标。
- 设置页的老板键一项:点一下输入框再按组合;改了先 `POST /api/boss-keys/check` 当场试一次,保存时 `PATCH` 再试一次;打开设置时按 `GET /api/boss-keys` 的 `taken` 在被占的键旁边标警告图标。`supported` 为 `false` 时不显示这一项。

`resolution` 只接受 1080 和 720。新建默认 1080,分别联动 1920×1080 / DPI 280 与 1280×720 / DPI 240;修改保留方向,下次开机生效。`InstanceInfo.config.resolution` 是按当前配置宽高识别的档位,运行中修改后表示下次开机的设置。
Expand Down
42 changes: 35 additions & 7 deletions internal/ui/bosskey.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ type trayHost interface {
Close()
}

// trayEvents are the tray's callbacks, each on a goroutine of its own.
// trayEvents are delivered in order off the native window thread.
type trayEvents struct {
hotKey func(id int)
click, open, exit func()
hotKey func(id int)
click, doubleClick, open, exit func()
}

type newTrayFunc func(tip string, ev trayEvents) (trayHost, error)
Expand Down Expand Up @@ -164,7 +164,7 @@ func (b *bossKeys) close() {
// manager then runs as before (it exits with its window) and has no boss keys.
func (s *server) startTray(newTray newTrayFunc) {
tip := "ModAVD 管理器 · " + s.root.Dir
host, err := newTray(tip, trayEvents{hotKey: s.onHotKey, click: s.onTrayClick, open: s.onTrayOpen, exit: s.onTrayExit})
host, err := newTray(tip, trayEvents{hotKey: s.onHotKey, click: s.onTrayClick, doubleClick: s.onTrayDoubleClick, open: s.onTrayOpen, exit: s.onTrayExit})
if err != nil {
s.log.Warnf("放不了托盘图标,老板键也用不了;关掉窗口管理器就退出: %v", err)
return
Expand Down Expand Up @@ -226,6 +226,9 @@ func (s *server) onHotKey(id int) {
}
s.toggleMu.Lock()
defer s.toggleMu.Unlock()
if s.trayStopped {
return
}
ctx, cancel := context.WithTimeout(s.ctx, 30*time.Second)
defer cancel()
if _, err := s.mgr.ToggleBossKey(ctx, key); err != nil && s.ctx.Err() == nil {
Expand All @@ -237,16 +240,32 @@ func (s *server) onHotKey(id int) {
func (s *server) onTrayClick() {
s.toggleMu.Lock()
defer s.toggleMu.Unlock()
if s.trayStopped {
return
}
ctx, cancel := context.WithTimeout(s.ctx, 30*time.Second)
defer cancel()
if _, err := s.mgr.ShowHiddenWindows(ctx); err != nil && s.ctx.Err() == nil {
s.log.Warnf("没能显示藏起来的窗口: %s", errs.As(err).Message)
}
}

// onTrayOpen opens a manager window, like a second launch does.
func (s *server) onTrayOpen() {
if s.base == "" {
// onTrayDoubleClick toggles every manager Edge window, or opens one when none exists.
func (s *server) onTrayDoubleClick() { s.trayManagerWindow(true) }

// onTrayOpen restores existing manager windows before opening a new one.
func (s *server) onTrayOpen() { s.trayManagerWindow(false) }

func (s *server) trayManagerWindow(toggle bool) {
s.toggleMu.Lock()
defer s.toggleMu.Unlock()
if s.trayStopped || s.base == "" {
return
}
if found, err := s.managerWindows(toggle); err != nil {
s.log.Warnf("没能切换管理器窗口: %v", err)
return
} else if found {
return
}
if _, err := s.launch(s.base + "?t=" + s.token); err != nil {
Expand All @@ -269,6 +288,15 @@ func (s *server) stopTray() {
return
}
s.toggleMu.Lock()
if s.trayStopped {
s.toggleMu.Unlock()
return
}
// Already-dispatched callbacks may arrive after Close. None may hide windows again after cleanup.
s.trayStopped = true
if _, err := s.managerWindows(false); err != nil {
s.log.Warnf("退出前没能显示管理器窗口: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
if shown, err := s.mgr.ShowHiddenWindows(ctx); err != nil {
s.log.Warnf("退出前没能显示藏起来的窗口: %s", errs.As(err).Message)
Expand Down
Loading
Loading