Compare commits

...

54 Commits

Author SHA1 Message Date
linshenjianlu
d93419eccc docs: 更新 README — 移除图标分类标签、自适应布局、2026-04-21 更新记录 2026-04-21 11:24:18 +08:00
linshenjianlu
b2c7cae355 feat(icon picker): 移除 ShortX 图标选择器的分类标签,简化 UI 布局
- 删除新版弹出框的 tabDefs / tabsScroll / tabsRow / refreshTabs
- 简化 filterCatalog,仅保留搜索过滤
- 删除旧版 shortxPickerState 的 activeTab / tabButtons 及关联逻辑
- 删除 getShortXIconCategory / shortxTabDefs / applyShortXTabStyles
- 调整 headerH 从 136dp 降至 110dp,腾出更多图标展示空间
- 修复删除 tabs 代码时遗留的重复 addView(searchEt) 导致 IllegalStateException
2026-04-21 11:21:30 +08:00
linshenjianlu
7d879527f4 fix(entry): sha256File 返回值转为 JS 原生字符串,修复 Rhino 中 Java String !== 比较引用导致的 SHA256 误判 2026-04-21 09:59:36 +08:00
linshenjianlu
e89eb9a904 fix(entry): 关键模块失败时暴露真实错误,__out 增加核心依赖前置检查 2026-04-21 09:57:17 +08:00
linshenjianlu
83444ba683 fix: 回滚 th_01_base.js 到原始版本排查 getProcessInfo 未定义问题 2026-04-21 08:32:17 +08:00
linshenjianlu
ed3ee6eaf6 fix(ToolHub): 关键模块加载失败时不再直接 throw,而是通过 __out 返回详细错误信息 2026-04-21 08:24:38 +08:00
linshenjianlu
fee6e0bc40 fix(ToolHub): 增强 loadScript 错误日志输出,方便排查入口报错原因 2026-04-21 08:11:40 +08:00
linshenjianlu
c32f29fdb7 fix: 回滚 th_16_entry.js 修改以排查入口报错 2026-04-21 08:08:40 +08:00
linshenjianlu
9ad01b436d fix: 代码审查6项修复
- 689处空catch块补全日志
- eval远程代码增加SHA256校验
- 删除ToolHubLogger重复定义
- getParentFile()增加null保护
- 提取buildButtonEditorPanelView内通用工具函数到文件级
- 修复HandlerThread/ValueAnimator资源泄漏
2026-04-21 07:42:23 +08:00
Hermes
7e23cd95a1 docs(readme): 更新 2026-04-20 更新记录,补充 bug 修复与代码清理详情 2026-04-20 23:15:43 +08:00
Hermes
a394684f42 refactor(ball): 清理悬浮球文字相关死代码 2026-04-20 22:40:39 +08:00
Hermes
068e4cdd3b feat(ball-settings): 统一图标弹窗并精简球显示 2026-04-20 22:32:31 +08:00
Hermes
256a8f716b fix(th_14_panels): 修复颜色弹窗确认时 recentGrid 引用异常 2026-04-20 21:46:14 +08:00
Hermes
9a0b618409 fix(th_14_panels): 强制应用弹窗选中的图标颜色 2026-04-20 21:42:30 +08:00
Hermes
f89ed88033 fix(th_14_panels): 改为独立维护 ShortX 图标颜色状态 2026-04-20 21:36:15 +08:00
Hermes
3c8c337401 fix(th_14_panels): 修复 ShortX 图标调色板确认后未应用颜色 2026-04-20 21:30:03 +08:00
Hermes
4ab42b274a fix(color-picker): 修复颜色参数回写失败问题
根因: Rhino JS 闭包中局部变量捕获异常,onSelect 回调无法访问 inputShortXIconTint

修复:
- 将输入框引用存储到 self.state._btnEditorTintInput,绕过闭包问题
- onSelect 回调从 self.state 读取输入框,同时直接更新 targetBtn.iconTint 作为双保险
- 保存按钮优先使用 targetBtn.iconTint,回退到输入框 getValue()
2026-04-20 21:00:13 +08:00
Hermes
d03f4d677f fix(color-picker): 修复颜色面板确认后无回写问题
- 确认按钮添加 try-catch,确保 onSelect 异常不阻断 closePopup
- finalColor 强制 String() 转换,避免 undefined 传入 Java setText
- onSelect 回调添加异常隔离 + invalidate() 强制刷新
- 同步两个调用点(颜色按钮 + 调色板选择按钮)
2026-04-20 20:51:28 +08:00
Hermes
86a4ed7927 feat: 弹出式颜色选择器 + 图标预览旁颜色入口
- showColorPickerPopup: 新增独立弹窗颜色选择器,支持最近使用颜色(8个)、RGB滑块实时调色、常用色网格、透明度滑块
- 图标预览区实时反馏,确定后自动保存到最近列表
- 在 ShortX 图标预览卡片旁新增“颜色”按钮,直接打开颜色选择器
- 更新 README 文档
- 修复调色板卡片未加入 form 的问题
2026-04-20 20:32:06 +08:00
Hermes
99547894b6 fix(th_14_panels): 修复ShortX图标选择器onSelect ReferenceError
删除未定义的 inputShortXIconName 引用,解除 ReferenceError 导致的回调中断,
使 updateShortXIconPreview() 能正常触发预览框刷新
2026-04-20 19:58:08 +08:00
Hermes
a39ecaf648 fix(th_14_panels): 修复ShortX图标选择器确认后预览框未刷新问题
- 将previewNameTv.setText提前到inputShortXIconTint.getValue()之前
- 为tintHex读取添加独立try-catch,避免异常阻断预览文本更新
- 为updateShortXIconPreview添加错误日志输出
2026-04-20 19:51:35 +08:00
Hermes
e5d540d1a5 fix(icon): 确定按钮绕过 guardClick 冷却,避免被其他按钮点击拦截
- createSolidButton/createFlatButton 内部使用 guardClick('ui_btn') 共享同一个 key
- 如果用户在短时间内点击了多个按钮(分类/翻页/关闭),确定按钮的点击可能被忽略
- 确定按钮现在直接创建 TextView + OnClickListener,不经 guardClick
- 添加 try-catch 捕获 onSelect 回调异常
2026-04-20 19:33:51 +08:00
Hermes
c67c0ccaf2 refactor(icon): 重写图标选择器为自适应布局 + WindowManager 直接创建
- 不再使用 showPopupOverlay,避免嵌套弹窗导致尺寸限制
- 弹窗宽度 = 屏幕宽度*0.92,自适应屏幕
- 列数、单元格宽度、每\u9875\u56fe\u6807\u6570\u6839\u636e\u5f39\u7a97\u5c3a\u5bf8\u81ea\u9002\u5e94\u8ba1\u7b97
- 网\u683c\u9ad8\u5ea6 = \u5c4f\u5e55\u9ad8\u5ea6*0.55,\u4fdd\u8bc1\u53ef\u89c1\u533a\u57df
2026-04-20 19:21:26 +08:00
Hermes
6e7a2fef7a fix(icon): 稳定翻页图标选择器,去掉 grid.post 动态测量依赖
- 固定 4 列 20 个/页,避免首次渲染时尺寸为 0 导致空白
- 去掉 resolvePageSize 动态计算
- 翻页按钮状态正确更新
2026-04-20 19:10:40 +08:00
Hermes
e07bd7e16f fix(icon): 添加资源 ID 遍历反射策略,绕过 R$drawable 混淆问题获取 ShortX 图标库
策略优先级:
1. 反射 R$drawable 类(未混淆时)
2. 直接遍历资源 ID 2131230000~2131240000(绕过混淆)
3. APK 文件扫描(最后兜底)
2026-04-20 19:08:01 +08:00
Hermes
f1b8f0a921 fix(icon): createRipple → createRippleDrawable 修复图标网格渲染崩溃
日志显示:Cannot find function createRipple in object [object Object]
原因:ui 对象上只有 createRippleDrawable,没有 createRipple。
修复:使用 self.ui.createRippleDrawable(card, pressed, radius) 替代。
2026-04-20 18:13:03 +08:00
Hermes
7c350724f6 修复图标选择器网格空白:移除 grid.post 依赖,直接使用默认尺寸渲染
问题:弹窗能打开但图标列表不显示,网格内容为空。
原因:grid.post Runnable 在后台线程中可能未执行或执行延迟。
解决:直接传入默认尺寸 (300dp x 360dp) 计算 pageSize,
然后直接调用 renderGrid(),不再等待 grid 布局完成。
2026-04-20 18:08:23 +08:00
Hermes
f13f3268c3 fix(icon): 添\u52a0 getShortXIconDrawable 方法和占位符,\u4fee\u590d\u56fe\u6807\u4e0d\u663e\u793a
- 补\u5145\u7f3a\u5931的 getShortXIconDrawable() 方法
- 图\u6807\u83b7\u53d6\u5931\u8d25\u65f6\u663e\u793a\u6d45\u8272\u5360\u4f4d\u7b26
2026-04-20 17:59:33 +08:00
Hermes
5036f48772 fix(icon): 增强诊\u65ad和\u5907\u7528\u56fe\u6807\u5217\u8868,\u4fee\u590d ShortX \u56fe\u6807\u4e0d\u663e\u793a
- APK 扫描增\u52a0 mipmap* 目\u5f55\u652f\u6301
- 增\u52a0诊\u65ad信\u606f\u663e\u793a(APK 路\u5f84、\u6587\u4ef6\u6570、\u9519\u8bef\u539f\u56e0)
- 当\u6240\u6709\u81ea\u52a8\u83b7\u53d6\u5931\u8d25\u65f6,\u56de\u9000\u5230 20 \u4e2a\u5e38\u7528\u5907\u7528\u56fe\u6807\u5217\u8868
2026-04-20 17:56:40 +08:00
Hermes
ec64ef07d9 fix(icon): 宽松 ShortX 图标库匹配规则,修复图标不显示问题
- 将 APK 扫描正则从 ic_remix_ 放宽到所有 ic_ 开头的图标
- 反射获取 R 时接受所有 ic_ 开头的字段,不再限\u5b9a ic_remix_
- 弹\u7a97\u9009\u62e9\u5668\u589e\u52a0\u9519\u8bef\u63d0\u793a\u548c\u5f3a\u5236\u91cd\u8bd5\u673a\u5236
2026-04-20 17:48:14 +08:00
Hermes
f2714d5101 feat: 调色板改为弹出式 UI,支持图标颜色预览
- 按钮编辑器中调色板改为弹出窗口,不再嵌入表单
- 添加 showColorPickerPopup(),支持图标实时预览着色效果
- 包含 21 色快捷网格、RGB 滑块、透明度滑块
- 选中后自动填回表单
2026-04-20 17:40:04 +08:00
Hermes
273c299ea0 feat: ShortX 图标选择器改为弹出式 UI
- 按钮编辑器中 ShortX 图标选择改为弹出窗口,不再嵌入表单
- 添加 showPopupOverlay() 通用弹窗框架,支持搜索/分类/分页/点选
- 点击选择图标按钮直接打开弹窗,选中后自动填回表单
2026-04-20 17:35:04 +08:00
Hermes
c68f0d1209 fix(ui): 修复 GridLayout.LayoutParams 构造函数参数错误导致设置面板打不开
- GridLayout.LayoutParams 不支持 (width, height) 双参数构造
- 改用无参构造 + 分别设置 width/height 属性
2026-04-20 16:34:19 +08:00
Hermes
b5e7695d1a fix(ui): 将设置面板的图标和颜色选择器改为内嵌式,修复 AlertDialog 在后台无法弹出问题
- ball_shortx_icon: 使用内嵌 ListView + 搜索框替代 AlertDialog
- ball_color: 使用内嵌调色板网格 + 输入框替代 AlertDialog
- 添加展开/收起按钮,与按钮编辑器的交互模式保持一致
- 移除所有 AlertDialog.Builder 调用(在 ShortX 后台服务环境无法显示)
2026-04-20 16:24:20 +08:00
Hermes
f2c80112c1 feat(settings): 设置面板改造,悬浮球图标支持 ShortX 选择器与调色板
- 从设置页面移除“执行与查看器”、“悬浮球文字/大小/颜色”设置项
- BALL_ICON_RES_NAME 改为 ball_shortx_icon 类型,支持弹窗列表+搜索选择 ShortX 图标
- BALL_ICON_TINT_HEX 改为 ball_color 类型,支持调色板选色+手动输入
- 移除无用的 BALL_ICON_PKG 通用文本输入(仅保留 file 路径输入作为兜底)
- 同步更新 README Changelog
2026-04-20 16:10:17 +08:00
Hermes
2bbe9c3609 docs: 添加更新记录(Changelog)
- 按天归档,同一天多次提交合并为一条
- 记录2026-04-20全部更新:自适应布局、分页图标选择器、默认收起、返回信息中文化
2026-04-20 15:26:03 +08:00
Hermes
26e1ebefef docs: 更新 README 图标选择器交互说明
- 取消“切到 ShortX 图标时自动展开图标库”的描述
- 增加“默认收起,点击展开按钮后手动打开”的说明
- 补充选中图标后自动回填并收起的交互要点
2026-04-20 15:19:22 +08:00
Hermes
b61c27ee85 fix(panels): 图标库和调色板默认收起
- 调色板展开状态不再从 saved state 恢复,默认始终收起
- 调色板 body 初始可见性设为 GONE,避免创建时显示
- 移除切换到 ShortX 图标模式时自动展开图标库的逻辑
- 保留展开/收起按钮,用户手动点击才展开
2026-04-20 14:55:08 +08:00
Hermes
ffbbcf647f feat(panels): 常用颜色改为自适应布局,图标选择器分页优化
- 常用颜色网格从固定 4x5 改为根据屏幕宽度自动计算列数
  (与 ShortX 图标列表一致的 adaptive layout)
- 图标浮窗选择器改为翻页模式,按当前可见容量动态分页
- 修复图标选择器关闭后无法再次打开的问题
- 入口返回信息增加中文 msg/syncMsg 及 updatedModules 字段
- 同步更新 README 文档
2026-04-20 14:48:01 +08:00
Hermes Bot
fbb8b6e682 refactor: 常用颜色按色相自动排序 2026-04-20 14:37:37 +08:00
Hermes Bot
4a9aff570c feat: 图标调色板增加 RGB 调色器 2026-04-20 14:26:38 +08:00
Hermes Bot
8d02291a27 feat: 升级图标调色板并移除名称输入 2026-04-20 14:16:09 +08:00
Hermes Bot
4a91b9631f feat: 按钮图标颜色增加快捷色板 2026-04-20 13:55:36 +08:00
Hermes Bot
313688275b refactor: 图标选择器按宽度自动排列 2026-04-20 13:45:37 +08:00
Hermes Bot
6bf2be1bf1 fix: 同步入口 ToolHub.js 与本地版本 2026-04-20 13:38:03 +08:00
Hermes Bot
96275e8028 feat: 优化图标分页选择器与中文返回信息 2026-04-20 13:34:28 +08:00
Hermes Bot
2022c2d1c1 feat: 图标选择器翻页+入口中文摘要
- ToolHub.js: __out 返回添加中文摘要字段(状态/按钮数量/面板布局/日志/崩溃处理/线程模型等)
- th_07_shortcut.js: showIconPicker 改为翻页模式,动态计算每页图标数
- th_07_shortcut.js: 修复关闭后再点击打不开的问题(api.show 不提前设置 state.hidden)
2026-04-20 13:19:31 +08:00
Hermes Bot
034ed61e22 feat: ShortX 图标库选择器 + 入口更新信息返回
- th_07_shortcut.js: 新增 showIconPicker() 多宫格自适应图标选择器
- th_14_panels.js: 按钮编辑器内联图标库改为浮窗选择器
- ToolHub.js: 返回 JSON 新增 updates/errors 字段
2026-04-20 13:07:30 +08:00
root
9ef8bdae40 refactor: replace version-based update with HEAD Last-Modified check
- Remove MODULE_MANIFEST and getFileVersion()
- Add getRemoteLastModified() via HTTP HEAD
- Add getLocalLastModified() / saveLocalLastModified() caching to .lm_* files
- loadScript now checks remote Last-Modified against local cache on every run
- File missing -> download; File exists + remote newer -> download; otherwise skip
- Network failure gracefully falls back to local file
- Entry file no longer needs version bumps when submodules update
2026-04-20 12:41:13 +08:00
root
9b07a888a5 chore: bump version for shell refactor (1.0.0 -> 1.0.1)
- th_01_base.js: 1.0.1 (Base64 decode comment cleanup)
- th_10_shell.js: 1.0.1 (remove Action-first path, keep only broadcast bridge)
- th_11_action.js: 1.0.1 (toast msg update)
- ToolHub.js: MODULE_MANIFEST synced
2026-04-20 12:32:55 +08:00
root
40a60a6912 refactor: remove Action-first shell path, keep only broadcast bridge
- th_10_shell.js: clean up outdated comments, title now 'Shell: broadcast execution'
- th_11_action.js: update toast msg from 'Action + bridge both failed' to 'broadcast bridge send failed'
- th_01_base.js: remove 'Action-first path' reference in Base64 decode comment
- th_16_entry.js already has useActionFirst: false / hasShellCommand: false
2026-04-20 12:29:04 +08:00
root
e375f7d7ba docs: update README for 16-module architecture
- Sync directory tree with new th_01 ~ th_16 file listing
- Update module descriptions and line counts
- Update load order and manifest count (5 -> 16)
- Update deployment notes
2026-04-20 12:11:11 +08:00
root
c7e9b92322 refactor: split th_2_core.js into 12 modules, rename all files to 2-digit numbering
- Split th_2_core.js (4715 lines, 177KB) into:
  th_02_core.js, th_03_icon.js, th_04_theme.js, th_05_persistence.js,
  th_06_icon_parser.js, th_07_shortcut.js, th_08_content.js,
  th_09_animation.js, th_10_shell.js, th_11_action.js,
  th_12_rebuild.js, th_13_panel_ui.js
- Rename existing: th_1_base→th_01_base, th_3_panels→th_14_panels,
  th_4_extra→th_15_extra, th_5_entry→th_16_entry
- Update ToolHub.js MODULE_MANIFEST, modules array, and critical module check
2026-04-20 11:53:13 +08:00
root
c64d4c336b chore: 更新 git 仓库链接 chenziran -> linshenjianlu 2026-04-20 11:03:36 +08:00
19 changed files with 8187 additions and 5445 deletions

291
README.md
View File

