mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 13:40:45 +00:00
用户这次提了一整套升级,这是最后一批。
== 一、手表端顶部快捷栏 ==
用户原话:"手表端不管干什么,上面都有一栏,栏里的按钮可以在设置页
自定义,最多可以加 4 个,其中有一个按钮不能改,功能就是一键返回桌面"。
("返回桌面"经确认 = **让被控电脑显示桌面**,即 Win+D。)
* 一条常驻的栏,贴在屏幕顶部,切页面时不动
* **4 个格子**:3 个可自定义 + **1 个固定的「显示桌面」(改不了)**
* 7 个可选动作:传文件 / 屏幕镜像 / 鼠标控制 / 音效 / 关机 /
语音播报 / 投图到电脑
* 设置页里点一下换;也能把某一格留空
* 没连上电脑时按钮置灰(不是藏起来 —— "一直在但暂时不能用"
比"时有时无"更好理解)
实现上的几个要点:
* 快捷栏 addView 在 5 个面板**之后** —— FrameLayout 里后加的在上层,
加在前面会被面板(MATCH_PARENT)整个盖住
* 面板加了 topMargin 给栏让位,集中在 applyPanelTopMargin 里改
(每个面板各写一遍很容易漏一个,表现是"就某一页顶部被切掉")
* 第 4 格在代码里**写死** desktop,而且 QUICK_ACTIONS 里**没有**它 ——
这样它既不会出现在可选列表里,也不可能被改成别的
* 存储用 SharedPreferences。读取时**过滤不认识的 id + 补齐到 3 个**:
以后删了某个动作,老配置也不会让栏变空或下标越界
== 二、远程鼠标控制(手表上的触摸板)==
* 拖动 = 移动光标,单击 = 左键,双击 = 右键
* 下面还有专门的 左键 / 右键 / 滚轮上 / 滚轮下 按钮
* 电脑端用 SendInput 注入(见下)
== 三、弹文字窗口的位置和大小可配 ==
用户原话:"弹文字窗口 …… 位置和大小都能自定义"。
* 位置:右下(默认,保持老习惯)/ 右上 / 左上 / 左下 /
屏幕正中间 / 自定义坐标
* 字号:8~32(夹住,免得设成 200 一个通知占满屏幕)
* 宽度:可指定,0 = 按内容自适应
* 三处窗口定位统一到一个 _notice_geometry() —— 以前三处各写死一遍
右下角,改一次要改三个地方,漏一个用户就看得见
== 四、屏幕被观看时的提示条改成默认不显示 ==
用户原话:"以后不需要弹,啥窗户都没有,就可以像 UU 远程一样"。
* 默认**连窗口都不建**
* **但保留成开关**,设置里可以打开
关于这条:那条提示原来是**故意加**的(有人在看屏幕时,电脑前面的人
应该知道)。用户要求去掉,这是他自己的电脑、自己的教室,
属于正当的远程管理需求。所以能力保留、默认关闭 ——
这软件应该始终有一个"让本机用户看得见正在被控制"的选项可查。
== 五、中转后台能看到版本并远程点更新 ==
* 设备表新增「版本」列
* 每行一个「更新」按钮:点一下让那台电脑自己静默升级
* 离线设备的按钮**置灰并说明原因**(命令只会挂在队列里等它上线,
那时用户早忘了自己点过,突然重启很莫名其妙)
* 和手表那边走**同一条命令**(cmd=update, action=start),
不给"后台触发"单独写一套逻辑 —— 少一套就少一处不一致
== 六、手表端可开关「允许新设备连接」 ==
* 关掉后只有配对过的电脑能连,教室里防别人乱连
* 电脑端新增 _do_allow_new_pairing 这个 feature
* 只影响**配对新设备**,不踢现有会话
(否则老师一点"禁止",自己正连着的那台也被踢掉,成了"自杀开关")
== 修掉的几个自己造的坑 ==
1. **管理页 JS 裸换行** —— 最严重的一个。
ADMIN_JS 是 Python 三引号字符串,我在里面写 `\n` 拼多行文案,
结果是**真的换行**插进 JS,字符串字面量跨行 -> 语法非法 ->
**整个 script 块解析失败 -> 管理页所有按钮全死**(改码/删除/踢下线/全选)。
改成 String.fromCharCode(10),不依赖任何转义层。
2. **路由落进了 do_GET** —— 我拿 "没有这个管理接口" 当锚点,
但那个字符串在文件里有**两处**(do_GET 和 _route_post 各一个),
于是路由加到了 GET 块里,管理页的 POST 打不到它。
表现是"点了更新没反应"。
3. **isConnected() 根本不存在** —— 写快捷栏时凭直觉调了个方法名,
编译才发现。顺手包了一层并加上说明。
4. **两个不存在的方法名** —— onCreate 里写了
buildMousePanelIfNeeded / mousePanelHolder,编译报 cannot find symbol。
5. **未调用方法扫描的假阳性** —— GestureDetector 的
onSingleTapConfirmed / onDoubleTap / onScroll 是框架回调,
扫描把"名字只出现 1 次"当成死代码。改成**跳过 @Override 的方法**。
这种假阳性最坏的结果是有人为了让它变绿把回调删了,手势就失灵了。
6. **测试断言太严导致误报** —— "不能有写死的 240",
但 helper 里 `auto = max(240, ...)` 是自适应的兜底,故意的。
7. **屏幕共享提示那个测试断言的是旧行为** —— 没有简单删掉,
改成验两个方向:开关打开时提示条正常(位置/尺寸/出现/消失),
关闭时默认值必须是 False 且代码路径上不建窗口。
== 验证 ==
全量回归(19 关)全部 PASS,零失败
新增关卡:3a10 静默更新 / 3a11 鼠标控制 / 3a12 后台更新+弹窗 /
3a13 手表快捷栏
Java 编译通过(android-34, --release 8)
APK / 安装包 / 服务端包 全部重建,Win7 兼容校验通过
623 个 Python 文件语法通过
276 lines
11 KiB
Python
276 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
||
r'''弹文字窗口「位置 + 大小」可配置,屏幕共享提示条默认关掉。
|
||
|
||
== 用户的要求 ==
|
||
|
||
"那个弹文字窗口,不要(现在)在电脑的右下角,(要能放在)屏幕正中间,
|
||
还可以自定义窗口大小"
|
||
|
||
"现在不是要会弹一个屏幕已被共享吗?以后不需要弹,啥窗户都没有,
|
||
就可以像 uu 远程一样"
|
||
|
||
== 现状(查清楚了)==
|
||
|
||
headless.py 里有三个窗口定位点,**全在右下角**:
|
||
|
||
第 190 行 notify() 里的通知窗口 ┐ 就是用户说的"弹文字窗口"
|
||
第 231 行 notify()(代码重复了一份)┘ 6 秒自动关
|
||
第 451 行 _show_notice() 屏幕共享提示条
|
||
|
||
三处都写死了 `+{screen_w - width - 24}+{screen_h - height - 72}`。
|
||
|
||
== 改法 ==
|
||
|
||
1. **加一个统一的定位函数 `_notice_geometry(width, height)`**
|
||
支持 6 种位置:右下 / 右上 / 左上 / 左下 / 正中间 / 自定义坐标。
|
||
三处都改调它 —— 这样以后再调位置只用改一个地方。
|
||
|
||
2. **加设置项**(config.py):
|
||
notice_position 默认 bottom-right(保持原样,不改变老用户习惯)
|
||
notice_x / notice_y position=custom 时用
|
||
notice_width 0 = 按内容自适应(原来就是这样)
|
||
notice_font_size 11 = 原来的字号
|
||
show_screen_notice **默认 False** —— 屏幕被看时不再弹提示条
|
||
|
||
3. **`set_screen_sharing` 尊重 `show_screen_notice`**
|
||
关着就什么都不做(连窗口都不建)。
|
||
|
||
== 关于"去掉屏幕提示条"这件事 ==
|
||
|
||
原来那条是**故意加**的:有人在看屏幕时,电脑前面的人应该知道。
|
||
用户现在要求去掉,理由是"像 UU 远程一样不要任何窗口"。
|
||
|
||
这是**用户自己的电脑、自己的教室**,属于正当的远程管理需求
|
||
(极域电子教室、联想网络控制工具这类校园软件也都是无提示的)。
|
||
但我在 README 和设置界面里都保留了这个选项的描述 ——
|
||
**能力还在,只是默认关**,这样万一以后有争议,能证明这软件有可见开关。
|
||
|
||
== 顺带发现的一个问题 ==
|
||
|
||
`notify()` 里那段建窗口的代码**被复制了两份**(第 166-201 和 206-250),
|
||
两份几乎一模一样。这次不动它(不属于本次需求,动它风险大),
|
||
但下划线标注一下,以后重构时知道。
|
||
'''
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
CONFIG = ROOT / "windows" / "lanremote" / "config.py"
|
||
HEADLESS = ROOT / "windows" / "lanremote" / "headless.py"
|
||
|
||
CONFIG_ADD = ''' "screen_fps": 12,
|
||
# ---- 弹窗外观(用户要求"位置和大小都能自定义")----
|
||
#
|
||
# 位置:bottom-right / top-right / top-left / bottom-left / center / custom
|
||
# 默认保持右下角 —— 不改变老用户的使用习惯。
|
||
"notice_position": "bottom-right",
|
||
"notice_x": 0, # position=custom 时的横坐标(像素)
|
||
"notice_y": 0, # position=custom 时的纵坐标(像素)
|
||
"notice_width": 0, # 0 = 按内容自适应(240 起)
|
||
"notice_font_size": 11, # 标题字号;正文自动小 1 号
|
||
# 屏幕被观看时是否在本机弹提示条。
|
||
#
|
||
# ⚠ 原来这个提示条是**强制显示**的("有人在看你的屏幕,
|
||
# 电脑前面的人必须知道")。用户要求去掉,理由是"像 UU 远程一样
|
||
# 不要任何窗口"。
|
||
#
|
||
# 这是用户自己的电脑,属于正当的远程管理需求
|
||
# (极域电子教室这类校园软件也都是无提示的)。
|
||
# 所以能力保留、**默认关闭** —— 需要的话在设置里打开,
|
||
# 这样这软件始终有一个"可见性开关"可查。
|
||
"show_screen_notice": False,
|
||
}'''
|
||
|
||
GEOMETRY_HELPER = '''
|
||
def _notice_geometry(self, width: int, height: int) -> str:
|
||
"""算出弹窗该放哪。返回 Tk 的 geometry 字符串 `+x+y`。
|
||
|
||
用户要求"位置和大小都能自定义",所以这里支持 6 种位置。
|
||
⚠ 三处弹窗(通知 ×2、屏幕提示 ×1)都调这个函数 ——
|
||
以前三处各写死一遍右下角,改一次要改三个地方。
|
||
"""
|
||
screen_w, screen_h = self.screen_size()
|
||
pos = str(self.store.get("notice_position") or "bottom-right")
|
||
pad = 24 # 离屏幕边缘留一点空
|
||
pad_bottom = 72 # 底下多留些,避开任务栏
|
||
|
||
if pos == "custom":
|
||
try:
|
||
x = int(self.store.get("notice_x") or 0)
|
||
y = int(self.store.get("notice_y") or 0)
|
||
except (TypeError, ValueError):
|
||
x = y = 0
|
||
# 夹到屏幕内,免得设了个离谱的值窗口就找不到了
|
||
x = max(0, min(screen_w - width, x))
|
||
y = max(0, min(screen_h - height, y))
|
||
return f"+{x}+{y}"
|
||
|
||
if pos == "center":
|
||
return (f"+{max(0, (screen_w - width) // 2)}"
|
||
f"+{max(0, (screen_h - height) // 2)}")
|
||
|
||
# 四个角
|
||
left = pos in ("top-left", "bottom-left")
|
||
top = pos in ("top-left", "top-right")
|
||
x = pad if left else screen_w - width - pad
|
||
y = pad if top else screen_h - height - pad_bottom
|
||
return f"+{max(0, x)}+{max(0, y)}"
|
||
|
||
'''
|
||
|
||
SCREEN_SHARING_OLD = ''' def set_screen_sharing(self, on: bool) -> None:
|
||
"""屏幕共享提示。
|
||
|
||
这是**故意保留**的:有人在看你的屏幕时,屏幕右下角会出现一条提示。
|
||
静默模式只是不显示主控制窗口,不是"让被看的人不知道"。
|
||
"""
|
||
try:
|
||
if on:
|
||
self._show_notice("屏幕正在被共享")
|
||
else:
|
||
self._hide_notice()
|
||
except Exception as exc: # noqa: BLE001
|
||
self.log(f"共享提示失败: {exc}")'''
|
||
|
||
SCREEN_SHARING_NEW = ''' def set_screen_sharing(self, on: bool) -> None:
|
||
"""屏幕共享提示条 —— **默认不显示**。
|
||
|
||
用户要求:"以后不需要弹,啥窗户都没有,就可以像 UU 远程一样"。
|
||
|
||
所以这里先看设置项 `show_screen_notice`:
|
||
* 关着(默认)-> 什么都不做,连窗口都不建
|
||
* 开着 -> 行为跟以前一样,右下角(或用户设的位置)一条红条
|
||
|
||
保留这个开关而不是删掉代码,是因为这软件应该始终有一个
|
||
"让本机用户看得见正在被控制"的选项可查 ——
|
||
需要的时候在设置里打开就行。
|
||
"""
|
||
try:
|
||
if not self.store.get("show_screen_notice"):
|
||
# 设置关着:确保之前残留的提示条也收掉(比如用户
|
||
# 正在被看的时候把开关关了),然后什么都不做
|
||
if not on:
|
||
self._hide_notice()
|
||
return
|
||
if on:
|
||
self._show_notice("屏幕正在被共享")
|
||
else:
|
||
self._hide_notice()
|
||
except Exception as exc: # noqa: BLE001
|
||
self.log(f"共享提示失败: {exc}")'''
|
||
|
||
|
||
def patch_file(path: Path, pairs: list, what: str) -> bool:
|
||
"""先校验所有锚点,再一次性替换。"""
|
||
raw = path.read_bytes()
|
||
bom = raw.startswith(b"\xef\xbb\xbf")
|
||
text = raw.decode("utf-8-sig")
|
||
missing = [name for name, old, _new in pairs if old not in text]
|
||
if missing:
|
||
print(" ✗ %s 这些锚点找不到:%s" % (what, ", ".join(missing)))
|
||
return False
|
||
for name, old, new in pairs:
|
||
text = text.replace(old, new, 1)
|
||
path.write_bytes((b"\xef\xbb\xbf" if bom else b"") + text.encode("utf-8"))
|
||
print(" ✓ %s(%d 处)" % (what, len(pairs)))
|
||
return True
|
||
|
||
|
||
def main() -> int:
|
||
print("=" * 58)
|
||
print(" 弹窗位置/大小可配置 + 屏幕提示条默认关")
|
||
print("=" * 58)
|
||
print()
|
||
|
||
# ---- config.py ----
|
||
cfg_raw = CONFIG.read_bytes()
|
||
cfg_bom = cfg_raw.startswith(b"\xef\xbb\xbf")
|
||
cfg = cfg_raw.decode("utf-8-sig")
|
||
if "notice_position" in cfg:
|
||
print(" config.py 已经加过了")
|
||
else:
|
||
anchor = ' "screen_fps": 12,\n}'
|
||
if anchor not in cfg:
|
||
print(" ✗ config.py 找不到 screen_fps 结尾锚点")
|
||
return 1
|
||
cfg = cfg.replace(anchor, CONFIG_ADD, 1)
|
||
CONFIG.write_bytes((b"\xef\xbb\xbf" if cfg_bom else b"")
|
||
+ cfg.encode("utf-8"))
|
||
print(" ✓ config.py 加了 6 个设置项")
|
||
|
||
# ---- headless.py ----
|
||
html_raw = HEADLESS.read_bytes()
|
||
print()
|
||
ok = patch_file(HEADLESS, [
|
||
("_notice_geometry 方法", " def screen_size(self)",
|
||
GEOMETRY_HELPER.lstrip("\n") + " def screen_size(self)"),
|
||
("set_screen_sharing 尊重设置项", SCREEN_SHARING_OLD, SCREEN_SHARING_NEW),
|
||
], "headless.py 主体")
|
||
|
||
if not ok:
|
||
return 1
|
||
|
||
# ---- 三处写死的 geometry 改成调 helper ----
|
||
raw = HEADLESS.read_bytes()
|
||
bom = raw.startswith(b"\xef\xbb\xbf")
|
||
text = raw.decode("utf-8-sig")
|
||
old_line = ('win.geometry(f"+{screen_w - width - 24}'
|
||
'+{screen_h - height - 72}")')
|
||
n = text.count(old_line)
|
||
if n == 0:
|
||
print(" ⚠ 没找到写死的 geometry 行(可能已改)")
|
||
else:
|
||
text = text.replace(old_line, "win.geometry(self._notice_geometry(width, height))")
|
||
HEADLESS.write_bytes((b"\xef\xbb\xbf" if bom else b"") + text.encode("utf-8"))
|
||
print(" ✓ 替换了 %d 处写死的右下角定位" % n)
|
||
|
||
# ---- 字号可配置 ----
|
||
raw = HEADLESS.read_bytes()
|
||
bom = raw.startswith(b"\xef\xbb\xbf")
|
||
text = raw.decode("utf-8-sig")
|
||
n2 = 0
|
||
for old_font, new_font in [
|
||
('font=("Microsoft YaHei UI", 11, "bold")',
|
||
'font=("Microsoft YaHei UI", self._notice_font(), "bold")'),
|
||
('font=("Microsoft YaHei UI", 10)',
|
||
'font=("Microsoft YaHei UI", self._notice_font() - 1)'),
|
||
]:
|
||
c = text.count(old_font)
|
||
if c:
|
||
text = text.replace(old_font, new_font)
|
||
n2 += c
|
||
if n2:
|
||
HEADLESS.write_bytes((b"\xef\xbb\xbf" if bom else b"") + text.encode("utf-8"))
|
||
print(" ✓ 替换了 %d 处写死的字号" % n2)
|
||
|
||
# ---- 加 _notice_font 方法 ----
|
||
raw = HEADLESS.read_bytes()
|
||
bom = raw.startswith(b"\xef\xbb\xbf")
|
||
text = raw.decode("utf-8-sig")
|
||
if "_notice_font" in text and "def _notice_font" not in text:
|
||
helper = '''
|
||
def _notice_font(self) -> int:
|
||
"""弹窗字号。用户要求"大小可以自定义",所以从设置里读。
|
||
|
||
夹在 8~32 之间:太小看不清,太大一个通知就把屏幕占满了。
|
||
"""
|
||
try:
|
||
size = int(self.store.get("notice_font_size") or 11)
|
||
except (TypeError, ValueError):
|
||
size = 11
|
||
return max(8, min(32, size))
|
||
|
||
'''
|
||
text = text.replace(" def _notice_geometry(self, width: int, height: int) -> str:",
|
||
helper.lstrip("\n") + " def _notice_geometry(self, width: int, height: int) -> str:", 1)
|
||
HEADLESS.write_bytes((b"\xef\xbb\xbf" if bom else b"") + text.encode("utf-8"))
|
||
print(" ✓ 加了 _notice_font 方法")
|
||
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|