@@ -1,6 +1,6 @@
# ShortX ToolHub # ShortX ToolHub
一个模块化的 ShortX JS 浮窗工具框架,支持广播关闭、子线程模型、日志记录、自动下载与热更新 一个模块化的 ShortX JS 浮窗工具框架,支持子模块自动下载、按 `Last-Modified` 热更新、启动日志记录,以及面向大图标库的分页式 ShortX 图标选择器
--- ---
@@ -10,150 +10,241 @@
``` ```
shortx.getShortXDir()/ shortx.getShortXDir()/
├── ToolHub/ ├── ToolHub/
── code/ ── code/
├── th_1_base.js ├── th_01_base.js
├── th_2_core.js ├── th_02_core.js
├── th_3_panels.js ├── th_03_icon.js
├── th_4_extra.js ├── th_04_theme.js
── th_5_entry.js ── th_05_persistence.js
└── ToolHub/logs/ │ │ ├── th_06_icon_parser.js
── init.log │ │ ── th_07_shortcut.js
│ │ ├── th_08_content.js
│ │ ├── th_09_animation.js
│ │ ├── th_10_shell.js
│ │ ├── th_11_action.js
│ │ ├── th_12_rebuild.js
│ │ ├── th_13_panel_ui.js
│ │ ├── th_14_panels.js
│ │ ├── th_15_extra.js
│ │ └── th_16_entry.js
│ └── logs/
│ └── init.log
``` ```
### 服务器(项目维护目录) ### 服务器(项目维护目录)
``` ```
ToolHub/ ToolHub/
├── ToolHub.js # 入口文件(粘贴到 ShortX 任务) ├── ToolHub.js
├── README.md
└── code/ └── code/
├── th_1_base.js ├── th_01_base.js
├── th_2_core.js ├── th_02_core.js
├── th_3_panels.js ├── th_03_icon.js
├── th_4_extra.js ├── th_04_theme.js
── th_5_entry.js ── th_05_persistence.js
├── th_06_icon_parser.js
├── th_07_shortcut.js
├── th_08_content.js
├── th_09_animation.js
├── th_10_shell.js
├── th_11_action.js
├── th_12_rebuild.js
├── th_13_panel_ui.js
├── th_14_panels.js
├── th_15_extra.js
└── th_16_entry.js
``` ```
--- ---
## 部署步骤 ## 部署步骤
### 1. 创建目录(可省略) ### 1. 放置入口文件
`ToolHub.js` 内容粘贴到 ShortX 任务中。
入口文件会自动检测并创建 `ToolHub/code/` 目录,无需手动操作。 ### 2. 直接运行
入口文件会自动:
1. 检查并创建 `ToolHub/code/`
2. 修复目录权限(`chmod 700` + `chown 1000:1000`
3. 对 16 个子模块做 HEAD 检测
4. 缺失或有更新时自动下载覆盖
5. 记录启动日志到 `ToolHub/logs/init.log`
若需手动创建,在 ShortX 数据根目录下执行: ---
```bash
mkdir -p ToolHub/code
chmod 700 ToolHub/code
chown 1000:1000 ToolHub/code
```
### 2. 放置入口文件 ## 返回信息格式
`ToolHub.js` 的内容粘贴到 ShortX 任务中。 成功启动示例:
子模块会自动从 git 仓库下载到 `ToolHub/code/`,无需手动复制。
### 3. 运行
执行 ShortX 任务,正常返回示例:
```json ```json
{ {
"ok": true, "ok": true,
"started": true, "started": true,
"msg": "已按 WM 专属 HandlerThread 模型启动", "msg": "ToolHub 启动成功:已按 WM 专属 HandlerThread 模型启动",
"syncMsg": "本次已覆盖更新 2 个子模块(新增 0 / 覆盖 2th_14_panels.js、th_16_entry.js",
"updatedCount": 2,
"updatedModules": ["th_14_panels.js", "th_16_entry.js"],
"closeAction": "shortx.wm.floatball.CLOSE", "closeAction": "shortx.wm.floatball.CLOSE",
"layout": {"cols": 2, "rows": 2} "layout": {"cols": 2, "rows": 2}
} }
``` ```
--- 无子模块更新时:
```json
## 自动下载与权限管理 {
"ok": true,
入口文件启动时会自动完成以下操作: "started": true,
"msg": "ToolHub 启动成功:已按 WM 专属 HandlerThread 模型启动",
1. **缺失自检**:检查 `ToolHub/code/` 下的 5 个模块文件,缺失则从 git raw URL 自动下载 "syncMsg": "子模块已是最新,本次未覆盖更新。",
2. **权限保障**:目录不存在时自动创建并设置 `chmod 700` + `chown 1000:1000` "updatedCount": 0,
3. **权限判断**:通过 `stat` 命令精确检查 uid/gid/mode不正确才修复 "updatedModules": [],
4. **单次检查**:一次启动中只检查一次目录权限,避免重复 shell 开销 "closeAction": "shortx.wm.floatball.CLOSE",
"layout": {"cols": 2, "rows": 2}
--- }
## 版本管理与热更新
每个模块文件第一行必须包含版本注释:
```javascript
// @version 1.0.0
``` ```
入口文件中的 `MODULE_MANIFEST` 定义各模块的期望版本。启动时若本地版本与期望版本不匹配,自动重新下载。 如果有非关键模块加载失败,会额外返回:
- `loadMsg`
- `loadErrors`
升级模块时只需 如果启动失败,会额外返回
1. 更新模块文件中的 `@version` 版本号 - `err`
2. 同步更新 `ToolHub.js``MODULE_MANIFEST` 的对应版本号
3. 推送到 git 仓库
4. 实机下次启动时自动检测并更新
--- ---
## 下载校验 ## 子模块热更新机制
- **大小校验**:对比 HTTP `Content-Length` 与实际写入字节数,不匹配则抛异常 当前机制不是版本号比对,而是:
- **内容校验**:读取下载文件前 200 字节,检测 `<!DOCTYPE``<html`,防止下到 404/502 错误页面 - 对每个子模块发送 HTTP HEAD 请求
- 读取远端 `Last-Modified`
- 与本地 `.lm_模块名` 缓存比较
- 不一致则重新下载覆盖
- 网络检查失败时优先回退到本地已有文件
因此:
- **入口文件无需维护 `MODULE_MANIFEST`**
- 模块里的 `@version` 仅可作为人工注释,不参与程序判断
---
## ShortX 图标选择器优化
按钮编辑页里的 ShortX 图标选择器已改为:
- **ShortX 图标名称编辑框已取消**,改为预览卡片 + 图标库点选
- **分页模式**,不再一次性塞入大批图标
- **图标列表按当前可用宽度自动排列列数,并结合可见高度计算每页容量**
- **自适应布局**:弹窗宽度、单元格尺寸、卡片高度均按屏幕比例自动计算,最大化利用可见空间
- 保留 **搜索 / 上一页 / 下一页**
- 选中图标后自动填回并收起
- 收起后再次点击 **展开图标库** 可正常重新打开
当前交互要点:
1. 图标库默认收起,点击 **展开图标库** 后打开
2. 列数按当前可用宽度自动计算,屏幕更宽时一页可显示更多图标
3. 每页容量按自动列数 × 当前可见行数实时计算,页面底部无空白
4. 搜索、翻页时都会回到顶部,减少卡顿感和误触
5. 选中图标后自动填回并收起图标库
## 弹出式颜色选择器
点击 **选择颜色** 按钮弹出独立颜色选择器弹窗:
- **图标预览区**:实时显示当前图标着色效果
- **最近使用**:最多 8 个,点击直接复用,自动去重并置顶
- **常用颜色**21 色固定网格7 列),点击直接选中
- **RGB 三色调色器**:红/绿/蓝各 0-255 拖动进度条,实时同步预览
- **透明度滑块**A 通道 0-255支持半透明图标着色
- **清空按钮**:一键恢复跟随主题
- **确定按钮**:确认选择并将颜色加入最近使用列表
交互要点:
1. 拖动 RGB 滑块时,预览图标实时变色,当前颜色值文本实时更新
2. 点击常用色或最近色后RGB 滑块自动同步到对应值
3. 清空后滑块重置为 255/255/255/255白/不透明)
4. 最近颜色持久化存储,跨会话保留
--- ---
## 日志系统 ## 日志系统
### 启动日志 启动日志路径:
路径:`shortx.getShortXDir() + "/ToolHub/logs/init.log"` ```text
shortx.getShortXDir() + "/ToolHub/logs/init.log"
记录内容:
- 目录创建/权限修复
- 模块下载开始/结束/异常
- 版本不匹配
- 模块加载失败
- 模块体积超阈警告(>200KB
### 运行日志
路径:`shortx.getShortXDir() + "/ToolHub/logs/"`
日志文件按天分割,默认保留 3 天。
---
## 关闭浮窗
通过 adb 或 ShortX Shell 执行:
```bash
am broadcast -a shortx.wm.floatball.CLOSE
``` ```
--- 记录内容包括:
- 目录创建 / 权限修复
## 模块说明 - 更新检测
- 下载开始 / 结束 / 异常
| 文件 | 职责 | 线数参考 | - 模块加载失败
|------|------|---------| - 模块体积告警(>200KB
| `th_1_base.js` | 基础工具函数、Logger、崩溃处理、进程信息获取 | ~1300 |
| `th_2_core.js` | 浮窗管理器、Shell 执行器、ContentProvider 读取器、图标缓存 | ~4700 |
| `th_3_panels.js` | 面板配置工厂、按钮构建器、对话框、文本查看器 | ~2900 |
| `th_4_extra.js` | 额外面板:设备信息、网络状态、快捷操作 | ~1600 |
| `th_5_entry.js` | 入口面板定义、广播接收器注册、启动流程 | ~300 |
--- ---
## 模块加载容错 ## 模块职责
- `for` 循环加载 5 个模块,单模块失败记录日志但不阻断后续加载 | 文件 | 职责 |
- `th_5_entry.js` 失败时直接抛异常(启动必备) |------|------|
- 错误信息落盘到 `ToolHub/logs/init.log`,便于实机排查 | `th_01_base.js` | 基础工具、日志、配置校验、通用辅助 |
| `th_02_core.js` | `FloatBallAppWM` 构造、基础状态与核心工具 |
| `th_03_icon.js` | 图标缓存、Bitmap 管理、悬浮球图标加载 |
| `th_04_theme.js` | 主题、颜色、样式工具 |
| `th_05_persistence.js` | 持久化与设置数据层 |
| `th_06_icon_parser.js` | 图标解析、ShortX 内置图标扫描与回退 |
| `th_07_shortcut.js` | 快捷方式选择器 |
| `th_08_content.js` | ContentProvider 读取与通用 query |
| `th_09_animation.js` | 面板动画、吸边、显示/隐藏管理 |
| `th_10_shell.js` | Shell 执行层 |
| `th_11_action.js` | 按钮动作分发与执行 |
| `th_12_rebuild.js` | 悬浮球重建逻辑 |
| `th_13_panel_ui.js` | 设置面板通用 UI 组件 |
| `th_14_panels.js` | 设置面板、按钮编辑器、ShortX 图标分页选择器 |
| `th_15_extra.js` | 主面板与附加展示层 |
| `th_16_entry.js` | 生命周期、广播注册、启动与销毁 |
--- ---
## 注意事项 ## 注意事项
- 入口文件通过 `loadScript()` 动态加载子模块,`var` 声明通过间接 `eval` 挂到全局作用域 - 入口文件只负责加载与汇总返回信息,不承载业务 UI
- 子模块加载顺序不可更改base → core → panels → extra → entry - `th_16_entry.js` 属于关键模块,加载失败会直接中止启动
- 调试请查看日志文件,不通过返回 JSON 暴露内部细节 - 不建议把调试细节直接塞进返回 JSON优先写日志返回信息只保留用户判断启动/更新所需的关键信息
- 单个模块建议不超过 200KB超过时启动日志会记录 WARN 提示拆分 - 若修改了模块结构、返回字段、图标选择器交互,记得同步更新 README 与相关技能说明
---
## 更新记录
> 规则:按天归档,同一天多次提交合并为一条。
### 2026-04-21
**功能改进**
- **移除 ShortX 图标选择器分类标签**:顶部水平滚动分类标签已删除,简化 UI图标列表仅保留搜索和分页
- **自适应布局参数调整**:弹窗宽度上限提升至 520dp单元格最小宽度降至 52dp卡片高度固定为屏幕高度 88%,滚动区域自动填充剩余空间
- **提升每页容量**:去掉 1240 的人为限制,行数按实际可见高度计算,屏幕更大时每页可显示更多图标
- **去除分类过滤逻辑**:旧版内嵌选择器的分类相关代码、函数、状态已彻底清理
**Bug 修复**
- **修复面板显示崩溃**:删除 tabs 代码时遗留了一行重复的 `addView(searchEt)`,导致旧版图标选择器构建时抛出 `IllegalStateException: already has a parent`
### 2026-04-20
**Bug 修复**
- **修复 ShortX 图标调色板确认后未应用颜色**:颜色弹窗确认回调未同步触发 ShortX 图标预览链路,新增统一入口 `applyTintSelectionFromPopup()` 规范化处理
- **修复颜色弹窗确认时 `recentGrid` 引用异常**`showColorPickerPopup()` 中最近颜色网格重建后未更新局部引用,改为延迟初始化并缓存 `recentGrid` 引用
- **改为独立维护 ShortX 图标颜色状态**:图标颜色与悬浮球颜色分离存储,避免互相覆盖
- **强制应用弹窗选中的图标颜色**:确保弹窗选色结果立即写入按钮编辑状态并刷新预览
**功能改进**
- **图标库与调色板** 默认收起,打开编辑面板时不再自动展开
- **常用颜色** 改为自适应布局,根据当前可用宽度自动计算列数(与 ShortX 图标列表一致)
- **ShortX 图标浮窗选择器** 改为分页模式,按当前可见容量动态计算每页图标数量
- 修复图标选择器关闭后无法再次打开的问题
- 入口返回信息增加中文 `msg` / `syncMsg``updatedModules` 字段
- 优化入口返回信息格式
- **设置面板改造**:取消“执行与查看器”、“悬浮球文字/大小/颜色”设置项
- **悬浮球图标配置** 支持 ShortX 图标选择器(弹窗列表+搜索)和调色板拖拽选色
- **弹出式颜色选择器**新增独立弹窗支持最近使用颜色8 个、RGB 实时调色、常用色网格、透明度滑块,最近颜色持久化存储
**代码清理**
- **统一图标弹窗**:按钮编辑与悬浮球设置复用同一套 `showShortXIconPickerPopup``showColorPickerPopup`
- **清理悬浮球文字相关死代码**:删除悬浮球文字开关、文字大小、文字颜色等 UI 渲染与持久化残留

View File

@@ -1,34 +1,76 @@
// ToolHub - 入口文件 (加载子模块并执行) // ToolHub - 入口文件 (加载子模块并执行)
// 将本文件放入 ShortX 任务,th_*.js 放入 ShortX 数据根目录/ToolHub/code/ 文件夹 // 将本文件粘贴到 ShortX 任务,子模块会自动从 git 下载到 ToolHub/code/
// 更新机制HEAD 请求对比 Last-Modified入口文件无需更新版本号
var MODULE_MANIFEST = { var GIT_BASE = "https://git.xin-blog.com/linshenjianlu/ShortX_ToolHub/raw/branch/main/code/";
"th_1_base.js": "1.0.0",
"th_2_core.js": "1.0.0",
"th_3_panels.js": "1.0.0",
"th_4_extra.js": "1.0.0",
"th_5_entry.js": "1.0.0"
};
var GIT_BASE = "https://git.xin-blog.com/chenziran/ShortX_ToolHub/raw/branch/main/code/";
var __dirChecked = false; var __dirChecked = false;
function getLogPath() { function getLogPath() {
return shortx.getShortXDir() + "/ToolHub/logs/init.log"; return shortx.getShortXDir() + "/ToolHub/logs/init.log";
} }
function getLmPath(relPath) {
return shortx.getShortXDir() + "/ToolHub/code/.lm_" + relPath;
}
function getShaPath(relPath) {
return shortx.getShortXDir() + "/ToolHub/code/.sha_" + relPath;
}
function sha256File(path) {
try {
var md = java.security.MessageDigest.getInstance("SHA-256");
var fis = new java.io.FileInputStream(path);
var buf = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 8192);
var n;
while ((n = fis.read(buf)) !== -1) {
md.update(buf, 0, n);
}
fis.close();
var digest = md.digest();
var sb = new java.lang.StringBuilder();
for (var i = 0; i < digest.length; i++) {
var hex = java.lang.Integer.toHexString(0xFF & digest[i]);
if (hex.length() === 1) sb.append("0");
sb.append(hex);
}
return String(sb.toString());
} catch (e) {
return null;
}
}
function saveSha256(relPath, hash) {
try {
var f = new java.io.File(getShaPath(relPath));
var w = new java.io.FileWriter(f, false);
w.write(String(hash || ""));
w.close();
} catch (e) { safeLog(null, 'e', "catch " + String(e)); }
}
function getLocalSha256(relPath) {
try {
var f = new java.io.File(getShaPath(relPath));
if (!f.exists()) return null;
var r = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(f), "UTF-8"));
var hash = r.readLine();
r.close();
return hash ? String(hash).trim() : null;
} catch (e) { return null; }
}
function writeLog(msg) { function writeLog(msg) {
try { try {
var f = new java.io.File(getLogPath()); var f = new java.io.File(getLogPath());
var dir = f.getParentFile(); var dir = f.getParentFile();
if (dir && !dir.exists()) { if (dir && !dir.exists()) dir.mkdirs();
dir.mkdirs();
}
var sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); var sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
var ts = sdf.format(new java.util.Date()); var ts = sdf.format(new java.util.Date());
var writer = new java.io.FileWriter(f, true); var writer = new java.io.FileWriter(f, true);
writer.write("[" + ts + "] " + String(msg) + "\n"); writer.write("[" + ts + "] " + String(msg) + "\n");
writer.close(); writer.close();
} catch (e) {} } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
} }
function runShell(cmdArr) { function runShell(cmdArr) {
@@ -36,9 +78,7 @@ function runShell(cmdArr) {
var proc = java.lang.Runtime.getRuntime().exec(cmdArr); var proc = java.lang.Runtime.getRuntime().exec(cmdArr);
proc.waitFor(); proc.waitFor();
return proc.exitValue() === 0; return proc.exitValue() === 0;
} catch (e) { } catch (e) { return false; }
return false;
}
} }
function checkDirPerms(path) { function checkDirPerms(path) {
@@ -51,13 +91,10 @@ function checkDirPerms(path) {
if (line) { if (line) {
var parts = String(line).trim().split(/\s+/); var parts = String(line).trim().split(/\s+/);
if (parts.length >= 3) { if (parts.length >= 3) {
var uid = String(parts[0]); return String(parts[0]) === "1000" && String(parts[1]) === "1000" && String(parts[2]) === "700";
var gid = String(parts[1]);
var mode = String(parts[2]);
return uid === "1000" && gid === "1000" && mode === "700";
} }
} }
} catch (e) {} } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
return false; return false;
} }
@@ -66,6 +103,43 @@ function setDirPerms(path) {
runShell(["chown", "1000:1000", path]); runShell(["chown", "1000:1000", path]);
} }
function getRemoteLastModified(urlStr) {
try {
var url = new java.net.URL(urlStr);
var conn = url.openConnection();
conn.setRequestMethod("HEAD");
conn.setConnectTimeout(5000);
conn.setReadTimeout(10000);
conn.setRequestProperty("User-Agent", "ShortX-ToolHub/1.0");
var code = conn.getResponseCode();
if (code !== 200) return null;
var lm = conn.getHeaderField("Last-Modified");
return lm ? String(lm) : null;
} catch (e) {
return null;
}
}
function getLocalLastModified(relPath) {
try {
var f = new java.io.File(getLmPath(relPath));
if (!f.exists()) return null;
var r = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(f), "UTF-8"));
var lm = r.readLine();
r.close();
return lm ? String(lm).trim() : null;
} catch (e) { return null; }
}
function saveLocalLastModified(relPath, lm) {
try {
var f = new java.io.File(getLmPath(relPath));
var w = new java.io.FileWriter(f, false);
w.write(String(lm || ""));
w.close();
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
function downloadFile(urlStr, destFile) { function downloadFile(urlStr, destFile) {
var url = new java.net.URL(urlStr); var url = new java.net.URL(urlStr);
var conn = url.openConnection(); var conn = url.openConnection();
@@ -73,21 +147,16 @@ function downloadFile(urlStr, destFile) {
conn.setReadTimeout(30000); conn.setReadTimeout(30000);
conn.setRequestProperty("User-Agent", "ShortX-ToolHub/1.0"); conn.setRequestProperty("User-Agent", "ShortX-ToolHub/1.0");
var code = conn.getResponseCode(); var code = conn.getResponseCode();
if (code !== 200) { if (code !== 200) throw "HTTP " + code;
throw "HTTP " + code;
}
var expectedLen = conn.getContentLength(); var expectedLen = conn.getContentLength();
var inStream = conn.getInputStream(); var inStream = conn.getInputStream();
var outStream = new java.io.FileOutputStream(destFile); var outStream = new java.io.FileOutputStream(destFile);
var buf = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 8192); var buf = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 8192);
var n; var n, total = 0;
var total = 0;
while ((n = inStream.read(buf)) !== -1) { while ((n = inStream.read(buf)) !== -1) {
outStream.write(buf, 0, n); outStream.write(buf, 0, n); total += n;
total += n;
} }
outStream.close(); outStream.close(); inStream.close();
inStream.close();
if (expectedLen > 0 && total !== expectedLen) { if (expectedLen > 0 && total !== expectedLen) {
throw "Size mismatch: expected=" + expectedLen + ", got=" + total; throw "Size mismatch: expected=" + expectedLen + ", got=" + total;
} }
@@ -104,27 +173,6 @@ function downloadFile(urlStr, destFile) {
return total; return total;
} }
function getFileVersion(filePath) {
try {
var f = new java.io.File(filePath);
if (!f.exists()) return null;
var r = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(f), "UTF-8"));
var line = r.readLine();
r.close();
if (line) {
var lineStr = String(line);
var idx = lineStr.indexOf("@version");
if (idx >= 0) {
var rest = lineStr.substring(idx + 8).trim();
var spaceIdx = rest.indexOf(" ");
var ver = spaceIdx >= 0 ? rest.substring(0, spaceIdx) : rest;
return ver;
}
}
} catch (e) {}
return null;
}
function loadScript(relPath) { function loadScript(relPath) {
try { try {
var base = shortx.getShortXDir(); var base = shortx.getShortXDir();
@@ -142,18 +190,25 @@ function loadScript(relPath) {
__dirChecked = true; __dirChecked = true;
} }
if (!dir.canWrite()) { if (!dir.canWrite()) throw "Dir not writable: " + dir.getAbsolutePath();
throw "Dir not writable: " + dir.getAbsolutePath();
}
var f = new java.io.File(dir, relPath); var f = new java.io.File(dir, relPath);
var expectedVer = MODULE_MANIFEST[relPath];
var localVer = getFileVersion(f.getAbsolutePath());
var needsDownload = !f.exists(); var needsDownload = !f.exists();
var isNew = !f.exists();
if (!needsDownload && expectedVer && localVer !== null && localVer !== expectedVer) { // 本地文件存在时HEAD 检查远程是否有更新
needsDownload = true; if (!needsDownload) {
writeLog("Version mismatch for " + relPath + ": local=" + localVer + ", expected=" + expectedVer); try {
var urlStr = GIT_BASE + relPath;
var remoteLm = getRemoteLastModified(urlStr);
var localLm = getLocalLastModified(relPath);
if (remoteLm && remoteLm !== localLm) {
needsDownload = true;
writeLog("Update detected for " + relPath + ": remote=" + remoteLm + ", local=" + localLm);
}
} catch (netErr) {
writeLog("Network check skipped for " + relPath + ": " + String(netErr));
}
} }
if (needsDownload) { if (needsDownload) {
@@ -161,9 +216,18 @@ function loadScript(relPath) {
var urlStr = GIT_BASE + relPath; var urlStr = GIT_BASE + relPath;
writeLog("Downloading " + relPath + " from " + urlStr); writeLog("Downloading " + relPath + " from " + urlStr);
var size = downloadFile(urlStr, f); var size = downloadFile(urlStr, f);
writeLog("Downloaded " + relPath + " (" + size + " bytes)"); var remoteLm = getRemoteLastModified(urlStr);
if (remoteLm) saveLocalLastModified(relPath, remoteLm);
var hash = sha256File(f.getAbsolutePath());
if (hash) saveSha256(relPath, hash);
writeLog("Downloaded " + relPath + " (" + size + " bytes, sha256=" + (hash || "null") + ")");
// 记录更新信息
__moduleUpdates.push({ module: relPath, isNew: isNew, size: size });
} catch (dlErr) { } catch (dlErr) {
throw "Not found: " + f.getAbsolutePath() + ", download failed: " + dlErr; if (!f.exists()) {
throw "Not found: " + f.getAbsolutePath() + ", download failed: " + dlErr;
}
writeLog("Download failed for " + relPath + ", using existing local file: " + String(dlErr));
} }
} }
@@ -172,66 +236,153 @@ function loadScript(relPath) {
writeLog("WARN: " + relPath + " is " + (fileSize / 1024) + "KB, consider splitting"); writeLog("WARN: " + relPath + " is " + (fileSize / 1024) + "KB, consider splitting");
} }
var actualHash = sha256File(f.getAbsolutePath());
var cachedHash = getLocalSha256(relPath);
if (cachedHash && actualHash && actualHash !== cachedHash) {
throw "SHA256 mismatch for " + relPath + ": expected=" + cachedHash + ", actual=" + actualHash;
}
if (actualHash && !cachedHash) {
saveSha256(relPath, actualHash);
}
var r = new java.io.BufferedReader(new java.io.InputStreamReader( var r = new java.io.BufferedReader(new java.io.InputStreamReader(
new java.io.FileInputStream(f), "UTF-8")); new java.io.FileInputStream(f), "UTF-8"));
var sb = new java.lang.StringBuilder(); var sb = new java.lang.StringBuilder();
var line; var line;
while ((line = r.readLine()) != null) { while ((line = r.readLine()) != null) sb.append(line).append("\n");
sb.append(line).append("\n");
}
r.close(); r.close();
var geval = eval; var geval = eval;
geval(String(sb.toString())); geval(String(sb.toString()));
} catch(e) { } catch(e) {
throw "loadScript(" + relPath + ") failed: " + e; var errMsg = "loadScript(" + relPath + ") failed: " + e;
try { android.util.Log.e("ToolHub", errMsg); } catch(eLog) {}
throw errMsg;
} }
} }
var modules = ["th_1_base.js", "th_2_core.js", "th_3_panels.js", "th_4_extra.js", "th_5_entry.js"]; var modules = ["th_01_base.js", "th_02_core.js", "th_03_icon.js", "th_04_theme.js", "th_05_persistence.js",
"th_06_icon_parser.js", "th_07_shortcut.js", "th_08_content.js", "th_09_animation.js",
"th_10_shell.js", "th_11_action.js", "th_12_rebuild.js", "th_13_panel_ui.js",
"th_14_panels.js", "th_15_extra.js", "th_16_entry.js"];
var __moduleUpdates = [];
var loadErrors = []; var loadErrors = [];
var criticalModules = { "th_01_base.js": true, "th_16_entry.js": true };
for (var i = 0; i < modules.length; i++) { for (var i = 0; i < modules.length; i++) {
try { try {
loadScript(modules[i]); loadScript(modules[i]);
} catch (e) { } catch (e) {
writeLog("Module load failed: " + modules[i] + " -> " + String(e)); var modErr = "Module load failed: " + modules[i] + " -> " + String(e);
writeLog(modErr);
try { android.util.Log.e("ToolHub", modErr); } catch(eLog) {}
loadErrors.push({ module: modules[i], err: String(e) }); loadErrors.push({ module: modules[i], err: String(e) });
if (modules[i] === "th_5_entry.js") { if (criticalModules[modules[i]]) {
throw "Critical module failed: " + modules[i]; throw "Critical module failed: " + modules[i] + " (" + String(e) + ")";
} }
} }
} }
var __out = (function() { var __out = (function() {
// 关键函数未加载成功时提前返回友好错误,避免 ReferenceError
if (typeof getProcessInfo !== "function") {
return {
ok: false,
started: false,
msg: "ToolHub 启动失败",
err: "核心函数 getProcessInfo 未定义,请检查 th_01_base.js 是否加载成功(网络下载失败或文件缺失)"
};
}
if (typeof ToolHubLogger !== "function") {
return {
ok: false,
started: false,
msg: "ToolHub 启动失败",
err: "核心类 ToolHubLogger 未定义,请检查 th_01_base.js 是否加载成功"
};
}
if (typeof FloatBallAppWM !== "function") {
return {
ok: false,
started: false,
msg: "ToolHub 启动失败",
err: "核心类 FloatBallAppWM 未定义,请检查 th_02_core.js / th_16_entry.js 是否加载成功"
};
}
function optStr(v) {
return (v === undefined || v === null) ? "" : String(v);
}
function summarizeModuleUpdates(list) {
var names = [];
var created = 0;
var overwritten = 0;
var i;
for (i = 0; i < list.length; i++) {
var item = list[i] || {};
var name = optStr(item.module);
if (name) names.push(name);
if (item.isNew) created++; else overwritten++;
}
if (names.length === 0) {
return {
count: 0,
modules: [],
msg: "子模块已是最新,本次未覆盖更新。"
};
}
return {
count: names.length,
modules: names,
msg: "本次已覆盖更新 " + names.length + " 个子模块(新增 " + created + " / 覆盖 " + overwritten + "" + names.join("、")
};
}
function summarizeLoadErrors(list) {
var names = [];
var i;
for (i = 0; i < list.length; i++) {
var item = list[i] || {};
var name = optStr(item.module);
if (name) names.push(name);
}
return {
count: names.length,
modules: names,
msg: names.length ? ("有 " + names.length + " 个子模块加载失败:" + names.join("、")) : "所有子模块加载正常。"
};
}
var entryInfo = getProcessInfo("entry"); var entryInfo = getProcessInfo("entry");
var logger = new ToolHubLogger(entryInfo); var logger = new ToolHubLogger(entryInfo);
installCrashHandler(logger); installCrashHandler(logger);
var app = new FloatBallAppWM(logger); var app = new FloatBallAppWM(logger);
var closeRule = String(app.config.ACTION_CLOSE_ALL_RULE || "shortx.wm.floatball.CLOSE"); var closeRule = String(app.config.ACTION_CLOSE_ALL_RULE || "shortx.wm.floatball.CLOSE");
var startRet = null; var startRet = null;
try { try {
startRet = app.startAsync(entryInfo, closeRule); startRet = app.startAsync(entryInfo, closeRule);
} catch (eTop) { } catch (eTop) {
try { logger.fatal("TOP startAsync crash err=" + String(eTop)); } catch (eLog) {} try { logger.fatal("TOP startAsync crash err=" + String(eTop)); } catch(eLog) { safeLog(null, 'e', "catch " + String(eLog)); }
startRet = { ok: false, err: String(eTop) }; startRet = { ok: false, err: String(eTop) };
} }
var syncInfo = summarizeModuleUpdates(__moduleUpdates);
function optStr(v) { var loadInfo = summarizeLoadErrors(loadErrors);
return (v === undefined || v === null) ? "" : String(v); var started = !!(startRet && startRet.ok);
} var rawMsg = optStr(startRet && startRet.msg);
var out = { var out = {
ok: true, ok: started,
started: startRet && startRet.ok, started: started,
msg: optStr(startRet && startRet.msg), msg: started ? (rawMsg ? ("ToolHub 启动成功:" + rawMsg) : "ToolHub 启动成功") : "ToolHub 启动失败",
syncMsg: syncInfo.msg,
updatedCount: syncInfo.count,
updatedModules: syncInfo.modules,
closeAction: optStr(startRet && startRet.closeAction), closeAction: optStr(startRet && startRet.closeAction),
layout: startRet && startRet.layout || null layout: startRet && startRet.layout || null
}; };
if (loadInfo.count > 0) {
if (!out.started) { out.loadMsg = loadInfo.msg;
out.err = optStr(startRet && startRet.err); out.loadErrors = loadInfo.modules;
if (!started) {
out.err = loadInfo.modules.join(", ");
}
} }
if (!started && !out.err) out.err = optStr(startRet && startRet.err) || "未知错误";
return out; return out;
})(); })();

View File

@@ -1,4 +1,4 @@
// @version 1.0.0 // @version 1.0.1
// ToolHub - Android 悬浮球工具 (ShortX / Rhino ES5) // ToolHub - Android 悬浮球工具 (ShortX / Rhino ES5)
// 来源: 阿然 (xin-blog.com) // 来源: 阿然 (xin-blog.com)
// //
@@ -86,6 +86,7 @@ var ConfigValidator = {
BALL_ICON_RES_ID: { type: "int", min: 0, max: 999999, default: 0 }, BALL_ICON_RES_ID: { type: "int", min: 0, max: 999999, default: 0 },
BALL_ICON_SIZE_DP: { type: "int", min: 16, max: 64, default: 22 }, BALL_ICON_SIZE_DP: { type: "int", min: 16, max: 64, default: 22 },
BALL_PNG_MODE: { type: "int", min: 0, max: 2, default: 1 }, BALL_PNG_MODE: { type: "int", min: 0, max: 2, default: 1 },
BALL_IDLE_ALPHA: { type: "float", min: 0.1, max: 1.0, default: 0.6 },
// 交互配置 // 交互配置
LONG_PRESS_MS: { type: "int", min: 200, max: 2000, default: 600 }, LONG_PRESS_MS: { type: "int", min: 200, max: 2000, default: 600 },
@@ -114,7 +115,6 @@ var ConfigValidator = {
// 内容查看器配置 // 内容查看器配置
CONTENT_MAX_ROWS: { type: "int", min: 5, max: 100, default: 20 }, CONTENT_MAX_ROWS: { type: "int", min: 5, max: 100, default: 20 },
CONTENT_VIEWER_TEXT_SP: { type: "int", min: 8, max: 24, default: 12 },
// ========== 以下配置在 Schema 中但原 ConfigValidator 中缺失 ========== // ========== 以下配置在 Schema 中但原 ConfigValidator 中缺失 ==========
// 图标文件配置 // 图标文件配置
@@ -124,12 +124,6 @@ var ConfigValidator = {
BALL_ICON_RES_NAME: { type: "string", default: "" }, BALL_ICON_RES_NAME: { type: "string", default: "" },
BALL_ICON_TINT_HEX: { type: "string", default: "" }, BALL_ICON_TINT_HEX: { type: "string", default: "" },
// 悬浮球外观
BALL_IDLE_ALPHA: { type: "float", min: 0.1, max: 1.0, default: 0.6 },
BALL_TEXT: { type: "string", default: "" },
BALL_TEXT_COLOR_HEX: { type: "string", default: "" },
BALL_TEXT_SIZE_SP: { type: "int", min: 6, max: 20, default: 10 },
// 回弹动画配置 // 回弹动画配置
BOUNCE_DECAY: { type: "float", min: 0.3, max: 0.95, default: 0.72 }, BOUNCE_DECAY: { type: "float", min: 0.3, max: 0.95, default: 0.72 },
BOUNCE_MAX_SCALE: { type: "float", min: 0.6, max: 0.99, default: 0.88 }, BOUNCE_MAX_SCALE: { type: "float", min: 0.6, max: 0.99, default: 0.88 },
@@ -335,7 +329,6 @@ var CONST_BALL_ICON_RES_ID = 0;
var CONST_BALL_ICON_FILE_MAX_BYTES = 524288; var CONST_BALL_ICON_FILE_MAX_BYTES = 524288;
var CONST_BALL_ICON_FILE_MAX_PX = 512; var CONST_BALL_ICON_FILE_MAX_PX = 512;
var CONST_BALL_PNG_MODE = 1; var CONST_BALL_PNG_MODE = 1;
var CONST_BALL_ICON_TEXT_GAP_DP = 1;
var CONST_BALL_INIT_X = 0; var CONST_BALL_INIT_X = 0;
var CONST_BALL_INIT_Y_DP = 220; var CONST_BALL_INIT_Y_DP = 220;
var CONST_BALL_FALLBACK_LIGHT = "#FF005BC0"; var CONST_BALL_FALLBACK_LIGHT = "#FF005BC0";
@@ -741,9 +734,6 @@ var ConfigManager = {
BALL_ICON_RES_NAME: "", BALL_ICON_RES_NAME: "",
BALL_ICON_SIZE_DP: 22, BALL_ICON_SIZE_DP: 22,
BALL_ICON_TINT_HEX: "", BALL_ICON_TINT_HEX: "",
BALL_TEXT: "",
BALL_TEXT_SIZE_SP: 10,
BALL_TEXT_COLOR_HEX: "",
BALL_IDLE_ALPHA: 0.6, BALL_IDLE_ALPHA: 0.6,
PANEL_POS_GRAVITY: "bottom", PANEL_POS_GRAVITY: "bottom",
PANEL_CUSTOM_OFFSET_Y: 0, PANEL_CUSTOM_OFFSET_Y: 0,
@@ -766,8 +756,7 @@ var ConfigManager = {
BALL_PANEL_GAP_DP: 10, BALL_PANEL_GAP_DP: 10,
LOG_ENABLE: true, LOG_ENABLE: true,
LOG_DEBUG: true, LOG_DEBUG: true,
LOG_KEEP_DAYS: 3, LOG_KEEP_DAYS: 3
CONTENT_VIEWER_TEXT_SP: 12
}, },
defaultButtons: [ defaultButtons: [
// # 默认按钮已迁移至 buttons.json // # 默认按钮已迁移至 buttons.json
@@ -785,20 +774,15 @@ CONTENT_VIEWER_TEXT_SP: 12
{ type: "section", name: "悬浮球" }, { type: "section", name: "悬浮球" },
{ key: "BALL_SIZE_DP", name: "悬浮球大小(dp)", type: "int", min: 28, max: 120, step: 1 }, { key: "BALL_SIZE_DP", name: "悬浮球大小(dp)", type: "int", min: 28, max: 120, step: 1 },
{ key: "BALL_PANEL_GAP_DP", name: "球与面板间距(dp)", type: "int", min: 0, max: 60, step: 1 }, { key: "BALL_PANEL_GAP_DP", name: "球与面板间距(dp)", type: "int", min: 0, max: 60, step: 1 },
{ key: "BALL_ICON_TYPE", name: "图标类型(app/file/android/shortx)", type: "single_choice", options: [ { key: "BALL_ICON_TYPE", name: "图标类型", type: "single_choice", options: [
{ label: "应用图标 (app)", value: "app" }, { label: "应用图标 (app)", value: "app" },
{ label: "文件图标 (file)", value: "file" }, { label: "文件图标 (file)", value: "file" },
{ label: "系统图标 (android)", value: "android" },
{ label: "ShortX内置 (shortx)", value: "shortx" } { label: "ShortX内置 (shortx)", value: "shortx" }
]}, ]},
{ key: "BALL_ICON_PKG", name: "图标包名(app模式)", type: "text" },
{ key: "BALL_ICON_FILE_PATH", name: "图标路径(file模式)", type: "text" }, { key: "BALL_ICON_FILE_PATH", name: "图标路径(file模式)", type: "text" },
{ key: "BALL_ICON_RES_NAME", name: "ShortX图标名(file/shortx模式兜底)", type: "text" }, { key: "BALL_ICON_RES_NAME", name: "ShortX图标", type: "ball_shortx_icon" },
{ key: "BALL_ICON_TINT_HEX", name: "图标着色(#RRGGBB, 空不着色)", type: "text" }, { key: "BALL_ICON_TINT_HEX", name: "图标颜色", type: "ball_color" },
{ key: "BALL_IDLE_ALPHA", name: "闲置不透明度(0.1~1.0)", type: "float", min: 0.1, max: 1.0, step: 0.05 }, { key: "BALL_IDLE_ALPHA", name: "闲置不透明度(0.1~1.0)", type: "float", min: 0.1, max: 1.0, step: 0.05 },
{ key: "BALL_TEXT", name: "悬浮球文字", type: "text" },
{ key: "BALL_TEXT_SIZE_SP", name: "文字大小(sp)", type: "int", min: 6, max: 20, step: 1 },
{ key: "BALL_TEXT_COLOR_HEX", name: "文字颜色(#RRGGBB)", type: "text" },
{ type: "section", name: "面板布局" }, { type: "section", name: "面板布局" },
{ key: "PANEL_ROWS", name: "面板可视行数(超出滚动)", type: "int", min: 1, max: 12, step: 1 }, { key: "PANEL_ROWS", name: "面板可视行数(超出滚动)", type: "int", min: 1, max: 12, step: 1 },
@@ -843,9 +827,6 @@ CONTENT_VIEWER_TEXT_SP: 12
{ key: "LONG_PRESS_HAPTIC_ENABLE", name: "长按震动反馈", type: "bool" }, { key: "LONG_PRESS_HAPTIC_ENABLE", name: "长按震动反馈", type: "bool" },
{ key: "LONG_PRESS_VIBRATE_MS", name: "震动时长(ms)", type: "int", min: 1, max: 120, step: 1 }, { key: "LONG_PRESS_VIBRATE_MS", name: "震动时长(ms)", type: "int", min: 1, max: 120, step: 1 },
{ type: "section", name: "执行与查看器" },
{ key: "CONTENT_VIEWER_TEXT_SP", name: "查看器文字大小(sp)", type: "int", min: 9, max: 18, step: 1 },
{ type: "section", name: "日志" }, { type: "section", name: "日志" },
{ key: "LOG_ENABLE", name: "写文件日志", type: "bool" }, { key: "LOG_ENABLE", name: "写文件日志", type: "bool" },
{ key: "LOG_DEBUG", name: "详细日志DEBUG", type: "bool" }, { key: "LOG_DEBUG", name: "详细日志DEBUG", type: "bool" },
@@ -877,7 +858,7 @@ CONTENT_VIEWER_TEXT_SP: 12
var needReset = false; var needReset = false;
if (s) { if (s) {
var sStr = JSON.stringify(s); var sStr = JSON.stringify(s);
if (sStr.indexOf("ENABLE_SNAP_TO_EDGE") < 0 || sStr.indexOf("ENABLE_ANIMATIONS") < 0 || sStr.indexOf("BALL_IDLE_ALPHA") < 0 || sStr.indexOf("PANEL_POS_GRAVITY") < 0 || sStr.indexOf("single_choice") < 0) { if (sStr.indexOf("ENABLE_SNAP_TO_EDGE") < 0 || sStr.indexOf("ENABLE_ANIMATIONS") < 0 || sStr.indexOf("BALL_IDLE_ALPHA") < 0 || sStr.indexOf("PANEL_POS_GRAVITY") < 0 || sStr.indexOf("single_choice") < 0 || sStr.indexOf("ball_shortx_icon") < 0 || sStr.indexOf("ball_color") < 0) {
needReset = true; needReset = true;
} }
} else { } else {
@@ -1097,7 +1078,7 @@ function getProcessInfo(tag) {
// =======================【工具Base64 解码UTF-8】======================= // =======================【工具Base64 解码UTF-8】=======================
// # 这段代码的主要内容/用途:把 cmd_b64 还原成原始 shell 文本(仅用于 Action 优先路径;广播桥永远传 b64 // # 这段代码的主要内容/用途:把 cmd_b64 还原成原始 shell 文本
function decodeBase64Utf8(b64) { function decodeBase64Utf8(b64) {
try { try {
var s = String(b64 || ""); var s = String(b64 || "");

123
code/th_02_core.js Normal file
View File

@@ -0,0 +1,123 @@
// @version 1.0.0
function FloatBallAppWM(logger) {
this.L = logger || null;
// # 加载配置
this.config = ConfigManager.loadSettings();
this.currentPanelKey = "main";
this.panels = { main: ConfigManager.loadButtons() };
// # 更新 Logger 配置(因为 Logger 初始化时是默认值)
if (this.L) this.L.updateConfig(this.config);
this.state = {
receivers: [], // 存储广播接收器引用,用于 close 时注销
wm: null,
dm: null,
density: 1.0,
screen: { w: 0, h: 0 },
lastRotation: -1,
lastMonitorTs: 0,
ht: null,
h: null,
addedBall: false,
addedPanel: false,
addedSettings: false,
addedViewer: false,
addedMask: false,
ballRoot: null,
ballContent: null,
ballLp: null,
panel: null,
panelLp: null,
settingsPanel: null,
settingsPanelLp: null,
viewerPanel: null,
viewerPanelLp: null,
mask: null,
maskLp: null,
loadedPos: null,
lastSaveTs: 0,
dragging: false,
rawX: 0,
rawY: 0,
downX: 0,
downY: 0,
docked: false,
dockSide: null,
lastMotionTs: 0,
idleDockRunnable: null,
longPressArmed: false,
longPressTriggered: false,
longPressRunnable: null,
displayListener: null,
// # 设置面板:临时编辑缓存
pendingUserCfg: null,
pendingDirty: false,
closing: false
};
// # 创建实例独立的 UI 工具对象,避免多实例共享颜色状态
this.ui = {};
var protoUi = FloatBallAppWM.prototype.ui;
for (var _uiKey in protoUi) {
this.ui[_uiKey] = protoUi[_uiKey];
}
this.ui.colors = {};
// # 初始化莫奈动态配色(传入当前主题避免重复检测)
try { this.refreshMonetColors(this.isDarkTheme()); } catch(eM) { safeLog(null, 'e', "catch " + String(eM)); }
}
// =======================【工具dp/now/clamp】======================
FloatBallAppWM.prototype.dp = function(v) { return Math.floor(Number(v) * this.state.density); };
FloatBallAppWM.prototype.sp = function(v) { try { return Math.floor(Number(v) * context.getResources().getDisplayMetrics().scaledDensity); } catch (e) { return Math.floor(Number(v) * this.state.density); } };
FloatBallAppWM.prototype.now = function() { return new Date().getTime(); };
FloatBallAppWM.prototype.clamp = function(v, min, max) { if (v < min) return min; if (v > max) return max; return v; };
FloatBallAppWM.prototype.rectIntersect = function(ax, ay, aw, ah, bx, by, bw, bh) {
return !(ax + aw <= bx || bx + bw <= ax || ay + ah <= by || by + bh <= ay);
};
// # 这段代码的主要内容/用途:安全地在 UI 线程执行代码(用于后台线程回调更新 UI
FloatBallAppWM.prototype.runOnUiThreadSafe = function(fn) {
try {
if (!fn) return;
var self = this;
// 优先使用 Activity 的 runOnUiThread否则使用 View.post
if (this.state && this.state.ballRoot) {
this.state.ballRoot.post(new java.lang.Runnable({
run: function() { try { fn.call(self); } catch(e) { safeLog(null, 'e', "catch " + String(e)); } }
}));
} else {
// 兜底:直接执行(如果已经在 UI 线程)或尝试使用 Handler
try {
var h = new android.os.Handler(android.os.Looper.getMainLooper());
h.post(new java.lang.Runnable({
run: function() { try { fn.call(self); } catch(e) { safeLog(null, 'e', "catch " + String(e)); } }
}));
} catch(e) {
// 最后兜底:直接执行
try { fn.call(self); } catch(e2) { safeLog(null, 'e', "catch " + String(e2)); }
}
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
};
// # 这段代码的主要内容/用途统一图标缓存LRU减少反复解码/反复走 PackageManager降低卡顿与内存波动不改变 UI 与功能)
// 优化后的图标缓存(带 Bitmap 回收,防止内存泄漏)

170
code/th_03_icon.js Normal file
View File

@@ -0,0 +1,170 @@
// @version 1.0.0
FloatBallAppWM.prototype._iconCache = {
map: {},
keys: [],
max: 80, // 减少缓存数量,降低内存压力
get: function(key) {
var item = this.map[key];
if (!item) return null;
// 移动到末尾(最近使用)
var idx = this.keys.indexOf(key);
if (idx > -1) {
this.keys.splice(idx, 1);
this.keys.push(key);
}
return item.dr;
},
put: function(key, drawable) {
// 清理旧的
if (this.map[key]) {
this._remove(key);
}
// 空间检查:超过 80% 时批量清理 20%
if (this.keys.length >= this.max * 0.8) {
var removeCount = Math.floor(this.max * 0.2);
for (var i = 0; i < removeCount && this.keys.length > 0; i++) {
var oldKey = this.keys.shift();
this._remove(oldKey);
}
}
this.keys.push(key);
this.map[key] = {dr: drawable, ts: Date.now()};
},
_remove: function(key) {
var item = this.map[key];
if (item && item.dr) {
// 关键:回收 Bitmap防止内存泄漏
try {
if (item.dr instanceof android.graphics.drawable.BitmapDrawable) {
var bmp = item.dr.getBitmap();
if (bmp && !bmp.isRecycled()) bmp.recycle();
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
delete this.map[key];
}
},
clear: function() {
for (var i = 0; i < this.keys.length; i++) {
this._remove(this.keys[i]);
}
this.keys = [];
}
};
// 兼容性封装(保持原有调用方式不变)
FloatBallAppWM.prototype._iconLruEnsure = function() {};
FloatBallAppWM.prototype._iconLruGet = function(key) {
return this._iconCache.get(key);
};
FloatBallAppWM.prototype._iconLruPut = function(key, val) {
try {
this._iconLruEnsure(120);
var k = String(key || "");
if (!k) return;
if (val == null) return;
// # 若已存在,先移除旧顺序位置
try {
var ord = this._iconLru.order;
for (var i = ord.length - 1; i >= 0; i--) {
if (ord[i] === k) { ord.splice(i, 1); break; }
}
ord.push(k);
} catch(eLru3) { safeLog(null, 'e', "catch " + String(eLru3)); }
this._iconLru.map[k] = val;
// # 超限清理:按最久未使用淘汰
try {
var maxN = Math.max(20, Math.floor(Number(this._iconLru.max || 120)));
var ord2 = this._iconLru.order;
while (ord2.length > maxN) {
var oldK = ord2.shift();
if (oldK != null) {
try { delete this._iconLru.map[oldK]; } catch(eDel) { safeLog(null, 'e', "catch " + String(eDel)); }
}
}
} catch(eLru4) { safeLog(null, 'e', "catch " + String(eLru4)); }
} catch(eLru5) { safeLog(null, 'e', "catch " + String(eLru5)); }
};
// =======================【工具悬浮球图标PNG 文件)】======================
// # 这段代码的主要内容/用途:从指定路径加载透明 PNG 作为悬浮球图标;带"文件大小/像素上限"保护;按目标尺寸采样解码,避免 system_server OOM。
FloatBallAppWM.prototype.loadBallIconDrawableFromFile = function(path, targetPx, maxBytes, maxPx) {
try {
var p = String(path || "");
if (!p) return null;
// # 统一 LRU 缓存:文件图标(按 path + targetPx + mtime + size 复用 Drawable避免反复解码
var f = new java.io.File(p);
if (!f.exists() || !f.isFile()) return null;
var ckLru = null;
try {
ckLru = "file|" + p + "@" + String(targetPx == null ? "" : targetPx) + "@" + String(f.lastModified()) + "@" + String(f.length());
var hitLru = this._iconLruGet(ckLru);
if (hitLru) return hitLru;
} catch(eLruF0) { safeLog(null, 'e', "catch " + String(eLruF0)); }
// # 文件大小限制(字节)
var limitBytes = Math.max(0, Math.floor(Number(maxBytes || 0)));
if (limitBytes > 0) {
try {
var sz = Number(f.length());
if (sz > limitBytes) return null;
} catch (eSz) { return null; }
}
// # 先只读尺寸(不解码)
var opt = new android.graphics.BitmapFactory.Options();
opt.inJustDecodeBounds = true;
try { android.graphics.BitmapFactory.decodeFile(p, opt); } catch (eB0) { return null; }
var w = Number(opt.outWidth || 0);
var h = Number(opt.outHeight || 0);
if (w <= 0 || h <= 0) return null;
// # 像素边长上限(宽/高任意一边超限则拒绝)
var limitPx = Math.max(0, Math.floor(Number(maxPx || 0)));
if (limitPx > 0) {
if (w > limitPx || h > limitPx) return null;
}
// # 计算采样倍率:按目标尺寸(一般为 iconSizePx采样
var tp = Math.max(1, Math.floor(Number(targetPx || 1)));
// # 允许解码到目标的 2 倍以内,减少锯齿又不浪费内存
var desired = Math.max(tp * 2, tp);
var sample = 1;
while ((w / sample) > desired || (h / sample) > desired) sample = sample * 2;
if (sample < 1) sample = 1;
var opt2 = new android.graphics.BitmapFactory.Options();
opt2.inJustDecodeBounds = false;
opt2.inSampleSize = sample;
opt2.inPreferredConfig = android.graphics.Bitmap.Config.ARGB_8888;
var bmp = null;
try { bmp = android.graphics.BitmapFactory.decodeFile(p, opt2); } catch (eB1) { bmp = null; }
if (bmp == null) return null;
var d = new android.graphics.drawable.BitmapDrawable(context.getResources(), bmp);
// # 写入统一 LRU 缓存
try {
if (ckLru) this._iconLruPut(ckLru, d);
} catch(eLruF1) { safeLog(null, 'e', "catch " + String(eLruF1)); }
return d;
} catch (e0) {
return null;
}
};

800
code/th_04_theme.js Normal file
View File

@@ -0,0 +1,800 @@
// @version 1.0.0
// =======================【工具:屏幕/旋转】======================
FloatBallAppWM.prototype.getScreenSizePx = function() {
var m = new android.util.DisplayMetrics();
try { this.state.wm.getDefaultDisplay().getRealMetrics(m); } catch (e) { this.state.wm.getDefaultDisplay().getMetrics(m); }
return { w: m.widthPixels, h: m.heightPixels };
};
FloatBallAppWM.prototype.getRotation = function() { try { return this.state.wm.getDefaultDisplay().getRotation(); } catch(e) { safeLog(null, 'e', "catch " + String(e)); } return -1; };
// =======================【工具alpha/toast/vibrate】======================
FloatBallAppWM.prototype.withAlpha = function(colorInt, alpha01) { var a = Math.floor(Number(alpha01) * 255); return (colorInt & 0x00FFFFFF) | (a << 24); };
FloatBallAppWM.prototype.toast = function(msg) { try { android.widget.Toast.makeText(context, String(msg), 0).show(); } catch(e) { safeLog(null, 'e', "catch " + String(e)); } };
FloatBallAppWM.prototype.vibrateOnce = function(ms) {
if (!this.config.LONG_PRESS_HAPTIC_ENABLE) return;
try {
var vib = context.getSystemService(android.content.Context.VIBRATOR_SERVICE);
if (!vib) return;
var dur = Math.max(1, Math.floor(ms));
if (android.os.Build.VERSION.SDK_INT >= 26) {
var ve = android.os.VibrationEffect.createOneShot(dur, android.os.VibrationEffect.DEFAULT_AMPLITUDE);
vib.vibrate(ve);
} else {
vib.vibrate(dur);
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
};
// =======================【工具UI样式辅助】======================
FloatBallAppWM.prototype.ui = {
// 基础颜色
colors: {
// 以下为默认回退值,实例化时会被 refreshMonetColors() 覆盖为系统莫奈色
primary: android.graphics.Color.parseColor("#005BC0"),
primaryDark: android.graphics.Color.parseColor("#041E49"),
accent: android.graphics.Color.parseColor("#00639B"),
danger: android.graphics.Color.parseColor("#BA1A1A"),
success: android.graphics.Color.parseColor("#15803d"),
warning: android.graphics.Color.parseColor("#b45309"),
bgLight: android.graphics.Color.parseColor("#F8F9FA"),
bgDark: android.graphics.Color.parseColor("#131314"),
cardLight: android.graphics.Color.parseColor("#E1E3E1"),
cardDark: android.graphics.Color.parseColor("#49454F"),
textPriLight: android.graphics.Color.parseColor("#1F1F1F"),
textPriDark: android.graphics.Color.parseColor("#E3E3E3"),
textSecLight: android.graphics.Color.parseColor("#5F6368"),
textSecDark: android.graphics.Color.parseColor("#C4C7C5"),
dividerLight: android.graphics.Color.parseColor("#747775"),
dividerDark: android.graphics.Color.parseColor("#8E918F"),
inputBgLight: android.graphics.Color.parseColor("#F8F9FA"),
inputBgDark: android.graphics.Color.parseColor("#131314"),
// Monet 扩展字段(供面板直接使用)
_monetSurface: android.graphics.Color.parseColor("#F8F9FA"),
_monetOnSurface: android.graphics.Color.parseColor("#1F1F1F"),
_monetOutline: android.graphics.Color.parseColor("#747775"),
_monetOnPrimary: android.graphics.Color.parseColor("#FFFFFF"),
_monetPrimaryContainer: android.graphics.Color.parseColor("#D3E3FD"),
_monetOnPrimaryContainer: android.graphics.Color.parseColor("#041E49"),
_monetSecondary: android.graphics.Color.parseColor("#00639B"),
_monetTertiary: android.graphics.Color.parseColor("#5C5891")
},
// 创建圆角背景 (Solid)
createRoundDrawable: function(color, radiusPx) {
var d = new android.graphics.drawable.GradientDrawable();
d.setShape(android.graphics.drawable.GradientDrawable.RECTANGLE);
d.setColor(color);
d.setCornerRadius(radiusPx);
return d;
},
// 创建圆角描边背景 (Stroke)
createStrokeDrawable: function(fillColor, strokeColor, strokeWidthPx, radiusPx) {
var d = new android.graphics.drawable.GradientDrawable();
d.setShape(android.graphics.drawable.GradientDrawable.RECTANGLE);
if (fillColor) d.setColor(fillColor);
d.setCornerRadius(radiusPx);
d.setStroke(strokeWidthPx, strokeColor);
return d;
},
// 创建按压反馈背景 (StateList)
createRippleDrawable: function(normalColor, pressedColor, radiusPx) {
var sd = new android.graphics.drawable.StateListDrawable();
var p = this.createRoundDrawable(pressedColor, radiusPx);
var n = this.createRoundDrawable(normalColor, radiusPx);
sd.addState([android.R.attr.state_pressed], p);
sd.addState([], n);
return sd;
},
// 创建纯色按压反馈 (StateList) - 用于透明背景按钮
createTransparentRippleDrawable: function(pressedColor, radiusPx) {
var sd = new android.graphics.drawable.StateListDrawable();
var p = this.createRoundDrawable(pressedColor, radiusPx);
var n = new android.graphics.drawable.ColorDrawable(android.graphics.Color.TRANSPARENT);
sd.addState([android.R.attr.state_pressed], p);
sd.addState([], n);
return sd;
},
// 辅助:创建扁平按钮
createFlatButton: function(app, txt, txtColor, onClick) {
var btn = new android.widget.TextView(context);
btn.setText(txt);
btn.setTextColor(txtColor);
btn.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
btn.setPadding(app.dp(12), app.dp(6), app.dp(12), app.dp(6));
btn.setGravity(android.view.Gravity.CENTER);
// use divider color or just low alpha text color for ripple
var rippleColor = app.withAlpha ? app.withAlpha(txtColor, 0.1) : 0x22888888;
btn.setBackground(this.createTransparentRippleDrawable(rippleColor, app.dp(8)));
btn.setOnClickListener(new android.view.View.OnClickListener({
onClick: function(v) { app.touchActivity(); app.guardClick("ui_btn", INTERACTION_CONSTANTS.CLICK_COOLDOWN_MS, function(){ if(onClick) onClick(v); }); }
}));
return btn;
},
// 辅助:创建实心按钮
createSolidButton: function(app, txt, bgColor, txtColor, onClick) {
var btn = new android.widget.TextView(context);
btn.setText(txt);
btn.setTextColor(txtColor);
btn.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
btn.setTypeface(null, android.graphics.Typeface.BOLD);
btn.setPadding(app.dp(16), app.dp(8), app.dp(16), app.dp(8));
btn.setGravity(android.view.Gravity.CENTER);
var pressedColor = app.withAlpha ? app.withAlpha(bgColor, 0.8) : bgColor;
btn.setBackground(this.createRippleDrawable(bgColor, pressedColor, app.dp(24)));
try { btn.setElevation(app.dp(2)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
btn.setOnClickListener(new android.view.View.OnClickListener({
onClick: function(v) { app.touchActivity(); app.guardClick("ui_btn", INTERACTION_CONSTANTS.CLICK_COOLDOWN_MS, function(){ if(onClick) onClick(v); }); }
}));
return btn;
},
// 辅助:创建带标签的输入组(支持粘贴)
createInputGroup: function(app, label, initVal, isMultiLine, hint) {
var box = new android.widget.LinearLayout(context);
box.setOrientation(android.widget.LinearLayout.VERTICAL);
box.setPadding(0, 0, 0, app.dp(12));
var topLine = new android.widget.LinearLayout(context);
topLine.setOrientation(android.widget.LinearLayout.HORIZONTAL);
topLine.setGravity(android.view.Gravity.CENTER_VERTICAL);
box.addView(topLine);
var lb = new android.widget.TextView(context);
lb.setText(label);
lb.setTextColor(this.colors.textSecLight); // 默认用浅色主题副文本色,外部可覆盖
try { if (app.isDarkTheme && app.isDarkTheme()) lb.setTextColor(this.colors.textSecDark); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
lb.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 12);
var lpLb = new android.widget.LinearLayout.LayoutParams(0, -2);
lpLb.weight = 1;
topLine.addView(lb, lpLb);
var et = new android.widget.EditText(context);
et.setText(initVal ? String(initVal) : "");
et.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
et.setTextColor(this.colors.textPriLight);
try { if (app.isDarkTheme && app.isDarkTheme()) et.setTextColor(this.colors.textPriDark); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
// 输入框背景优化
var strokeColor = this.colors.dividerLight;
try { if (app.isDarkTheme && app.isDarkTheme()) strokeColor = this.colors.dividerDark; } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
var bg = this.createStrokeDrawable(this.colors.inputBgLight, strokeColor, app.dp(1), app.dp(8));
try { if (app.isDarkTheme && app.isDarkTheme()) bg = this.createStrokeDrawable(this.colors.inputBgDark, strokeColor, app.dp(1), app.dp(8)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
et.setBackground(bg);
et.setPadding(app.dp(8), app.dp(8), app.dp(8), app.dp(8));
if (hint) et.setHint(hint);
if (isMultiLine) {
et.setSingleLine(false);
et.setMaxLines(4);
} else {
et.setSingleLine(true);
}
// 粘贴功能
var pasteBtn = this.createFlatButton(app, "粘贴", this.colors.accent, function() {
try {
var cb = context.getSystemService(android.content.Context.CLIPBOARD_SERVICE);
if (cb.hasPrimaryClip()) {
var item = cb.getPrimaryClip().getItemAt(0);
if (item) {
var txt = item.getText();
if (txt) {
var st = String(txt);
var old = String(et.getText());
if (old.length > 0) et.setText(old + st);
else et.setText(st);
et.setSelection(et.getText().length());
}
}
} else {
app.toast("剪贴板为空");
}
} catch (eP) {
app.toast("粘贴失败: " + eP);
}
});
// 调整粘贴按钮样式使其更紧凑
pasteBtn.setPadding(app.dp(8), app.dp(2), app.dp(8), app.dp(2));
pasteBtn.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 12);
topLine.addView(pasteBtn);
box.addView(et);
// 错误提示
var errTv = new android.widget.TextView(context);
errTv.setTextColor(this.colors.danger);
errTv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 10);
errTv.setVisibility(android.view.View.GONE);
box.addView(errTv);
var self = this;
return {
view: box,
input: et,
getValue: function() { return String(et.getText()); },
setError: function(msg) {
if (msg) {
errTv.setText(msg);
errTv.setVisibility(android.view.View.VISIBLE);
et.setBackground(self.createRoundDrawable(app.withAlpha(self.colors.danger, 0.2), app.dp(4)));
} else {
errTv.setVisibility(android.view.View.GONE);
var strokeColor = self.colors.dividerLight;
try { if (app.isDarkTheme && app.isDarkTheme()) strokeColor = self.colors.dividerDark; } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
var normalBg = self.createStrokeDrawable(self.colors.inputBgLight, strokeColor, app.dp(1), app.dp(8));
try { if (app.isDarkTheme && app.isDarkTheme()) normalBg = self.createStrokeDrawable(self.colors.inputBgDark, strokeColor, app.dp(1), app.dp(8)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
et.setBackground(normalBg);
}
}
};
},
// 辅助:创建标准面板容器
createStyledPanel: function(app, paddingDp) {
var isDark = app.isDarkTheme();
var bgColor = isDark ? this.colors.bgDark : this.colors.bgLight;
var panel = new android.widget.LinearLayout(context);
panel.setOrientation(android.widget.LinearLayout.VERTICAL);
var bgDr = new android.graphics.drawable.GradientDrawable();
bgDr.setColor(bgColor);
bgDr.setCornerRadius(app.dp(16));
panel.setBackground(bgDr);
try { panel.setElevation(app.dp(8)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
var p = (paddingDp !== undefined) ? app.dp(paddingDp) : app.dp(16);
panel.setPadding(p, p, p, p);
return panel;
},
// 辅助:创建标准标题栏容器
createStyledHeader: function(app, paddingBottomDp) {
var header = new android.widget.LinearLayout(context);
header.setOrientation(android.widget.LinearLayout.HORIZONTAL);
header.setGravity(android.view.Gravity.CENTER_VERTICAL);
var pb = (paddingBottomDp !== undefined) ? app.dp(paddingBottomDp) : app.dp(8);
header.setPadding(0, 0, 0, pb);
return header;
},
// 辅助:创建占位符(撑开空间)
createSpacer: function(app) {
var dummy = new android.view.View(context);
var dummyLp = new android.widget.LinearLayout.LayoutParams(0, 1);
dummyLp.weight = 1;
dummy.setLayoutParams(dummyLp);
return dummy;
}
};
// =======================【工具:主题/类莫奈颜色】======================
// # 主题调试日志工具(仅打印,不改变逻辑)
function _th_hex(c) {
try {
var Integer = Packages.java.lang.Integer;
var s = Integer.toHexString(c);
while (s.length < 8) s = "0" + s;
return "0x" + s;
} catch (e) {
try { return String(c); } catch (e2) { return "<?>"; }
}
}
function _th_argb(c) {
try {
var Color = Packages.android.graphics.Color;
var ci = Math.floor(Number(c));
if (isNaN(ci)) return "NaN";
return "a=" + Color.alpha(ci) + " r=" + Color.red(ci) + " g=" + Color.green(ci) + " b=" + Color.blue(ci);
} catch (e) {
return "argb_err";
}
}
function _th_log(L, level, msg) {
try {
if (!L) return;
if (level === "e" && L.e) { L.e(msg); return; }
if (level === "w" && L.w) { L.w(msg); return; }
if (level === "i" && L.i) { L.i(msg); return; }
if (L.d) { L.d(msg); return; }
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
// =======================【莫奈动态取色工具】======================
var MonetColorProvider = {
_cacheLight: null,
_cacheDark: null,
_getResColor: function(resName, fallbackHex) {
try {
var res = android.content.res.Resources.getSystem();
var id = res.getIdentifier(resName, "color", "android");
if (id > 0) return res.getColor(id, null);
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
try { return android.graphics.Color.parseColor(fallbackHex); } catch (e) { return 0; }
},
_getFallbackLight: function() {
return {
// 使用更接近 AOSP Monet 标准值的 fallback日间 primary 更饱和、对比度更高
primary: android.graphics.Color.parseColor("#005BC0"),
onPrimary: android.graphics.Color.parseColor("#FFFFFF"),
primaryContainer: android.graphics.Color.parseColor("#D3E3FD"),
onPrimaryContainer: android.graphics.Color.parseColor("#041E49"),
secondary: android.graphics.Color.parseColor("#00639B"),
secondaryContainer: android.graphics.Color.parseColor("#C2E4FF"),
tertiary: android.graphics.Color.parseColor("#5C5891"),
surface: android.graphics.Color.parseColor("#F8F9FA"),
onSurface: android.graphics.Color.parseColor("#1F1F1F"),
surfaceVariant: android.graphics.Color.parseColor("#E1E3E1"),
onSurfaceVariant: android.graphics.Color.parseColor("#5F6368"),
outline: android.graphics.Color.parseColor("#747775"),
outlineVariant: android.graphics.Color.parseColor("#C4C7C5"),
error: android.graphics.Color.parseColor("#BA1A1A"),
errorContainer: android.graphics.Color.parseColor("#F9DEDC"),
onErrorContainer: android.graphics.Color.parseColor("#410E0B")
};
},
_getFallbackDark: function() {
return {
primary: android.graphics.Color.parseColor("#A8C7FA"),
onPrimary: android.graphics.Color.parseColor("#062E6F"),
primaryContainer: android.graphics.Color.parseColor("#0842A0"),
onPrimaryContainer: android.graphics.Color.parseColor("#D3E3FD"),
secondary: android.graphics.Color.parseColor("#7FCFFF"),
secondaryContainer: android.graphics.Color.parseColor("#004A77"),
tertiary: android.graphics.Color.parseColor("#C2C5DD"),
surface: android.graphics.Color.parseColor("#131314"),
onSurface: android.graphics.Color.parseColor("#E3E3E3"),
surfaceVariant: android.graphics.Color.parseColor("#49454F"),
onSurfaceVariant: android.graphics.Color.parseColor("#C4C7C5"),
outline: android.graphics.Color.parseColor("#8E918F"),
outlineVariant: android.graphics.Color.parseColor("#49454F"),
error: android.graphics.Color.parseColor("#F2B8B5"),
errorContainer: android.graphics.Color.parseColor("#8C1D18"),
onErrorContainer: android.graphics.Color.parseColor("#F9DEDC")
};
},
_loadMonet: function(isDark) {
var c = isDark ? this._getFallbackDark() : this._getFallbackLight();
try {
var res = android.content.res.Resources.getSystem();
var map = isDark ? {
primary: "system_accent1_200",
onPrimary: "system_accent1_800",
primaryContainer: "system_accent1_700",
onPrimaryContainer: "system_accent1_100",
secondary: "system_accent2_200",
secondaryContainer: "system_accent2_700",
tertiary: "system_accent3_200",
surface: "system_neutral1_900",
onSurface: "system_neutral1_100",
surfaceVariant: "system_neutral2_700",
onSurfaceVariant: "system_neutral2_200",
outline: "system_neutral2_400",
outlineVariant: "system_neutral2_700",
error: "system_accent3_200",
errorContainer: "system_accent3_800",
onErrorContainer: "system_accent3_100"
} : {
primary: "system_accent1_600",
onPrimary: "system_accent1_0",
primaryContainer: "system_accent1_100",
onPrimaryContainer: "system_accent1_900",
secondary: "system_accent2_600",
secondaryContainer: "system_accent2_100",
tertiary: "system_accent3_600",
surface: "system_neutral1_10",
onSurface: "system_neutral1_900",
surfaceVariant: "system_neutral2_100",
onSurfaceVariant: "system_neutral2_700",
outline: "system_neutral2_500",
outlineVariant: "system_neutral2_200",
error: "system_accent3_600",
errorContainer: "system_accent3_100",
onErrorContainer: "system_accent3_900"
};
for (var name in map) {
try {
var id = res.getIdentifier(map[name], "color", "android");
if (id > 0) c[name] = res.getColor(id, null);
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
return c;
},
getColors: function(isDark) {
if (isDark) {
if (!this._cacheDark) this._cacheDark = this._loadMonet(true);
return this._cacheDark;
} else {
if (!this._cacheLight) this._cacheLight = this._loadMonet(false);
return this._cacheLight;
}
},
invalidate: function() {
this._cacheLight = null;
this._cacheDark = null;
}
};
// =======================【兼容兜底themeTextInt/themeBgInt】======================
// 这段代码的主要内容/用途:兼容旧代码或异步回调里误引用 themeTextInt/themeBgInt 导致 ReferenceError 崩溃。
// 说明:当前版本文字色应通过 getPanelTextColorInt(bgInt) 获取;这里仅作为"兜底全局变量",避免回调炸线程。
// 声明全局变量(避免 ReferenceError
var themeBgInt = 0;
var themeTextInt = 0;
// =======================【API 兼容性辅助函数】======================
// 这段代码的主要内容/用途:处理 Android API 级别差异,避免在旧版本上崩溃
/**
* 安全创建 UserHandle兼容 API 17 以下)
* @param {number} userId - 用户 ID
* @returns {android.os.UserHandle} UserHandle 对象或 null
*/
function createUserHandle(userId) {
try {
// UserHandle.of 在 API 17+ 可用
if (android.os.Build.VERSION.SDK_INT >= 17) {
return android.os.UserHandle.of(userId);
}
// API 17 以下返回当前用户句柄
return android.os.Process.myUserHandle();
} catch (e) {
return null;
}
}
/**
* 安全启动 Activity 跨用户(兼容 API 17 以下)
* @param {Context} ctx - Context
* @param {Intent} intent - Intent
* @param {number} userId - 用户 IDAPI 17+ 有效)
*/
function startActivityAsUserSafe(ctx, intent, userId) {
try {
if (android.os.Build.VERSION.SDK_INT >= 17 && userId !== 0) {
var uh = android.os.UserHandle.of(userId);
ctx.startActivityAsUser(intent, uh);
} else {
ctx.startActivity(intent);
}
} catch (e) {
// 降级到普通启动
try {
ctx.startActivity(intent);
} catch(e2) { safeLog(null, 'e', "catch " + String(e2)); }
}
}
FloatBallAppWM.prototype.isDarkTheme = function() {
// 0) 优先检查用户强制设置 (0=跟随系统, 1=白天, 2=黑夜)
var mode = 0;
try { mode = Math.floor(Number(this.config.THEME_MODE || 0)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
if (mode === 2) return true;
if (mode === 1) return false;
// mode === 0 (or others) -> Fallback to system detection
// 这段代码的主要内容/用途:更稳健地判断当前是否处于"夜间/暗色"模式(并打印调试日志)。
// 说明system_server 场景下 Configuration.uiMode 偶发不一致,因此再用 UiModeManager 兜底交叉验证。
var result = false;
var from = "unknown";
try {
// # 1) 优先用 Configuration最快
var uiMode = context.getResources().getConfiguration().uiMode;
var nightMask = (uiMode & android.content.res.Configuration.UI_MODE_NIGHT_MASK);
if (nightMask === android.content.res.Configuration.UI_MODE_NIGHT_YES) { result = true; from = "Configuration(UI_MODE_NIGHT_YES)"; }
else if (nightMask === android.content.res.Configuration.UI_MODE_NIGHT_NO) { result = false; from = "Configuration(UI_MODE_NIGHT_NO)"; }
} catch(e1) { safeLog(null, 'e', "catch " + String(e1)); }
if (from === "unknown") {
try {
// # 2) 再用 UiModeManager更"系统态"
var um = context.getSystemService(android.content.Context.UI_MODE_SERVICE);
if (um) {
var nm = um.getNightMode();
if (nm === android.app.UiModeManager.MODE_NIGHT_YES) { result = true; from = "UiModeManager(MODE_NIGHT_YES)"; }
else if (nm === android.app.UiModeManager.MODE_NIGHT_NO) { result = false; from = "UiModeManager(MODE_NIGHT_NO)"; }
else { from = "UiModeManager(mode=" + String(nm) + ")"; }
}
} catch(e2) { safeLog(null, 'e', "catch " + String(e2)); }
}
// # 3) 实在判断不了,就按"非暗色"处理,避免自动主题背景黑成一片
if (from === "unknown") { result = false; from = "fallback(false)"; }
// 仅在状态改变时打印日志,避免刷屏
var logKey = String(result) + "|" + from + "|" + mode;
if (this._lastDarkThemeLog !== logKey) {
this._lastDarkThemeLog = logKey;
try { _th_log(this.L, "d", "[theme] isDarkTheme=" + String(result) + " via=" + from + " mode=" + mode); } catch(e3) { safeLog(null, 'e', "catch " + String(e3)); }
}
// # 主题切换时刷新莫奈配色(传入 result 避免递归)
// 注:构造函数中会初始化,这里只在构造完成后的切换时触发
if (this._lastDarkResult !== undefined && this._lastDarkResult !== result) {
this._lastDarkResult = result;
try { this.refreshMonetColors(result); } catch(eM) { safeLog(null, 'e', "catch " + String(eM)); }
} else if (this._lastDarkResult === undefined) {
this._lastDarkResult = result;
}
return result;
};
FloatBallAppWM.prototype.refreshMonetColors = function(forceDark) {
try {
var isDark = (forceDark !== undefined) ? forceDark : this.isDarkTheme();
var m = MonetColorProvider.getColors(isDark);
var ml = MonetColorProvider.getColors(false);
var md = MonetColorProvider.getColors(true);
var c = this.ui.colors;
// 浅色配色
c.bgLight = ml.surface;
c.cardLight = ml.surfaceVariant;
c.textPriLight = ml.onSurface;
c.textSecLight = ml.onSurfaceVariant;
c.dividerLight = ml.outline;
c.inputBgLight = ml.surface;
// 深色配色
c.bgDark = md.surface;
c.cardDark = md.surfaceVariant;
c.textPriDark = md.onSurface;
c.textSecDark = md.onSurfaceVariant;
c.dividerDark = md.outline;
c.inputBgDark = md.surface;
// 当前主题配色(随主题切换)
c.primary = m.primary;
// primaryDark 修正为 onPrimaryContainer日间为深蓝(#041E49),夜间为浅蓝(#D3E3FD),符合"深色变体"语义
c.primaryDark = m.onPrimaryContainer;
c.accent = m.secondary;
c.danger = m.error;
// success/warning 优化对比度:日间更深确保可见,夜间保持适度亮度不刺眼
c.success = isDark ? android.graphics.Color.parseColor("#4ade80") : android.graphics.Color.parseColor("#15803d");
c.warning = isDark ? android.graphics.Color.parseColor("#fbbf24") : android.graphics.Color.parseColor("#b45309");
// 扩展:完整 Monet 语义字段(供面板方法直接使用)
c._monetSurface = m.surface;
c._monetOnSurface = m.onSurface;
c._monetOutline = m.outline;
c._monetOnPrimary = m.onPrimary;
c._monetPrimaryContainer = m.primaryContainer;
c._monetOnPrimaryContainer = m.onPrimaryContainer;
c._monetSecondary = m.secondary;
c._monetTertiary = m.tertiary;
try { _th_log(this.L, "d", "[monet] refreshed isDark=" + isDark + " primary=" + _th_hex(c.primary) + " primaryDark=" + _th_hex(c.primaryDark) + " accent=" + _th_hex(c.accent)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
} catch (e) {
try { _th_log(this.L, "e", "[monet] refresh err=" + String(e)); } catch(e2) { safeLog(null, 'e', "catch " + String(e2)); }
}
};
FloatBallAppWM.prototype.getMonetAccentForBall = function() {
// 这段代码的主要内容/用途尝试读取系统动态强调色Monet/accent失败则使用兜底颜色并打印命中信息。
// 优化点:
// 1) 日间使用 500/600 档(更深、对比度更高),夜间使用 400/300 档(柔和、不刺眼)
// 2) 移除 system_neutral1_* 中性灰色(不是强调色)
var res = context.getResources();
var dark = this.isDarkTheme();
var names = dark
? ["system_accent1_400", "system_accent1_300", "system_accent2_400"]
: ["system_accent1_500", "system_accent1_600", "system_accent2_500"];
var i;
for (i = 0; i < names.length; i++) {
try {
var id = res.getIdentifier(names[i], "color", "android");
if (id > 0) {
var c = res.getColor(id, null);
var logKey = "hit|" + names[i] + "|" + c;
if (this._lastAccentLog !== logKey) {
this._lastAccentLog = logKey;
try { _th_log(this.L, "d", "[theme] hit accent=" + names[i] + " id=" + String(id) + " c=" + _th_hex(c) + " " + _th_argb(c)); } catch(eL0) { safeLog(null, 'e', "catch " + String(eL0)); }
}
return c;
}
} catch (e) {
try { _th_log(this.L, "w", "[theme] err accent=" + names[i] + " e=" + String(e)); } catch(eL2) { safeLog(null, 'e', "catch " + String(eL2)); }
}
}
var fbHex = dark
? (this.config.BALL_FALLBACK_DARK || CONST_BALL_FALLBACK_DARK)
: (this.config.BALL_FALLBACK_LIGHT || CONST_BALL_FALLBACK_LIGHT);
var fb = android.graphics.Color.parseColor(fbHex);
var logKeyFb = "miss_all|" + fb;
if (this._lastAccentLog !== logKeyFb) {
this._lastAccentLog = logKeyFb;
try { _th_log(this.L, "w", "[theme] accent miss all, fallback=" + _th_hex(fb) + " " + _th_argb(fb)); } catch(eL3) { safeLog(null, 'e', "catch " + String(eL3)); }
}
return fb;
};
FloatBallAppWM.prototype.updateBallContentBackground = function(contentView) {
try {
var ballColor = this.getMonetAccentForBall();
var dark = this.isDarkTheme();
var alpha01 = dark ? this.config.BALL_RIPPLE_ALPHA_DARK : this.config.BALL_RIPPLE_ALPHA_LIGHT;
var rippleColor = this.withAlpha(ballColor, alpha01);
// # 自定义 PNG/APP 模式下:背景透明
var fillColor = ballColor;
var _usedKind = "none";
try { _usedKind = this.state.usedIconKind || "none"; } catch(eK) { safeLog(null, 'e', "catch " + String(eK)); }
try {
var _pngModeBg = Number(this.config.BALL_PNG_MODE || 0);
if ((_pngModeBg === 1 && _usedKind === "file") || _usedKind === "app") {
fillColor = android.graphics.Color.TRANSPARENT;
}
} catch(eBg) { safeLog(null, 'e', "catch " + String(eBg)); }
var content = new android.graphics.drawable.GradientDrawable();
content.setShape(android.graphics.drawable.GradientDrawable.OVAL);
content.setColor(fillColor);
// # 描边:根据球体颜色亮度自动选择白/黑描边,确保任何背景下都可见
if (_usedKind !== "file" && _usedKind !== "app") {
try {
var Color = android.graphics.Color;
var lum = (Color.red(fillColor)*0.299 + Color.green(fillColor)*0.587 + Color.blue(fillColor)*0.114) / 255.0;
var strokeInt = lum > 0.55
? Color.parseColor("#33000000") // 浅球用半透明黑边
: Color.parseColor("#55FFFFFF"); // 深球用半透明白边
content.setStroke(this.dp(1), strokeInt);
} catch(eS) { safeLog(null, 'e', "catch " + String(eS)); }
}
var mask = new android.graphics.drawable.GradientDrawable();
mask.setShape(android.graphics.drawable.GradientDrawable.OVAL);
mask.setColor(android.graphics.Color.WHITE);
contentView.setBackground(new android.graphics.drawable.RippleDrawable(
android.content.res.ColorStateList.valueOf(rippleColor),
content,
mask
));
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
};
FloatBallAppWM.prototype.safeParseColor = function(hex, fallbackInt) {
// 这段代码的主要内容/用途:安全解析 #RRGGBB/#AARRGGBB解析失败直接回退避免 system_server 抛异常。
try { return android.graphics.Color.parseColor(String(hex)); } catch (e) { return fallbackInt; }
};
FloatBallAppWM.prototype.getPanelBgColorInt = function() {
// 这段代码的主要内容/用途:配合"白天/夜晚"两档主题,返回统一的背景颜色(不再依赖自动亮度推断)。
var isDark = this.isDarkTheme();
var dayBgHex = (this.config.THEME_DAY_BG_HEX != null) ? String(this.config.THEME_DAY_BG_HEX) : null;
var nightBgHex = (this.config.THEME_NIGHT_BG_HEX != null) ? String(this.config.THEME_NIGHT_BG_HEX) : null;
// # 兼容旧版默认配色:若仍为旧默认值,自动回退到莫奈色
if (dayBgHex === "#FAF4E3") dayBgHex = null;
if (nightBgHex === "#191928") nightBgHex = null;
// # 未配置时使用莫奈 surface 色作为回退
var dayFallback = this.ui.colors.bgLight || android.graphics.Color.parseColor("#F8F9FA");
var nightFallback = this.ui.colors.bgDark || android.graphics.Color.parseColor("#131314");
var base = isDark
? (nightBgHex ? this.safeParseColor(nightBgHex, nightFallback) : nightFallback)
: (dayBgHex ? this.safeParseColor(dayBgHex, dayFallback) : dayFallback);
// # 继承原配置面板背景透明度0~1
var a = 1.0;
try { a = Number(this.config.PANEL_BG_ALPHA); } catch (e1) { a = 0.85; }
if (!(a >= 0.0 && a <= 1.0)) a = 0.85;
var out = this.withAlpha(base, a);
try { _th_log(this.L, "d", "[t]bg isDark=" + isDark + " o=" + _th_hex(out)); } catch(e2) { safeLog(null, 'e', "catch " + String(e2)); }
return out;
};
FloatBallAppWM.prototype.getPanelTextColorInt = function(bgInt) {
// 这段代码的主要内容/用途:配合"白天/夜晚"两档主题,返回统一的文字颜色(不再依赖自动亮度推断)。
var isDark = this.isDarkTheme();
var dayTextHex = (this.config.THEME_DAY_TEXT_HEX != null) ? String(this.config.THEME_DAY_TEXT_HEX) : null;
var nightTextHex = (this.config.THEME_NIGHT_TEXT_HEX != null) ? String(this.config.THEME_NIGHT_TEXT_HEX) : null;
// # 兼容旧版默认配色:若仍为旧默认值,自动回退到莫奈色
if (dayTextHex === "#333333") dayTextHex = null;
if (nightTextHex === "#E6E6F0") nightTextHex = null;
// # 未配置时使用莫奈 onSurface 色作为回退
var dayFallback = this.ui.colors.textPriLight || android.graphics.Color.parseColor("#1F1F1F");
var nightFallback = this.ui.colors.textPriDark || android.graphics.Color.parseColor("#E3E3E3");
if (!isDark) return dayTextHex ? this.safeParseColor(dayTextHex, dayFallback) : dayFallback;
return nightTextHex ? this.safeParseColor(nightTextHex, nightFallback) : nightFallback;
};
FloatBallAppWM.prototype.applyTextColorRecursive = function(v, colorInt) {
// 这段代码的主要内容/用途:递归设置面板内所有 TextView 的文字颜色(标题/按钮文字/描述/设置项等)。
try {
if (!v) return;
if (v instanceof android.widget.TextView) {
v.setTextColor(colorInt);
}
if (v instanceof android.view.ViewGroup) {
var i, n = v.getChildCount();
for (i = 0; i < n; i++) {
this.applyTextColorRecursive(v.getChildAt(i), colorInt);
}
}
} catch(e0) { safeLog(null, 'e', "catch " + String(e0)); }
};
FloatBallAppWM.prototype.updatePanelBackground = function(panelView) {
// 这段代码的主要内容/用途:统一为"主面板/设置面板/查看器面板"应用背景与文字颜色(自动/亮/暗三档),并输出调试日志(命中哪个颜色)。
try {
var bg = new android.graphics.drawable.GradientDrawable();
bg.setCornerRadius(this.dp(22));
var bgInt = this.getPanelBgColorInt();
bg.setColor(bgInt);
// 轻量描边:亮色时更明显,暗色时也保留一点边界(不提供自定义输入,避免设置页复杂化)
var sw = this.dp(1);
var isDark = this.isDarkTheme();
var outlineColor = this.ui.colors._monetOutline || (isDark ? android.graphics.Color.parseColor("#8E918F") : android.graphics.Color.parseColor("#747775"));
var stroke = this.withAlpha(outlineColor, isDark ? 0.26 : 0.20);
try { bg.setStroke(sw, stroke); } catch(eS) { safeLog(null, 'e', "catch " + String(eS)); }
panelView.setBackground(bg);
var tc = this.getPanelTextColorInt(bgInt);
try { themeBgInt = bgInt; themeTextInt = tc; } catch(eT) { safeLog(null, 'e', "catch " + String(eT)); }
this.applyTextColorRecursive(panelView, tc);
try { _th_log(this.L, "d", "[t]apply bg=" + _th_hex(bgInt) + " tx=" + _th_hex(tc)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
try {
_th_log(this.L, "d",
"[theme:apply] isDark=" + isDark +
" bg=" + _th_hex(bgInt) + " " + _th_argb(bgInt) +
" text=" + _th_hex(tc) + " " + _th_argb(tc) +
" stroke=" + _th_hex(stroke)
);
} catch(eL0) { safeLog(null, 'e', "catch " + String(eL0)); }
} catch (e) {
try { _th_log(this.L, "e", "[theme:apply] err=" + String(e)); } catch(eL1) { safeLog(null, 'e', "catch " + String(eL1)); }
}
};

298
code/th_05_persistence.js Normal file
View File

@@ -0,0 +1,298 @@
// @version 1.0.0
// =======================【工具:面板位置持久化】======================
FloatBallAppWM.prototype.savePanelState = function(key, state) {
if (!key || !state) return;
try {
if (!this.config.PANEL_STATES) this.config.PANEL_STATES = {};
this.config.PANEL_STATES[key] = state;
// 节流或立即保存? 面板拖动结束通常不频繁,立即保存即可
// 但为了避免连续事件,还是可以复用 savePos 的节流逻辑,或者直接保存
ConfigManager.saveSettings(this.config);
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
};
FloatBallAppWM.prototype.loadPanelState = function(key) {
if (!key || !this.config.PANEL_STATES) return null;
return this.config.PANEL_STATES[key];
};
// =======================【工具:位置持久化】======================
FloatBallAppWM.prototype.savePos = function(x, y) {
try {
this.config.BALL_INIT_X = Math.floor(x);
this.config.BALL_INIT_Y_DP = Math.floor(y / this.state.density);
// # 节流保存
return ConfigManager.saveSettings(this.config);
} catch (e) { return false; }
};
FloatBallAppWM.prototype.loadSavedPos = function() {
// # 直接从 config 返回,因为 config 已经是持久化的
var x = Number(this.config.BALL_INIT_X || 0);
var y = this.dp(Number(this.config.BALL_INIT_Y_DP || 100));
return { x: x, y: y };
};
FloatBallAppWM.prototype.trySavePosThrottled = function(x, y) {
var t = this.now();
if (t - this.state.lastSaveTs < this.config.SAVE_THROTTLE_MS) return false;
this.state.lastSaveTs = t;
return this.savePos(x, y);
};
// =======================【工具:配置持久化】======================
FloatBallAppWM.prototype.saveConfig = function(obj) {
try {
for (var k in obj) {
if (obj.hasOwnProperty(k)) {
this.config[k] = obj[k];
}
}
if (this.L) this.L.updateConfig(this.config);
return ConfigManager.saveSettings(this.config);
} catch (e) { return false; }
};
// =======================【设置面板schema】======================
FloatBallAppWM.prototype.getConfigSchema = function() {
return ConfigManager.loadSchema();
};
// =======================【设置面板:临时编辑缓存】======================
FloatBallAppWM.prototype.beginEditConfig = function() {
try {
var schema = this.getConfigSchema();
var p = {};
var i;
for (i = 0; i < schema.length; i++) {
if (!schema[i] || !schema[i].key) continue;
var k = String(schema[i].key);
p[k] = this.config[k];
}
this.state.pendingUserCfg = p;
this.state.pendingDirty = false;
return true;
} catch (e0) {
this.state.pendingUserCfg = null;
this.state.pendingDirty = false;
return false;
}
};
FloatBallAppWM.prototype.getPendingValue = function(k) {
if (this.state.pendingUserCfg && this.state.pendingUserCfg.hasOwnProperty(k)) return this.state.pendingUserCfg[k];
return this.config[k];
};
FloatBallAppWM.prototype.setPendingValue = function(k, v) {
if (!this.state.pendingUserCfg) this.beginEditConfig();
this.state.pendingUserCfg[k] = v;
this.state.pendingDirty = true;
if (this.state.previewMode) {
this.refreshPreview(k);
}
};
FloatBallAppWM.prototype.getEffectiveConfig = function() {
if (!this.state.previewMode || !this.state.pendingUserCfg) return this.config;
var cfg = {};
for (var k in this.config) { cfg[k] = this.config[k]; }
for (var k in this.state.pendingUserCfg) { cfg[k] = this.state.pendingUserCfg[k]; }
return cfg;
};
FloatBallAppWM.prototype.refreshPreview = function(changedKey) {
if (this.state.closing) return;
var self = this;
// Post to next tick to avoid destroying view during event dispatch (fixes crash on switch toggle)
if (this.state.h) {
this.state.h.post(new JavaAdapter(java.lang.Runnable, {
run: function() { self._refreshPreviewInternal(changedKey); }
}));
} else {
self._refreshPreviewInternal(changedKey);
}
};
FloatBallAppWM.prototype._refreshPreviewInternal = function(changedKey) {
if (this.state.closing) return;
var originalConfig = this.config;
try {
// 使用临时配置
this.config = this.getEffectiveConfig();
var needBall = false;
var needPanel = false;
if (!changedKey) {
needBall = true;
needPanel = true;
} else {
// 根据修改的 key 判断需要刷新什么,避免全量刷新导致闪烁
if (changedKey.indexOf("BALL_") === 0) needBall = true;
if (changedKey.indexOf("PANEL_") === 0) needPanel = true;
// 球大小改变会影响面板位置
if (changedKey === "BALL_SIZE_DP") needPanel = true;
}
// 1. 刷新悬浮球 (保持面板不关闭)
if (needBall) {
this.rebuildBallForNewSize(true);
}
// 2. 刷新主面板预览
if (needPanel) {
// 如果当前没有显示主面板,则创建并显示;如果已显示,则替换
var panel = this.buildPanelView("main");
// 计算位置 (使用当前球的位置)
var maxH = Math.floor(this.state.screen.h * 0.75);
panel.measure(
android.view.View.MeasureSpec.makeMeasureSpec(this.state.screen.w, android.view.View.MeasureSpec.AT_MOST),
android.view.View.MeasureSpec.makeMeasureSpec(maxH, android.view.View.MeasureSpec.AT_MOST)
);
var pw = panel.getMeasuredWidth();
var ph = panel.getMeasuredHeight();
if (ph > maxH) ph = maxH;
var bx = this.state.ballLp.x;
var by = this.state.ballLp.y;
var px = this.computePanelX(bx, pw);
var py = by;
// 尝试调整 Y
var r = this.tryAdjustPanelY(px, py, pw, ph, bx, by);
var finalX = r.ok ? r.x : px;
var finalY = r.ok ? r.y : this.clamp(py, 0, this.state.screen.h - ph);
// 优化闪烁:先添加新面板,再移除旧面板 (这样新面板会在最上层,符合预览需求)
var oldPanel = this.state.panel;
var oldAdded = this.state.addedPanel;
// 添加新面板 (addPanel 会更新 this.state.panel)
// 注意addPanel 中已为 main 添加 FLAG_NOT_FOCUSABLE所以即使在最上层也不会抢走 Settings 的输入焦点
this.addPanel(panel, finalX, finalY, "main");
// 移除旧面板
if (oldAdded && oldPanel) {
try { this.state.wm.removeView(oldPanel); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
}
} catch(e) {
safeLog(this.L, 'e', "refreshPreview err=" + e);
} finally {
this.config = originalConfig;
}
};
FloatBallAppWM.prototype.persistUserCfgFromObject = function(obj) {
// # 这段代码的主要内容/用途:从临时编辑对象里按 schema 白名单抽取并保存(跳过 section 标题等无 key 项)
try {
var schema = this.getConfigSchema();
var out = {};
var i;
for (i = 0; i < schema.length; i++) {
if (!schema[i] || !schema[i].key) continue;
var k = String(schema[i].key);
out[k] = obj[k];
}
return this.saveConfig(out);
} catch (e0) { return false; }
};
FloatBallAppWM.prototype.applyImmediateEffectsForKey = function(k) {
try {
if (k === "LOG_ENABLE") {
try {
if (this.L) {
this.L.enable = !!this.config.LOG_ENABLE;
this.L.cfg.LOG_ENABLE = !!this.config.LOG_ENABLE;
this.L.i("apply LOG_ENABLE=" + String(this.config.LOG_ENABLE));
}
} catch(eLE) { safeLog(null, 'e', "catch " + String(eLE)); }
return;
}
if (k === "LOG_DEBUG") {
try {
if (this.L) {
this.L.debug = !!this.config.LOG_DEBUG;
this.L.cfg.LOG_DEBUG = !!this.config.LOG_DEBUG;
this.L.i("apply LOG_DEBUG=" + String(this.config.LOG_DEBUG));
}
} catch(eLD) { safeLog(null, 'e', "catch " + String(eLD)); }
return;
}
if (k === "LOG_KEEP_DAYS") {
try {
var n = Math.max(1, Math.floor(Number(this.config.LOG_KEEP_DAYS || 3)));
this.config.LOG_KEEP_DAYS = n;
if (this.L) {
this.L.keepDays = n;
this.L.cfg.LOG_KEEP_DAYS = n;
this.L.i("apply LOG_KEEP_DAYS=" + String(n));
this.L.cleanupOldFiles();
}
} catch(eLK) { safeLog(null, 'e', "catch " + String(eLK)); }
return;
}
if (k === "BALL_SIZE_DP" || k === "BALL_PNG_MODE" || k === "BALL_ICON_TYPE" || k === "BALL_ICON_FILE_PATH" || k === "BALL_ICON_RES_ID" || k === "BALL_ICON_RES_NAME" || k === "BALL_ICON_SIZE_DP" || k === "BALL_ICON_TINT_HEX") { this.rebuildBallForNewSize(); return; }
if (k === "PANEL_ROWS" || k === "PANEL_COLS" ||
k === "PANEL_ITEM_SIZE_DP" || k === "PANEL_GAP_DP" ||
k === "PANEL_PADDING_DP" || k === "PANEL_ICON_SIZE_DP" ||
k === "PANEL_LABEL_ENABLED" || k === "PANEL_LABEL_TEXT_SIZE_SP" ||
k === "PANEL_LABEL_TOP_MARGIN_DP") {
if (this.state.addedPanel) this.hideMainPanel();
if (this.state.addedSettings) this.hideSettingsPanel();
if (this.state.addedViewer) this.hideViewerPanel();
return;
}
if (k === "EDGE_VISIBLE_RATIO") {
if (this.state.addedBall && this.state.docked) {
this.state.docked = false;
this.snapToEdgeDocked(false);
}
return;
}
} catch(e0) { safeLog(null, 'e', "catch " + String(e0)); }
};
FloatBallAppWM.prototype.commitPendingUserCfg = function() {
try {
if (!this.state.pendingUserCfg) return { ok: false, reason: "no_pending" };
var schema = this.getConfigSchema();
var changedKeys = [];
var i;
for (i = 0; i < schema.length; i++) {
if (!schema[i] || !schema[i].key) continue;
var k = String(schema[i].key);
var oldV = this.config[k];
var newV = this.state.pendingUserCfg[k];
if (String(oldV) !== String(newV)) {
this.config[k] = newV;
changedKeys.push(k);
}
}
this.persistUserCfgFromObject(this.state.pendingUserCfg);
var j;
for (j = 0; j < changedKeys.length; j++) {
if (changedKeys[j] === "BALL_SIZE_DP") { this.applyImmediateEffectsForKey("BALL_SIZE_DP"); break; }
}
for (j = 0; j < changedKeys.length; j++) {
if (changedKeys[j] !== "BALL_SIZE_DP") this.applyImmediateEffectsForKey(changedKeys[j]);
}
this.state.pendingDirty = false;
safeLog(this.L, 'i', "commit settings changed=" + JSON.stringify(changedKeys));
return { ok: true, changed: changedKeys };
} catch (e0) {
safeLog(this.L, 'e', "commitPendingUserCfg err=" + String(e0));
return { ok: false, err: String(e0) };
}
};

532
code/th_06_icon_parser.js Normal file
View File

@@ -0,0 +1,532 @@
// @version 1.0.0
// =======================【工具:吸边数据】======================
FloatBallAppWM.prototype.getDockInfo = function() {
var ballSize = this.dp(this.config.BALL_SIZE_DP);
var visible = Math.max(1, Math.round(ballSize * this.config.EDGE_VISIBLE_RATIO));
var hidden = ballSize - visible;
return { ballSize: ballSize, visiblePx: visible, hiddenPx: hidden };
};
// =======================【工具:图标解析】======================
// =======================【工具:快捷方式图标文件路径】======================
FloatBallAppWM.prototype.getShortcutIconFilePath = function(pkg, shortcutId, userId) {
// # 主要用途:把快捷方式图标持久化到 shortcut_icons 目录,供按钮页/按钮管理页稳定显示(桌面移除后仍可显示)
try {
var p = (pkg == null) ? "" : String(pkg);
var s = (shortcutId == null) ? "" : String(shortcutId);
var u = (userId == null) ? "0" : String(userId);
// # 文件名去非法字符,避免路径注入或创建失败
function _sn(v) {
try {
var t = String(v == null ? "" : v);
t = t.replace(/[^a-zA-Z0-9._-]+/g, "_");
if (t.length > 120) t = t.substring(0, 120);
return t;
} catch(e) { return ""; }
}
var dir = String(APP_ROOT_DIR) + "/shortcut_icons";
var fn = _sn(p) + "__" + _sn(s) + "__u" + _sn(u) + ".png";
return dir + "/" + fn;
} catch (e0) {
return "";
}
};
FloatBallAppWM.prototype.resolveIconDrawable = function(btn) {
// # 主要用途:解析面板按钮图标(优先 app 包名图标,其次自定义 resId最后兜底
try {
if (!btn) return context.getResources().getDrawable(android.R.drawable.ic_menu_help, null);
// # 0) 优先检查 iconPath (绝对路径)
// # 引用优化:复用 loadBallIconDrawableFromFile 安全加载逻辑
if (btn.iconPath) {
try {
var path = String(btn.iconPath);
if (path) {
// targetPx: 面板图标大小; Limit: 1MB, 1024px
var sizeDp = this.config.PANEL_ICON_SIZE_DP || 32;
var dr = this.loadBallIconDrawableFromFile(path, this.dp(sizeDp), 1048576, 1024);
if (dr) return dr;
}
} catch(ePath) { safeLog(null, 'e', "catch " + String(ePath)); }
}
// # 1) type=app 且配置了 pkg自动取应用图标
try {
var t = (btn.type == null) ? "" : String(btn.type);
if (t === "app") {
var pkg = (btn.pkg == null) ? "" : String(btn.pkg);
if (pkg.length > 0) {
// # 统一 LRU 缓存:避免频繁走 PackageManagerDrawable 可复用);并带容量上限,防止无限增长
var kApp = "app|" + pkg;
var hitApp = this._iconLruGet(kApp);
if (hitApp) return hitApp;
var pm = context.getPackageManager();
var drApp = pm.getApplicationIcon(pkg);
if (drApp != null) {
this._iconLruPut(kApp, drApp);
return drApp;
}
}
}
} catch(eApp) { safeLog(null, 'e', "catch " + String(eApp)); }
// # 1.5) type=shortcut尝试取 Shortcuts 快捷方式图标(显示与 shortcuts.js 页面一致)
// # 说明btn 需要包含 pkg + shortcutId图标获取可能较重因此做简单缓存失败则回退到应用图标。
try {
var t2 = (btn.type == null) ? "" : String(btn.type);
if (t2 === "shortcut") {
var pkg2 = (btn.pkg == null) ? "" : String(btn.pkg);
var sid2 = (btn.shortcutId == null) ? "" : String(btn.shortcutId);
if (pkg2.length > 0 && sid2.length > 0) {
// # 1.5.1) 优先从 shortcut_icons 持久化目录读取(桌面移除后仍可显示正确图标)
try {
var iconFilePath0 = this.getShortcutIconFilePath(pkg2, sid2, (btn.userId != null ? String(btn.userId) : "0"));
if (iconFilePath0) {
try {
var f0 = new java.io.File(iconFilePath0);
if (f0.exists() && f0.isFile()) {
var sizeDp0 = this.config.PANEL_ICON_SIZE_DP || 32;
var iconSizePx0 = this.dp(sizeDp0);
var dr0 = this.loadBallIconDrawableFromFile(iconFilePath0, iconSizePx0, 1048576, 1024);
if (dr0) {
// # 写入统一 LRU 缓存:同一个 shortcut 复用 Drawable避免反复解码 PNG
try {
var sk0 = pkg2 + "@" + sid2 + "@" + (btn.userId != null ? String(btn.userId) : "");
this._iconLruPut("sc|" + sk0, dr0);
} catch(eSc0) { safeLog(null, 'e', "catch " + String(eSc0)); }
return dr0;
}
}
} catch(eF0) { safeLog(null, 'e', "catch " + String(eF0)); }
}
} catch(eFile0) { safeLog(null, 'e', "catch " + String(eFile0)); }
var skey = pkg2 + "@" + sid2 + "@" + (btn.userId != null ? String(btn.userId) : "");
var kSc = "sc|" + skey;
var hitSc = this._iconLruGet(kSc);
if (hitSc) return hitSc;
// # 失败冷却:某些 ROM/桌面在短时间内反复查询 Shortcuts 会很慢或直接抛异常。
// # 这里对同一个 shortcut key 做短暂冷却(默认 10 秒),避免面板频繁刷新导致卡顿/ANR 风险。
if (!this._shortcutIconFailTs) this._shortcutIconFailTs = {};
var nowTs = 0;
try { nowTs = new Date().getTime(); } catch(eNow) { nowTs = 0; }
var lastFailTs = this._shortcutIconFailTs[skey];
if (lastFailTs && nowTs > 0 && (nowTs - lastFailTs) < 10000) {
// # 冷却期内直接跳过 shortcut icon 查询,走回退逻辑(应用图标)
} else {
var la = context.getSystemService(android.content.Context.LAUNCHER_APPS_SERVICE);
if (la) {
// # 修复:按钮管理页/按钮面板中,微信小程序等 shortcut 图标显示成宿主 App 图标
// # 根因(工作假设):用 setShortcutIds() 精确查询时,部分 ROM 的实现会返回"退化"的 ShortcutInfo
// # getShortcutIconDrawable 结果被降级为宿主 App 图标。
// # 方案:完全复用 shortcuts.js 的做法--先按 package + flags 拉取该包的 shortcuts 列表,再在列表里按 id 过滤命中项取图标。
// # 注意:不依赖外部 buildShortcutItemsIndex(),避免主程序环境下未加载 shortcuts.js 导致直接回退。
// # 方案:完全复用"选择快捷方式列表"的取数路径,确保微信小程序等 pinned shortcut 能拿到正确的 ShortcutInfo
// # 1) 优先直连 shortcut service选择器同款信息更完整
// # 2) 失败再回退 LauncherApps.getShortcuts
var list = null;
// # 1) shortcut service 直连(与选择器保持一致)
try {
var userIdIntForSvc = 0;
try {
var buid2 = null;
if (btn.userId != null) buid2 = btn.userId;
if (buid2 == null && btn.user != null) buid2 = btn.user;
if (buid2 != null) {
var tmpUid2 = parseInt(String(buid2), 10);
if (!isNaN(tmpUid2)) userIdIntForSvc = tmpUid2;
}
} catch(eUidSvc) { safeLog(null, 'e', "catch " + String(eUidSvc)); }
var sm2 = android.os.ServiceManager;
var shortcutSvc = null;
try { shortcutSvc = sm2.getService("shortcut"); } catch(eSm2) { shortcutSvc = null; }
if (shortcutSvc) {
var CFG_MATCH_ALL2 = 0x0000000F;
var slice2 = null;
try { slice2 = shortcutSvc.getShortcuts(String(pkg2), CFG_MATCH_ALL2, userIdIntForSvc); } catch(eS0b) { slice2 = null; }
if (slice2) {
var listObj2 = null;
try { listObj2 = slice2.getList(); } catch(eS1b) { listObj2 = null; }
if (listObj2) list = listObj2;
}
}
} catch(eSvc2) { safeLog(null, 'e', "catch " + String(eSvc2)); }
// # 2) LauncherApps 回退(当 shortcut service 不可用或返回空时)
if (list == null) {
var q = new android.content.pm.LauncherApps.ShortcutQuery();
try { q.setPackage(pkg2); } catch(eSP) { safeLog(null, 'e', "catch " + String(eSP)); }
// # 重要:必须设置 QueryFlags否则 getShortcuts 可能返回空(默认 flags=0
// # 兼容性:不同 Android/ROM 可能缺少部分 FLAG逐个 try 叠加
try {
var qFlags = 0;
try { qFlags = qFlags | android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC; } catch(eF1) { safeLog(null, 'e', "catch " + String(eF1)); }
try { qFlags = qFlags | android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED; } catch(eF2) { safeLog(null, 'e', "catch " + String(eF2)); }
try { qFlags = qFlags | android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST; } catch(eF3) { safeLog(null, 'e', "catch " + String(eF3)); }
try { qFlags = qFlags | android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED; } catch(eF4) { safeLog(null, 'e', "catch " + String(eF4)); }
try { q.setQueryFlags(qFlags); } catch(eSF) { safeLog(null, 'e', "catch " + String(eSF)); }
} catch(eQF) { safeLog(null, 'e', "catch " + String(eQF)); }
// # 重要:用户句柄优先用按钮携带的 userId如有否则使用当前用户
var uh = android.os.Process.myUserHandle();
try {
var buid = null;
if (btn.userId != null) buid = btn.userId;
if (buid == null && btn.user != null) buid = btn.user;
if (buid != null) {
var uidInt = parseInt(String(buid), 10);
if (!isNaN(uidInt)) {
uh = android.os.UserHandle.of(uidInt);
}
}
} catch(eUH) { safeLog(null, 'e', "catch " + String(eUH)); }
try { list = la.getShortcuts(q, uh); } catch(eGS) { list = null; }
}
if (list && list.size && list.size() > 0) {
// # 在返回列表中按 shortcutId 精确命中
var si = null;
try {
for (var kk = 0; kk < list.size(); kk++) {
var s0 = list.get(kk);
if (s0 != null) {
var id0 = "";
try { id0 = String(s0.getId()); } catch(eId0) { id0 = ""; }
if (id0 === sid2) { si = s0; break; }
}
}
} catch(eFind) { si = null; }
if (si != null) {
// # 与 shortcuts.js 一致:优先 la.getShortcutIconDrawable(shortcutInfo, 0),再兜底 Icon.loadDrawable
var drSc = null;
try { drSc = la.getShortcutIconDrawable(si, 0); } catch(eIcon0) { drSc = null; }
if (drSc == null) {
try {
var ic = si.getIcon();
if (ic != null) {
var d2 = ic.loadDrawable(context);
if (d2 != null) drSc = d2;
}
} catch(eIcon1) { safeLog(null, 'e', "catch " + String(eIcon1)); }
}
if (drSc != null) {
try { this._iconLruPut("sc|" + skey, drSc); } catch(eSc1) { safeLog(null, 'e', "catch " + String(eSc1)); }
return drSc;
}
}
}
}
// # 如果没拿到 shortcut 图标,记录一次失败时间,触发冷却
try {
if (nowTs > 0) this._shortcutIconFailTs[skey] = nowTs;
else this._shortcutIconFailTs[skey] = new Date().getTime();
} catch(eFT) { safeLog(null, 'e', "catch " + String(eFT)); }
}
// # 回退:取应用图标,至少保证按钮有图标可见
try {
var pm2 = context.getPackageManager();
var drApp2 = pm2.getApplicationIcon(pkg2);
if (drApp2 != null) return drApp2;
} catch(eFall) { safeLog(null, 'e', "catch " + String(eFall)); }
}
}
} catch(eSc) { safeLog(null, 'e', "catch " + String(eSc)); }
// # 2) 显式指定 iconResName (String) 或 iconResId (int)
try {
if (btn.iconResName) {
var drShortx = this.resolveShortXDrawable(btn.iconResName, btn && btn.iconTint ? String(btn.iconTint) : "");
if (drShortx != null) return drShortx;
var name = this.normalizeShortXIconName(btn.iconResName, true);
// # 回退到 android 系统图标
var id = context.getResources().getIdentifier(name, "drawable", "android");
if (id > 0) return context.getResources().getDrawable(id, null);
}
if (btn.iconResId) return context.getResources().getDrawable(btn.iconResId, null);
} catch(e1) { safeLog(null, 'e', "catch " + String(e1)); }
// # 3) 兜底
return context.getResources().getDrawable(android.R.drawable.ic_menu_help, null);
} catch(e2) { safeLog(null, 'e', "catch " + String(e2)); }
return null;
};
FloatBallAppWM.prototype.getShortXResHandle = function() {
if (this._shortxResHandle) return this._shortxResHandle;
try {
var flags = 0;
try { flags = android.content.Context.CONTEXT_INCLUDE_CODE | android.content.Context.CONTEXT_IGNORE_SECURITY; } catch (eF) { flags = android.content.Context.CONTEXT_RESTRICTED; }
var sxCtx = context.createPackageContext(CONST_SHORTX_PACKAGE, flags);
this._shortxResHandle = {
ctx: sxCtx,
res: sxCtx.getResources(),
cl: sxCtx.getClassLoader(),
pkg: CONST_SHORTX_PACKAGE
};
return this._shortxResHandle;
} catch (e) {
safeLog(this.L, 'w', "getShortXResHandle failed: " + String(e));
return null;
}
};
FloatBallAppWM.prototype.normalizeShortXIconName = function(name, keepPrefix) {
try {
var s = String(name == null ? "" : name).replace(/^\s+|\s+$/g, "");
if (!s) return "";
if (s.indexOf("@drawable/") === 0) s = s.substring(10);
if (s.indexOf(".") > 0) {
var parts = s.split(".");
s = parts[parts.length - 1];
}
if (s.indexOf("ic_remix_") === 0) {
return keepPrefix ? s : s.substring("ic_remix_".length);
}
if (s.indexOf("ic_") === 0) {
return s;
}
return keepPrefix ? ("ic_remix_" + s) : s;
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
return "";
};
FloatBallAppWM.prototype.getShortXApkPaths = function() {
var out = [];
try {
var handle = this.getShortXResHandle();
var ai = null;
try { if (handle && handle.ctx) ai = handle.ctx.getApplicationInfo(); } catch (eAi0) { ai = null; }
if (ai == null) {
try { ai = context.getPackageManager().getApplicationInfo(CONST_SHORTX_PACKAGE, 0); } catch (eAi1) { ai = null; }
}
function pushPath(p) {
try {
p = String(p || "");
if (!p) return;
if (out.indexOf(p) < 0) out.push(p);
} catch(eP) { safeLog(null, 'e', "catch " + String(eP)); }
}
if (ai) {
try { pushPath(ai.sourceDir); } catch(e0) { safeLog(null, 'e', "catch " + String(e0)); }
try { pushPath(ai.publicSourceDir); } catch(e1) { safeLog(null, 'e', "catch " + String(e1)); }
try {
var ss = ai.splitSourceDirs;
if (ss) {
var i;
for (i = 0; i < ss.length; i++) pushPath(ss[i]);
}
} catch(e2) { safeLog(null, 'e', "catch " + String(e2)); }
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
return out;
};
FloatBallAppWM.prototype.scanShortXIconsFromApk = function() {
var out = [];
var seen = {};
var paths = this.getShortXApkPaths();
// 宽松匹配:匹配 res/drawable* 和 res/mipmap* 下所有以 ic_ 开头的图标
var regex = /^res\/(drawable[^\/]*|mipmap[^\/]*)\/(ic_[a-z0-9_]+)\.(xml|png|webp|jpg|jpeg)$/;
var lastErr = "";
var totalFiles = 0;
var pi;
for (pi = 0; pi < paths.length; pi++) {
var zip = null;
try {
zip = new java.util.zip.ZipFile(String(paths[pi]));
var en = zip.entries();
while (en.hasMoreElements()) {
var ze = en.nextElement();
var name = String(ze.getName());
totalFiles++;
var m = regex.exec(name);
if (!m) continue;
var fullName = String(m[2]);
// 过滤掉系统图标
if (fullName.indexOf("ic_launcher") === 0 || fullName.indexOf("ic_menu_") === 0) continue;
if (seen[fullName]) continue;
seen[fullName] = true;
out.push({
name: fullName,
shortName: this.normalizeShortXIconName(fullName, false),
id: 0
});
}
} catch (eZip) {
lastErr = String(eZip);
} finally {
try { if (zip) zip.close(); } catch(eClose) { safeLog(null, 'e', "catch " + String(eClose)); }
}
}
if ((!out || out.length === 0) && lastErr) this._shortxIconCatalogError = "APK扫描: " + lastErr + " (路径数=" + paths.length + ", 文件数=" + totalFiles + ")";
return out;
};
FloatBallAppWM.prototype.getShortXIconLookupNames = function(name) {
var out = [];
try {
var s = String(name == null ? "" : name).replace(/^\s+|\s+$/g, "");
if (!s) return out;
if (s.indexOf("@drawable/") === 0) s = s.substring(10);
if (s.indexOf(".") > 0) {
var parts = s.split(".");
s = parts[parts.length - 1];
}
function add(v) {
if (!v) return;
if (out.indexOf(v) < 0) out.push(v);
}
add(s);
if (s.indexOf("ic_remix_") === 0) {
add(s.substring("ic_remix_".length));
} else if (s.indexOf("ic_") !== 0) {
add("ic_remix_" + s);
add("ic_" + s);
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
return out;
};
FloatBallAppWM.prototype.resolveShortXDrawableResId = function(name) {
try {
var handle = this.getShortXResHandle();
if (!handle || !handle.res) return 0;
var cands = this.getShortXIconLookupNames(name);
var i;
for (i = 0; i < cands.length; i++) {
var resId = 0;
try { resId = handle.res.getIdentifier(String(cands[i]), "drawable", handle.pkg); } catch (e1) { resId = 0; }
if (resId > 0) return resId;
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
return 0;
};
FloatBallAppWM.prototype.resolveShortXDrawable = function(name, tintHex) {
try {
var handle = this.getShortXResHandle();
if (!handle || !handle.res) return null;
var resId = this.resolveShortXDrawableResId(name);
if (resId <= 0) return null;
var dr = handle.res.getDrawable(resId, null);
if (dr && tintHex) {
try {
dr = dr.mutate();
dr.setColorFilter(android.graphics.Color.parseColor(String(tintHex)), android.graphics.PorterDuff.Mode.SRC_IN);
} catch(eTint) { safeLog(null, 'e', "catch " + String(eTint)); }
}
return dr;
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
return null;
};
FloatBallAppWM.prototype.getShortXIconDrawable = function(name) {
try {
return this.resolveShortXDrawable(name, null);
} catch (e) {
safeLog(this.L, 'w', "getShortXIconDrawable failed: " + String(e));
return null;
}
};
FloatBallAppWM.prototype.getShortXIconCatalog = function(forceReload) {
if (!forceReload && this._shortxIconCatalog) return this._shortxIconCatalog;
var out = [];
this._shortxIconCatalogError = "";
try {
var handle = this.getShortXResHandle();
if (handle && handle.cl) {
// 策略1反射 R$drawable 类(未混淆时可用)
try {
var clz = handle.cl.loadClass(CONST_SHORTX_PACKAGE + ".R$drawable");
var fields = clz.getFields();
var i;
for (i = 0; i < fields.length; i++) {
try {
var f = fields[i];
var fname = String(f.getName());
if (fname.indexOf("ic_remix_") !== 0 && fname.indexOf("ic_") !== 0) continue;
out.push({
name: fname,
shortName: (fname.indexOf("ic_remix_") === 0) ? fname.substring("ic_remix_".length) : fname,
id: f.getInt(null)
});
} catch(eField) { safeLog(null, 'e', "catch " + String(eField)); }
}
} catch (eClz) {
this._shortxIconCatalogError = "R$drawable reflect: " + String(eClz);
safeLog(this.L, 'w', "getShortXIconCatalog R$drawable failed: " + String(eClz));
}
// 策略2直接遍历资源 ID绕过混淆更稳定
if (!out || out.length === 0) {
try {
var ctx = context.createPackageContext(CONST_SHORTX_PACKAGE, android.content.Context.CONTEXT_IGNORE_SECURITY);
var res = ctx.getResources();
var startId = 2131230000;
var endId = 2131240000;
var count = 0;
for (var id = startId; id < endId && count < 20000; id++) {
try {
var rname = res.getResourceName(id);
if (rname && rname.indexOf("/ic_remix_") > 0) {
var parts = rname.split("/");
var resName = parts[parts.length - 1];
out.push({
name: resName,
shortName: resName.substring("ic_remix_".length),
id: id
});
count++;
}
} catch (eId) {
// ID 无效,跳过
}
}
} catch (eRes) {
this._shortxIconCatalogError = (this._shortxIconCatalogError ? this._shortxIconCatalogError + "; " : "") + "res scan: " + String(eRes);
safeLog(this.L, 'w', "getShortXIconCatalog res scan failed: " + String(eRes));
}
}
}
// 策略3APK 文件扫描(最后兜底)
if (!out || out.length === 0) {
out = this.scanShortXIconsFromApk();
}
out.sort(function(a, b) {
var aw = a.name.indexOf("ic_remix_") === 0 ? 1 : 0;
var bw = b.name.indexOf("ic_remix_") === 0 ? 1 : 0;
if (aw !== bw) return aw - bw;
var as = String(a.shortName || a.name);
var bs = String(b.shortName || b.name);
return as < bs ? -1 : (as > bs ? 1 : 0);
});
if (!out || out.length === 0) {
if (!this._shortxIconCatalogError) this._shortxIconCatalogError = "所有策略均未获取到图标";
} else {
this._shortxIconCatalogError = "";
}
} catch (e) {
this._shortxIconCatalogError = String(e);
safeLog(this.L, 'w', "getShortXIconCatalog failed: " + String(e));
}
this._shortxIconCatalog = out;
return out;
};

1855
code/th_07_shortcut.js Normal file

File diff suppressed because it is too large Load Diff

209
code/th_08_content.js Normal file
View File

@@ -0,0 +1,209 @@
// @version 1.0.0
// =======================【Content解析 settings URI】======================
// 这段代码的主要内容/用途:识别 content://settings/(system|secure|global)/KEY 并用 Settings.* get/put 更稳
FloatBallAppWM.prototype.parseSettingsUri = function(uriStr) {
try {
var s = String(uriStr || "");
if (s.indexOf("content://settings/") !== 0) return null;
// content://settings/system/accelerometer_rotation
var rest = s.substring("content://settings/".length);
var parts = rest.split("/");
if (!parts || parts.length < 1) return null;
var table = String(parts[0] || "");
var key = "";
if (parts.length >= 2) key = String(parts[1] || "");
if (table !== "system" && table !== "secure" && table !== "global") return null;
return { table: table, key: key };
} catch (e) { return null; }
};
FloatBallAppWM.prototype.settingsGetStringByTable = function(table, key) {
try {
var cr = context.getContentResolver();
if (table === "system") return android.provider.Settings.System.getString(cr, String(key));
if (table === "secure") return android.provider.Settings.Secure.getString(cr, String(key));
if (table === "global") return android.provider.Settings.Global.getString(cr, String(key));
return null;
} catch (e) { return null; }
};
FloatBallAppWM.prototype.settingsPutStringByTable = function(table, key, value) {
try {
var cr = context.getContentResolver();
if (table === "system") return android.provider.Settings.System.putString(cr, String(key), String(value));
if (table === "secure") return android.provider.Settings.Secure.putString(cr, String(key), String(value));
if (table === "global") return android.provider.Settings.Global.putString(cr, String(key), String(value));
return false;
} catch (e) { return false; }
};
// =======================【Content通用 query】======================
// 这段代码的主要内容/用途ContentResolver.query 并把 Cursor 转成文本(用于查看器面板)
FloatBallAppWM.prototype.contentQueryToText = function(uriStr, projection, selection, selectionArgs, sortOrder, maxRows) {
var out = { ok: false, uri: String(uriStr || ""), rows: 0, text: "", err: "" };
var cr = null;
var cur = null;
try {
cr = context.getContentResolver();
var uri = android.net.Uri.parse(String(uriStr));
var projArr = null;
if (projection && projection.length) {
projArr = java.lang.reflect.Array.newInstance(java.lang.String, projection.length);
var i0;
for (i0 = 0; i0 < projection.length; i0++) projArr[i0] = String(projection[i0]);
}
var sel = (selection === undefined || selection === null) ? null : String(selection);
var selArgsArr = null;
if (selectionArgs && selectionArgs.length) {
selArgsArr = java.lang.reflect.Array.newInstance(java.lang.String, selectionArgs.length);
var i1;
for (i1 = 0; i1 < selectionArgs.length; i1++) selArgsArr[i1] = String(selectionArgs[i1]);
}
var so = (sortOrder === undefined || sortOrder === null) ? null : String(sortOrder);
cur = cr.query(uri, projArr, sel, selArgsArr, so);
if (!cur) {
out.err = "query return null cursor";
return out;
}
var colCount = cur.getColumnCount();
var cols = [];
var ci;
for (ci = 0; ci < colCount; ci++) cols.push(String(cur.getColumnName(ci)));
var sb = [];
sb.push("URI: " + String(uriStr));
sb.push("Columns(" + String(colCount) + "): " + cols.join(", "));
sb.push("");
var limit = Math.max(1, Math.floor(Number(maxRows || this.config.CONTENT_MAX_ROWS || 20)));
var row = 0;
while (cur.moveToNext()) {
row++;
sb.push("#" + String(row));
var cj;
for (cj = 0; cj < colCount; cj++) {
var v = "";
try {
if (cur.isNull(cj)) v = "null";
else v = String(cur.getString(cj));
} catch (eV) {
try { v = String(cur.getLong(cj)); } catch (eV2) { v = "<??>"; }
}
sb.push(" " + cols[cj] + " = " + v);
}
sb.push("");
if (row >= limit) break;
}
out.ok = true;
out.rows = row;
out.text = sb.join("\n");
return out;
} catch (e) {
out.err = String(e);
return out;
} finally {
try { if (cur) cur.close(); } catch(eC) { safeLog(null, 'e', "catch " + String(eC)); }
}
};
// =======================【Content统一入口】======================
// 这段代码的主要内容/用途:处理按钮里的 type:"content"
FloatBallAppWM.prototype.execContentAction = function(btn) {
var mode = btn.mode ? String(btn.mode) : ((btn.value !== undefined && btn.value !== null) ? "put" : "get");
var uri = btn.uri ? String(btn.uri) : "";
if (!uri) return { ok: false, err: "missing uri" };
// settings uri 优先走 Settings API
var su = this.parseSettingsUri(uri);
if (mode === "get") {
if (su && su.key) {
var v = this.settingsGetStringByTable(su.table, su.key);
return { ok: true, mode: "get", kind: "settings", table: su.table, key: su.key, value: (v === null ? "null" : String(v)) };
}
// 非 settings尝试 query 一行
var q1 = this.contentQueryToText(uri, btn.projection, btn.selection, btn.selectionArgs, btn.sortOrder, 1);
if (!q1.ok) return { ok: false, mode: "get", kind: "query", err: q1.err };
return { ok: true, mode: "get", kind: "query", text: q1.text, rows: q1.rows };
}
if (mode === "put") {
var val = (btn.value === undefined || btn.value === null) ? "" : String(btn.value);
if (su && su.key) {
var ok = this.settingsPutStringByTable(su.table, su.key, val);
return { ok: !!ok, mode: "put", kind: "settings", table: su.table, key: su.key, value: val };
}
// 非 settings尽力走 update(ContentValues)
try {
var cr = context.getContentResolver();
var u = android.net.Uri.parse(uri);
var cv = new android.content.ContentValues();
// 支持 btn.values = {col1: "...", col2: "..."};否则写 value 列
if (btn.values) {
var k;
for (k in btn.values) {
if (!btn.values.hasOwnProperty(k)) continue;
cv.put(String(k), String(btn.values[k]));
}
} else {
cv.put("value", val);
}
var where = (btn.selection === undefined || btn.selection === null) ? null : String(btn.selection);
var whereArgs = null;
if (btn.selectionArgs && btn.selectionArgs.length) {
whereArgs = java.lang.reflect.Array.newInstance(java.lang.String, btn.selectionArgs.length);
var i2;
for (i2 = 0; i2 < btn.selectionArgs.length; i2++) whereArgs[i2] = String(btn.selectionArgs[i2]);
}
var n = cr.update(u, cv, where, whereArgs);
return { ok: true, mode: "put", kind: "update", updated: Number(n) };
} catch (eU) {
return { ok: false, mode: "put", kind: "update", err: String(eU) };
}
}
if (mode === "query") {
var maxRows = (btn.maxRows === undefined || btn.maxRows === null) ? this.config.CONTENT_MAX_ROWS : Number(btn.maxRows);
var q = this.contentQueryToText(uri, btn.projection, btn.selection, btn.selectionArgs, btn.sortOrder, maxRows);
if (!q.ok) return { ok: false, mode: "query", err: q.err };
return { ok: true, mode: "query", rows: q.rows, text: q.text };
}
if (mode === "view") {
try {
var it = new android.content.Intent(android.content.Intent.ACTION_VIEW);
it.setData(android.net.Uri.parse(uri));
it.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(it);
return { ok: true, mode: "view" };
} catch (eV) {
return { ok: false, mode: "view", err: String(eV) };
}
}
return { ok: false, err: "unknown mode=" + mode };
};
/* =======================
下面开始WM 动画、面板、触摸、启动、输出
======================= */

666
code/th_09_animation.js Normal file
View File

@@ -0,0 +1,666 @@
// @version 1.0.0
FloatBallAppWM.prototype.animateBallLayout = function(toX, toY, toW, durMs, endCb) {
var st = this.state;
if (!st.addedBall || !st.ballRoot || !st.ballLp) { if (endCb) endCb(); return; }
var fromX = st.ballLp.x;
var fromY = st.ballLp.y;
var fromW = st.ballLp.width;
try {
var va = android.animation.ValueAnimator.ofFloat(0.0, 1.0);
va.setDuration(durMs);
try {
// 使用 OvershootInterpolator 产生轻微的回弹效果,更加生动
// 0.7 的张力适中,不会过于夸张
va.setInterpolator(new android.view.animation.OvershootInterpolator(0.7));
} catch (eI) {
try { va.setInterpolator(new android.view.animation.DecelerateInterpolator()); } catch(eI2) { safeLog(null, 'e', "catch " + String(eI2)); }
}
var self = this;
va.addUpdateListener(new android.animation.ValueAnimator.AnimatorUpdateListener({
onAnimationUpdate: function(anim) {
try {
if (self.state.closing) return;
if (!self.state.addedBall) return;
var f = anim.getAnimatedValue();
var nx = Math.round(fromX + (toX - fromX) * f);
var ny = Math.round(fromY + (toY - fromY) * f);
var nw = Math.round(fromW + (toW - fromW) * f);
// 性能优化:只有坐标真正变化时才请求 WindowManager 更新
if (nx !== self.state.ballLp.x || ny !== self.state.ballLp.y || nw !== self.state.ballLp.width) {
self.state.ballLp.x = nx;
self.state.ballLp.y = ny;
self.state.ballLp.width = nw;
// # 关键操作使用 safeOperation 封装
safeOperation("dockAnimation.updateViewLayout", function() {
self.state.wm.updateViewLayout(self.state.ballRoot, self.state.ballLp);
}, true, self.L);
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
}));
va.addListener(new android.animation.Animator.AnimatorListener({
onAnimationStart: function() {},
onAnimationRepeat: function() {},
onAnimationCancel: function() {
try { self.state.ballAnimator = null; } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
},
onAnimationEnd: function() {
try { self.state.ballAnimator = null; } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
try {
if (!self.state.closing && self.state.addedBall) {
self.state.ballLp.x = toX;
self.state.ballLp.y = toY;
self.state.ballLp.width = toW;
self.state.wm.updateViewLayout(self.state.ballRoot, self.state.ballLp);
self.savePos(self.state.ballLp.x, self.state.ballLp.y);
}
} catch(e2) { safeLog(null, 'e', "catch " + String(e2)); }
try { if (endCb) endCb(); } catch (eCb) { try { if (self && self.L && self.L.e) self.L.e("animateBallLayout endCb err=" + String(eCb)); } catch(eLog) { safeLog(null, 'e', "catch " + String(eLog)); } }
}
}));
this.state.ballAnimator = va;
va.start();
} catch (e0) {
try {
st.ballLp.x = toX;
st.ballLp.y = toY;
st.ballLp.width = toW;
st.wm.updateViewLayout(st.ballRoot, st.ballLp);
this.savePos(st.ballLp.x, st.ballLp.y);
} catch(e1) { safeLog(null, 'e', "catch " + String(e1)); }
try { if (endCb) endCb(); } catch (eCb2) { try { if (this && this.L && this.L.e) this.L.e("animateBallLayout endCb err=" + String(eCb2)); } catch(eLog2) { safeLog(null, 'e', "catch " + String(eLog2)); } }
}
};
FloatBallAppWM.prototype.playBounce = function(v) {
if (!this.config.ENABLE_BOUNCE) return;
if (!this.config.ENABLE_ANIMATIONS) return;
try { v.animate().cancel(); } catch(e0) { safeLog(null, 'e', "catch " + String(e0)); }
var self = this;
var i = 0;
function step() {
if (self.state.closing) return;
if (i >= self.config.BOUNCE_TIMES) {
try { v.setScaleX(1); v.setScaleY(1); } catch(e2) { safeLog(null, 'e', "catch " + String(e2)); }
return;
}
var amp = (self.config.BOUNCE_MAX_SCALE - 1) * Math.pow(self.config.BOUNCE_DECAY, i);
var s = 1 + amp;
v.animate()
.scaleX(s)
.scaleY(s)
.setDuration(self.config.BOUNCE_STEP_MS)
.setInterpolator(new android.view.animation.OvershootInterpolator())
.withEndAction(new JavaAdapter(java.lang.Runnable, {
run: function() {
v.animate()
.scaleX(1)
.scaleY(1)
.setDuration(self.config.BOUNCE_STEP_MS)
.setInterpolator(new android.view.animation.AccelerateDecelerateInterpolator())
.withEndAction(new JavaAdapter(java.lang.Runnable, {
run: function() { i++; step(); }
}))
.start();
}
}))
.start();
}
step();
};
FloatBallAppWM.prototype.safeRemoveView = function(v, whichName) {
try {
if (!v) return { ok: true, skipped: true };
this.state.wm.removeView(v);
return { ok: true };
} catch (e) {
safeLog(this.L, 'w', "removeView fail which=" + String(whichName || "") + " err=" + String(e));
return { ok: false, err: String(e), where: whichName || "" };
}
};
FloatBallAppWM.prototype.hideMask = function() {
if (!this.state.addedMask) return;
if (!this.state.mask) return;
this.safeRemoveView(this.state.mask, "mask");
this.state.mask = null;
this.state.maskLp = null;
this.state.addedMask = false;
};
FloatBallAppWM.prototype.hideMainPanel = function() {
if (!this.state.addedPanel) return;
if (!this.state.panel) return;
this.safeRemoveView(this.state.panel, "panel");
this.state.panel = null;
this.state.panelLp = null;
this.state.addedPanel = false;
this.hideMask();
this.touchActivity();
this._clearHeavyCachesIfAllHidden("hideMainPanel");
};
FloatBallAppWM.prototype.hideSettingsPanel = function() {
if (!this.state.addedSettings) return;
if (!this.state.settingsPanel) return;
this.safeRemoveView(this.state.settingsPanel, "settingsPanel");
this.state.settingsPanel = null;
this.state.settingsPanelLp = null;
this.state.addedSettings = false;
this.state.pendingUserCfg = null;
this.state.pendingDirty = false;
this.hideMask();
this.touchActivity();
this._clearHeavyCachesIfAllHidden("hideSettingsPanel");
};
FloatBallAppWM.prototype.hideViewerPanel = function() {
if (!this.state.addedViewer) return;
if (!this.state.viewerPanel) return;
this.safeRemoveView(this.state.viewerPanel, "viewerPanel");
this.state.viewerPanel = null;
this.state.viewerPanelLp = null;
this.state.addedViewer = false;
this.hideMask();
this.touchActivity();
this._clearHeavyCachesIfAllHidden("hideViewerPanel");
};
FloatBallAppWM.prototype.clearHeavyCaches = function(reason) {
// 这段代码的主要内容/用途:在所有面板都关闭后,主动清理"图标/快捷方式"等重缓存,降低 system_server 常驻内存。
// 说明:仅清理缓存引用,不强行 recycle Bitmap避免误伤仍被使用的 Drawable。
// # 防抖5秒内相同 reason 不重复清理
var now = Date.now();
var cacheKey = "_lastClear_" + (reason || "default");
var lastClear = this.state[cacheKey] || 0;
if (now - lastClear < 5000) {
return; // 5秒内已清理过跳过
}
this.state[cacheKey] = now;
try { this._iconLru = null; } catch(eLruClr) { safeLog(null, 'e', "catch " + String(eLruClr)); }
try { this._shortcutIconFailTs = {}; } catch(e2) { safeLog(null, 'e', "catch " + String(e2)); }
// # Shortcuts 相关全局缓存(按钮编辑页/快捷方式选择器可能会创建)
try { if (typeof __scIconCache !== "undefined") __scIconCache = {}; } catch(e3) { safeLog(null, 'e', "catch " + String(e3)); }
try { if (typeof __scAppLabelCache !== "undefined") __scAppLabelCache = {}; } catch(e4) { safeLog(null, 'e', "catch " + String(e4)); }
// # 记录一次清理日志(精简:只记录关键 reason且 5秒防抖
var keyReasons = ["memory_pressure", "screen_changed", "close"];
var isKeyReason = keyReasons.indexOf(reason) >= 0;
try {
if (isKeyReason && this.L && this.L.i) {
this.L.i("clearHeavyCaches reason=" + String(reason));
}
} catch(e5) { safeLog(null, 'e', "catch " + String(e5)); }
};
FloatBallAppWM.prototype._clearHeavyCachesIfAllHidden = function(reason) {
// 这段代码的主要内容/用途:只在"主面板/设置/查看器"全部关闭后清理缓存,避免页面切换时反复重建导致卡顿。
try {
if (!this.state.addedPanel && !this.state.addedSettings && !this.state.addedViewer) {
this.clearHeavyCaches(reason || "all_hidden");
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
};
FloatBallAppWM.prototype.hideAllPanels = function() {
this.hideMainPanel();
this.hideSettingsPanel();
this.hideViewerPanel();
this.hideMask();
this._clearHeavyCachesIfAllHidden("hideAllPanels");
};
FloatBallAppWM.prototype.showMask = function() {
if (this.state.addedMask) return;
if (this.state.closing) return;
var self = this;
var mask = new android.widget.FrameLayout(context);
// 遮罩层背景:轻微的黑色半透明,提升层次感
try { mask.setBackgroundColor(android.graphics.Color.parseColor("#33000000")); } catch (e0) {
mask.setBackgroundColor(0x33000000);
}
mask.setOnTouchListener(new JavaAdapter(android.view.View.OnTouchListener, {
onTouch: function(v, e) {
var a = e.getAction();
if (a === android.view.MotionEvent.ACTION_DOWN) {
self.touchActivity();
self.hideAllPanels();
return true;
}
return true;
}
}));
var lp = new android.view.WindowManager.LayoutParams(
android.view.WindowManager.LayoutParams.MATCH_PARENT,
android.view.WindowManager.LayoutParams.MATCH_PARENT,
android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
android.graphics.PixelFormat.TRANSLUCENT
);
lp.gravity = android.view.Gravity.TOP | android.view.Gravity.START;
lp.x = 0;
lp.y = 0;
try {
this.state.wm.addView(mask, lp);
this.state.mask = mask;
this.state.maskLp = lp;
this.state.addedMask = true;
// 简单的淡入动画
try {
if (this.config.ENABLE_ANIMATIONS) {
mask.setAlpha(0);
mask.animate().alpha(1).setDuration(200).start();
} else {
mask.setAlpha(1);
}
} catch(eAnim) { safeLog(null, 'e', "catch " + String(eAnim)); }
} catch (e1) {
safeLog(this.L, 'e', "add mask fail err=" + String(e1));
this.state.addedMask = false;
}
};
FloatBallAppWM.prototype.snapToEdgeDocked = function(withAnim, forceSide) {
if (this.state.closing) return;
// 移除对面板/Mask的检查允许在任何情况下强制吸边如果调用方逻辑正确
// 如果需要保护,调用方自己判断
if (this.state.dragging) return;
var di = this.getDockInfo();
var ballSize = di.ballSize;
var visible = di.visiblePx;
var hidden = di.hiddenPx;
var snapLeft;
if (forceSide === "left") snapLeft = true;
else if (forceSide === "right") snapLeft = false;
else {
// 默认根据中心点判断
var centerX = this.state.ballLp.x + Math.round(ballSize / 2);
snapLeft = centerX < Math.round(this.state.screen.w / 2);
}
var targetW = visible;
var targetY = this.clamp(this.state.ballLp.y, 0, this.state.screen.h - ballSize);
if (snapLeft) {
this.state.dockSide = "left";
this.state.docked = true;
try { this.state.ballContent.setX(-hidden); } catch(eL) { safeLog(null, 'e', "catch " + String(eL)); }
if (withAnim) {
this.animateBallLayout(0, targetY, targetW, this.config.DOCK_ANIM_MS, null);
} else {
this.state.ballLp.x = 0;
this.state.ballLp.y = targetY;
this.state.ballLp.width = targetW;
try { this.state.wm.updateViewLayout(this.state.ballRoot, this.state.ballLp); } catch(eU1) { safeLog(null, 'e', "catch " + String(eU1)); }
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
}
safeLog(this.L, 'd', "dock left x=0 y=" + String(targetY) + " w=" + String(targetW));
// 闲置变暗
try {
if (this.config.ENABLE_ANIMATIONS) {
this.state.ballContent.animate().alpha(this.config.BALL_IDLE_ALPHA).setDuration(300).start();
} else {
this.state.ballContent.setAlpha(this.config.BALL_IDLE_ALPHA);
}
} catch(eA) { safeLog(null, 'e', "catch " + String(eA)); }
return;
}
this.state.dockSide = "right";
this.state.docked = true;
try { this.state.ballContent.setX(0); } catch(eR) { safeLog(null, 'e', "catch " + String(eR)); }
var x2 = this.state.screen.w - visible;
if (withAnim) {
this.animateBallLayout(x2, targetY, targetW, this.config.DOCK_ANIM_MS, null);
} else {
this.state.ballLp.x = x2;
this.state.ballLp.y = targetY;
this.state.ballLp.width = targetW;
try { this.state.wm.updateViewLayout(this.state.ballRoot, this.state.ballLp); } catch(eU2) { safeLog(null, 'e', "catch " + String(eU2)); }
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
}
// # 日志精简dock 事件添加防抖10秒内不重复记录相同边
var dockNow = Date.now();
var lastDock = this.state._lastDockLog || 0;
if (dockNow - lastDock > 10000) {
safeLog(this.L, 'i', "dock right x=" + String(x2) + " y=" + String(targetY));
this.state._lastDockLog = dockNow;
}
// 闲置变暗
try {
if (this.config.ENABLE_ANIMATIONS) {
this.state.ballContent.animate().alpha(this.config.BALL_IDLE_ALPHA).setDuration(300).start();
} else {
this.state.ballContent.setAlpha(this.config.BALL_IDLE_ALPHA);
}
} catch(eA) { safeLog(null, 'e', "catch " + String(eA)); }
};
FloatBallAppWM.prototype.undockToFull = function(withAnim, endCb) {
if (this.state.closing) { if (endCb) endCb(); return; }
if (!this.state.docked) { if (endCb) endCb(); return; }
if (!this.state.addedBall) { if (endCb) endCb(); return; }
var di = this.getDockInfo();
var ballSize = di.ballSize;
var targetW = ballSize;
var targetY = this.clamp(this.state.ballLp.y, 0, this.state.screen.h - ballSize);
try { this.state.ballContent.setX(0); } catch(e0) { safeLog(null, 'e', "catch " + String(e0)); }
if (this.state.dockSide === "left") {
this.state.docked = false;
this.state.dockSide = null;
if (withAnim) this.animateBallLayout(0, targetY, targetW, this.config.UNDOCK_ANIM_MS, endCb);
else {
this.state.ballLp.x = 0;
this.state.ballLp.y = targetY;
this.state.ballLp.width = targetW;
try { this.state.wm.updateViewLayout(this.state.ballRoot, this.state.ballLp); } catch(eU1) { safeLog(null, 'e', "catch " + String(eU1)); }
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
if (endCb) endCb();
}
// 恢复不透明度
try {
if (withAnim && this.config.ENABLE_ANIMATIONS) {
this.state.ballContent.animate().alpha(1.0).setDuration(150).start();
} else {
this.state.ballContent.setAlpha(1.0);
}
} catch(eA) { safeLog(null, 'e', "catch " + String(eA)); }
safeLog(this.L, 'i', "undock from left");
return;
}
var x = this.state.screen.w - ballSize;
this.state.docked = false;
this.state.dockSide = null;
if (withAnim) this.animateBallLayout(x, targetY, targetW, this.config.UNDOCK_ANIM_MS, endCb);
else {
this.state.ballLp.x = x;
this.state.ballLp.y = targetY;
this.state.ballLp.width = targetW;
try { this.state.wm.updateViewLayout(this.state.ballRoot, this.state.ballLp); } catch(eU2) { safeLog(null, 'e', "catch " + String(eU2)); }
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
if (endCb) endCb();
}
// 恢复不透明度
try {
if (withAnim && this.config.ENABLE_ANIMATIONS) {
this.state.ballContent.animate().alpha(1.0).setDuration(150).start();
} else {
this.state.ballContent.setAlpha(1.0);
}
} catch(eA) { safeLog(null, 'e', "catch " + String(eA)); }
// # 日志精简undock 事件改为 INFO 级别,且记录方向
var undockSide = this.state.dockSide || "right";
safeLog(this.L, 'i', "undock from " + undockSide);
};
FloatBallAppWM.prototype.cancelDockTimer = function() {
try { if (this.state.idleDockRunnable && this.state.h) this.state.h.removeCallbacks(this.state.idleDockRunnable); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
this.state.idleDockRunnable = null;
};
FloatBallAppWM.prototype.armDockTimer = function() {
if (this.state.closing) return;
if (!this.state.h) return;
if (!this.state.addedBall) return;
if (this.state.docked) return;
this.cancelDockTimer();
var hasPanel = (this.state.addedPanel || this.state.addedSettings || this.state.addedViewer || this.state.addedMask);
var targetMs = hasPanel ? this.config.PANEL_IDLE_CLOSE_AND_DOCK_MS : this.config.DOCK_AFTER_IDLE_MS;
var self = this;
this.state.idleDockRunnable = new java.lang.Runnable({
run: function() {
try {
if (self.state.closing) return;
if (self.state.docked) return;
if (self.state.dragging) return;
var hasPanel2 = (self.state.addedPanel || self.state.addedSettings || self.state.addedViewer || self.state.addedMask);
var needMs = hasPanel2 ? self.config.PANEL_IDLE_CLOSE_AND_DOCK_MS : self.config.DOCK_AFTER_IDLE_MS;
var idle = self.now() - self.state.lastMotionTs;
if (idle < needMs) { self.armDockTimer(); return; }
// if (hasPanel2) self.hideAllPanels(); // 用户要求不再自动关闭面板
if (self.config.ENABLE_SNAP_TO_EDGE) {
self.snapToEdgeDocked(true);
}
} catch (e0) {
if (self.L) self.L.e("dockTimer run err=" + String(e0));
}
}
});
this.state.h.postDelayed(this.state.idleDockRunnable, targetMs);
};
FloatBallAppWM.prototype.touchActivity = function() {
this.state.lastMotionTs = this.now();
this.armDockTimer();
}
// # 点击防抖与安全执行
// 这段代码的主要内容/用途:防止在悬浮面板上快速/乱点导致重复 add/remove、状态机被打穿从而引发 system_server 异常重启。
FloatBallAppWM.prototype.guardClick = function(key, cooldownMs, fn) {
try {
var now = android.os.SystemClock.uptimeMillis();
if (!this.state._clickGuards) this.state._clickGuards = {};
var last = this.state._clickGuards[key] || 0;
var cd = (cooldownMs != null ? cooldownMs : INTERACTION_CONSTANTS.CLICK_COOLDOWN_MS);
if (now - last < cd) return false;
this.state._clickGuards[key] = now;
try {
fn && fn();
} catch (e1) {
safeLog(this.L, 'e', "guardClick err key=" + String(key) + " err=" + String(e1));
}
return true;
} catch (e0) {
// 兜底:绝不让点击回调异常冒泡到 system_server
try { fn && fn(); } catch(e2) { safeLog(null, 'e', "catch " + String(e2)); }
return true;
}
};
FloatBallAppWM.prototype.safeUiCall = function(tag, fn) {
try {
fn && fn();
} catch (e) {
safeLog(this.L, 'e', "safeUiCall err tag=" + String(tag || "") + " err=" + String(e));
}
};
;
FloatBallAppWM.prototype.onScreenChangedReflow = function() {
if (this.state.closing) return;
if (!this.state.addedBall) return;
var di = this.getDockInfo();
var oldW = this.state.screen.w;
var oldH = this.state.screen.h;
var newScreen = this.getScreenSizePx();
var newW = newScreen.w;
var newH = newScreen.h;
if (newW <= 0 || newH <= 0) return;
if (oldW <= 0) oldW = newW;
if (oldH <= 0) oldH = newH;
this.state.screen = { w: newW, h: newH };
var ballSize = di.ballSize;
var visible = di.visiblePx;
var hidden = di.hiddenPx;
var oldMaxX = Math.max(1, oldW - ballSize);
var oldMaxY = Math.max(1, oldH - ballSize);
var newMaxX = Math.max(1, newW - ballSize);
var newMaxY = Math.max(1, newH - ballSize);
var xRatio = this.state.ballLp.x / oldMaxX;
var yRatio = this.state.ballLp.y / oldMaxY;
var mappedX = Math.round(xRatio * newMaxX);
var mappedY = Math.round(yRatio * newMaxY);
mappedX = this.clamp(mappedX, 0, newMaxX);
mappedY = this.clamp(mappedY, 0, newMaxY);
if (this.state.docked) {
this.state.ballLp.y = mappedY;
this.state.ballLp.width = visible;
if (this.state.dockSide === "left") {
this.state.ballLp.x = 0;
try { this.state.ballContent.setX(-hidden); } catch(eL) { safeLog(null, 'e', "catch " + String(eL)); }
} else {
this.state.ballLp.x = newW - visible;
try { this.state.ballContent.setX(0); } catch(eR) { safeLog(null, 'e', "catch " + String(eR)); }
}
// 重新进入闲置变暗逻辑(如果需要)
try { this.state.ballContent.setAlpha(this.config.BALL_IDLE_ALPHA); } catch(eA) { safeLog(null, 'e', "catch " + String(eA)); }
} else {
this.state.ballLp.x = mappedX;
this.state.ballLp.y = mappedY;
this.state.ballLp.width = ballSize;
try { this.state.ballContent.setX(0); } catch(e0) { safeLog(null, 'e', "catch " + String(e0)); }
try { this.state.ballContent.setAlpha(1.0); } catch(eA) { safeLog(null, 'e', "catch " + String(eA)); }
}
try { this.state.wm.updateViewLayout(this.state.ballRoot, this.state.ballLp); } catch(eU) { safeLog(null, 'e', "catch " + String(eU)); }
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
safeLog(this.L, 'i', "screen reflow w=" + String(newW) + " h=" + String(newH) + " x=" + String(this.state.ballLp.x) + " y=" + String(this.state.ballLp.y));
};
FloatBallAppWM.prototype.setupDisplayMonitor = function() {
if (this.state.closing) return;
try {
var dm = context.getSystemService(android.content.Context.DISPLAY_SERVICE);
if (!dm) return;
this.state.dm = dm;
this.state.lastRotation = this.getRotation();
var self = this;
var listener = new JavaAdapter(android.hardware.display.DisplayManager.DisplayListener, {
onDisplayAdded: function(displayId) {},
onDisplayRemoved: function(displayId) {},
onDisplayChanged: function(displayId) {
try {
if (self.state.closing) return;
var nowTs = self.now();
if (nowTs - self.state.lastMonitorTs < self.config.SCREEN_MONITOR_THROTTLE_MS) return;
self.state.lastMonitorTs = nowTs;
self.state.h.post(new JavaAdapter(java.lang.Runnable, {
run: function() {
try {
if (self.state.closing) return;
if (!self.state.addedBall) return;
var rot = self.getRotation();
var sz = self.getScreenSizePx();
var changed = false;
if (rot !== self.state.lastRotation) { self.state.lastRotation = rot; changed = true; }
if (sz.w !== self.state.screen.w || sz.h !== self.state.screen.h) changed = true;
if (changed) {
self.cancelDockTimer();
self.onScreenChangedReflow();
self.touchActivity();
}
} catch (e1) {
if (self.L) self.L.e("displayChanged run err=" + String(e1));
}
}
}));
} catch(e0) { safeLog(null, 'e', "catch " + String(e0)); }
}
});
this.state.displayListener = listener;
dm.registerDisplayListener(listener, this.state.h);
safeLog(this.L, 'i', "display monitor registered");
} catch (e2) {
safeLog(this.L, 'e', "setupDisplayMonitor err=" + String(e2));
}
};
FloatBallAppWM.prototype.stopDisplayMonitor = function() {
try { if (this.state.dm && this.state.displayListener) this.state.dm.unregisterDisplayListener(this.state.displayListener); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
this.state.displayListener = null;
this.state.dm = null;
};

28
code/th_10_shell.js Normal file
View File

@@ -0,0 +1,28 @@
// @version 1.0.1
// =======================【Shell广播桥执行】======================
// 仅通过广播桥发送 shell 命令,由外部接收器实际执行。
// 注意system_server 进程本身不直接执行 shell。
FloatBallAppWM.prototype.execShellSmart = function(cmdB64, needRoot) {
var ret = { ok: false, via: "", err: "" };
try {
var action = String(this.config.SHELL_BRIDGE_ACTION || CONST_SHELL_BRIDGE_ACTION || "shortx.toolhub.SHELL");
var it = new android.content.Intent(action);
// 广播协议cmd_b64 + root + from
it.putExtra(CONST_SHELL_BRIDGE_EXTRA_CMD, String(cmdB64));
it.putExtra(CONST_SHELL_BRIDGE_EXTRA_ROOT, !!needRoot);
it.putExtra(CONST_SHELL_BRIDGE_EXTRA_FROM, "ToolHub");
context.sendBroadcast(it);
ret.ok = true;
ret.via = "BroadcastBridge";
safeLog(this.L, 'i', "shell via broadcast ok action=" + action + " root=" + String(!!needRoot));
} catch (eB) {
ret.err = "Broadcast err=" + String(eB);
safeLog(this.L, 'e', "shell via broadcast fail err=" + String(eB));
}
return ret;
};

320
code/th_11_action.js Normal file
View File

@@ -0,0 +1,320 @@
// @version 1.0.1
// =======================【WM 线程:按钮动作执行】======================
FloatBallAppWM.prototype.execButtonAction = function(btn, idx) {
// # 点击防抖
// 这段代码的主要内容/用途:防止在按钮面板上连续/乱点导致重复执行与 UI 状态机冲突(可能触发 system_server 异常重启)。
if (!this.guardClick("btn_exec_" + String(idx), 380, null)) return;
try {
if (!btn || !btn.type) {
this.toast("按钮#" + idx + " 未配置");
safeLog(this.L, 'w', "btn#" + String(idx) + " no type");
return;
}
var t = String(btn.type);
safeLog(this.L, 'i', "btn click idx=" + String(idx) + " type=" + t + " title=" + String(btn.title || ""));
if (t === "open_settings") {
this.showPanelAvoidBall("settings");
return;
}
if (t === "open_viewer") {
var logPath = (this.L && this.L._filePathForToday) ? this.L._filePathForToday() : "";
if (!logPath) logPath = PATH_LOG_DIR + "/ShortX_ToolHub_" + (new java.text.SimpleDateFormat("yyyyMMdd").format(new java.util.Date())) + ".log";
var content = FileIO.readText(logPath);
if (!content) content = "(日志文件不存在或为空: " + logPath + ")";
if (content.length > 30000) {
content = "[...前略...]\n" + content.substring(content.length - 30000);
}
// 简单的按行倒序,方便查看最新日志
try {
var lines = content.split("\n");
if (lines.length > 1) {
content = lines.reverse().join("\n");
}
} catch(eRev) { safeLog(null, 'e', "catch " + String(eRev)); }
this.showViewerPanel("今日日志 (倒序)", content);
return;
}
if (t === "toast") {
var msg = "";
if (btn.text !== undefined && btn.text !== null) msg = String(btn.text);
else if (btn.title) msg = String(btn.title);
else msg = "按钮#" + idx;
this.toast(msg);
return;
}
if (t === "app") {
var pkg = btn.pkg ? String(btn.pkg) : "";
if (!pkg) { this.toast("按钮#" + idx + " 缺少 pkg"); return; }
var it = context.getPackageManager().getLaunchIntentForPackage(pkg);
if (!it) { this.toast("无法启动 " + pkg); return; }
it.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
// # 系统级跨用户启动Context.startActivityAsUser
// 这段代码的主要内容/用途:支持"主应用/分身应用"选择,避免弹出选择器或误启动到另一用户。
// 说明:当未配置 launchUserId 时,默认使用 0主用户失败则回退 startActivity。
var launchUid = 0;
try {
if (btn.launchUserId != null && String(btn.launchUserId).length > 0) launchUid = parseInt(String(btn.launchUserId), 10);
} catch(eLU0) { launchUid = 0; }
if (isNaN(launchUid)) launchUid = 0;
try {
// 运行日志:记录跨用户启动参数(便于定位分身启动失败原因)
safeLog(this.L, 'i', "startAsUser(app) idx=" + idx + " pkg=" + pkg + " launchUserId=" + launchUid);
if (launchUid !== 0) {
context.startActivityAsUser(it, android.os.UserHandle.of(launchUid));
} else {
context.startActivity(it);
}
} catch (eA) {
// # 兜底:某些 ROM/权限限制下 startActivityAsUser 可能抛异常,回退普通启动
try { context.startActivity(it); } catch(eA2) { safeLog(null, 'e', "catch " + String(eA2)); }
this.toast("启动失败");
safeLog(this.L, 'e', "start app fail pkg=" + pkg + " uid=" + String(launchUid) + " err=" + String(eA));
}
return;
}
if (t === "shell") {
// # 这段代码的主要内容/用途:执行 shell支持 cmd 明文 与 cmd_b64最终会确保发送/执行的是"真正的 base64"
// # 修复点:历史配置里有些按钮把"明文命令"误存进 cmd_b64或 b64 被破坏),会导致广播接收端解码失败→看起来"没效果"。
var cmdB64 = (btn.cmd_b64 !== undefined && btn.cmd_b64 !== null) ? String(btn.cmd_b64) : "";
var cmdPlain = (btn.cmd !== undefined && btn.cmd !== null) ? String(btn.cmd) : "";
// # 1) 只有明文但没有 b64自动补齐 b64避免特殊字符在多层字符串传递中被破坏
if ((!cmdB64 || cmdB64.length === 0) && cmdPlain && cmdPlain.length > 0) {
try {
var b64x = encodeBase64Utf8(cmdPlain);
if (b64x && b64x.length > 0) cmdB64 = String(b64x);
} catch(eB64a) { safeLog(null, 'e', "catch " + String(eB64a)); }
}
// # 2) cmd_b64 非空但无法解码:把它当作"明文命令"重新编码(保证广播桥/Action 都能吃到正确命令)
// # 说明decodeBase64Utf8 返回空串通常意味着 b64 非法或被破坏;而真实命令不太可能是空串。
if (cmdB64 && cmdB64.length > 0) {
try {
var testPlain = decodeBase64Utf8(cmdB64);
if ((!testPlain || testPlain.length === 0) && (!cmdPlain || cmdPlain.length === 0)) {
cmdPlain = String(cmdB64);
cmdB64 = "";
}
} catch(eB64b) { safeLog(null, 'e', "catch " + String(eB64b)); }
}
if ((!cmdB64 || cmdB64.length === 0) && cmdPlain && cmdPlain.length > 0) {
try {
var b64y = encodeBase64Utf8(cmdPlain);
if (b64y && b64y.length > 0) cmdB64 = String(b64y);
} catch(eB64c) { safeLog(null, 'e', "catch " + String(eB64c)); }
}
if (!cmdB64 || cmdB64.length === 0) {
this.toast("按钮#" + idx + " 缺少 cmd/cmd_b64");
safeLog(this.L, 'e', "shell missing cmd idx=" + String(idx));
return;
}
// # 广播桥接收端默认以 root 执行,强制使用 root
var needRoot = true;
var r = this.execShellSmart(cmdB64, needRoot);
if (r && r.ok) return;
this.toast("shell 广播桥发送失败");
safeLog(this.L, 'e', "shell all failed cmd_b64=" + cmdB64 + " ret=" + JSON.stringify(r || {}));
return;
}
if (t === "broadcast") {
// 这段代码的主要内容/用途:发送自定义广播(兼容 btn.extra / btn.extras并对 Shell 广播桥shortx.toolhub.SHELL做额外兼容cmd/cmd_b64/root
var action = btn.action ? String(btn.action) : "";
if (!action) { this.toast("按钮#" + idx + " 缺少 action"); return; }
var it2 = new android.content.Intent(action);
// # 1) 兼容字段extra / extras两种都认
var ex = null;
try {
if (btn.extras) ex = btn.extras;
else if (btn.extra) ex = btn.extra;
} catch (eEx0) { ex = null; }
// # 2) 写入 extras支持 number / boolean / string其他类型一律转字符串
if (ex) {
try {
var k;
for (k in ex) {
if (!ex.hasOwnProperty(k)) continue;
var v = ex[k];
if (typeof v === "number") it2.putExtra(String(k), Number(v));
else if (typeof v === "boolean") it2.putExtra(String(k), !!v);
else it2.putExtra(String(k), String(v));
}
} catch(eE) { safeLog(null, 'e', "catch " + String(eE)); }
}
// # 3) 对"Shell 广播桥"做额外兼容:
// - 你可以在 cfg 里写 extra.cmd明文或 extra.cmd_b64Base64
// - 同时会补齐 root/from并且把 cmd 明文也塞一份,方便外部 MVEL 直接读取 cmd 进行验证
try {
var bridgeAction = String(this.config.SHELL_BRIDGE_ACTION || "shortx.toolhub.SHELL");
if (action === bridgeAction) {
var kCmdB64 = String(this.config.SHELL_BRIDGE_EXTRA_CMD || "cmd_b64");
var kFrom = String(this.config.SHELL_BRIDGE_EXTRA_FROM || "from");
var kRoot = String(this.config.SHELL_BRIDGE_EXTRA_ROOT || "root");
var cmdPlain = "";
var cmdB64 = "";
try { cmdB64 = String(it2.getStringExtra(kCmdB64) || ""); } catch (eC0) { cmdB64 = ""; }
try { cmdPlain = String(it2.getStringExtra("cmd") || ""); } catch (eC1) { cmdPlain = ""; }
// # 有明文但没 b64自动补 b64
if ((!cmdB64 || cmdB64.length === 0) && cmdPlain && cmdPlain.length > 0) {
try {
var b64x = encodeBase64Utf8(cmdPlain);
if (b64x && b64x.length > 0) {
cmdB64 = b64x;
it2.putExtra(kCmdB64, String(cmdB64));
}
} catch(eC2) { safeLog(null, 'e', "catch " + String(eC2)); }
}
// # 有 b64 但没明文:也补一份明文(便于外部规则验证;真正执行仍建议用 cmd_b64
if ((!cmdPlain || cmdPlain.length === 0) && cmdB64 && cmdB64.length > 0) {
try {
var decoded = decodeBase64Utf8(cmdB64);
if (decoded && decoded.length > 0) {
cmdPlain = decoded;
it2.putExtra("cmd", String(cmdPlain));
}
} catch(eC3) { safeLog(null, 'e', "catch " + String(eC3)); }
}
// # root广播桥接收端默认以 root 执行,强制传递 true
try {
if (!it2.hasExtra(kRoot)) {
it2.putExtra(kRoot, true);
}
} catch (eR0) {
try {
it2.putExtra(kRoot, true);
} catch(eR1) { safeLog(null, 'e', "catch " + String(eR1)); }
}
// # root 类型纠正:如果外部 cfg 用了字符串 "true"/"false",这里纠正为 boolean避免外部 getBooleanExtra 读不到
try {
if (it2.hasExtra(kRoot)) {
var bdl = it2.getExtras();
if (bdl) {
var raw = bdl.get(kRoot);
if (raw != null) {
var rawStr = String(raw);
if (rawStr === "true" || rawStr === "false") {
it2.removeExtra(kRoot);
it2.putExtra(kRoot, rawStr === "true");
}
}
}
}
} catch(eRB) { safeLog(null, 'e', "catch " + String(eRB)); }
// # from标识来源便于外部执行器做白名单/审计)
try {
if (!it2.hasExtra(kFrom)) it2.putExtra(kFrom, "ToolHub@system_server");
} catch (eF0) { try { it2.putExtra(kFrom, "ToolHub@system_server"); } catch(eF1) { safeLog(null, 'e', "catch " + String(eF1)); } }
if (this.L) {
try {
this.L.i("broadcast(shell_bridge) action=" + action + " cmd_len=" + String(cmdPlain ? cmdPlain.length : 0) +
" cmd_b64_len=" + String(cmdB64 ? cmdB64.length : 0) + " root=" + String(it2.getBooleanExtra(kRoot, false)));
} catch(eLg) { safeLog(null, 'e', "catch " + String(eLg)); }
}
}
} catch(eSB) { safeLog(null, 'e', "catch " + String(eSB)); }
try { context.sendBroadcast(it2); } catch (eB) { this.toast("广播失败"); safeLog(this.L, 'e', "broadcast fail action=" + action + " err=" + String(eB)); }
return;
}
if (t === "shortcut") {
// 这段代码的主要内容/用途:仅使用 JavaScript(startActivityAsUser) 执行快捷方式,取消 Shell 与所有兜底,避免弹出主/分身选择器。
// 说明:
// 1) 运行时只执行按钮字段 shortcutJsCode由"选择快捷方式列表"点选自动生成,可手动微调)
// 2) 不再调用 am start不再回退 LauncherApps.startShortcut用户要求取消 shell、取消兜底
// 3) 目标 userIdlaunchUserId > userId用于锁定主/分身)
var spkg = btn.pkg ? String(btn.pkg) : "";
var sid = btn.shortcutId ? String(btn.shortcutId) : "";
var iu = (btn.intentUri != null) ? String(btn.intentUri) : "";
var uid = 0;
try { uid = (btn.userId != null) ? parseInt(String(btn.userId), 10) : 0; } catch(eUid0) { uid = 0; }
if (isNaN(uid)) uid = 0;
// # 启动 userId 优先级launchUserId > userId
try {
if (btn.launchUserId != null && String(btn.launchUserId).length > 0) {
var lu0 = parseInt(String(btn.launchUserId), 10);
if (!isNaN(lu0)) uid = lu0;
}
} catch(eLu0) { safeLog(null, 'e', "catch " + String(eLu0)); }
if (!spkg) { this.toast("按钮#" + idx + " 缺少 pkg"); return; }
if (!sid) { this.toast("按钮#" + idx + " 缺少 shortcutId"); return; }
// # JavaScript 执行:只执行 shortcutJsCode
var jsCode = (btn.shortcutJsCode != null) ? String(btn.shortcutJsCode) : "";
if (!jsCode || jsCode.length === 0) {
this.toast("按钮#" + idx + " 未配置 JS 启动代码");
return;
}
try {
// # 提供少量上下文变量给脚本使用(可选)
// - __sc_intentUri: 当前按钮 intentUri
// - __sc_userId: 当前目标 userId已合并 launchUserId
var __sc_intentUri = iu;
var __sc_userId = uid;
var rjs = eval(jsCode);
// # 约定:返回值以 ok 开头视为成功;以 err 开头视为失败(失败也不兜底)
var sret = (rjs == null) ? "" : String(rjs);
if (sret.indexOf("ok") === 0) {
safeLog(this.L, 'i', "shortcut(js-only) ok pkg=" + spkg + " id=" + sid + " user=" + String(uid));
return;
}
safeLog(this.L, 'e', "shortcut(js-only) fail pkg=" + spkg + " id=" + sid + " user=" + String(uid) + " ret=" + sret);
this.toast("快捷方式 JS 启动失败: " + sret);
return;
} catch (eJsSc) {
safeLog(this.L, 'e', "shortcut(js-only) exception pkg=" + spkg + " id=" + sid + " err=" + eJsSc);
this.toast("快捷方式 JS 异常: " + String(eJsSc));
return;
}
}
this.toast("未知 type=" + t);
safeLog(this.L, 'w', "unknown btn type=" + t);
} catch (eBtn) {
try { this.toast("按钮执行异常"); } catch(e0) { safeLog(null, 'e', "catch " + String(e0)); }
safeLog(this.L, 'e', "execButtonAction crash idx=" + String(idx) + " err=" + String(eBtn));
}
};

76
code/th_12_rebuild.js Normal file
View File

@@ -0,0 +1,76 @@
// @version 1.0.0
// =======================【新增:改大小后安全重建悬浮球】======================
FloatBallAppWM.prototype.rebuildBallForNewSize = function(keepPanels) {
if (this.state.closing) return false;
if (!this.state.wm) return false;
if (!this.state.addedBall) return false;
if (!this.state.ballRoot) return false;
if (!this.state.ballLp) return false;
if (this.state.dragging) return false;
var oldSize = this.state.ballLp.height;
if (!oldSize || oldSize <= 0) oldSize = this.getDockInfo().ballSize;
var oldX = this.state.ballLp.x;
var oldY = this.state.ballLp.y;
var oldCenterX = oldX + Math.round(oldSize / 2);
var oldCenterY = oldY + Math.round(oldSize / 2);
if (!keepPanels) {
this.hideAllPanels();
}
this.cancelDockTimer();
this.state.docked = false;
this.state.dockSide = null;
this.safeRemoveView(this.state.ballRoot, "ballRoot-rebuild");
this.state.ballRoot = null;
this.state.ballContent = null;
this.state.ballLp = null;
this.state.addedBall = false;
this.createBallViews();
var di = this.getDockInfo();
var newSize = di.ballSize;
var newX = oldCenterX - Math.round(newSize / 2);
var newY = oldCenterY - Math.round(newSize / 2);
var maxX = Math.max(0, this.state.screen.w - newSize);
var maxY = Math.max(0, this.state.screen.h - newSize);
newX = this.clamp(newX, 0, maxX);
newY = this.clamp(newY, 0, maxY);
var lp = new android.view.WindowManager.LayoutParams(
newSize,
newSize,
android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
android.graphics.PixelFormat.TRANSLUCENT
);
lp.gravity = android.view.Gravity.TOP | android.view.Gravity.START;
lp.x = newX;
lp.y = newY;
try {
this.state.wm.addView(this.state.ballRoot, lp);
this.state.ballLp = lp;
this.state.addedBall = true;
} catch (eAdd) {
try { this.toast("重建悬浮球失败: " + String(eAdd)); } catch(eT) { safeLog(null, 'e', "catch " + String(eT)); }
safeLog(this.L, 'e', "rebuildBall add fail err=" + String(eAdd));
return false;
}
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
this.touchActivity();
safeLog(this.L, 'i', "rebuildBall ok size=" + String(newSize) + " x=" + String(newX) + " y=" + String(newY));
return true;
};

528
code/th_13_panel_ui.js Normal file
View File

@@ -0,0 +1,528 @@
// @version 1.0.0
// =======================【设置面板UI右上角确认】======================
FloatBallAppWM.prototype.createSectionHeader = function(item, parent) {
var isDark = this.isDarkTheme();
var C = this.ui.colors;
var color = C.primary;
var h = new android.widget.TextView(context);
h.setText(String(item.name || ""));
h.setTextColor(color);
h.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 15);
h.setTypeface(null, android.graphics.Typeface.BOLD);
h.setPadding(this.dp(16), this.dp(24), this.dp(16), this.dp(8));
parent.addView(h);
};
FloatBallAppWM.prototype.createSettingItemView = function(item, parent, needDivider) {
var isDark = this.isDarkTheme();
var C = this.ui.colors;
var textColor = isDark ? C.textPriDark : C.textPriLight;
var secColor = isDark ? C.textSecDark : C.textSecLight;
var dividerColor = isDark ? C.dividerDark : C.dividerLight;
var primary = C.primary;
var switchOff = isDark ? (0xFF555555 | 0) : (0xFFCCCCCC | 0);
// 增加内边距
var padH = this.dp(16);
var padV = this.dp(16);
// 分割线 (顶部)
if (needDivider) {
var line = new android.view.View(context);
var lineLp = new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
1 // 1px
);
lineLp.setMargins(padH, 0, padH, 0);
line.setLayoutParams(lineLp);
line.setBackgroundColor(dividerColor);
parent.addView(line);
}
// 容器
var row = new android.widget.LinearLayout(context);
row.setOrientation(android.widget.LinearLayout.VERTICAL);
// 增加点击波纹反馈
try {
var outValue = new android.util.TypedValue();
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true);
row.setBackgroundResource(outValue.resourceId);
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
row.setPadding(padH, padV, padH, padV);
var self = this;
if (item.type === "bool") {
// === 开关类型 ===
row.setOrientation(android.widget.LinearLayout.HORIZONTAL);
row.setGravity(android.view.Gravity.CENTER_VERTICAL);
row.setClickable(true);
var tv = new android.widget.TextView(context);
tv.setText(String(item.name));
tv.setTextColor(textColor);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16);
var tvLp = new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT);
tvLp.weight = 1;
tv.setLayoutParams(tvLp);
row.addView(tv);
var sw = new android.widget.Switch(context);
try { sw.setTextOn(""); sw.setTextOff(""); } catch(eT) { safeLog(null, 'e', "catch " + String(eT)); }
// 优化开关颜色
try {
var states = [
[android.R.attr.state_checked],
[-android.R.attr.state_checked]
];
var thumbColors = [primary, switchOff];
var trackColors = [self.withAlpha(primary, 0.5), self.withAlpha(switchOff, 0.5)];
var thumbList = new android.content.res.ColorStateList(states, thumbColors);
var trackList = new android.content.res.ColorStateList(states, trackColors);
sw.setThumbTintList(thumbList);
sw.setTrackTintList(trackList);
} catch(eColor) { safeLog(null, 'e', "catch " + String(eColor)); }
var cur = !!self.getPendingValue(item.key);
sw.setChecked(cur);
// 监听器
sw.setOnCheckedChangeListener(new android.widget.CompoundButton.OnCheckedChangeListener({
onCheckedChanged: function(btn, checked) {
try {
self.touchActivity();
self.setPendingValue(item.key, !!checked);
if (self.L) self.L.d("pending " + String(item.key) + "=" + String(!!checked));
} catch(e0) { safeLog(null, 'e', "catch " + String(e0)); }
}
}));
// 点击行也触发开关
row.setOnClickListener(new android.view.View.OnClickListener({
onClick: function(v) {
sw.setChecked(!sw.isChecked());
}
}));
row.addView(sw);
parent.addView(row);
} else if (item.type === "int" || item.type === "float") {
// === 数值类型 (SeekBar) ===
// 垂直布局:上面是 标题+数值,下面是 SeekBar
// 第一行:标题 + 数值
var line1 = new android.widget.LinearLayout(context);
line1.setOrientation(android.widget.LinearLayout.HORIZONTAL);
line1.setLayoutParams(new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
));
var tv = new android.widget.TextView(context);
tv.setText(String(item.name));
tv.setTextColor(textColor);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16);
var tvLp = new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT);
tvLp.weight = 1;
tv.setLayoutParams(tvLp);
line1.addView(tv);
var valTv = new android.widget.TextView(context);
valTv.setTextColor(primary);
valTv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
valTv.setTypeface(null, android.graphics.Typeface.BOLD);
line1.addView(valTv);
row.addView(line1);
// 第二行SeekBar
var sb = new android.widget.SeekBar(context);
var sbLp = new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
);
sbLp.topMargin = self.dp(16); // 增加间距
sb.setLayoutParams(sbLp);
// 优化 SeekBar 颜色
try {
sb.getThumb().setTint(primary);
sb.getProgressDrawable().setTint(primary);
} catch(eColor) { safeLog(null, 'e', "catch " + String(eColor)); }
// 配置 SeekBar
var min = (item.min !== undefined) ? Number(item.min) : 0;
var max = (item.max !== undefined) ? Number(item.max) : 100;
var step = (item.step !== undefined) ? Number(item.step) : 1;
var curV = Number(self.getPendingValue(item.key));
if (isNaN(curV)) curV = min;
curV = self.clamp(curV, min, max);
var maxP = Math.floor((max - min) / step);
if (maxP < 1) maxP = 1;
sb.setMax(maxP);
var curP = Math.floor((curV - min) / step);
if (curP < 0) curP = 0;
if (curP > maxP) curP = maxP;
sb.setProgress(curP);
function formatVal(v) {
if (item.type === "float") return String(Math.round(v * 1000) / 1000);
return String(Math.round(v));
}
function computeValByProgress(p) {
var v = min + p * step;
v = self.clamp(v, min, max);
if (item.type === "int") v = Math.round(v);
if (item.type === "float") v = Math.round(v * 1000) / 1000;
return v;
}
valTv.setText(formatVal(curV));
sb.setOnSeekBarChangeListener(new android.widget.SeekBar.OnSeekBarChangeListener({
onProgressChanged: function(seek, progress, fromUser) {
try {
self.touchActivity();
var v = computeValByProgress(progress);
valTv.setText(formatVal(v));
if (fromUser) {
self.setPendingValue(item.key, v);
}
} catch(e1) { safeLog(null, 'e', "catch " + String(e1)); }
},
onStartTrackingTouch: function() { try { self.touchActivity(); } catch(e2) { safeLog(null, 'e', "catch " + String(e2)); } },
onStopTrackingTouch: function() { try { self.touchActivity(); } catch(e3) { safeLog(null, 'e', "catch " + String(e3)); } }
}));
row.addView(sb);
parent.addView(row);
} else if (item.type === "action") {
// === 动作按钮 ===
row.setOrientation(android.widget.LinearLayout.HORIZONTAL);
row.setGravity(android.view.Gravity.CENTER_VERTICAL);
row.setClickable(true);
var tv = new android.widget.TextView(context);
tv.setText(String(item.name));
tv.setTextColor(textColor);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16);
var tvLp = new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT);
tvLp.weight = 1;
tv.setLayoutParams(tvLp);
row.addView(tv);
// 样式化文本按钮
var btn = new android.widget.TextView(context);
btn.setText("打开");
btn.setTextColor(primary);
btn.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
btn.setTypeface(null, android.graphics.Typeface.BOLD);
btn.setGravity(android.view.Gravity.CENTER);
btn.setPadding(self.dp(16), self.dp(8), self.dp(16), self.dp(8));
// 透明波纹背景
btn.setBackground(self.ui.createTransparentRippleDrawable(primary, self.dp(16)));
btn.setOnClickListener(new android.view.View.OnClickListener({
onClick: function(v) {
try {
self.touchActivity();
if (item.action === "open_btn_mgr") {
self.showPanelAvoidBall("btn_editor");
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
}));
row.addView(btn);
// 行点击也触发
row.setOnClickListener(new android.view.View.OnClickListener({
onClick: function(v) {
try {
self.touchActivity();
if (item.action === "open_btn_mgr") {
self.showPanelAvoidBall("btn_editor");
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
}));
parent.addView(row);
} else if (item.type === "text") {
// === 文本输入 ===
var tv = new android.widget.TextView(context);
tv.setText(String(item.name));
tv.setTextColor(textColor);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16);
row.addView(tv);
var et = new android.widget.EditText(context);
var curVal = self.getPendingValue(item.key);
if (curVal === undefined || curVal === null) curVal = "";
et.setText(String(curVal));
et.setTextColor(textColor);
et.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
et.setBackground(self.ui.createRoundDrawable(isDark ? C.inputBgDark : C.inputBgLight, self.dp(6)));
et.setPadding(self.dp(8), self.dp(8), self.dp(8), self.dp(8));
et.setSingleLine(true);
var etLp = new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
);
etLp.topMargin = self.dp(8);
et.setLayoutParams(etLp);
// Explicitly request keyboard on click
et.setOnClickListener(new android.view.View.OnClickListener({
onClick: function(v) {
try {
v.requestFocus();
var imm = context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(v, 0);
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
}));
et.addTextChangedListener(new android.text.TextWatcher({
beforeTextChanged: function(s, start, count, after) {},
onTextChanged: function(s, start, before, count) {},
afterTextChanged: function(s) {
try {
self.touchActivity();
self.setPendingValue(item.key, String(s));
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
}));
row.addView(et);
parent.addView(row);
} else if (item.type === "single_choice") {
// === 单选类型 (RadioGroup) ===
var tv = new android.widget.TextView(context);
tv.setText(String(item.name));
tv.setTextColor(textColor);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16);
row.addView(tv);
var rg = new android.widget.RadioGroup(context);
rg.setOrientation(android.widget.RadioGroup.VERTICAL);
var rgLp = new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
);
rgLp.topMargin = self.dp(8);
rg.setLayoutParams(rgLp);
var curVal = String(self.getPendingValue(item.key) || "");
if (!curVal) curVal = "auto"; // default
var options = item.options || [];
for (var i = 0; i < options.length; i++) {
(function(opt) {
var rb = new android.widget.RadioButton(context);
rb.setText(String(opt.label));
rb.setTextColor(textColor);
rb.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
// 颜色优化
try {
var states = [[android.R.attr.state_checked], [-android.R.attr.state_checked]];
var colors = [primary, secColor];
rb.setButtonTintList(new android.content.res.ColorStateList(states, colors));
} catch(eC) { safeLog(null, 'e', "catch " + String(eC)); }
rb.setId(android.view.View.generateViewId ? android.view.View.generateViewId() : i);
// Check state
if (String(opt.value) === curVal) {
rb.setChecked(true);
}
rb.setOnCheckedChangeListener(new android.widget.CompoundButton.OnCheckedChangeListener({
onCheckedChanged: function(btn, checked) {
if (checked) {
try {
self.touchActivity();
self.setPendingValue(item.key, String(opt.value));
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
}
}));
rg.addView(rb);
})(options[i]);
}
row.addView(rg);
parent.addView(row);
} else if (item.type === "ball_shortx_icon") {
// === 悬浮球 ShortX 图标选择器(复用按钮图标同款弹窗)===
row.setOrientation(android.widget.LinearLayout.VERTICAL);
var tv = new android.widget.TextView(context);
tv.setText(String(item.name));
tv.setTextColor(textColor);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16);
row.addView(tv);
var iconRow = new android.widget.LinearLayout(context);
iconRow.setOrientation(android.widget.LinearLayout.HORIZONTAL);
iconRow.setGravity(android.view.Gravity.CENTER_VERTICAL);
iconRow.setPadding(0, self.dp(8), 0, 0);
var previewIv = new android.widget.ImageView(context);
var previewIvLp = new android.widget.LinearLayout.LayoutParams(self.dp(36), self.dp(36));
previewIvLp.rightMargin = self.dp(10);
previewIv.setLayoutParams(previewIvLp);
previewIv.setScaleType(android.widget.ImageView.ScaleType.FIT_CENTER);
iconRow.addView(previewIv);
var nameTv = new android.widget.TextView(context);
nameTv.setTextColor(secColor);
nameTv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 13);
var nameTvLp = new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, 1);
nameTv.setLayoutParams(nameTvLp);
iconRow.addView(nameTv);
function refreshBallShortXPreview() {
try {
var curIconName0 = String(self.getPendingValue(item.key) || "");
var curTint0 = String(self.getPendingValue("BALL_ICON_TINT_HEX") || "");
nameTv.setText(curIconName0 || "未选择");
if (curIconName0) {
var dr0 = self.resolveShortXDrawable(curIconName0, curTint0);
if (dr0) previewIv.setImageDrawable(dr0);
else previewIv.setImageDrawable(null);
} else {
previewIv.setImageDrawable(null);
}
} catch(ePreview0) { safeLog(null, 'e', "catch " + String(ePreview0)); }
}
refreshBallShortXPreview();
var btnPick = self.ui.createFlatButton(self, "选择图标", primary, function() {
self.touchActivity();
self.showShortXIconPickerPopup({
currentName: String(self.getPendingValue(item.key) || ""),
currentTint: String(self.getPendingValue("BALL_ICON_TINT_HEX") || ""),
onSelect: function(name) {
try {
var selectedName = String(name || "");
self.setPendingValue(item.key, selectedName);
self.setPendingValue("BALL_ICON_TYPE", "shortx");
refreshBallShortXPreview();
} catch(ePickBallIcon) {
safeLog(self.L, 'e', "ball shortx picker err=" + String(ePickBallIcon));
}
}
});
});
iconRow.addView(btnPick);
var gapView = new android.view.View(context);
gapView.setLayoutParams(new android.widget.LinearLayout.LayoutParams(self.dp(8), 1));
iconRow.addView(gapView);
var btnClear = self.ui.createFlatButton(self, "清空", secColor, function() {
self.touchActivity();
try {
self.setPendingValue(item.key, "");
refreshBallShortXPreview();
} catch(eClearBallIcon) { safeLog(null, 'e', "catch " + String(eClearBallIcon)); }
});
iconRow.addView(btnClear);
row.addView(iconRow);
parent.addView(row);
} else if (item.type === "ball_color") {
// === 悬浮球图标颜色选择器(复用按钮图标同款弹窗)===
row.setOrientation(android.widget.LinearLayout.VERTICAL);
var tv = new android.widget.TextView(context);
tv.setText(String(item.name));
tv.setTextColor(textColor);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16);
row.addView(tv);
var colorRow = new android.widget.LinearLayout(context);
colorRow.setOrientation(android.widget.LinearLayout.HORIZONTAL);
colorRow.setGravity(android.view.Gravity.CENTER_VERTICAL);
colorRow.setPadding(0, self.dp(8), 0, 0);
var colorDot = new android.view.View(context);
var colorDotLp = new android.widget.LinearLayout.LayoutParams(self.dp(28), self.dp(28));
colorDotLp.rightMargin = self.dp(10);
colorDot.setLayoutParams(colorDotLp);
colorRow.addView(colorDot);
var colorValueTv = new android.widget.TextView(context);
colorValueTv.setTextColor(secColor);
colorValueTv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 13);
var colorValueLp = new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, 1);
colorValueTv.setLayoutParams(colorValueLp);
colorRow.addView(colorValueTv);
function refreshBallColorPreview() {
try {
var curHex0 = String(self.getPendingValue(item.key) || "");
colorValueTv.setText(curHex0 || "默认");
if (curHex0) {
colorDot.setBackground(self.ui.createRoundDrawable(android.graphics.Color.parseColor(curHex0), self.dp(14)));
} else {
colorDot.setBackground(self.ui.createRoundDrawable(0xFFCCCCCC | 0, self.dp(14)));
}
} catch(eDot0) {
try { colorDot.setBackground(self.ui.createRoundDrawable(0xFFCCCCCC | 0, self.dp(14))); } catch(eDot1) { safeLog(null, 'e', "catch " + String(eDot1)); }
colorValueTv.setText("默认");
}
}
refreshBallColorPreview();
var btnColor = self.ui.createFlatButton(self, "选择颜色", primary, function() {
self.touchActivity();
self.showColorPickerPopup({
currentColor: String(self.getPendingValue(item.key) || ""),
currentIconName: String(self.getPendingValue("BALL_ICON_RES_NAME") || ""),
onSelect: function(colorHex) {
try {
self.setPendingValue(item.key, String(colorHex || ""));
refreshBallColorPreview();
} catch(ePickBallColor) {
safeLog(self.L, 'e', "ball color picker err=" + String(ePickBallColor));
}
}
});
});
colorRow.addView(btnColor);
var gapColorView = new android.view.View(context);
gapColorView.setLayoutParams(new android.widget.LinearLayout.LayoutParams(self.dp(8), 1));
colorRow.addView(gapColorView);
var btnClearColor = self.ui.createFlatButton(self, "清空", secColor, function() {
self.touchActivity();
try {
self.setPendingValue(item.key, "");
refreshBallColorPreview();
} catch(eClearBallColor) { safeLog(null, 'e', "catch " + String(eClearBallColor)); }
});
colorRow.addView(btnClearColor);
row.addView(colorRow);
parent.addView(row);
} else {
// 兜底文本
var tv = new android.widget.TextView(context);
tv.setText(String(item.name));
tv.setTextColor(secColor);
row.addView(tv);
parent.addView(row);
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,7 @@ FloatBallAppWM.prototype.buildViewerPanelView = function(titleText, bodyText) {
bgDr.setColor(bgColor); bgDr.setColor(bgColor);
bgDr.setCornerRadius(this.dp(16)); bgDr.setCornerRadius(this.dp(16));
panel.setBackground(bgDr); panel.setBackground(bgDr);
try { panel.setElevation(this.dp(8)); } catch(e){} try { panel.setElevation(this.dp(8)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
panel.setPadding( panel.setPadding(
this.dp(16), this.dp(16),
@@ -39,8 +39,8 @@ FloatBallAppWM.prototype.buildViewerPanelView = function(titleText, bodyText) {
panel.addView(sep); panel.addView(sep);
var scroll = new android.widget.ScrollView(context); var scroll = new android.widget.ScrollView(context);
try { scroll.setOverScrollMode(android.view.View.OVER_SCROLL_NEVER); } catch (eOS) {} try { scroll.setOverScrollMode(android.view.View.OVER_SCROLL_NEVER); } catch(eOS) { safeLog(null, 'e', "catch " + String(eOS)); }
try { scroll.setVerticalScrollBarEnabled(true); } catch (eSB) {} try { scroll.setVerticalScrollBarEnabled(true); } catch(eSB) { safeLog(null, 'e', "catch " + String(eSB)); }
// 给内容加一点边距 // 给内容加一点边距
var contentBox = new android.widget.LinearLayout(context); var contentBox = new android.widget.LinearLayout(context);
@@ -53,11 +53,11 @@ FloatBallAppWM.prototype.buildViewerPanelView = function(titleText, bodyText) {
tv.setTextColor(codeColor); tv.setTextColor(codeColor);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, Number(this.config.CONTENT_VIEWER_TEXT_SP || 12)); tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, Number(this.config.CONTENT_VIEWER_TEXT_SP || 12));
// 增加行距优化阅读 // 增加行距优化阅读
try { tv.setLineSpacing(this.dp(4), 1.0); } catch(eLS) {} try { tv.setLineSpacing(this.dp(4), 1.0); } catch(eLS) { safeLog(null, 'e', "catch " + String(eLS)); }
// 使用等宽字体显示代码/日志 // 使用等宽字体显示代码/日志
try { tv.setTypeface(android.graphics.Typeface.MONOSPACE); } catch(eTF) {} try { tv.setTypeface(android.graphics.Typeface.MONOSPACE); } catch(eTF) { safeLog(null, 'e', "catch " + String(eTF)); }
// WindowManager 环境下禁用文本选择,否则长按/选择会因缺少 Token 崩溃 // WindowManager 环境下禁用文本选择,否则长按/选择会因缺少 Token 崩溃
try { tv.setTextIsSelectable(false); } catch (eSel) {} try { tv.setTextIsSelectable(false); } catch(eSel) { safeLog(null, 'e', "catch " + String(eSel)); }
contentBox.addView(tv); contentBox.addView(tv);
@@ -96,7 +96,7 @@ FloatBallAppWM.prototype.buildPanelView = function(panelType) {
bgDr.setColor(this.withAlpha(bgColor, this.config.PANEL_BG_ALPHA)); bgDr.setColor(this.withAlpha(bgColor, this.config.PANEL_BG_ALPHA));
bgDr.setCornerRadius(this.dp(16)); bgDr.setCornerRadius(this.dp(16));
panel.setBackground(bgDr); panel.setBackground(bgDr);
try { panel.setElevation(this.dp(8)); } catch(e){} try { panel.setElevation(this.dp(8)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
var padDp = this.config.PANEL_PADDING_DP; var padDp = this.config.PANEL_PADDING_DP;
panel.setPadding( panel.setPadding(
@@ -149,9 +149,9 @@ FloatBallAppWM.prototype.buildPanelView = function(panelType) {
} }
var scroll = new android.widget.ScrollView(context); var scroll = new android.widget.ScrollView(context);
try { scroll.setOverScrollMode(android.view.View.OVER_SCROLL_NEVER); } catch (eOS) {} try { scroll.setOverScrollMode(android.view.View.OVER_SCROLL_NEVER); } catch(eOS) { safeLog(null, 'e', "catch " + String(eOS)); }
try { scroll.setVerticalScrollBarEnabled(false); } catch (eSB) {} try { scroll.setVerticalScrollBarEnabled(false); } catch(eSB) { safeLog(null, 'e', "catch " + String(eSB)); }
try { scroll.setFillViewport(true); } catch (eFV) {} try { scroll.setFillViewport(true); } catch(eFV) { safeLog(null, 'e', "catch " + String(eFV)); }
var scrollLp = new android.widget.LinearLayout.LayoutParams(contentW, scrollH); var scrollLp = new android.widget.LinearLayout.LayoutParams(contentW, scrollH);
scroll.setLayoutParams(scrollLp); scroll.setLayoutParams(scrollLp);
@@ -170,7 +170,7 @@ FloatBallAppWM.prototype.buildPanelView = function(panelType) {
// var totalCells = totalBtns > minCells ? totalBtns : minCells; // var totalCells = totalBtns > minCells ? totalBtns : minCells;
var rowCount = Math.ceil(totalCells / cols2); var rowCount = Math.ceil(totalCells / cols2);
try { grid.setRowCount(rowCount); } catch (eRC) {} try { grid.setRowCount(rowCount); } catch(eRC) { safeLog(null, 'e', "catch " + String(eRC)); }
grid.setOnTouchListener(new JavaAdapter(android.view.View.OnTouchListener, { grid.setOnTouchListener(new JavaAdapter(android.view.View.OnTouchListener, {
onTouch: function(v, e) { self.touchActivity(); return false; } onTouch: function(v, e) { self.touchActivity(); return false; }
@@ -193,7 +193,7 @@ FloatBallAppWM.prototype.buildPanelView = function(panelType) {
// 单元格背景:如果是有功能的按钮,给个卡片背景;否则透明 // 单元格背景:如果是有功能的按钮,给个卡片背景;否则透明
if (btnCfg) { if (btnCfg) {
cell.setBackground(self.ui.createRoundDrawable(cardColor, self.dp(12))); cell.setBackground(self.ui.createRoundDrawable(cardColor, self.dp(12)));
try { cell.setElevation(self.dp(2)); } catch(e){} try { cell.setElevation(self.dp(2)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
} else { } else {
// 空格子占位 // 空格子占位
} }
@@ -206,7 +206,7 @@ FloatBallAppWM.prototype.buildPanelView = function(panelType) {
// 但 resolveIconDrawable 逻辑比较复杂,这里暂时不强制染色,除非用户配置了 TINT // 但 resolveIconDrawable 逻辑比较复杂,这里暂时不强制染色,除非用户配置了 TINT
if (!isDark && btnCfg && !btnCfg.type && !btnCfg.pkg) { if (!isDark && btnCfg && !btnCfg.type && !btnCfg.pkg) {
// 简单的系统图标在亮色模式下可能看不清,染成深色 // 简单的系统图标在亮色模式下可能看不清,染成深色
try { iv.setColorFilter(C.textPriLight, android.graphics.PorterDuff.Mode.SRC_IN); } catch(eCF){} try { iv.setColorFilter(C.textPriLight, android.graphics.PorterDuff.Mode.SRC_IN); } catch(eCF) { safeLog(null, 'e', "catch " + String(eCF)); }
} }
} }
@@ -224,7 +224,7 @@ FloatBallAppWM.prototype.buildPanelView = function(panelType) {
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, this.config.PANEL_LABEL_TEXT_SIZE_SP); tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, this.config.PANEL_LABEL_TEXT_SIZE_SP);
tv.setTextColor(textColor); tv.setTextColor(textColor);
tv.setGravity(android.view.Gravity.CENTER); tv.setGravity(android.view.Gravity.CENTER);
try { tv.setLines(1); tv.setEllipsize(android.text.TextUtils.TruncateAt.END); } catch(eL){} try { tv.setLines(1); tv.setEllipsize(android.text.TextUtils.TruncateAt.END); } catch(eL) { safeLog(null, 'e', "catch " + String(eL)); }
var tvLp = new android.widget.LinearLayout.LayoutParams( var tvLp = new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT, // 宽度填满,方便居中 android.widget.LinearLayout.LayoutParams.MATCH_PARENT, // 宽度填满,方便居中
@@ -251,8 +251,8 @@ FloatBallAppWM.prototype.buildPanelView = function(panelType) {
})); }));
})(i, btnCfg); })(i, btnCfg);
} else { } else {
try { iv.setAlpha(0); } catch (eA0) {} try { iv.setAlpha(0); } catch(eA0) { safeLog(null, 'e', "catch " + String(eA0)); }
try { cell.setClickable(false); } catch (eC0) {} try { cell.setClickable(false); } catch(eC0) { safeLog(null, 'e', "catch " + String(eC0)); }
} }
grid.addView(cell); grid.addView(cell);
@@ -460,7 +460,7 @@ FloatBallAppWM.prototype.addPanel = function(panel, x, y, which) {
panel.setScaleY(1); panel.setScaleY(1);
panel.setAlpha(1); panel.setAlpha(1);
} }
} catch (eA) {} } catch(eA) { safeLog(null, 'e', "catch " + String(eA)); }
// # 日志防抖5秒内相同面板类型不重复记录 // # 日志防抖5秒内相同面板类型不重复记录
var now = Date.now(); var now = Date.now();
@@ -516,8 +516,8 @@ FloatBallAppWM.prototype.showPanelAvoidBall = function(which) {
// 需要处理 newPanel 的 LayoutParams // 需要处理 newPanel 的 LayoutParams
var contentLp = new android.widget.LinearLayout.LayoutParams(-1, 0); var contentLp = new android.widget.LinearLayout.LayoutParams(-1, 0);
contentLp.weight = 1; contentLp.weight = 1;
try { newPanel.setBackground(null); } catch(e){} // 移除背景,使用 Container 背景 try { newPanel.setBackground(null); } catch(e) { safeLog(null, 'e', "catch " + String(e)); } // 移除背景,使用 Container 背景
try { newPanel.setElevation(0); } catch(e){} try { newPanel.setElevation(0); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
newPanel.setLayoutParams(contentLp); newPanel.setLayoutParams(contentLp);
container.addView(newPanel); container.addView(newPanel);
@@ -651,7 +651,7 @@ FloatBallAppWM.prototype.showPanelAvoidBall = function(which) {
self.touchActivity(); self.touchActivity();
} catch (e) { } catch (e) {
if (self.L) self.L.e("showPanelAvoidBall callback err=" + String(e)); if (self.L) self.L.e("showPanelAvoidBall callback err=" + String(e));
try { self.toast("面板显示失败: " + String(e)); } catch (et) {} try { self.toast("面板显示失败: " + String(e)); } catch(et) { safeLog(null, 'e', "catch " + String(et)); }
} }
}; };
@@ -691,7 +691,7 @@ FloatBallAppWM.prototype.wrapDraggablePanel = function(contentView, optionsOrTit
bgDr.setColor(isDark ? C.bgDark : C.bgLight); bgDr.setColor(isDark ? C.bgDark : C.bgLight);
bgDr.setCornerRadius(this.dp(12)); bgDr.setCornerRadius(this.dp(12));
container.setBackground(bgDr); container.setBackground(bgDr);
try { container.setElevation(this.dp(8)); } catch(e){} try { container.setElevation(this.dp(8)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
// Header // Header
var header = new android.widget.LinearLayout(context); var header = new android.widget.LinearLayout(context);
@@ -717,7 +717,7 @@ FloatBallAppWM.prototype.wrapDraggablePanel = function(contentView, optionsOrTit
else self.hideAllPanels(); else self.hideAllPanels();
}); });
btnClose.setPadding(self.dp(8), self.dp(4), self.dp(8), self.dp(4)); btnClose.setPadding(self.dp(8), self.dp(4), self.dp(8), self.dp(4));
try { btnClose.setElevation(this.dp(25)); } catch(e){} // Ensure on top of resize handles try { btnClose.setElevation(this.dp(25)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); } // Ensure on top of resize handles
header.addView(btnClose); header.addView(btnClose);
// Spacer to avoid overlap with Top-Right resize handle // Spacer to avoid overlap with Top-Right resize handle
@@ -733,8 +733,8 @@ FloatBallAppWM.prototype.wrapDraggablePanel = function(contentView, optionsOrTit
// Add Content // Add Content
// 移除 content 原有的背景和 elevation避免重复 // 移除 content 原有的背景和 elevation避免重复
try { contentView.setBackground(null); } catch(e){} try { contentView.setBackground(null); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
try { contentView.setElevation(0); } catch(e){} try { contentView.setElevation(0); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
var contentLp = new android.widget.LinearLayout.LayoutParams(-1, 0); var contentLp = new android.widget.LinearLayout.LayoutParams(-1, 0);
contentLp.weight = 1; contentLp.weight = 1;
@@ -746,7 +746,7 @@ FloatBallAppWM.prototype.wrapDraggablePanel = function(contentView, optionsOrTit
// Resize Handle (Bottom-Right Corner) - Invisible // Resize Handle (Bottom-Right Corner) - Invisible
var handleBR = new android.view.View(context); var handleBR = new android.view.View(context);
try { handleBR.setElevation(this.dp(20)); } catch(e){} try { handleBR.setElevation(this.dp(20)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
var handleBRLp = new android.widget.FrameLayout.LayoutParams(this.dp(30), this.dp(30)); var handleBRLp = new android.widget.FrameLayout.LayoutParams(this.dp(30), this.dp(30));
handleBRLp.gravity = android.view.Gravity.BOTTOM | android.view.Gravity.END; handleBRLp.gravity = android.view.Gravity.BOTTOM | android.view.Gravity.END;
handleBRLp.rightMargin = 0; handleBRLp.rightMargin = 0;
@@ -755,7 +755,7 @@ FloatBallAppWM.prototype.wrapDraggablePanel = function(contentView, optionsOrTit
// Resize Handle (Bottom-Left Corner) - Invisible // Resize Handle (Bottom-Left Corner) - Invisible
var handleBL = new android.view.View(context); var handleBL = new android.view.View(context);
try { handleBL.setElevation(this.dp(20)); } catch(e){} try { handleBL.setElevation(this.dp(20)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
var handleBLLp = new android.widget.FrameLayout.LayoutParams(this.dp(30), this.dp(30)); var handleBLLp = new android.widget.FrameLayout.LayoutParams(this.dp(30), this.dp(30));
handleBLLp.gravity = android.view.Gravity.BOTTOM | android.view.Gravity.START; handleBLLp.gravity = android.view.Gravity.BOTTOM | android.view.Gravity.START;
handleBLLp.bottomMargin = 0; handleBLLp.bottomMargin = 0;
@@ -764,7 +764,7 @@ FloatBallAppWM.prototype.wrapDraggablePanel = function(contentView, optionsOrTit
// Resize Handle (Top-Left Corner) - Invisible // Resize Handle (Top-Left Corner) - Invisible
var handleTL = new android.view.View(context); var handleTL = new android.view.View(context);
try { handleTL.setElevation(this.dp(20)); } catch(e){} try { handleTL.setElevation(this.dp(20)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
var handleTLLp = new android.widget.FrameLayout.LayoutParams(this.dp(30), this.dp(30)); var handleTLLp = new android.widget.FrameLayout.LayoutParams(this.dp(30), this.dp(30));
handleTLLp.gravity = android.view.Gravity.TOP | android.view.Gravity.START; handleTLLp.gravity = android.view.Gravity.TOP | android.view.Gravity.START;
handleTLLp.topMargin = 0; handleTLLp.topMargin = 0;
@@ -773,7 +773,7 @@ FloatBallAppWM.prototype.wrapDraggablePanel = function(contentView, optionsOrTit
// Resize Handle (Top-Right Corner) - Invisible // Resize Handle (Top-Right Corner) - Invisible
var handleTR = new android.view.View(context); var handleTR = new android.view.View(context);
try { handleTR.setElevation(this.dp(20)); } catch(e){} try { handleTR.setElevation(this.dp(20)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
var handleTRLp = new android.widget.FrameLayout.LayoutParams(this.dp(30), this.dp(30)); var handleTRLp = new android.widget.FrameLayout.LayoutParams(this.dp(30), this.dp(30));
handleTRLp.gravity = android.view.Gravity.TOP | android.view.Gravity.END; handleTRLp.gravity = android.view.Gravity.TOP | android.view.Gravity.END;
handleTRLp.topMargin = 0; handleTRLp.topMargin = 0;
@@ -867,7 +867,7 @@ FloatBallAppWM.prototype.attachDragResizeListeners = function(rootView, headerVi
lp.x = targetX; lp.x = targetX;
lp.y = targetY; lp.y = targetY;
try { wm.updateViewLayout(rootView, lp); } catch(e){} try { wm.updateViewLayout(rootView, lp); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
} }
} }
return true; return true;
@@ -938,10 +938,10 @@ FloatBallAppWM.prototype.attachDragResizeListeners = function(rootView, headerVi
lpCur.height = initialH; lpCur.height = initialH;
lpCur.x = initialX; lpCur.x = initialX;
lpCur.y = initialY; lpCur.y = initialY;
try { wm.updateViewLayout(rootView, lpCur); } catch(e){} try { wm.updateViewLayout(rootView, lpCur); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
} }
resizing = true; resizing = true;
} catch(e) {} } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
} }
}); });
self.state.h.postDelayed(longPressRunnable, 300); // 300ms hold to activate resize self.state.h.postDelayed(longPressRunnable, 300); // 300ms hold to activate resize
@@ -1022,7 +1022,7 @@ FloatBallAppWM.prototype.attachDragResizeListeners = function(rootView, headerVi
lp.y = Math.round(newY); lp.y = Math.round(newY);
} }
try { wm.updateViewLayout(rootView, lp); } catch(e){} try { wm.updateViewLayout(rootView, lp); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
} }
} }
return true; return true;
@@ -1138,7 +1138,7 @@ FloatBallAppWM.prototype.showViewerPanel = function(title, text) {
}; };
FloatBallAppWM.prototype.cancelLongPressTimer = function() { FloatBallAppWM.prototype.cancelLongPressTimer = function() {
try { if (this.state.longPressRunnable && this.state.h) this.state.h.removeCallbacks(this.state.longPressRunnable); } catch (e) {} try { if (this.state.longPressRunnable && this.state.h) this.state.h.removeCallbacks(this.state.longPressRunnable); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
this.state.longPressArmed = false; this.state.longPressArmed = false;
this.state.longPressRunnable = null; this.state.longPressRunnable = null;
}; };
@@ -1208,7 +1208,7 @@ FloatBallAppWM.prototype.setupTouchListener = function() {
self.touchActivity(); self.touchActivity();
// 恢复不透明度 // 恢复不透明度
try { v.setAlpha(1.0); } catch(eA) {} try { v.setAlpha(1.0); } catch(eA) { safeLog(null, 'e', "catch " + String(eA)); }
if (self.state.docked) { if (self.state.docked) {
self.undockToFull(false, null); self.undockToFull(false, null);
@@ -1224,14 +1224,14 @@ FloatBallAppWM.prototype.setupTouchListener = function() {
lastTouchX = e.getRawX(); lastTouchX = e.getRawX();
lastTouchY = e.getRawY(); lastTouchY = e.getRawY();
try { v.setPressed(true); } catch (eP) {} try { v.setPressed(true); } catch(eP) { safeLog(null, 'e', "catch " + String(eP)); }
try { v.drawableHotspotChanged(e.getX(), e.getY()); } catch (eH) {} try { v.drawableHotspotChanged(e.getX(), e.getY()); } catch(eH) { safeLog(null, 'e', "catch " + String(eH)); }
// 按下缩小反馈 // 按下缩小反馈
if (self.config.ENABLE_ANIMATIONS) { if (self.config.ENABLE_ANIMATIONS) {
try { try {
v.animate().scaleX(0.9).scaleY(0.9).setDuration(100).start(); v.animate().scaleX(0.9).scaleY(0.9).setDuration(100).start();
} catch(eS){} } catch(eS) { safeLog(null, 'e', "catch " + String(eS)); }
} }
self.armLongPress(); self.armLongPress();
@@ -1268,11 +1268,11 @@ FloatBallAppWM.prototype.setupTouchListener = function() {
self.state.ballLp.y = self.clamp(self.state.ballLp.y, 0, self.state.screen.h - di.ballSize); self.state.ballLp.y = self.clamp(self.state.ballLp.y, 0, self.state.screen.h - di.ballSize);
self.state.ballLp.width = di.ballSize; self.state.ballLp.width = di.ballSize;
try { self.state.ballContent.setX(0); } catch (e0) {} try { self.state.ballContent.setX(0); } catch(e0) { safeLog(null, 'e', "catch " + String(e0)); }
var now = java.lang.System.currentTimeMillis(); var now = java.lang.System.currentTimeMillis();
if (now - lastUpdateTs > 10) { // 10ms 节流 if (now - lastUpdateTs > 10) { // 10ms 节流
try { self.state.wm.updateViewLayout(self.state.ballRoot, self.state.ballLp); } catch (eU) {} try { self.state.wm.updateViewLayout(self.state.ballRoot, self.state.ballLp); } catch(eU) { safeLog(null, 'e', "catch " + String(eU)); }
lastUpdateTs = now; lastUpdateTs = now;
} }
@@ -1286,16 +1286,16 @@ FloatBallAppWM.prototype.setupTouchListener = function() {
if (a === android.view.MotionEvent.ACTION_UP || a === android.view.MotionEvent.ACTION_CANCEL) { if (a === android.view.MotionEvent.ACTION_UP || a === android.view.MotionEvent.ACTION_CANCEL) {
self.touchActivity(); self.touchActivity();
try { v.setPressed(false); } catch (eP2) {} try { v.setPressed(false); } catch(eP2) { safeLog(null, 'e', "catch " + String(eP2)); }
self.cancelLongPressTimer(); self.cancelLongPressTimer();
// 恢复缩放 // 恢复缩放
if (self.config.ENABLE_ANIMATIONS) { if (self.config.ENABLE_ANIMATIONS) {
try { try {
v.animate().scaleX(1.0).scaleY(1.0).setDuration(150).start(); v.animate().scaleX(1.0).scaleY(1.0).setDuration(150).start();
} catch(eS){} } catch(eS) { safeLog(null, 'e', "catch " + String(eS)); }
} else { } else {
try { v.setScaleX(1); v.setScaleY(1); } catch(eS){} try { v.setScaleX(1); v.setScaleY(1); } catch(eS) { safeLog(null, 'e', "catch " + String(eS)); }
} }
if (self.state.longPressTriggered) { if (self.state.longPressTriggered) {
@@ -1305,7 +1305,7 @@ FloatBallAppWM.prototype.setupTouchListener = function() {
} }
if (!self.state.dragging && a === android.view.MotionEvent.ACTION_UP) { if (!self.state.dragging && a === android.view.MotionEvent.ACTION_UP) {
try { self.playBounce(v); } catch (eB) {} try { self.playBounce(v); } catch(eB) { safeLog(null, 'e', "catch " + String(eB)); }
if (self.state.addedPanel) self.hideMainPanel(); if (self.state.addedPanel) self.hideMainPanel();
else self.showPanelAvoidBall("main"); else self.showPanelAvoidBall("main");
@@ -1315,7 +1315,7 @@ FloatBallAppWM.prototype.setupTouchListener = function() {
} else { } else {
// 拖拽结束 // 拖拽结束
// 确保最后位置被更新 // 确保最后位置被更新
try { self.state.wm.updateViewLayout(self.state.ballRoot, self.state.ballLp); } catch (eU) {} try { self.state.wm.updateViewLayout(self.state.ballRoot, self.state.ballLp); } catch(eU) { safeLog(null, 'e', "catch " + String(eU)); }
var forceSide = null; var forceSide = null;
// 计算速度 // 计算速度
@@ -1360,7 +1360,7 @@ FloatBallAppWM.prototype.createBallViews = function() {
var root = new android.widget.FrameLayout(context); var root = new android.widget.FrameLayout(context);
root.setClipToPadding(true); root.setClipToPadding(true);
root.setClipChildren(true); root.setClipChildren(true);
try { root.setElevation(this.dp(6)); } catch(e){} try { root.setElevation(this.dp(6)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
var content = new android.widget.FrameLayout(context); var content = new android.widget.FrameLayout(context);
var lp = new android.widget.FrameLayout.LayoutParams(di.ballSize, di.ballSize); var lp = new android.widget.FrameLayout.LayoutParams(di.ballSize, di.ballSize);
@@ -1370,7 +1370,6 @@ try {
var iconResId = Number(this.config.BALL_ICON_RES_ID || 0); var iconResId = Number(this.config.BALL_ICON_RES_ID || 0);
var iconType = this.config.BALL_ICON_TYPE ? String(this.config.BALL_ICON_TYPE) : "android"; var iconType = this.config.BALL_ICON_TYPE ? String(this.config.BALL_ICON_TYPE) : "android";
var iconFilePath = (this.config.BALL_ICON_FILE_PATH == null) ? "" : String(this.config.BALL_ICON_FILE_PATH); var iconFilePath = (this.config.BALL_ICON_FILE_PATH == null) ? "" : String(this.config.BALL_ICON_FILE_PATH);
var textStr = (this.config.BALL_TEXT == null) ? "" : String(this.config.BALL_TEXT);
// # 是否显示图标file 只看路径app 优先看包名,其次可回退 iconResIdandroid 走 iconResIdshortx 总是显示 // # 是否显示图标file 只看路径app 优先看包名,其次可回退 iconResIdandroid 走 iconResIdshortx 总是显示
var showIcon = false; var showIcon = false;
@@ -1385,10 +1384,9 @@ try {
showIcon = (iconResId > 0); showIcon = (iconResId > 0);
} }
if (!showIcon && textStr.length === 0) showIcon = true; if (!showIcon) showIcon = true;
var showText = textStr.length > 0; if (showIcon) {
if (showIcon || showText) {
var box = new android.widget.LinearLayout(context); var box = new android.widget.LinearLayout(context);
box.setOrientation(android.widget.LinearLayout.VERTICAL); box.setOrientation(android.widget.LinearLayout.VERTICAL);
box.setGravity(android.view.Gravity.CENTER); box.setGravity(android.view.Gravity.CENTER);
@@ -1400,9 +1398,6 @@ try {
box.setLayoutParams(boxLp); box.setLayoutParams(boxLp);
var tintHex = (this.config.BALL_ICON_TINT_HEX == null) ? "" : String(this.config.BALL_ICON_TINT_HEX); var tintHex = (this.config.BALL_ICON_TINT_HEX == null) ? "" : String(this.config.BALL_ICON_TINT_HEX);
var textColorHex = (this.config.BALL_TEXT_COLOR_HEX == null) ? "" : String(this.config.BALL_TEXT_COLOR_HEX);
var defaultColor = android.graphics.Color.WHITE;
if (showIcon) { if (showIcon) {
var iv = new android.widget.ImageView(context); var iv = new android.widget.ImageView(context);
@@ -1424,7 +1419,7 @@ try {
} else { } else {
safeLog(this.L, 'w', "Ball icon file load failed: " + iconFilePath); safeLog(this.L, 'w', "Ball icon file load failed: " + iconFilePath);
} }
} catch (eF) {} } catch(eF) { safeLog(null, 'e', "catch " + String(eF)); }
} }
// # 2) app加载应用图标 (file 失败也会尝试 app) // # 2) app加载应用图标 (file 失败也会尝试 app)
@@ -1439,7 +1434,7 @@ try {
usedKind = "app"; usedKind = "app";
} }
} }
} catch (eA) {} } catch(eA) { safeLog(null, 'e', "catch " + String(eA)); }
} }
// # 2.5) shortx专门加载 ShortX 内置图标(也作为 file 模式的兜底) // # 2.5) shortx专门加载 ShortX 内置图标(也作为 file 模式的兜底)
@@ -1460,7 +1455,7 @@ try {
safeLog(this.L, 'i', "File icon failed, fallback to shortx icon"); safeLog(this.L, 'i', "File icon failed, fallback to shortx icon");
} }
} }
} catch (eShortx2) {} } catch(eShortx2) { safeLog(null, 'e', "catch " + String(eShortx2)); }
} }
// # 3) android或所有兜底走资源 id优先尝试 ShortX 内置图标) // # 3) android或所有兜底走资源 id优先尝试 ShortX 内置图标)
@@ -1474,13 +1469,13 @@ try {
if (usedDrawable != null) { if (usedDrawable != null) {
usedKind = "shortx"; usedKind = "shortx";
} }
} catch (eShortx) {} } catch(eShortx) { safeLog(null, 'e', "catch " + String(eShortx)); }
} }
if (usedDrawable != null) { if (usedDrawable != null) {
iv.setImageDrawable(usedDrawable); iv.setImageDrawable(usedDrawable);
} else if (iconResId > 0) { } else if (iconResId > 0) {
try { iv.setImageResource(iconResId); usedKind = "android"; } catch (eR) {} try { iv.setImageResource(iconResId); usedKind = "android"; } catch(eR) { safeLog(null, 'e', "catch " + String(eR)); }
} else { } else {
// # 没有任何可用图标,直接不加到布局 // # 没有任何可用图标,直接不加到布局
usedKind = "none"; usedKind = "none";
@@ -1510,49 +1505,20 @@ try {
try { try {
var tintColor2 = android.graphics.Color.parseColor(tintHex); var tintColor2 = android.graphics.Color.parseColor(tintHex);
iv.setColorFilter(tintColor2, android.graphics.PorterDuff.Mode.SRC_IN); iv.setColorFilter(tintColor2, android.graphics.PorterDuff.Mode.SRC_IN);
} catch (eTint2) {} } catch(eTint2) { safeLog(null, 'e', "catch " + String(eTint2)); }
} else if (usedKind === "android") { } else if (usedKind === "android") {
try { iv.setColorFilter(android.graphics.Color.WHITE, android.graphics.PorterDuff.Mode.SRC_IN); } catch (eCF) {} try { iv.setColorFilter(android.graphics.Color.WHITE, android.graphics.PorterDuff.Mode.SRC_IN); } catch(eCF) { safeLog(null, 'e', "catch " + String(eCF)); }
} else { } else {
try { iv.clearColorFilter(); } catch (eCL) {} try { iv.clearColorFilter(); } catch(eCL) { safeLog(null, 'e', "catch " + String(eCL)); }
} }
box.addView(iv); box.addView(iv);
} }
} }
if (showText) {
var tv = new android.widget.TextView(context);
tv.setText(textStr);
tv.setGravity(android.view.Gravity.CENTER);
try { tv.setIncludeFontPadding(false); } catch (eFP) {}
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, Number(this.config.BALL_TEXT_SIZE_SP || 10));
var txtColor = defaultColor;
if (textColorHex.length > 0) {
try { txtColor = android.graphics.Color.parseColor(textColorHex); } catch (eTC) {}
} else if (tintHex.length > 0) {
// # 如果没单独指定文字颜色,则跟随图标颜色
try { txtColor = android.graphics.Color.parseColor(tintHex); } catch (eTC2) {}
}
tv.setTextColor(txtColor);
// # 设置一点点阴影,提高可读性
try { tv.setShadowLayer(1.2, 0, 1.0, 0x66000000); } catch (eSH) {}
// # 图标与文字间距
if (showIcon) {
var gap = this.dp(Number(this.config.BALL_ICON_TEXT_GAP_DP || 1));
var padTop = Math.max(0, gap);
tv.setPadding(0, padTop, 0, 0);
}
box.addView(tv);
}
content.addView(box); content.addView(box);
} }
} catch (eBallInner) {} } catch(eBallInner) { safeLog(null, 'e', "catch " + String(eBallInner)); }
this.updateBallContentBackground(content); this.updateBallContentBackground(content);
@@ -1560,7 +1526,7 @@ try {
// # 阴影控制file/app 模式下不加阴影(避免透明背景带黑框) // # 阴影控制file/app 模式下不加阴影(避免透明背景带黑框)
var _uk = this.state.usedIconKind; var _uk = this.state.usedIconKind;
if (_uk !== "file" && _uk !== "app") { if (_uk !== "file" && _uk !== "app") {
try { root.setElevation(this.dp(6)); } catch(e){} try { root.setElevation(this.dp(6)); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
} }
content.setClickable(true); content.setClickable(true);

File diff suppressed because it is too large Load Diff