Compare commits

...

116 Commits

Author SHA1 Message Date
7015725
407cd2200c fix: align ToolApp top with status bar 2026-05-23 03:12:39 +08:00
7015725
1d49609131 fix: stabilize ToolHub logging 2026-05-23 02:42:46 +08:00
7015725
c62328c419 refactor: polish schema editor blueprint UI 2026-05-23 02:12:28 +08:00
7015725
52b13bea19 refactor: split schema editor module 2026-05-23 01:48:19 +08:00
7015725
c8f8f46054 fix: keep color picker compatible with existing entry 2026-05-23 01:29:05 +08:00
7015725
954caa9d9b refactor: split color picker module 2026-05-23 01:15:56 +08:00
7015725
61eeac6646 refactor: split ShortX icon picker module 2026-05-23 01:05:16 +08:00
7015725
0aa7cbfbfd refactor: add icon picker module placeholder 2026-05-23 00:53:25 +08:00
7015725
3f93a42a00 refactor: remove legacy ToolApp back strips 2026-05-22 13:04:51 +08:00
7015725
7bc4a1bb7d fix: disable legacy ToolApp back strips 2026-05-22 12:55:46 +08:00
7015725
a9db5faf1b fix: prefer ToolApp surface back gesture 2026-05-22 12:41:05 +08:00
7015725
736212fd80 fix: stabilize ToolApp back gesture stack 2026-05-22 12:30:50 +08:00
7015725
83249ab534 Fix ToolApp surface swipe back blocking 2026-05-22 09:16:25 +08:00
7015725
8fd7a560d8 Add ToolApp surface back gesture mode 2026-05-22 08:56:24 +08:00
Hermes
479a23592c Improve ToolApp blank-edge back gestures 2026-05-22 08:38:40 +08:00
Hermes
adc93338cf Improve ToolApp edge back gesture sensitivity 2026-05-22 08:27:23 +08:00
Hermes
4800536de8 Improve ToolApp edge back responsiveness 2026-05-22 08:01:11 +08:00
Hermes
fd56e14fc8 Fix ToolApp edge back touch interception 2026-05-22 07:54:19 +08:00
Hermes
557950f692 Fix ToolApp back preview snapshots 2026-05-22 06:54:34 +08:00
Hermes
855352bfd9 Fix ToolApp edge back preview layers 2026-05-22 06:41:54 +08:00
Hermes
454090248d Move ToolApp root during edge back drag 2026-05-22 06:29:54 +08:00
Hermes
25b9f5c52b Fix ToolApp back preview drag fallback 2026-05-22 06:22:57 +08:00
Hermes
67fad1d7f8 Fix ToolApp edge back drag progress 2026-05-22 06:14:44 +08:00
Hermes
dcd3fcb9ee Fix ToolApp screen edge drag follow 2026-05-22 06:00:15 +08:00
Hermes
be2156a634 Fix ToolApp edge back strip defaults 2026-05-22 05:52:12 +08:00
Hermes
a5bd0a5b15 fix: restore ToolApp edge back gesture toggles 2026-05-22 05:39:58 +08:00
7015725
858b1c5f54 fix: debounce panel back callback registration 2026-05-22 05:29:30 +08:00
7015725
c52598bd44 fix: show toolhub editor feedback inline 2026-05-22 05:25:41 +08:00
7015725
195e61d810 fix button editor validation feedback 2026-05-22 05:15:54 +08:00
7015725
544494df8c fix: isolate UI button click guards 2026-05-22 05:06:49 +08:00
7015725
eaeeba2b0a Fix ToolApp edge strips blocking buttons 2026-05-22 04:59:50 +08:00
7015725
3511b3f1ce Fix inert ToolApp UI actions 2026-05-22 04:40:56 +08:00
7015725
32fe099fab fix: make ToolHub UI actions responsive 2026-05-22 04:27:45 +08:00
7015725
78218ce1f5 fix: reduce color picker popup max height 2026-05-22 04:10:53 +08:00
7015725
854e80de80 fix: use valid color picker scroll layout params 2026-05-22 03:59:36 +08:00
7015725
2a97a7a790 fix: make color picker popup responsive 2026-05-22 03:57:31 +08:00
7015725
cc23da6904 fix: increase color picker action button size 2026-05-22 03:50:00 +08:00
7015725
ad58737c67 fix: enlarge color picker actions to match settings 2026-05-22 03:47:39 +08:00
7015725
9c3b628a83 fix: soften color picker action colors 2026-05-22 03:43:51 +08:00
7015725
9ed8b39d0b fix: match color picker actions to settings style 2026-05-22 03:38:59 +08:00
7015725
04818a909e fix: refine color picker action sizing 2026-05-22 03:32:05 +08:00
7015725
2cd1fdd660 fix: restyle color picker action buttons 2026-05-22 03:30:09 +08:00
7015725
3069e4ec4d fix: align color picker bottom actions 2026-05-22 03:26:51 +08:00
7015725
c15869e319 fix: restore stable color picker overlay 2026-05-22 03:24:12 +08:00
7015725
451cfb6502 fix: restore color picker and top bar clicks 2026-05-22 03:15:34 +08:00
7015725
7af30639d1 fix: repair color picker overlay and back preview 2026-05-22 03:06:32 +08:00
7015725
86dca84a27 fix: improve ToolApp landscape edge back handling 2026-05-22 01:34:08 +08:00
7015725
66fa86b37d feat: keep settings detail in tablet landscape panes 2026-05-22 01:20:32 +08:00
7015725
0605201b97 Add settings master detail landscape layout 2026-05-22 01:00:46 +08:00
7015725
918c36ebc2 Optimize ToolHub settings responsive layout 2026-05-22 00:43:33 +08:00
7015725
a6b0e1b41b Revert "refactor: split picker module and improve diagnostics"
This reverts commit b36af7f78a.
2026-05-22 00:11:08 +08:00
7015725
b36af7f78a refactor: split picker module and improve diagnostics 2026-05-21 23:46:33 +08:00
7015725
2643bf9cdf docs: update ToolHub README 2026-05-19 05:24:23 +08:00
7015725
eeac0baa88 Add update source switch 2026-05-19 05:13:06 +08:00
7015725
a6b5736f9a Fix float ball reflow in landscape 2026-05-19 05:06:27 +08:00
7015725
d557bdefa2 fix: lighten animal island main panel 2026-05-19 03:37:25 +08:00
7015725
4ce1924299 style: align main panel with animal island theme 2026-05-19 03:24:24 +08:00
7015725
63fdfb813e Fix long-press move jitter closing settings 2026-05-19 03:02:52 +08:00
7015725
6d8f50267c fix toolapp inner back edge width setting 2026-05-19 01:02:41 +08:00
7015725
eda34d24b1 fix: refresh stale ToolHub settings schema 2026-05-19 00:45:59 +08:00
7015725
3479cb25f2 feat: expand ToolApp back gesture ranges 2026-05-19 00:36:53 +08:00
7015725
1a7cefc630 feat: add ToolHub badge and back gesture settings 2026-05-19 00:26:11 +08:00
7015725
4c054dd4a3 feat: add floating ball background color setting 2026-05-19 00:10:25 +08:00
7015725
49ecd1e74e fix: align ToolApp back preview UI 2026-05-18 23:54:29 +08:00
7015725
e60d4e1b42 Increase ToolApp edge back zone to 48dp 2026-05-18 23:26:06 +08:00
7015725
ae8abc2908 fix: refine island settings bottom layout 2026-05-18 21:38:21 +08:00
7015725
df6f548c76 feat: add icon picker favorites 2026-05-18 14:50:37 +08:00
7015725
3492feb944 feat: optimize island icon picker layout 2026-05-18 14:42:18 +08:00
7015725
25ea8b7837 feat: unify picker island style 2026-05-18 14:28:59 +08:00
7015725
2e3ff489a0 feat: refine island settings UI 2026-05-18 14:20:29 +08:00
7015725
c7705348b4 feat: show settings save status 2026-05-18 14:03:36 +08:00
7015725
eff53e4156 feat: material you settings monet theme 2026-05-18 13:54:24 +08:00
7015725
342171e2ac fix: adapt settings pages to Monet theme 2026-05-18 03:49:54 +08:00
7015725
61a6bf1e72 fix: 按钮管理/按钮编辑等所有设置页 UI 元素跟随 SETTINGS_THEME
之前只覆盖了设置首页和分组页的配色切换,但按钮管理页内的
统计卡片、操作 chip、分区卡片、折叠分组、编辑面板等 UI
组件仍然直接硬编码 getAnimalIslandTheme()。

补全遗漏的 6 个函数:
- createButtonManagerSummaryCard
- createButtonManagerActionChip
- createButtonManagerPolishedCard
- createButtonEditorSectionCard
- createButtonEditorCollapsibleSection
- buildButtonEditorPanelView
2026-05-15 02:31:39 +08:00
7015725
efdcd3f187 fix: 设置页所有 UI 元素统一跟随 SETTINGS_THEME 切换
之前只有 buildSettingsGroupPanelView(分组页)检查了
SETTINGS_THEME,但首页面板 buildSettingsHomePanelView
和入口卡片 createSettingsHomeEntry 直接硬编码
getAnimalIslandTheme(),导致切换到 Monet 后首页仍是
动物岛风。

修复: 提取公共方法 applySettingsTheme(T, isDark, C, cfgTpl),
在三个构建位置统一调用:
- createSettingsHomeEntry (入口卡片:徽章/标题/按钮颜色)
- buildSettingsHomePanelView (设置首页:背景/统计卡/按钮)
- buildSettingsGroupPanelView (分组页:背景/卡片/预览开关)
2026-05-15 02:29:03 +08:00
7015725
b79961b08e fix: 设置页主题切换不再依赖预览模式开关
之前 SETTINGS_THEME 切换只在 previewMode=true 时重建
设置页 UI,用户必须手动打开边调边看才能看到变化。

改为: SETTINGS_THEME 改变时无条件重建设置页,立即生效。
2026-05-15 02:26:54 +08:00
7015725
32a30babcc refactor: 精简主题模板为设置页主题二选一
去掉之前多余的6套模板配色(ocean/sunset/forest/mono等),
只保留一个清晰的二选一功能:

- 动物岛风 (默认) — 当前 Animal Island 固定配色,不变
- 系统莫奈色 — 设置页 UI 使用系统 Monet 色系,与主面板一致

改动:
- THEME_TEMPLATE → SETTINGS_THEME (animal/monet)
- 删除 th_04_theme.js 的 getThemeTemplate/applyThemeTemplate
- buildSettingsGroupPanelView 直接根据 SETTINGS_THEME 用
  ui.colors 的 Monet 色构造 T 结构
- setPendingValue 中 SETTINGS_THEME 切换时重建设置页
2026-05-15 02:24:28 +08:00
7015725
cb01591369 fix: 主题模板真正应用到设置页 UI
之前主题模板只影响了 getPanelBgColorInt/getPanelTextColorInt,
但设置页 UI(buildSettingsGroupPanelView)的颜色全部来自
getAnimalIslandTheme() 硬编码,完全不经过模板逻辑。

修复:
1. buildSettingsGroupPanelView 开头判断是否选了非 system 模板,
   若是则根据模板的 dayBg/dayText/nightBg/nightText 自动生成
   一套兼容 T 结构的配色,覆盖 Animal Island 的 T 对象
2. 模板值从 pendingUserCfg 读取(预览态)
3. setPendingValue 中当 key=THEME_TEMPLATE 时,通过
   replaceToolAppPage('settings_group') 重建整个设置页 UI,
   使模板切换立即生效
2026-05-15 02:11:38 +08:00
7015725
1c802f6948 fix: 主题模板真正应用到主面板渲染
根本问题: buildPanelView('main') 构建主面板时用的是
ui.colors.bgLight/bgDark(Monet 配色),完全不经过
getPanelBgColorInt/getPanelTextColorInt 的模板填充逻辑。
updatePanelBackground 虽然封装了统一取色,但实际没有在
主面板构建流程中被调用。

修复:
1. buildPanelView('main') 末尾调用 updatePanelBackground(panel)
   应用统一主题色(Monet/模板/自定义)
2. 保留临时背景色避免构建过程中的裸窗口闪烁
2026-05-15 02:08:58 +08:00
7015725
49d73786bf fix: 修复主题模板在设置页预览时不生效的问题
根因: applyThemeTemplate 读/写 this.config,但设置页预览
走的是 pendingUserCfg(临时编辑缓存),模板值在选择后只写入了
pendingUserCfg.THEME_TEMPLATE,applyThemeTemplate 读 this.config
则一直为 system/d 返回,导致模板不生效。

修复:
- applyThemeTemplate 接受 preview 参数,预览态从 pendingUserCfg
  读写,非预览态从 this.config 读写
- getPanelBgColorInt/getPanelTextColorInt 预览态优先从
  pendingUserCfg 读取那4个颜色配置
2026-05-15 02:00:25 +08:00
7015725
d2abbcca9f fix: schema 完整性检测添加 THEME_TEMPLATE 字段
旧设备上持久化的 schema.json 不含 THEME_TEMPLATE 项,
导致设置页的换装分组不显示主题模板选择器。
在 needReset 检测中补充该字段,触发旧 schema 自动重置。
2026-05-15 01:56:58 +08:00
7015725
d8a25aced9 feat: 添加主题模板一键切换功能
- 新增 THEME_TEMPLATE 配置项(system/animal/ocean/sunset/forest/mono)
- 新增 getThemeTemplate() 模板颜色映射
- 新增 applyThemeTemplate() 自动填充面板背景/文字色
- getPanelBgColorInt/getPanelTextColorInt 支持模板覆盖
- 设置页换装分组新增单选选择器
2026-05-15 01:54:40 +08:00
7015725
bba3d800af style: refine island settings home 2026-05-14 23:30:38 +08:00
7015725
7079b633bb style: soften balloon settings page 2026-05-14 23:24:48 +08:00
7015725
8907a2d5d8 style: soften animal island ToolApp copy 2026-05-14 23:17:09 +08:00
7015725
edb26dc593 feat: apply animal island style to ToolApp 2026-05-14 22:28:51 +08:00
7015725
b9409c9056 refactor: prioritize OnBackAnimationCallback 2026-05-14 18:44:57 +08:00
7015725
c92606246e fix: register ToolApp back animation after attach 2026-05-14 18:33:03 +08:00
7015725
47ca2f782f fix: stop fallback strips blocking system back 2026-05-14 18:28:56 +08:00
7015725
2b55e2bdd4 style: refresh ToolApp visual back experience 2026-05-14 18:23:31 +08:00
7015725
f1e2ab7c37 fix: register ToolApp animated back callback 2026-05-14 03:22:14 +08:00
7015725
a8cd7c193d fix: capture screen edge back for ToolApp 2026-05-14 03:10:25 +08:00
7015725
f8463490e1 fix: show ToolApp preview during system back gesture 2026-05-14 03:04:35 +08:00
7015725
65367f0d06 feat: add interactive ToolApp back preview 2026-05-14 03:00:15 +08:00
7015725
1d31638073 fix: add ToolApp edge swipe back fallback 2026-05-14 02:51:46 +08:00
7015725
96bbb77127 docs: update README for ToolApp settings 2026-05-13 18:45:33 +08:00
7015725
e394a0f078 Remove duplicate inline color palette 2026-05-13 18:28:06 +08:00
7015725
3d0e57e813 Match ShortX icon picker with settings UI 2026-05-13 18:21:55 +08:00
7015725
b9d3dc89da style: simplify button editor section descriptions 2026-05-13 18:10:07 +08:00
7015725
7fd1430207 fix: add bottom gap to button manager footer 2026-05-13 08:32:06 +08:00
7015725
a5ecbec23b fix: keep button manager footer visible 2026-05-13 08:27:51 +08:00
7015725
e9b6052c6a style: apply minimal button management layout 2026-05-13 08:21:12 +08:00
7015725
648b45585a style: simplify button manager layout 2026-05-13 08:15:45 +08:00
7015725
3b6830e0ec style: polish button manager and editor layout 2026-05-13 07:38:48 +08:00
7015725
09f2aa4d07 feat: adapt ToolApp size to screen 2026-05-13 07:31:28 +08:00
7015725
080cb9abf7 refactor: keep ToolApp as single root view 2026-05-13 07:27:39 +08:00
7015725
2a53c872eb fix: show predictive back affordance for settings overlay 2026-05-13 07:20:26 +08:00
7015725
efb14bcde0 feat: support predictive back gesture 2026-05-12 20:43:42 +08:00
7015725
c4b886f83d style: refine button icon appearance editor 2026-05-12 20:32:08 +08:00
7015725
53967f77d2 feat: add collapsible button editor sections 2026-05-12 20:17:11 +08:00
7015725
46393fe9ba Revert "feat: split button editor into app subpages"
This reverts commit 5a4e3f6423.
2026-05-12 20:10:23 +08:00
7015725
5a4e3f6423 feat: split button editor into app subpages 2026-05-12 20:01:45 +08:00
7015725
c1643924b7 feat: app-style button manager page 2026-05-12 19:11:29 +08:00
7015725
c41e1479b9 feat: app-style ToolHub settings home 2026-05-12 17:31:02 +08:00
7015725
ea72f6b2eb fix: keep ToolHub app stack state on editor return 2026-05-12 11:06:54 +08:00
7015725
a0b6b92f57 feat: introduce ToolHub settings page stack 2026-05-12 10:47:13 +08:00
23 changed files with 5396 additions and 1248 deletions

View File

@@ -9,6 +9,12 @@ ShortX ToolHub 是一个面向 **ShortX / Rhino ES5 JS** 的模块化悬浮工
https://git.xin-blog.com/linshenjianlu/ShortX_ToolHub
```
GitHub 镜像地址:
```text
https://github.com/7015725/Toolhub-FloatBall
```
---
## 核心特性
@@ -18,8 +24,12 @@ https://git.xin-blog.com/linshenjianlu/ShortX_ToolHub
- **SHA256 文件校验**:每个子模块按清单中的 `sha256``size` 校验,通过后才覆盖本地文件。
- **防回滚**:入口内置 `MIN_TRUSTED_MANIFEST_VERSION`,并记录本地已信任清单版本,拒绝旧版本清单。
- **本地可信回退**:网络或远端清单异常时,不盲目覆盖;已验证过的本地模块可继续使用。
- **更新源切换**:入口支持在 Gitea 主源与 GitHub 镜像之间切换,下载流程仍统一走签名清单与 SHA256 校验。
- **App 化设置页**:设置主页、按钮管理、按钮编辑等页面使用统一 Shell 与页面栈,支持顶部返回/关闭。
- **系统返回适配**:支持导航键返回;悬浮窗内置左右边缘交互式滑动返回,拖动时实时露出上级页面,松手后完成返回。
- **ShortX 图标选择器**:支持图标点选、搜索、分页、自适应列数,不再依赖手填图标名。
- **颜色选择器**支持常用色、最近色、RGB、透明度和实时预览。
- **颜色选择器**使用折叠式完整调色板,支持常用色、最近色、RGB、透明度和实时预览;避免重复内联色板
- **自适应布局**ToolApp 根据屏幕尺寸调整宽高,按钮管理页底部操作区保持可见;悬浮球在横竖屏切换后会重新计算屏幕尺寸并保持吸边位置。
- **日志记录**:启动、更新、验签、加载异常写入 `ToolHub/logs/init.log`
---
@@ -151,6 +161,13 @@ ShortX_ToolHub/
9. 校验通过才覆盖本地模块。
10. 所有模块加载正常后,保存本地可信清单版本。
入口顶部可通过 `UPDATE_SOURCE` 选择更新源:
- `0`Gitea 主源
- `1`GitHub 镜像
无论选择哪个源,`manifest.json` / `manifest.sig` 验签、防回滚和子模块 SHA256 校验流程都保持一致。
当前 keyId
```text
@@ -176,13 +193,23 @@ toolhub-targets-2026-rsa3072
| `th_11_action.js` | 按钮动作分发与执行 |
| `th_12_rebuild.js` | 悬浮球重建逻辑 |
| `th_13_panel_ui.js` | 设置面板通用 UI 组件 |
| `th_14_panels.js` | 设置面、按钮编辑器、图标选择器、颜色选择器 |
| `th_15_extra.js` | 主面板与附加展示层 |
| `th_16_entry.js` | 生命周期、广播注册、启动与销毁 |
| `th_14_panels.js` | 设置子页面、按钮编辑器、图标选择器、颜色选择器 |
| `th_15_extra.js` | 主面板、ToolApp 页面栈、设置主页、按钮管理页 |
| `th_16_entry.js` | 生命周期、广播注册、系统返回处理、启动与销毁 |
---
## 图标颜色交互
## 图标颜色与设置交互
### ToolApp 设置页
- 设置、按钮管理、按钮编辑统一运行在 ToolApp Shell 内。
- 顶部栏提供返回与关闭,子页面优先通过页面栈返回。
- 系统返回键会优先回到上一页;没有上一页时关闭 ToolApp。
- 悬浮窗无法稳定接入系统级预测性返回动画,因此 ToolHub 内置左右边缘交互式滑动返回作为替代。
- 拖动过程中当前页面跟随手指横移,上级页面在底层实时露出;松手达到阈值后完成返回,未达到阈值则回弹。
- 面板尺寸按屏幕自适应,减少小屏溢出与大屏空白。
- 按钮管理页底部操作区保持可见,避免被列表内容挤出屏幕。
### ShortX 图标选择器
@@ -258,6 +285,38 @@ ToolHub.js.sha256
## 更新记录
### 2026-05-19
**功能改进**
- README 补充 GitHub 镜像地址与更新源切换说明。
- ToolHub 入口支持通过 `UPDATE_SOURCE` 在 Gitea 主源与 GitHub 镜像之间切换。
- 主面板动物岛主题继续优化为更轻量的融合色视觉,避免高频主面板出现过重图标气泡。
**稳定性修复**
- 改进悬浮球横竖屏切换后的屏幕尺寸刷新与吸边重排,减少横屏时位置跑到屏幕中间的问题。
- 修复长按打开设置后手指轻微移动导致设置页被普通拖拽逻辑关闭的问题。
- 修复设置 schema 持久化导致旧用户看不到最新滑块范围或文案的问题。
- 优化 ToolApp 页面内返回边界宽度设置,变更后可即时作用于当前页面。
### 2026-05-13
**功能改进**
- 新增 ToolApp 式设置主页与页面栈,设置、按钮管理、按钮编辑改为更接近 App 的层级导航。
- 支持系统导航返回;因 ToolHub 是悬浮窗 Overlay系统级预测性返回动画不稳定改为内置左右边缘交互式滑动返回。
- 边缘返回拖动时会预渲染上级页面,当前页面随手指横移并露出上级页面,松手后完成返回或回弹。
- ToolApp 尺寸按屏幕自适应,按钮管理页底部操作区保持可见。
- 按钮管理与按钮编辑布局继续轻量化,减少说明文字和视觉负担。
- ShortX 图标选择器风格与设置 UI 对齐。
- 移除重复内联颜色调色板,统一使用折叠式颜色选择器。
**稳定性修复**
- 改进 ToolHub 启动与清理流程。
- 修复设置页返回按钮状态、编辑器返回后页面栈状态、系统手势返回提示等细节问题。
### 2026-05-09
**文档更新**

View File

@@ -2,7 +2,16 @@
// 安全更新机制:入口内置 RSA 公钥,先验证 manifest.json/manifest.sig再按 SHA256 下载子模块。
// Gitea 只负责分发;未通过签名/哈希/防回滚校验时,不覆盖本地模块。
var GIT_ROOT = "https://git.xin-blog.com/linshenjianlu/ShortX_ToolHub/raw/branch/main/";
var UPDATE_SOURCE = 0; // 0: Gitea, 1: GitHub
var UPDATE_ROOTS = [
"https://git.xin-blog.com/linshenjianlu/ShortX_ToolHub/raw/branch/main/",
"https://raw.githubusercontent.com/7015725/Toolhub-FloatBall/main/"
];
if (UPDATE_SOURCE !== 1) UPDATE_SOURCE = 0;
var GIT_ROOT = UPDATE_ROOTS[UPDATE_SOURCE];
var GIT_BASE = GIT_ROOT + "code/";
var TRUSTED_PUBLIC_KEYS = {
"toolhub-targets-2026-rsa3072": "MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEApiyhtMDJce7dVCxH1/oDu8kbiECYoT5XXmXvR/XNYuJ/5FuL83SbpCQ3QmUnqkbfNyOFqnxac/qlbXJtx6eeSotLP1HmrKI0LGymgxG6b1FfGHBfIKNZfBLIvzVDQob+HJfshlsS1JRlW5Jhm25TMh8dJCQQQZWW/ZItbtOvPYbLwG8cnqEdX8gqyB304+r2l35GPTfxZIGEK/9PcE3AMuqwTolMJsBHtG61hmMdz3dzTTEZQoOcciGWuwr2ZW8XkF6f5SgWkC29ZxZqAxceK4FJ8BsYirpFQxVKyZ6eiYlpNiYz+pHLP2U7JTO6ImmT1rlYSS6xw2tlWf0xq72nuOPC+VzEivuEhnC4y9WBSvauRa/ViIDgQ3yXl2MajuAvGSVWRfZ5Gz5Up8PQD7vxmHT2r0fA4xq4GIvUvGCqOG/d1FRrlVyEuNhCZ7KgpEKPno7fLnC6/ftnYcN5ZNOSWwjWH/e4fBxM5s6RRIYzIY2N0f/fqsRH42lWAhX5stujAgMBAAE="
@@ -24,16 +33,31 @@ function getTrustedShaPath(relPath) { return getCodeDirPath() + ".trusted_sha_"
function getTrustedVersionPath() { return getCodeDirPath() + ".trusted_manifest_version"; }
function writeLog(msg) {
var writer = null;
try {
var f = new java.io.File(getLogPath());
var dir = f.getParentFile();
if (dir && !dir.exists()) dir.mkdirs();
try {
var maxBytes = 512 * 1024;
if (f.exists() && f.length() > maxBytes) {
var bak = new java.io.File(String(f.getAbsolutePath()) + ".bak");
try { if (bak.exists()) bak.delete(); } catch (eBak0) {}
var moved = false;
try { moved = f.renameTo(bak); } catch (eMv) { moved = false; }
if (!moved) {
try { f.delete(); } catch (eDel) {}
}
}
} catch (eTrim) {}
var sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
var ts = sdf.format(new java.util.Date());
var writer = new java.io.FileWriter(f, true);
writer = new java.io.FileWriter(f, true);
writer.write("[" + ts + "] " + String(msg) + "\n");
writer.close();
} catch (e) {}
} catch (e) {
} finally {
try { if (writer) writer.close(); } catch (eClose) {}
}
}
function runShell(cmdArr) {
@@ -332,7 +356,7 @@ function loadScript(relPath) {
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"];
"th_14_panels.js", "th_14_color_picker.js", "th_14_icon_picker.js", "th_14_schema_editor.js", "th_15_extra.js", "th_16_entry.js"];
var __moduleUpdates = [];
var loadErrors = [];
var criticalModules = { "th_01_base.js": true, "th_16_entry.js": true };

View File

@@ -1 +1 @@
55dbcacaaa31b031e9a0fcef1253c2e0403fca423ad969e0e1387815e69de3e7 ToolHub.js
01d93690bdfc402ad59d6f51db45e3e057a3ebcbbf146a5667ecc5486e2d3ed6 ToolHub.js

View File

@@ -83,6 +83,7 @@ var ConfigValidator = {
PANEL_PADDING_DP: { type: "int", min: 8, max: 32, default: 12 },
// 主题配置
SETTINGS_THEME: { type: "enum", values: ["animal", "monet"], default: "animal" },
THEME_MODE: { type: "enum", values: [0, 1, 2], default: 1 },
THEME_ACCENT_LIGHT: { type: "string", default: "#FF3A86FF" },
THEME_ACCENT_DARK: { type: "string", default: "#FF90CAF9" },
@@ -90,14 +91,20 @@ var ConfigValidator = {
// 图标配置
BALL_ICON_TYPE: { type: "enum", values: ["app", "file", "android", "shortx"], default: "app" },
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: 12, max: 80, default: 22 },
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_TRIGGERED_MOVE_SLOP_DP: { type: "int", min: 8, max: 80, default: 28 },
LONG_PRESS_VIBRATE_MS: { type: "int", min: 10, max: 100, default: 40 },
CLICK_SLOP_DP: { type: "int", min: 2, max: 20, default: 6 },
TOOLAPP_BACK_GESTURE_MODE: { type: "enum", values: ["edge", "surface", "off"], default: "surface" },
TOOLAPP_BACK_EDGE_WIDTH_DP: { type: "int", min: 1, max: 120, default: 72 },
TOOLAPP_BACK_COMMIT_DISTANCE_DP: { type: "int", min: 1, max: 480, default: 36 },
TOOLAPP_BACK_SURFACE_SLOP_DP: { type: "int", min: 8, max: 96, default: 24 },
TOOLAPP_BACK_PROGRESS_DISTANCE_DP: { type: "int", min: 1, max: 720, default: 96 },
// 功能开关
ENABLE_SNAP_TO_EDGE: { type: "bool", default: true },
@@ -129,6 +136,7 @@ var ConfigValidator = {
BALL_ICON_PKG: { type: "string", default: "" },
BALL_ICON_RES_NAME: { type: "string", default: "" },
BALL_ICON_TINT_HEX: { type: "string", default: "" },
BALL_BG_COLOR_HEX: { type: "string", default: "" },
// 回弹动画配置
BOUNCE_DECAY: { type: "float", min: 0.3, max: 0.95, default: 0.72 },
@@ -726,7 +734,13 @@ var ConfigManager = {
LONG_PRESS_VIBRATE_MS: 18,
ENABLE_LONG_PRESS: true,
LONG_PRESS_MS: 520,
LONG_PRESS_TRIGGERED_MOVE_SLOP_DP: 28,
CLICK_SLOP_DP: 6,
TOOLAPP_BACK_GESTURE_MODE: "surface",
TOOLAPP_BACK_EDGE_WIDTH_DP: 72,
TOOLAPP_BACK_COMMIT_DISTANCE_DP: 36,
TOOLAPP_BACK_SURFACE_SLOP_DP: 24,
TOOLAPP_BACK_PROGRESS_DISTANCE_DP: 96,
ENABLE_BOUNCE: true,
BOUNCE_TIMES: 2,
BOUNCE_MAX_SCALE: 0.88,
@@ -740,6 +754,7 @@ var ConfigManager = {
BALL_ICON_RES_NAME: "",
BALL_ICON_SIZE_DP: 22,
BALL_ICON_TINT_HEX: "",
BALL_BG_COLOR_HEX: "",
BALL_IDLE_ALPHA: 0.6,
PANEL_POS_GRAVITY: "bottom",
PANEL_CUSTOM_OFFSET_Y: 0,
@@ -754,6 +769,7 @@ var ConfigManager = {
PANEL_LABEL_TOP_MARGIN_DP: 4,
PANEL_BG_FALLBACK_HEX: "#EE1E1E1E",
PANEL_BG_ALPHA: 0.85,
SETTINGS_THEME: "animal",
THEME_MODE: 1,
THEME_DAY_BG_HEX: null,
THEME_DAY_TEXT_HEX: null,
@@ -770,6 +786,10 @@ var ConfigManager = {
],
defaultSchema: [
{ type: "section", name: "外观" },
{ key: "SETTINGS_THEME", name: "设置页主题", type: "single_choice", options: [
{ label: "动物岛风", value: "animal" },
{ label: "系统莫奈色", value: "monet" }
]},
{ key: "THEME_MODE", name: "主题(0跟随/1白/2黑)", type: "int", min: 0, max: 2, step: 1 },
{ key: "THEME_DAY_BG_HEX", name: "日间背景色(#RRGGBB)", type: "text" },
{ key: "THEME_DAY_TEXT_HEX", name: "日间文字色(#RRGGBB)", type: "text" },
@@ -778,17 +798,19 @@ var ConfigManager = {
{ key: "PANEL_BG_ALPHA", name: "面板背景透明度(0.1~1.0)", type: "float", min: 0.1, max: 1.0, step: 0.05 },
{ type: "section", name: "悬浮球" },
{ 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_ICON_TYPE", name: "图标类型", type: "single_choice", options: [
{ label: "应用图标 (app)", value: "app" },
{ label: "文件图标 (file)", value: "file" },
{ label: "ShortX内置 (shortx)", value: "shortx" }
{ key: "BALL_SIZE_DP", name: "球大小", type: "int", min: 28, max: 120, step: 1 },
{ key: "BALL_PANEL_GAP_DP", name: "离小屋的距离", type: "int", min: 0, max: 60, step: 1 },
{ key: "BALL_ICON_TYPE", name: "气球徽章", type: "single_choice", options: [
{ label: "应用头像", value: "app" },
{ label: "文件选择", value: "file" },
{ label: "岛上图标库", value: "shortx" }
]},
{ key: "BALL_ICON_FILE_PATH", name: "图标路径(file模式)", type: "text" },
{ key: "BALL_ICON_RES_NAME", name: "ShortX图标", type: "ball_shortx_icon" },
{ 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_ICON_FILE_PATH", name: "已选择的图标文件", type: "text" },
{ key: "BALL_ICON_RES_NAME", name: "岛上图标", type: "ball_shortx_icon" },
{ key: "BALL_ICON_TINT_HEX", name: "徽章颜色", type: "ball_color" },
{ key: "BALL_ICON_SIZE_DP", name: "徽章大小", type: "int", min: 12, max: 80, step: 1 },
{ key: "BALL_BG_COLOR_HEX", name: "球体背景", type: "ball_color" },
{ key: "BALL_IDLE_ALPHA", name: "安静时透明度", type: "float", min: 0.1, max: 1.0, step: 0.05 },
{ type: "section", name: "面板布局" },
{ key: "PANEL_ROWS", name: "面板可视行数(超出滚动)", type: "int", min: 1, max: 12, step: 1 },
@@ -826,10 +848,20 @@ var ConfigManager = {
{ key: "BOUNCE_STEP_MS", name: "回弹步进时长(ms)", type: "int", min: 20, max: 500, step: 10 },
{ key: "BOUNCE_DECAY", name: "回弹衰减(0~1)", type: "float", min: 0.30, max: 0.95, step: 0.01 },
{ type: "section", name: "触摸与手势" },
{ type: "section", name: "动作与手势" },
{ key: "CLICK_SLOP_DP", name: "点击位移阈值(dp)", type: "int", min: 1, max: 40, step: 1 },
{ key: "TOOLAPP_BACK_GESTURE_MODE", name: "设置页滑动返回模式", type: "single_choice", options: [
{ label: "全表面横滑", value: "surface" },
{ label: "仅面板内部左右边缘", value: "edge" },
{ label: "关闭", value: "off" }
]},
{ key: "TOOLAPP_BACK_EDGE_WIDTH_DP", name: "内部边缘起手宽度", type: "int", min: 1, max: 120, step: 1 },
{ key: "TOOLAPP_BACK_COMMIT_DISTANCE_DP", name: "设置页返回触发距离", type: "int", min: 1, max: 480, step: 1 },
{ key: "TOOLAPP_BACK_SURFACE_SLOP_DP", name: "表面横滑起手阈值", type: "int", min: 8, max: 96, step: 1 },
{ key: "TOOLAPP_BACK_PROGRESS_DISTANCE_DP", name: "设置页返回动画距离", type: "int", min: 1, max: 720, step: 1 },
{ key: "ENABLE_LONG_PRESS", name: "启用长按", type: "bool" },
{ key: "LONG_PRESS_MS", name: "长按判定(ms)", type: "int", min: 200, max: 2000, step: 10 },
{ key: "LONG_PRESS_TRIGGERED_MOVE_SLOP_DP", name: "长按后抖动容忍距离", type: "int", min: 8, max: 80, step: 1 },
{ key: "LONG_PRESS_HAPTIC_ENABLE", name: "长按震动反馈", type: "bool" },
{ key: "LONG_PRESS_VIBRATE_MS", name: "震动时长(ms)", type: "int", min: 1, max: 120, step: 1 },
@@ -864,9 +896,49 @@ var ConfigManager = {
var needReset = false;
if (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 || sStr.indexOf("ball_shortx_icon") < 0 || sStr.indexOf("ball_color") < 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 || sStr.indexOf("SETTINGS_THEME") < 0 || sStr.indexOf("BALL_BG_COLOR_HEX") < 0 || sStr.indexOf("BALL_ICON_SIZE_DP") < 0 || sStr.indexOf("TOOLAPP_BACK_GESTURE_MODE") < 0 || sStr.indexOf("TOOLAPP_BACK_EDGE_WIDTH_DP") < 0 || sStr.indexOf("TOOLAPP_BACK_COMMIT_DISTANCE_DP") < 0 || sStr.indexOf("TOOLAPP_BACK_SURFACE_SLOP_DP") < 0 || sStr.indexOf("TOOLAPP_BACK_PROGRESS_DISTANCE_DP") < 0 || sStr.indexOf("LONG_PRESS_TRIGGERED_MOVE_SLOP_DP") < 0) {
needReset = true;
}
if (!needReset && (sStr.indexOf("ENABLE_TOOLAPP_INNER_BACK_STRIPS") >= 0 || sStr.indexOf("ENABLE_TOOLAPP_SCREEN_BACK_STRIPS") >= 0)) {
needReset = true;
}
// 旧 schema.json 可能已经含有 key但 UI 文案/范围仍是旧值;关键字段不一致时也强制刷新
var findSchemaItemByKey = function(arr, key) {
if (!arr) return null;
for (var i = 0; i < arr.length; i++) {
var it = arr[i];
if (it && it.key === key) return it;
var child = null;
if (it && it.children) child = findSchemaItemByKey(it.children, key);
if (!child && it && it.items) child = findSchemaItemByKey(it.items, key);
if (child) return child;
}
return null;
};
var schemaItemDiffers = function(key, fields) {
var cur = findSchemaItemByKey(s, key);
var def = findSchemaItemByKey(ConfigManager.defaultSchema, key);
if (!cur || !def) return true;
for (var i = 0; i < fields.length; i++) {
var f = fields[i];
if (typeof def[f] !== "undefined" && String(cur[f]) !== String(def[f])) return true;
}
return false;
};
if (!needReset) {
if (schemaItemDiffers("BALL_ICON_TINT_HEX", ["name", "type"]) ||
schemaItemDiffers("BALL_ICON_SIZE_DP", ["name", "type", "min", "max", "step"]) ||
schemaItemDiffers("BALL_BG_COLOR_HEX", ["name", "type"]) ||
schemaItemDiffers("TOOLAPP_BACK_GESTURE_MODE", ["name", "type"]) ||
schemaItemDiffers("TOOLAPP_BACK_EDGE_WIDTH_DP", ["name", "type", "min", "max", "step"]) ||
schemaItemDiffers("TOOLAPP_BACK_COMMIT_DISTANCE_DP", ["name", "type", "min", "max", "step"]) ||
schemaItemDiffers("TOOLAPP_BACK_SURFACE_SLOP_DP", ["name", "type", "min", "max", "step"]) ||
schemaItemDiffers("TOOLAPP_BACK_PROGRESS_DISTANCE_DP", ["name", "type", "min", "max", "step"]) ||
schemaItemDiffers("LONG_PRESS_TRIGGERED_MOVE_SLOP_DP", ["name", "type", "min", "max", "step"])) {
needReset = true;
}
}
} else {
// # 仅当文件不存在时才标记为需要重置(新建),避免因读取失败导致覆盖
try {
@@ -1116,8 +1188,15 @@ function applyRule(rule, kv) {
}
// =======================【日志:文件写入器(尽力落盘 + 自动清理旧日志)】=======================
// =======================【日志:文件写入器(全局统一目录 + 分级)】=======================
// 优化后的日志系统(带缓冲,减少文件 IO
function sanitizeLogMessage(msg) {
try {
var s = String(msg == null ? "" : msg);
s = s.replace(/(authorization\s*[:=]\s*bearer\s+)[^\s,;]+/ig, "$1[REDACTED]");
s = s.replace(/((access_)?token|api[_-]?key|password|passwd|secret)(\s*[=:]\s*)[^\s,;]+/ig, "$1$3[REDACTED]");
return s;
} catch (e) { return String(msg == null ? "" : msg); }
}
function ToolHubLogger(procInfo) {
this.proc = procInfo || {};
this.dir = PATH_LOG_DIR;
@@ -1127,17 +1206,10 @@ function ToolHubLogger(procInfo) {
this.debug = false;
this.initOk = false;
this.lastInitErr = "";
// 新增:日志缓冲
this._buffer = [];
this._bufferSize = 20; // 每 20 条写一次磁盘
this._flushTimer = null;
this._initOnce();
}
ToolHubLogger.prototype._now = function() { return new Date().getTime(); };
ToolHubLogger.prototype._initOnce = function() {
try {
if (FileIO.ensureDir(this.dir)) {
@@ -1152,110 +1224,22 @@ ToolHubLogger.prototype._initOnce = function() {
this.lastInitErr = String(e);
}
};
ToolHubLogger.prototype.updateConfig = function(cfg) {
if (!cfg) return;
if (typeof cfg.LOG_KEEP_DAYS === "number") this.keepDays = cfg.LOG_KEEP_DAYS;
if (typeof cfg.LOG_ENABLE !== "undefined") this.enable = !!cfg.LOG_ENABLE;
if (typeof cfg.LOG_DEBUG !== "undefined") this.debug = !!cfg.LOG_DEBUG;
};
ToolHubLogger.prototype._line = function(level, msg) {
var ts = this._now();
var d = new Date(ts);
function pad2(x) { return (x < 10 ? "0" : "") + x; }
var t = d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate()) +
" " + pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds());
return t + " [" + level + "] " + msg + "\n";
};
ToolHubLogger.prototype._scheduleFlush = function() {
if (this._flushTimer) try { this._flushTimer.cancel(); } catch(e) {}
var self = this;
this._flushTimer = new java.util.Timer();
this._flushTimer.schedule(new java.util.TimerTask({
run: function() { self._flushBuffer(); }
}), 3000); // 3秒后强制刷新
};
ToolHubLogger.prototype._flushBuffer = function() {
if (this._buffer.length === 0) return;
var content = this._buffer.join('');
this._buffer = [];
var path = this.dir + "/" + this.prefix + "_" + this._ymd() + ".log";
FileIO.appendText(path, content);
};
ToolHubLogger.prototype._ymd = function() {
var d = new Date();
return "" + d.getFullYear() +
((d.getMonth() < 9 ? "0" : "") + (d.getMonth() + 1)) +
((d.getDate() < 10 ? "0" : "") + d.getDate());
};
ToolHubLogger.prototype._write = function(level, msg) {
if (!this.enable) return false;
this._buffer.push(this._line(level, msg));
// 缓冲满或错误级别立即写入
if (this._buffer.length >= this._bufferSize || level === 'F' || level === 'E') {
this._flushBuffer();
} else {
this._scheduleFlush(); // 延迟写入
}
return true;
};
ToolHubLogger.prototype.d = function(msg) { if (this.debug) this._write("D", msg); };
ToolHubLogger.prototype.i = function(msg) { this._write("I", msg); };
ToolHubLogger.prototype.w = function(msg) { this._write("W", msg); };
ToolHubLogger.prototype.e = function(msg) { this._write("E", msg); };
ToolHubLogger.prototype.fatal = function(msg) { this._write("F", msg); this._flushBuffer(); };
ToolHubLogger.prototype.cleanupOldFiles = function() {
try {
if (!this.initOk) return false;
var dirF = new java.io.File(this.dir);
var files = dirF.listFiles();
if (!files) return false;
var now = this._now();
var cutoff = now - this.keepDays * 24 * 60 * 60 * 1000;
for (var i = 0; i < files.length; i++) {
var f = files[i];
if (f && f.isFile() && f.getName().indexOf(this.prefix) === 0 && f.lastModified() < cutoff) {
f["delete"]();
}
}
return true;
} catch (e) { return false; };
};
ToolHubLogger.prototype._filePathForToday = function() {
var name = this.prefix + "_" + this._ymd(this._now()) + ".log";
var name = this.prefix + "_" + this._ymd() + ".log";
return this.dir + "/" + name;
};
ToolHubLogger.prototype._initOnce = function() {
try {
// # 尝试创建目录
if (FileIO.ensureDir(this.dir)) {
this.initOk = true;
// # 清理旧日志
this.cleanupOldFiles();
} else {
this.initOk = false;
this.lastInitErr = "Mkdirs failed: " + this.dir;
}
} catch (e) {
this.initOk = false;
this.lastInitErr = String(e);
}
};
ToolHubLogger.prototype.updateConfig = function(cfg) {
if (!cfg) return;
if (typeof cfg.LOG_KEEP_DAYS === "number") this.keepDays = cfg.LOG_KEEP_DAYS;
if (typeof cfg.LOG_ENABLE !== "undefined") this.enable = !!cfg.LOG_ENABLE;
if (typeof cfg.LOG_DEBUG !== "undefined") this.debug = !!cfg.LOG_DEBUG;
};
ToolHubLogger.prototype._line = function(level, msg) {
var ts = this._now();
var d = new Date(ts);
@@ -1267,7 +1251,7 @@ ToolHubLogger.prototype._line = function(level, msg) {
proc = " uid=" + String(this.proc.uid) + " pid=" + String(this.proc.pid) + " tid=" + String(this.proc.tid) +
" th=" + String(this.proc.threadName) + " proc=" + String(this.proc.processName);
} catch (e0) {}
return t + " [" + String(level) + "] " + String(msg) + proc + "\n";
return t + " [" + String(level) + "] " + sanitizeLogMessage(msg) + proc + "\n";
};
ToolHubLogger.prototype._writeRaw = function(level, msg) {
if (!this.initOk) return false;

View File

@@ -42,6 +42,25 @@ function FloatBallAppWM(logger) {
viewerPanel: null,
viewerPanelLp: null,
viewerPanelType: null,
panelBackCallbackEntries: [],
predictiveBackIndicatorView: null,
predictiveBackIndicatorLp: null,
// 设置类 UI App 化单窗口页面栈settings -> 子页面 -> 编辑页)
toolAppActive: false,
toolAppNavStack: [],
toolAppRoute: null,
toolAppRoot: null,
toolAppBody: null,
toolAppContentHost: null,
toolAppBackPreviewView: null,
toolAppBackPreviewRoute: null,
toolAppBackPreviewReady: false,
toolAppTitleView: null,
toolAppBackButton: null,
settingsGroupKey: null,
settingsHomeSelectedCategoryId: null,
settingsHomeSelectedItemId: null,
mask: null,
maskLp: null,
@@ -71,6 +90,9 @@ function FloatBallAppWM(logger) {
pendingUserCfg: null,
pendingDirty: false,
// 按钮管理首页:搜索过滤状态
buttonManagerQuery: "",
closing: false
};

View File

@@ -2,13 +2,101 @@
// =======================【工具:屏幕/旋转】======================
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 };
var w = 0;
var h = 0;
try {
this.state.wm.getDefaultDisplay().getRealMetrics(m);
w = Number(m.widthPixels || 0);
h = Number(m.heightPixels || 0);
} catch (e0) {
try {
this.state.wm.getDefaultDisplay().getMetrics(m);
w = Number(m.widthPixels || 0);
h = Number(m.heightPixels || 0);
} catch (e1) {}
}
try {
var rot = this.getRotation ? this.getRotation() : -1;
var isLandscape = (rot === android.view.Surface.ROTATION_90 || rot === android.view.Surface.ROTATION_270);
var isPortrait = (rot === android.view.Surface.ROTATION_0 || rot === android.view.Surface.ROTATION_180);
if (isLandscape && w > 0 && h > 0 && w < h) {
var t = w; w = h; h = t;
} else if (isPortrait && w > 0 && h > 0 && w > h) {
var t2 = w; w = h; h = t2;
}
} catch (eRot) {}
if (w <= 0 || h <= 0) {
try {
var dm = context.getResources().getDisplayMetrics();
if (w <= 0) w = Number(dm.widthPixels || 0);
if (h <= 0) h = Number(dm.heightPixels || 0);
} catch (eRes) {}
}
return { w: Math.max(1, Math.floor(w)), h: Math.max(1, Math.floor(h)) };
};
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); };
// Animal Island LiteToolApp 专用视觉主题。只供设置/按钮管理/编辑页调用,避免覆盖悬浮球 Monet 取色。
FloatBallAppWM.prototype.getAnimalIslandTheme = function() {
var isDark = false;
try { isDark = this.isDarkTheme(); } catch(eDark) { isDark = false; }
var Color = android.graphics.Color;
if (isDark) {
return {
bg: Color.parseColor("#2F4034"),
bg2: Color.parseColor("#3C5142"),
leaf: Color.parseColor("#4E6F5B"),
card: Color.parseColor("#405243"),
card2: Color.parseColor("#4A5D4E"),
cream: Color.parseColor("#FFF1D2"),
text: Color.parseColor("#FFF1D2"),
sub: Color.parseColor("#E3CFA8"),
brown: Color.parseColor("#D0AE7A"),
primary: Color.parseColor("#8BD7A8"),
primaryDeep: Color.parseColor("#5FB980"),
primarySoft: Color.parseColor("#3F684F"),
danger: Color.parseColor("#F0A08F"),
dangerSoft: Color.parseColor("#68453C"),
stroke: Color.parseColor("#8B754E"),
onPrimary: Color.parseColor("#173524")
};
}
return {
bg: Color.parseColor("#A8DDB4"),
bg2: Color.parseColor("#DDF3D8"),
leaf: Color.parseColor("#7DC395"),
card: Color.parseColor("#FFF9E6"),
card2: Color.parseColor("#FFFFFF"),
cream: Color.parseColor("#FFF9E6"),
text: Color.parseColor("#5E472D"),
sub: Color.parseColor("#7C5734"),
brown: Color.parseColor("#8B643D"),
primary: Color.parseColor("#19C8B9"),
primaryDeep: Color.parseColor("#0E9E91"),
primarySoft: Color.parseColor("#DDF7F2"),
danger: Color.parseColor("#D86962"),
dangerSoft: Color.parseColor("#FFE7E2"),
stroke: Color.parseColor("#E0C79E"),
onPrimary: Color.parseColor("#FFFFFF")
};
};
FloatBallAppWM.prototype.createAnimalCardDrawable = function(fillColor, radiusDp) {
var T = this.getAnimalIslandTheme();
return this.ui.createStrokeDrawable(fillColor || T.card, this.withAlpha(T.stroke, this.isDarkTheme() ? 0.36 : 0.55), this.dp(1), this.dp(radiusDp || 18));
};
FloatBallAppWM.prototype.createAnimalButtonDrawable = function(normalColor, pressedColor, radiusDp) {
return this.ui.createRippleDrawable(normalColor, pressedColor, this.dp(radiusDp || 18));
};
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;
@@ -58,7 +146,12 @@ FloatBallAppWM.prototype.ui = {
// Monet 扩展字段(供面板直接使用)
_monetSurface: android.graphics.Color.parseColor("#F8F9FA"),
_monetOnSurface: android.graphics.Color.parseColor("#1F1F1F"),
_monetSurfaceVariant: android.graphics.Color.parseColor("#E1E3E1"),
_monetSurfaceContainerLow: android.graphics.Color.parseColor("#F1F3F4"),
_monetSurfaceContainer: android.graphics.Color.parseColor("#ECEEEF"),
_monetSurfaceContainerHigh: android.graphics.Color.parseColor("#E6E8EA"),
_monetOutline: android.graphics.Color.parseColor("#747775"),
_monetOutlineVariant: android.graphics.Color.parseColor("#C4C7C5"),
_monetOnPrimary: android.graphics.Color.parseColor("#FFFFFF"),
_monetPrimaryContainer: android.graphics.Color.parseColor("#D3E3FD"),
_monetOnPrimaryContainer: android.graphics.Color.parseColor("#041E49"),
@@ -117,7 +210,12 @@ FloatBallAppWM.prototype.ui = {
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); }); }
onClick: function(v) {
app.touchActivity();
var guardKey = "ui_btn";
try { guardKey = "ui_btn_" + String(java.lang.System.identityHashCode(v || btn)); } catch(eKey) {}
app.guardClick(guardKey, INTERACTION_CONSTANTS.CLICK_COOLDOWN_MS, function(){ if(onClick) onClick(v); });
}
}));
return btn;
},
@@ -135,7 +233,12 @@ FloatBallAppWM.prototype.ui = {
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); }); }
onClick: function(v) {
app.touchActivity();
var guardKey = "ui_btn";
try { guardKey = "ui_btn_" + String(java.lang.System.identityHashCode(v || btn)); } catch(eKey) {}
app.guardClick(guardKey, INTERACTION_CONSTANTS.CLICK_COOLDOWN_MS, function(){ if(onClick) onClick(v); });
}
}));
return btn;
},
@@ -188,21 +291,30 @@ FloatBallAppWM.prototype.ui = {
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 {
if (!cb || !cb.hasPrimaryClip()) {
app.toast("剪贴板为空");
return;
}
var clip = cb.getPrimaryClip();
if (!clip || clip.getItemCount() <= 0) {
app.toast("剪贴板为空");
return;
}
var item = clip.getItemAt(0);
if (!item) {
app.toast("剪贴板为空");
return;
}
var txt = item.getText();
if (txt === null || txt === undefined || String(txt).length === 0) {
app.toast("剪贴板不是文本内容");
return;
}
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());
} catch (eP) {
app.toast("粘贴失败: " + eP);
}
@@ -345,6 +457,9 @@ var MonetColorProvider = {
surface: android.graphics.Color.parseColor("#F8F9FA"),
onSurface: android.graphics.Color.parseColor("#1F1F1F"),
surfaceVariant: android.graphics.Color.parseColor("#E1E3E1"),
surfaceContainerLow: android.graphics.Color.parseColor("#F1F3F4"),
surfaceContainer: android.graphics.Color.parseColor("#ECEEEF"),
surfaceContainerHigh: android.graphics.Color.parseColor("#E6E8EA"),
onSurfaceVariant: android.graphics.Color.parseColor("#5F6368"),
outline: android.graphics.Color.parseColor("#747775"),
outlineVariant: android.graphics.Color.parseColor("#C4C7C5"),
@@ -366,6 +481,9 @@ var MonetColorProvider = {
surface: android.graphics.Color.parseColor("#131314"),
onSurface: android.graphics.Color.parseColor("#E3E3E3"),
surfaceVariant: android.graphics.Color.parseColor("#49454F"),
surfaceContainerLow: android.graphics.Color.parseColor("#1B1B1F"),
surfaceContainer: android.graphics.Color.parseColor("#202124"),
surfaceContainerHigh: android.graphics.Color.parseColor("#2B2C30"),
onSurfaceVariant: android.graphics.Color.parseColor("#C4C7C5"),
outline: android.graphics.Color.parseColor("#8E918F"),
outlineVariant: android.graphics.Color.parseColor("#49454F"),
@@ -390,6 +508,9 @@ var MonetColorProvider = {
surface: "system_neutral1_900",
onSurface: "system_neutral1_100",
surfaceVariant: "system_neutral2_700",
surfaceContainerLow: "system_neutral1_800",
surfaceContainer: "system_neutral1_800",
surfaceContainerHigh: "system_neutral1_700",
onSurfaceVariant: "system_neutral2_200",
outline: "system_neutral2_400",
outlineVariant: "system_neutral2_700",
@@ -407,6 +528,9 @@ var MonetColorProvider = {
surface: "system_neutral1_10",
onSurface: "system_neutral1_900",
surfaceVariant: "system_neutral2_100",
surfaceContainerLow: "system_neutral1_50",
surfaceContainer: "system_neutral1_100",
surfaceContainerHigh: "system_neutral1_200",
onSurfaceVariant: "system_neutral2_700",
outline: "system_neutral2_500",
outlineVariant: "system_neutral2_200",
@@ -558,19 +682,19 @@ FloatBallAppWM.prototype.refreshMonetColors = function(forceDark) {
// 浅色配色
c.bgLight = ml.surface;
c.cardLight = ml.surfaceVariant;
c.cardLight = ml.surfaceContainerLow || ml.surfaceVariant;
c.textPriLight = ml.onSurface;
c.textSecLight = ml.onSurfaceVariant;
c.dividerLight = ml.outline;
c.inputBgLight = ml.surface;
c.dividerLight = ml.outlineVariant || ml.outline;
c.inputBgLight = ml.surfaceContainerHigh || ml.surface;
// 深色配色
c.bgDark = md.surface;
c.cardDark = md.surfaceVariant;
c.cardDark = md.surfaceContainerLow || md.surfaceVariant;
c.textPriDark = md.onSurface;
c.textSecDark = md.onSurfaceVariant;
c.dividerDark = md.outline;
c.inputBgDark = md.surface;
c.dividerDark = md.outlineVariant || md.outline;
c.inputBgDark = md.surfaceContainerHigh || md.surface;
// 当前主题配色(随主题切换)
c.primary = m.primary;
@@ -585,7 +709,12 @@ FloatBallAppWM.prototype.refreshMonetColors = function(forceDark) {
// 扩展:完整 Monet 语义字段(供面板方法直接使用)
c._monetSurface = m.surface;
c._monetOnSurface = m.onSurface;
c._monetSurfaceVariant = m.surfaceVariant;
c._monetSurfaceContainerLow = m.surfaceContainerLow || m.surfaceVariant;
c._monetSurfaceContainer = m.surfaceContainer || m.surfaceVariant;
c._monetSurfaceContainerHigh = m.surfaceContainerHigh || m.surfaceVariant;
c._monetOutline = m.outline;
c._monetOutlineVariant = m.outlineVariant || m.outline;
c._monetOnPrimary = m.onPrimary;
c._monetPrimaryContainer = m.primaryContainer;
c._monetOnPrimaryContainer = m.onPrimaryContainer;
@@ -642,6 +771,14 @@ FloatBallAppWM.prototype.getMonetAccentForBall = function() {
FloatBallAppWM.prototype.updateBallContentBackground = function(contentView) {
try {
var ballColor = this.getMonetAccentForBall();
try {
var bgHex = String(this.config.BALL_BG_COLOR_HEX || "").trim();
if (bgHex.length > 0) {
ballColor = android.graphics.Color.parseColor(bgHex);
}
} catch(eCustomBg) {
safeLog(this.L, 'e', "BALL_BG_COLOR_HEX parse failed, fallback Monet accent: " + String(eCustomBg));
}
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);
@@ -761,33 +898,49 @@ FloatBallAppWM.prototype.applyTextColorRecursive = function(v, colorInt) {
};
FloatBallAppWM.prototype.updatePanelBackground = function(panelView) {
// 这段代码的主要内容/用途:统一为"主面板/设置面板/查看器面板"应用背景与文字颜色(自动/亮/暗三档),并输出调试日志(命中哪个颜色)
// 这段代码的主要内容/用途:统一为"主面板/设置面板/查看器面板"应用背景与文字颜色SETTINGS_THEME=animal 时使用动物岛色monet 时保持系统莫奈
try {
var bg = new android.graphics.drawable.GradientDrawable();
bg.setCornerRadius(this.dp(22));
var isDark = this.isDarkTheme();
var settTheme = "animal";
try { settTheme = String(this.config.SETTINGS_THEME || "animal"); } catch(eSetTheme) { settTheme = "animal"; }
var bgInt = this.getPanelBgColorInt();
var bgInt = 0;
var tc = 0;
var stroke = 0;
var radiusDp = 22;
if (settTheme === "animal" && this.getAnimalIslandTheme) {
var Color = android.graphics.Color;
bgInt = Color.parseColor(isDark ? "#27362E" : "#E4F1DF");
tc = Color.parseColor(isDark ? "#F5EBD2" : "#3F3528");
stroke = this.withAlpha(Color.parseColor(isDark ? "#526454" : "#E8DEC8"), isDark ? 0.50 : 0.52);
radiusDp = 30;
} else {
bgInt = this.getPanelBgColorInt();
tc = this.getPanelTextColorInt(bgInt);
var outlineColor = this.ui.colors._monetOutline || (isDark ? android.graphics.Color.parseColor("#8E918F") : android.graphics.Color.parseColor("#747775"));
stroke = this.withAlpha(outlineColor, isDark ? 0.26 : 0.20);
}
bg.setCornerRadius(this.dp(radiusDp));
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", "[t]apply theme=" + settTheme + " 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 +
"[theme:apply] theme=" + settTheme +
" isDark=" + isDark +
" bg=" + _th_hex(bgInt) + " " + _th_argb(bgInt) +
" text=" + _th_hex(tc) + " " + _th_argb(tc) +
" stroke=" + _th_hex(stroke)

View File

@@ -153,7 +153,23 @@ 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) {
// 设置页主题切换:不论 previewMode 都重建设置页 UI
if (String(k) === "SETTINGS_THEME") {
try {
if (this.state.toolAppActive && this.replaceToolAppPage) {
this.replaceToolAppPage(String(this.state.toolAppRoute || "settings_group"));
} else {
if (this.state.settingsPanel) {
this.safeRemoveView(this.state.settingsPanel, "settingsPanel");
this.state.settingsPanel = null;
this.state.settingsPanelLp = null;
this.state.addedSettings = false;
}
this.replaceToolAppPage("settings_group");
}
} catch(eReb) { safeLog(null, 'e', "catch " + String(eReb)); }
} else if (this.state.previewMode) {
this.refreshPreview(k);
}
};
@@ -272,7 +288,6 @@ FloatBallAppWM.prototype.applyImmediateEffectsForKey = function(k) {
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)); }
@@ -282,7 +297,6 @@ FloatBallAppWM.prototype.applyImmediateEffectsForKey = function(k) {
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)); }
@@ -294,14 +308,17 @@ FloatBallAppWM.prototype.applyImmediateEffectsForKey = function(k) {
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 === "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" || k === "BALL_BG_COLOR_HEX") { this.rebuildBallForNewSize(); return; }
if (k === "TOOLAPP_BACK_EDGE_WIDTH_DP") {
return;
}
if (k === "PANEL_ROWS" || k === "PANEL_COLS" ||
k === "PANEL_ITEM_SIZE_DP" || k === "PANEL_GAP_DP" ||

View File

@@ -1577,12 +1577,13 @@ FloatBallAppWM.prototype.showIconPicker = function(opts) {
if (e.getAction() === android.view.MotionEvent.ACTION_DOWN) {
try {
var rect = new android.graphics.Rect();
if (state.root) {
state.root.getGlobalVisibleRect(rect);
if (panel) {
panel.getGlobalVisibleRect(rect);
var x = e.getRawX();
var y = e.getRawY();
if (!rect.contains(x, y)) {
hide();
return true;
}
}
} catch(eOut) { safeLog(null, 'e', "catch " + String(eOut)); }

View File

@@ -127,6 +127,7 @@ FloatBallAppWM.prototype.playBounce = function(v) {
FloatBallAppWM.prototype.safeRemoveView = function(v, whichName) {
try {
if (!v) return { ok: true, skipped: true };
try { if (this.unregisterPanelPredictiveBack) this.unregisterPanelPredictiveBack(v); } catch (eBack) {}
this.state.wm.removeView(v);
return { ok: true };
} catch (e) {
@@ -182,10 +183,21 @@ FloatBallAppWM.prototype.hideViewerPanel = function() {
if (!this.state.addedViewer) return;
if (!this.state.viewerPanel) return;
var oldViewerType = String(this.state.viewerPanelType || "");
this.safeRemoveView(this.state.viewerPanel, "viewerPanel");
this.state.viewerPanel = null;
this.state.viewerPanelLp = null;
this.state.viewerPanelType = null;
if (oldViewerType === "tool_app") {
this.state.toolAppRoot = null;
this.state.toolAppBody = null;
this.state.toolAppContentHost = null;
this.state.toolAppBackPreviewView = null;
this.state.toolAppBackPreviewRoute = null;
this.state.toolAppBackPreviewReady = false;
this.state.toolAppTitleView = null;
this.state.toolAppBackButton = null;
}
this.state.addedViewer = false;
this.hideMask();
@@ -205,6 +217,9 @@ FloatBallAppWM.prototype.handlePanelBack = function(which, reason) {
if (this.state.addedViewer) {
var vt = String(this.state.viewerPanelType || w || "viewer");
if (vt === "tool_app" && this.state.toolAppActive && this.popToolAppPage) {
return this.popToolAppPage(reason || "back_key");
}
if (vt === "btn_editor") {
if (this.state.editingButtonIndex !== null && this.state.editingButtonIndex !== undefined) {
this.state.editingButtonIndex = null;
@@ -262,6 +277,254 @@ FloatBallAppWM.prototype.handleSystemUiDismiss = function(reason) {
return false;
};
FloatBallAppWM.prototype.hidePanelPredictiveBackIndicator = function() {
try {
var v = this.state.predictiveBackIndicatorView;
if (v && this.state.wm) {
try { this.state.wm.removeView(v); } catch (eRm) {}
}
this.state.predictiveBackIndicatorView = null;
this.state.predictiveBackIndicatorLp = null;
} catch (e) {}
};
FloatBallAppWM.prototype.showPanelPredictiveBackIndicator = function(edge) {
try {
if (!this.state.wm) return null;
var v = this.state.predictiveBackIndicatorView;
var lp = this.state.predictiveBackIndicatorLp;
var size = this.dp(46);
var edgeLeft = Number(edge) !== 1;
if (!v) {
v = new android.widget.TextView(context);
v.setText(edgeLeft ? "" : "");
v.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 30);
v.setTypeface(null, android.graphics.Typeface.BOLD);
v.setGravity(android.view.Gravity.CENTER);
v.setTextColor(android.graphics.Color.WHITE);
try {
var bg = new android.graphics.drawable.GradientDrawable();
bg.setShape(android.graphics.drawable.GradientDrawable.OVAL);
var c = (this.ui && this.ui.colors && this.ui.colors.primary) ? this.ui.colors.primary : android.graphics.Color.parseColor("#005BC0");
bg.setColor(this.withAlpha ? this.withAlpha(c, 0.92) : c);
v.setBackground(bg);
v.setElevation(this.dp(12));
} catch (eBg) {}
lp = new android.view.WindowManager.LayoutParams(
size,
size,
android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |
android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
android.graphics.PixelFormat.TRANSLUCENT
);
lp.gravity = (edgeLeft ? android.view.Gravity.START : android.view.Gravity.END) | android.view.Gravity.CENTER_VERTICAL;
lp.x = this.dp(6);
lp.y = 0;
v.setAlpha(0);
try { this.state.wm.addView(v, lp); } catch (eAdd) { return null; }
this.state.predictiveBackIndicatorView = v;
this.state.predictiveBackIndicatorLp = lp;
} else {
try { v.setText(edgeLeft ? "" : ""); } catch (eTxt) {}
if (lp) {
lp.gravity = (edgeLeft ? android.view.Gravity.START : android.view.Gravity.END) | android.view.Gravity.CENTER_VERTICAL;
}
}
return v;
} catch (e) {
safeLog(this.L, 'w', "show predictive back indicator fail: " + String(e));
}
return null;
};
FloatBallAppWM.prototype.resetPanelPredictiveBackVisual = function(panel) {
try {
if (panel && this.state && this.state.toolAppRoot === panel && this.clearToolAppBackPreview) {
this.clearToolAppBackPreview(true);
this.hidePanelPredictiveBackIndicator();
return;
}
if (panel) {
panel.setAlpha(1.0);
panel.setTranslationX(0);
panel.setScaleX(1.0);
panel.setScaleY(1.0);
}
this.hidePanelPredictiveBackIndicator();
} catch (e) {}
};
FloatBallAppWM.prototype.applyPanelPredictiveBackProgress = function(panel, event) {
try {
if (!panel || !event) return;
var p = 0;
try { p = Number(event.getProgress()); } catch (eP) { p = 0; }
if (isNaN(p)) p = 0;
if (p < 0) p = 0;
if (p > 1) p = 1;
var edge = 0;
try { edge = Number(event.getSwipeEdge()); } catch (eE) { edge = 0; }
if (panel && this.state && this.state.toolAppRoot === panel && this.applyToolAppBackPreviewProgress && this.hasToolAppBackTarget && this.hasToolAppBackTarget()) {
this.state.toolAppBackEdge = edge;
var dragPx = 0;
try {
var panelW = 0;
try { panelW = Number(panel.getWidth ? panel.getWidth() : 0); } catch(ePW) { panelW = 0; }
if (!panelW || panelW < this.dp(120)) {
try { panelW = Number((this.state.viewerPanelLp && this.state.viewerPanelLp.width) || 0); } catch(eLpW) { panelW = 0; }
}
if (!panelW || panelW < this.dp(120)) panelW = this.dp(320);
var maxFollow = Math.min(this.dp(220), Math.floor(panelW * 0.45));
dragPx = Math.round(maxFollow * p);
} catch(eDrag) {
dragPx = Math.round(this.dp(180) * p);
}
try {
var nowPb = Date.now();
if (!this.state.toolAppPredictiveBackLogAt || (nowPb - Number(this.state.toolAppPredictiveBackLogAt || 0)) > 300) {
this.state.toolAppPredictiveBackLogAt = nowPb;
safeLog(this.L, 'd', 'predictive back progress edge=' + String(edge) + ' p=' + String(p) + ' dragPx=' + String(dragPx));
}
} catch(eLogPb) {}
this.applyToolAppBackPreviewProgress(edge, p, dragPx);
return;
}
var dir = edge === 1 ? -1 : 1;
panel.setAlpha(1.0 - 0.18 * p);
panel.setTranslationX(dir * this.dp(36) * p);
var s = 1.0 - 0.025 * p;
panel.setScaleX(s);
panel.setScaleY(s);
// overlay 窗口下系统自己的预测性返回箭头在部分 ColorOS 版本不可见,额外绘制一个轻量边缘提示。
var ind = this.showPanelPredictiveBackIndicator(edge);
if (ind) {
ind.setAlpha(Math.min(1.0, 0.20 + 0.80 * p));
ind.setScaleX(0.82 + 0.22 * p);
ind.setScaleY(0.82 + 0.22 * p);
ind.setTranslationX(dir * this.dp(18) * p);
}
} catch (e) {}
};
FloatBallAppWM.prototype.unregisterPanelPredictiveBack = function(panel) {
try {
var entries = this.state.panelBackCallbackEntries || [];
var kept = [];
for (var i = 0; i < entries.length; i++) {
var it = entries[i];
if (!it || it.view === panel) {
try { if (it && it.dispatcher && it.callback) it.dispatcher.unregisterOnBackInvokedCallback(it.callback); } catch (eUnreg) {}
} else {
kept.push(it);
}
}
this.state.panelBackCallbackEntries = kept;
this.resetPanelPredictiveBackVisual(panel);
} catch (e) {
safeLog(this.L, 'w', "unregister predictive back fail: " + String(e));
}
};
FloatBallAppWM.prototype.registerPanelPredictiveBack = function(panel, which) {
// 返回注册优先级Android 14+ OnBackAnimationCallback有 progress→ Android 13 OnBackInvokedCallback仅最终返回→ 旧系统 KEYCODE_BACK。
try {
if (!panel) return false;
if (android.os.Build.VERSION.SDK_INT < 33) return false;
// attach listener + post fallback may fire almost together; avoid unregister/register churn and duplicate logs.
try {
var nowReg = Date.now();
var entries0 = this.state.panelBackCallbackEntries || [];
for (var ei0 = 0; ei0 < entries0.length; ei0++) {
var it0 = entries0[ei0];
if (it0 && it0.view === panel && String(it0.which || "") === String(which || "") && (nowReg - Number(it0.registeredAt || 0)) < 300) {
return true;
}
}
} catch(eRegDebounce) {}
this.unregisterPanelPredictiveBack(panel);
var dispatcher = null;
try { dispatcher = panel.findOnBackInvokedDispatcher(); } catch (eFind) { dispatcher = null; }
if (!dispatcher) {
safeLog(this.L, 'w', "predictive back dispatcher missing which=" + String(which || ""));
return false;
}
var self = this;
var cb = null;
var mode = "none";
var usedAnimation = false;
function finishBack(reason) {
if (String(which || "") === "tool_app" && self.finishToolAppBackPreview && self.hasToolAppBackTarget && self.hasToolAppBackTarget()) {
var edge = 0;
try { edge = Number(self.state.toolAppBackEdge || 0); } catch (eEdge) { edge = 0; }
self.finishToolAppBackPreview(edge, true);
return;
}
self.resetPanelPredictiveBackVisual(panel);
self.handlePanelBack(which, reason || "predictive_back");
}
if (android.os.Build.VERSION.SDK_INT >= 34) {
try {
// 核心:优先创建真正的 android.window.OnBackAnimationCallback。
// Class.forName 只用于预热/instanceof 校验JavaAdapter 必须使用 Packages 下的接口对象。
var animCls = java.lang.Class.forName("android.window.OnBackAnimationCallback");
cb = new JavaAdapter(Packages.android.window.OnBackAnimationCallback, {
onBackStarted: function(event) { self.applyPanelPredictiveBackProgress(panel, event); },
onBackProgressed: function(event) { self.applyPanelPredictiveBackProgress(panel, event); },
onBackCancelled: function() { self.resetPanelPredictiveBackVisual(panel); },
onBackInvoked: function() { finishBack("predictive_back"); }
});
usedAnimation = !!animCls.isInstance(cb);
mode = usedAnimation ? "OnBackAnimationCallback" : "OnBackAnimationCallback-proxy-not-instance";
if (!usedAnimation) {
safeLog(self.L, 'w', "OnBackAnimationCallback proxy not instance; fallback to final-only callback");
cb = null;
}
} catch (eAnim) {
safeLog(self.L, 'w', "create OnBackAnimationCallback fail: " + String(eAnim));
cb = null;
}
}
if (!cb) {
try {
var cbCls = java.lang.Class.forName("android.window.OnBackInvokedCallback");
cb = new JavaAdapter(cbCls, {
onBackInvoked: function() { finishBack("on_back_invoked"); }
});
mode = "OnBackInvokedCallback";
} catch (eCb) {
safeLog(self.L, 'w', "create OnBackInvokedCallback fail: " + String(eCb));
cb = null;
}
}
if (!cb) return false;
var priority = 0;
try {
// 与规则文件实现保持一致:默认优先级最容易拿到系统 back pipelineoverlay priority 在 ColorOS 上可能只给最终回调。
priority = android.window.OnBackInvokedDispatcher.PRIORITY_DEFAULT;
} catch (ePri) { priority = 0; }
dispatcher.registerOnBackInvokedCallback(priority, cb);
if (!this.state.panelBackCallbackEntries) this.state.panelBackCallbackEntries = [];
this.state.panelBackCallbackEntries.push({ view: panel, dispatcher: dispatcher, callback: cb, which: String(which || ""), animation: usedAnimation, mode: mode, registeredAt: Date.now() });
safeLog(this.L, 'i', "back callback registered which=" + String(which || "") + " mode=" + mode + " priority=" + String(priority));
return true;
} catch (e) {
safeLog(this.L, 'w', "register predictive back fail which=" + String(which || "") + " err=" + String(e));
}
return false;
};
FloatBallAppWM.prototype.attachPanelSystemKeyHandler = function(panel, which) {
try {
if (!panel) return;
@@ -279,7 +542,25 @@ FloatBallAppWM.prototype.attachPanelSystemKeyHandler = function(panel, which) {
return false;
}
}));
panel.post(new java.lang.Runnable({ run: function() { try { panel.requestFocus(); } catch(eFocus) {} } }));
var registerAfterAttach = function() {
try { panel.requestFocus(); } catch(eFocus) {}
try { self.registerPanelPredictiveBack(panel, which); } catch(eBack) {
try { safeLog(self.L, 'w', "panel predictive register after attach fail: " + String(eBack)); } catch(eLog) {}
}
};
try {
panel.addOnAttachStateChangeListener(new android.view.View.OnAttachStateChangeListener({
onViewAttachedToWindow: function(v) {
try { v.post(new java.lang.Runnable({ run: registerAfterAttach })); } catch(ePost) { registerAfterAttach(); }
},
onViewDetachedFromWindow: function(v) {
try { self.unregisterPanelPredictiveBack(v); } catch(eUnreg) {}
}
}));
} catch (eAttach) {
safeLog(self.L, 'w', "add attach listener fail: " + String(eAttach));
}
panel.post(new java.lang.Runnable({ run: registerAfterAttach }));
} catch (e) {
safeLog(this.L, 'e', "attachPanelSystemKeyHandler fail which=" + String(which || "") + " err=" + String(e));
}
@@ -328,6 +609,10 @@ FloatBallAppWM.prototype.hideAllPanels = function() {
this.hideMainPanel();
this.hideSettingsPanel();
this.hideViewerPanel();
this.state.toolAppActive = false;
this.state.toolAppRoute = null;
this.state.toolAppNavStack = [];
this.state.settingsGroupKey = null;
this.hideMask();
this._clearHeavyCachesIfAllHidden("hideAllPanels");
@@ -397,6 +682,13 @@ FloatBallAppWM.prototype.snapToEdgeDocked = function(withAnim, forceSide) {
// 如果需要保护,调用方自己判断
if (this.state.dragging) return;
try {
var freshScreen = this.getScreenSizePx();
if (freshScreen && freshScreen.w > 0 && freshScreen.h > 0) {
this.state.screen = freshScreen;
}
} catch (eScreen) {}
var di = this.getDockInfo();
var ballSize = di.ballSize;
var visible = di.visiblePx;
@@ -629,12 +921,10 @@ FloatBallAppWM.prototype.safeUiCall = function(tag, fn) {
;
FloatBallAppWM.prototype.onScreenChangedReflow = function() {
FloatBallAppWM.prototype.onScreenChangedReflow = function(reason) {
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;
@@ -643,11 +933,23 @@ FloatBallAppWM.prototype.onScreenChangedReflow = function() {
var newH = newScreen.h;
if (newW <= 0 || newH <= 0) return;
var rotNow = -1;
try { rotNow = this.getRotation ? this.getRotation() : -1; } catch (eRotNow) { rotNow = -1; }
try {
var rotLandscape = (rotNow === android.view.Surface.ROTATION_90 || rotNow === android.view.Surface.ROTATION_270);
var rotPortrait = (rotNow === android.view.Surface.ROTATION_0 || rotNow === android.view.Surface.ROTATION_180);
if ((rotLandscape && newW < newH) || (rotPortrait && newW > newH)) {
safeLog(this.L, 'w', "screen reflow skip unstable size reason=" + String(reason || "") + " new=" + newW + "x" + newH + " rot=" + String(rotNow));
return;
}
} catch (eStable) {}
if (oldW <= 0) oldW = newW;
if (oldH <= 0) oldH = newH;
this.state.screen = { w: newW, h: newH };
var di = this.getDockInfo();
var ballSize = di.ballSize;
var visible = di.visiblePx;
var hidden = di.hiddenPx;
@@ -690,7 +992,41 @@ FloatBallAppWM.prototype.onScreenChangedReflow = function() {
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));
safeLog(this.L, 'i',
"screen reflow reason=" + String(reason || "") +
" old=" + oldW + "x" + oldH +
" new=" + newW + "x" + newH +
" rot=" + String(rotNow) +
" docked=" + String(this.state.docked) +
" side=" + String(this.state.dockSide || "") +
" x=" + String(this.state.ballLp.x) +
" y=" + String(this.state.ballLp.y)
);
};
FloatBallAppWM.prototype.scheduleScreenReflow = function(reason) {
try {
var self = this;
if (!this.state.h) {
this.onScreenChangedReflow(reason);
return;
}
this.onScreenChangedReflow(reason);
this.state.h.postDelayed(new JavaAdapter(java.lang.Runnable, {
run: function() {
try {
if (self.state.closing) return;
self.onScreenChangedReflow(String(reason || "") + ":delayed");
} catch (e) {
safeLog(self.L, "w", "delayed screen reflow fail reason=" + String(reason || "") + " err=" + String(e));
}
}
}), 260);
} catch (e0) {
try { this.onScreenChangedReflow(reason); } catch(e1) {}
}
};
FloatBallAppWM.prototype.setupDisplayMonitor = function() {
@@ -730,7 +1066,7 @@ FloatBallAppWM.prototype.setupDisplayMonitor = function() {
if (changed) {
self.cancelDockTimer();
self.onScreenChangedReflow();
self.scheduleScreenReflow("display_changed");
self.touchActivity();
}
} catch (e1) {

View File

@@ -21,24 +21,23 @@ FloatBallAppWM.prototype.execButtonAction = function(btn, idx) {
}
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);
function tailLogText(path, maxLen) {
var txt = FileIO.readText(path);
if (!txt) return "(日志文件不存在或为空: " + path + ")";
txt = String(txt);
if (txt.length > maxLen) txt = "[...前略...]\n" + txt.substring(txt.length - maxLen);
try {
var lines = txt.split("\n");
if (lines.length > 1) txt = lines.reverse().join("\n");
} catch(eRev) { safeLog(null, 'e', "catch " + String(eRev)); }
return txt;
}
// 简单的按行倒序,方便查看最新日志
try {
var lines = content.split("\n");
if (lines.length > 1) {
content = lines.reverse().join("\n");
}
} catch(eRev) { safeLog(null, 'e', "catch " + String(eRev)); }
var runLogPath = (this.L && this.L._filePathForToday) ? this.L._filePathForToday() : "";
if (!runLogPath) runLogPath = PATH_LOG_DIR + "/ShortX_ToolHub_" + (new java.text.SimpleDateFormat("yyyyMMdd").format(new java.util.Date())) + ".log";
var initLogPath = PATH_LOG_DIR + "/init.log";
var content = "【启动/更新日志】\n" + tailLogText(initLogPath, 15000) +
"\n\n【运行日志】\n" + tailLogText(runLogPath, 15000);
if (content.length > 32000) content = content.substring(0, 32000) + "\n[...后略...]";
this.showViewerPanel("今日日志 (倒序)", content);
return;
}
@@ -132,7 +131,7 @@ return;
if (r && r.ok) return;
this.toast("shell 广播桥发送失败");
safeLog(this.L, 'e', "shell all failed cmd_b64=" + cmdB64 + " ret=" + JSON.stringify(r || {}));
safeLog(this.L, 'e', "shell all failed cmd_b64_len=" + String(cmdB64 ? cmdB64.length : 0) + " ret=" + JSON.stringify(r || {}));
return;
}

View File

@@ -3,7 +3,9 @@
FloatBallAppWM.prototype.createSectionHeader = function(item, parent) {
var isDark = this.isDarkTheme();
var C = this.ui.colors;
var color = C.primary;
var T = this.getAnimalIslandTheme ? this.getAnimalIslandTheme() : null;
try { if (this.applySettingsTheme && T) this.applySettingsTheme(T, isDark, C, this.state.pendingUserCfg || this.config); } catch(eTheme) { safeLog(null, 'e', "catch " + String(eTheme)); }
var color = T ? T.primary : C.primary;
var h = new android.widget.TextView(context);
h.setText(String(item.name || ""));
@@ -17,11 +19,14 @@ FloatBallAppWM.prototype.createSectionHeader = function(item, parent) {
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 T = this.getAnimalIslandTheme ? this.getAnimalIslandTheme() : null;
try { if (this.applySettingsTheme && T) this.applySettingsTheme(T, isDark, C, this.state.pendingUserCfg || this.config); } catch(eTheme) { safeLog(null, 'e', "catch " + String(eTheme)); }
var textColor = T ? T.text : (isDark ? C.textPriDark : C.textPriLight);
var secColor = T ? T.sub : (isDark ? C.textSecDark : C.textSecLight);
var dividerColor = T ? T.stroke : (isDark ? C.dividerDark : C.dividerLight);
var primary = T ? T.primary : C.primary;
var inputBgColor = T ? T.card2 : (isDark ? C.inputBgDark : C.inputBgLight);
var switchOff = T ? T.card2 : (isDark ? (0xFF555555 | 0) : (0xFFCCCCCC | 0));
// 增加内边距
var padH = this.dp(16);
@@ -232,14 +237,46 @@ FloatBallAppWM.prototype.createSettingItemView = function(item, parent, needDivi
// 透明波纹背景
btn.setBackground(self.ui.createTransparentRippleDrawable(primary, self.dp(16)));
function runSettingAction() {
try {
self.touchActivity();
var action = String(item.action || "");
if (action === "open_btn_mgr") {
self.showPanelAvoidBall("btn_editor");
return;
}
if (action === "open_schema_editor") {
self.showPanelAvoidBall("schema_editor");
return;
}
if (action === "open_settings") {
self.showPanelAvoidBall("settings");
return;
}
if (action === "open_manual" || action === "open_doc") {
var intent = new android.content.Intent(android.content.Intent.ACTION_VIEW);
intent.setData(android.net.Uri.parse("https://xin-blog.com/114.html"));
intent.setFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
return;
}
if (action && (action.indexOf("http://") === 0 || action.indexOf("https://") === 0)) {
var urlIntent = new android.content.Intent(android.content.Intent.ACTION_VIEW);
urlIntent.setData(android.net.Uri.parse(action));
urlIntent.setFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(urlIntent);
return;
}
self.toast(action ? ("暂不支持动作: " + action) : "动作未配置");
} catch(e) {
try { self.toast("动作执行失败: " + String(e)); } catch(eToast) {}
safeLog(null, 'e', "setting action fail " + String(e));
}
}
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)); }
runSettingAction();
}
}));
row.addView(btn);
@@ -247,12 +284,7 @@ FloatBallAppWM.prototype.createSettingItemView = function(item, parent, needDivi
// 行点击也触发
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)); }
runSettingAction();
}
}));
@@ -271,7 +303,8 @@ FloatBallAppWM.prototype.createSettingItemView = function(item, parent, needDivi
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)));
try { et.setHintTextColor(secColor); } catch(eHint) { safeLog(null, 'e', "catch " + String(eHint)); }
et.setBackground(self.ui.createStrokeDrawable(inputBgColor, dividerColor, self.dp(1), self.dp(8)));
et.setPadding(self.dp(8), self.dp(8), self.dp(8), self.dp(8));
et.setSingleLine(true);
@@ -407,7 +440,7 @@ FloatBallAppWM.prototype.createSettingItemView = function(item, parent, needDivi
}
refreshBallShortXPreview();
var btnPick = self.ui.createFlatButton(self, "选择图标", primary, function() {
var btnPick = self.ui.createFlatButton(self, "换一个", primary, function() {
self.touchActivity();
self.showShortXIconPickerPopup({
currentName: String(self.getPendingValue(item.key) || ""),
@@ -430,7 +463,7 @@ FloatBallAppWM.prototype.createSettingItemView = function(item, parent, needDivi
gapView.setLayoutParams(new android.widget.LinearLayout.LayoutParams(self.dp(8), 1));
iconRow.addView(gapView);
var btnClear = self.ui.createFlatButton(self, "清空", secColor, function() {
var btnClear = self.ui.createFlatButton(self, "不用图标", secColor, function() {
self.touchActivity();
try {
self.setPendingValue(item.key, "");
@@ -484,7 +517,7 @@ FloatBallAppWM.prototype.createSettingItemView = function(item, parent, needDivi
}
refreshBallColorPreview();
var btnColor = self.ui.createFlatButton(self, "选择颜色", primary, function() {
var btnColor = self.ui.createFlatButton(self, "颜色", primary, function() {
self.touchActivity();
self.showColorPickerPopup({
currentColor: String(self.getPendingValue(item.key) || ""),
@@ -505,7 +538,7 @@ FloatBallAppWM.prototype.createSettingItemView = function(item, parent, needDivi
gapColorView.setLayoutParams(new android.widget.LinearLayout.LayoutParams(self.dp(8), 1));
colorRow.addView(gapColorView);
var btnClearColor = self.ui.createFlatButton(self, "清空", secColor, function() {
var btnClearColor = self.ui.createFlatButton(self, "恢复默认", secColor, function() {
self.touchActivity();
try {
self.setPendingValue(item.key, "");

551
code/th_14_color_picker.js Normal file
View File

@@ -0,0 +1,551 @@
// ToolHub - 颜色选择器弹窗模块
// 依赖th_14_panels.js 中的 showPopupOverlay()、th_04_theme.js 主题工具、th_06_icon_parser.js 图标解析。
FloatBallAppWM.prototype.showColorPickerPopup = function(opts) {
var self = this;
var opt = opts || {};
var currentColor = String(opt.currentColor || "");
var currentIconName = String(opt.currentIconName || "");
var onSelect = (typeof opt.onSelect === "function") ? opt.onSelect : null;
var onDismiss = (typeof opt.onDismiss === "function") ? opt.onDismiss : null;
var PT = this.getIslandPickerTheme ? this.getIslandPickerTheme() : null;
var isDark = PT ? PT.isDark : this.isDarkTheme();
var C = this.ui.colors;
var T = PT ? PT.T : this.getAnimalIslandTheme();
var textColor = PT ? PT.text : (isDark ? C.textPriDark : C.textPriLight);
var subTextColor = PT ? PT.sub : (isDark ? C.textSecDark : C.textSecLight);
function getThemeTintHex() {
try {
if (self.ui.colors && self.ui.colors.accent) {
var c = self.ui.colors.accent;
return "#" + ("00000000" + (c >>> 0).toString(16)).slice(-8);
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
return "#FF4081";
}
function buildArgbHex(alphaByte, rgbHex) {
var a = ("00" + (Math.max(0, Math.min(255, Number(alphaByte || 0))) >>> 0).toString(16)).slice(-2);
var rgb = String(rgbHex || "000000").replace(/^#/, "");
if (rgb.length === 3) rgb = rgb.split("").map(function(c){ return c+c; }).join("");
if (rgb.length > 6) rgb = rgb.slice(-6);
while (rgb.length < 6) rgb = "0" + rgb;
return "#" + a + rgb;
}
function extractTintRgbHex(hex) {
var h = String(hex || "").replace(/^#/, "");
if (h.length >= 8) return h.slice(-6);
if (h.length === 6) return h;
if (h.length === 3) return h.split("").map(function(c){ return c+c; }).join("");
return "000000";
}
function extractTintAlphaByte(hex) {
var h = String(hex || "").replace(/^#/, "");
if (h.length >= 8) return parseInt(h.slice(0, 2), 16);
return 255;
}
function normalizeTintColorValue(val) {
var s = String(val || "").trim();
if (!s) return "";
if (s.charAt(0) === "#") s = s.substring(1);
if (/^[0-9A-Fa-f]{1,8}$/.test(s)) {
while (s.length < 6) s = "0" + s;
if (s.length === 6) s = "FF" + s;
else if (s.length > 8) s = s.substring(0, 8);
return "#" + s.toUpperCase();
}
return "";
}
var commonTintHexValues = [
"#FFFF0000", "#FFFF5722", "#FFFF9800", "#FFFFC107", "#FFFFEB3B",
"#FFCDDC39", "#FF8BC34A", "#FF4CAF50", "#FF009688", "#FF00BCD4",
"#FF03A9F4", "#FF2196F3", "#FF3F51B5", "#FF673AB7", "#FF9C27B0",
"#FFE91E63", "#FF795548", "#FF9E9E9E", "#FF607D8B", "#FF000000", "#FFFFFFFF"
];
// ========== 最近使用颜色 ==========
var RECENT_COLORS_KEY = "color_picker_recent";
var MAX_RECENT_COLORS = 8;
var recentColors = [];
try {
var recentSaved = self.loadPanelState(RECENT_COLORS_KEY);
if (recentSaved && recentSaved.colors && recentSaved.colors.length) {
var rc;
for (rc = 0; rc < recentSaved.colors.length && rc < MAX_RECENT_COLORS; rc++) {
var rn = normalizeTintColorValue(recentSaved.colors[rc], false);
if (rn) recentColors.push(rn);
}
}
} catch(eRecentLoad) { safeLog(null, 'e', "catch " + String(eRecentLoad)); }
function saveRecentColors() {
try {
self.savePanelState(RECENT_COLORS_KEY, { colors: recentColors.slice(0, MAX_RECENT_COLORS) });
} catch(eRecentSave) { safeLog(null, 'e', "catch " + String(eRecentSave)); }
}
function pushRecentColor(hex) {
var normalized = normalizeTintColorValue(hex, false);
if (!normalized) return;
var next = [normalized];
var i;
for (i = 0; i < recentColors.length; i++) {
if (recentColors[i] !== normalized) {
next.push(recentColors[i]);
}
if (next.length >= MAX_RECENT_COLORS) break;
}
recentColors = next;
saveRecentColors();
}
var selectedColor = currentColor;
var isFollowTheme = !currentColor;
var currentBaseRgbHex = extractTintRgbHex(currentColor);
var currentAlphaByte = extractTintAlphaByte(currentColor);
var popupResult = self.showPopupOverlay({
title: "换颜色",
onDismiss: onDismiss,
builder: function(content, closePopup) {
// 图标预览区
var previewRow = new android.widget.LinearLayout(context);
previewRow.setOrientation(android.widget.LinearLayout.HORIZONTAL);
previewRow.setGravity(android.view.Gravity.CENTER_VERTICAL);
previewRow.setPadding(self.dp(12), self.dp(10), self.dp(12), self.dp(10));
previewRow.setBackground(self.ui.createStrokeDrawable(T.primarySoft, self.withAlpha(T.primaryDeep, isDark ? 0.24 : 0.18), self.dp(1), self.dp(18)));
var previewIv = new android.widget.ImageView(context);
previewIv.setLayoutParams(new android.widget.LinearLayout.LayoutParams(self.dp(48), self.dp(48)));
previewIv.setScaleType(android.widget.ImageView.ScaleType.FIT_CENTER);
previewRow.addView(previewIv);
var previewLabel = new android.widget.TextView(context);
previewLabel.setText("小图标试衣间");
previewLabel.setTextColor(subTextColor);
previewLabel.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 12);
previewLabel.setPadding(self.dp(12), 0, 0, 0);
previewRow.addView(previewLabel);
content.addView(previewRow);
function updatePreview() {
try {
var dr = null;
if (currentIconName) {
try { dr = self.getShortXIconDrawable(currentIconName); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
if (dr) {
if (!isFollowTheme && selectedColor) {
try {
var parsed = android.graphics.Color.parseColor(selectedColor);
dr.setColorFilter(parsed, android.graphics.PorterDuff.Mode.SRC_IN);
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
} else {
try { dr.clearColorFilter(); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
previewIv.setImageDrawable(dr);
} else {
previewIv.setImageDrawable(null);
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
updatePreview();
// ========== 最近使用颜色 ==========
var recentTitle = new android.widget.TextView(context);
recentTitle.setText("最近用过的小颜色");
recentTitle.setTextColor(subTextColor);
recentTitle.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 12);
recentTitle.setPadding(self.dp(12), self.dp(8), self.dp(12), self.dp(4));
content.addView(recentTitle);
var recentGrid = new android.widget.GridLayout(context);
recentGrid.setColumnCount(8);
recentGrid.setPadding(self.dp(8), self.dp(4), self.dp(8), self.dp(4));
content.addView(recentGrid);
var recentEmptyTv = new android.widget.TextView(context);
recentEmptyTv.setText("还没有最近颜色");
recentEmptyTv.setTextColor(subTextColor);
recentEmptyTv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 11);
recentEmptyTv.setPadding(self.dp(12), self.dp(4), self.dp(12), self.dp(8));
recentEmptyTv.setVisibility(android.view.View.GONE);
content.addView(recentEmptyTv);
function refreshRecentGrid() {
try {
recentGrid.removeAllViews();
if (!recentColors.length) {
recentEmptyTv.setVisibility(android.view.View.VISIBLE);
return;
}
recentEmptyTv.setVisibility(android.view.View.GONE);
var ri;
for (ri = 0; ri < recentColors.length && ri < MAX_RECENT_COLORS; ri++) {
(function(hex) {
var cell = new android.widget.FrameLayout(context);
var margin = self.dp(3);
try {
var lp = new android.widget.GridLayout.LayoutParams();
lp.width = self.dp(28);
lp.height = self.dp(28);
lp.setMargins(margin, margin, margin, margin);
cell.setLayoutParams(lp);
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
var swatch = new android.view.View(context);
swatch.setLayoutParams(new android.widget.FrameLayout.LayoutParams(android.widget.FrameLayout.LayoutParams.MATCH_PARENT, android.widget.FrameLayout.LayoutParams.MATCH_PARENT));
try {
var bg = new android.graphics.drawable.GradientDrawable();
bg.setColor(android.graphics.Color.parseColor(hex));
bg.setCornerRadius(self.dp(5));
swatch.setBackground(bg);
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
cell.addView(swatch);
if (selectedColor === hex) {
try {
var border = new android.graphics.drawable.GradientDrawable();
border.setColor(android.graphics.Color.TRANSPARENT);
border.setCornerRadius(self.dp(5));
border.setStroke(self.dp(2), T.primaryDeep);
cell.setForeground(border);
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
cell.setOnClickListener(new android.view.View.OnClickListener({
onClick: function() {
self.touchActivity();
isFollowTheme = false;
selectedColor = hex;
currentBaseRgbHex = extractTintRgbHex(hex);
currentAlphaByte = extractTintAlphaByte(hex);
updatePreview();
updateValueTv();
refreshRecentGrid();
refreshCommonGrid();
syncRgbSeeks();
}
}));
recentGrid.addView(cell);
})(recentColors[ri]);
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
refreshRecentGrid();
// 21 色常用颜色
var commonTitle = new android.widget.TextView(context);
commonTitle.setText("糖果常用色");
commonTitle.setTextColor(subTextColor);
commonTitle.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 12);
commonTitle.setPadding(self.dp(12), self.dp(8), self.dp(12), self.dp(4));
content.addView(commonTitle);
var commonGrid = new android.widget.GridLayout(context);
commonGrid.setColumnCount(7);
commonGrid.setPadding(self.dp(8), self.dp(4), self.dp(8), self.dp(8));
var ci;
for (ci = 0; ci < commonTintHexValues.length; ci++) {
(function(hex) {
var cell = new android.widget.FrameLayout(context);
var margin = self.dp(4);
try {
var lp = new android.widget.GridLayout.LayoutParams();
lp.width = self.dp(32);
lp.height = self.dp(32);
lp.setMargins(margin, margin, margin, margin);
cell.setLayoutParams(lp);
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
var swatch = new android.view.View(context);
swatch.setLayoutParams(new android.widget.FrameLayout.LayoutParams(android.widget.FrameLayout.LayoutParams.MATCH_PARENT, android.widget.FrameLayout.LayoutParams.MATCH_PARENT));
try {
var bg = new android.graphics.drawable.GradientDrawable();
bg.setColor(android.graphics.Color.parseColor(hex));
bg.setCornerRadius(self.dp(6));
swatch.setBackground(bg);
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
cell.addView(swatch);
if (selectedColor === hex) {
try {
var border = new android.graphics.drawable.GradientDrawable();
border.setColor(android.graphics.Color.TRANSPARENT);
border.setCornerRadius(self.dp(6));
border.setStroke(self.dp(3), T.primaryDeep);
cell.setForeground(border);
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
cell.setOnClickListener(new android.view.View.OnClickListener({
onClick: function() {
self.touchActivity();
isFollowTheme = false;
selectedColor = hex;
currentBaseRgbHex = extractTintRgbHex(hex);
currentAlphaByte = extractTintAlphaByte(hex);
updatePreview();
updateValueTv();
refreshRecentGrid();
refreshCommonGrid();
syncRgbSeeks();
}
}));
commonGrid.addView(cell);
})(commonTintHexValues[ci]);
}
content.addView(commonGrid);
function refreshCommonGrid() {
try {
var count = commonGrid.getChildCount();
var i;
for (i = 0; i < count; i++) {
var cell = commonGrid.getChildAt(i);
if (!cell) continue;
try { cell.setForeground(null); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
var idx = commonTintHexValues.indexOf(selectedColor);
if (idx >= 0 && idx < count) {
var matchedCell = commonGrid.getChildAt(idx);
if (matchedCell) {
try {
var border = new android.graphics.drawable.GradientDrawable();
border.setColor(android.graphics.Color.TRANSPARENT);
border.setCornerRadius(self.dp(6));
border.setStroke(self.dp(3), T.primaryDeep);
matchedCell.setForeground(border);
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
// 颜色值显示
var valueTv = new android.widget.TextView(context);
valueTv.setTextColor(textColor);
valueTv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 13);
valueTv.setPadding(self.dp(12), self.dp(4), self.dp(12), self.dp(4));
content.addView(valueTv);
function updateValueTv() {
valueTv.setText(isFollowTheme ? "当前:跟随岛屿主题" : ("当前:" + (selectedColor || "无")));
}
updateValueTv();
// RGB 滑块
var rgbLabels = ["R", "G", "B"];
var rgbSeeks = [];
var rgbValTvs = [];
var ri;
for (ri = 0; ri < 3; ri++) {
(function(idx) {
var row = new android.widget.LinearLayout(context);
row.setOrientation(android.widget.LinearLayout.HORIZONTAL);
row.setGravity(android.view.Gravity.CENTER_VERTICAL);
row.setPadding(self.dp(12), self.dp(4), self.dp(12), self.dp(4));
var label = new android.widget.TextView(context);
label.setText(rgbLabels[idx]);
label.setTextColor(textColor);
label.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 12);
label.setMinWidth(self.dp(20));
row.addView(label);
var seek = new android.widget.SeekBar(context);
seek.setMax(255);
var seekLp = new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, 1);
seekLp.setMargins(self.dp(8), 0, self.dp(8), 0);
seek.setLayoutParams(seekLp);
row.addView(seek);
rgbSeeks.push(seek);
var valTv = new android.widget.TextView(context);
valTv.setTextColor(subTextColor);
valTv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 11);
valTv.setMinWidth(self.dp(28));
row.addView(valTv);
rgbValTvs.push(valTv);
seek.setOnSeekBarChangeListener(new android.widget.SeekBar.OnSeekBarChangeListener({
onProgressChanged: function(s, progress, fromUser) {
if (!fromUser) return;
valTv.setText(String(progress));
var r = rgbSeeks[0].getProgress();
var g = rgbSeeks[1].getProgress();
var b = rgbSeeks[2].getProgress();
var hex = ("00" + (r >>> 0).toString(16)).slice(-2) + ("00" + (g >>> 0).toString(16)).slice(-2) + ("00" + (b >>> 0).toString(16)).slice(-2);
currentBaseRgbHex = hex;
isFollowTheme = false;
selectedColor = buildArgbHex(currentAlphaByte, currentBaseRgbHex);
updatePreview();
updateValueTv();
refreshRecentGrid();
refreshCommonGrid();
},
onStartTrackingTouch: function() {},
onStopTrackingTouch: function() {}
}));
content.addView(row);
})(ri);
}
function syncRgbSeeks() {
try {
var initR = parseInt(currentBaseRgbHex.slice(0, 2), 16) || 0;
var initG = parseInt(currentBaseRgbHex.slice(2, 4), 16) || 0;
var initB = parseInt(currentBaseRgbHex.slice(4, 6), 16) || 0;
rgbSeeks[0].setProgress(initR);
rgbSeeks[1].setProgress(initG);
rgbSeeks[2].setProgress(initB);
rgbValTvs[0].setText(String(initR));
rgbValTvs[1].setText(String(initG));
rgbValTvs[2].setText(String(initB));
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
}
syncRgbSeeks();
// 透明度滑块
var alphaRow = new android.widget.LinearLayout(context);
alphaRow.setOrientation(android.widget.LinearLayout.HORIZONTAL);
alphaRow.setGravity(android.view.Gravity.CENTER_VERTICAL);
alphaRow.setPadding(self.dp(12), self.dp(4), self.dp(12), self.dp(8));
var alphaLabel = new android.widget.TextView(context);
alphaLabel.setText("A");
alphaLabel.setTextColor(textColor);
alphaLabel.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 12);
alphaLabel.setMinWidth(self.dp(20));
alphaRow.addView(alphaLabel);
var alphaSeek = new android.widget.SeekBar(context);
alphaSeek.setMax(255);
var alphaSeekLp = new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, 1);
alphaSeekLp.setMargins(self.dp(8), 0, self.dp(8), 0);
alphaSeek.setLayoutParams(alphaSeekLp);
alphaRow.addView(alphaSeek);
var alphaValTv = new android.widget.TextView(context);
alphaValTv.setTextColor(subTextColor);
alphaValTv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 11);
alphaValTv.setMinWidth(self.dp(28));
alphaRow.addView(alphaValTv);
alphaSeek.setOnSeekBarChangeListener(new android.widget.SeekBar.OnSeekBarChangeListener({
onProgressChanged: function(s, progress, fromUser) {
if (!fromUser) return;
alphaValTv.setText(String(progress));
currentAlphaByte = progress;
isFollowTheme = false;
selectedColor = buildArgbHex(currentAlphaByte, currentBaseRgbHex);
updatePreview();
updateValueTv();
refreshRecentGrid();
refreshCommonGrid();
},
onStartTrackingTouch: function() {},
onStopTrackingTouch: function() {}
}));
alphaSeek.setProgress(currentAlphaByte);
alphaValTv.setText(String(currentAlphaByte));
content.addView(alphaRow);
// 操作按钮:对齐设置页/按钮管理页的 chip + 主按钮视觉。
function createColorPanelActionButton(label, primary, onClick) {
var b = new android.widget.TextView(context);
b.setText(label);
b.setGravity(android.view.Gravity.CENTER);
b.setSingleLine(true);
b.setTypeface(null, android.graphics.Typeface.BOLD);
try { b.setIncludeFontPadding(false); } catch(eFontPad) {}
if (primary) {
// 与设置页底部“保存布置”一致主色胶囊、44dp 高、轻描边。
b.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 15);
// 颜色面板里不用高饱和青色,改成更沉稳的深绿主按钮。
b.setTextColor(android.graphics.Color.WHITE);
b.setPadding(self.dp(18), 0, self.dp(18), 0);
try { b.setMinHeight(self.dp(52)); } catch(eMinH1) {}
try { b.setBackground(self.ui.createStrokeDrawable(T.primaryDeep, self.withAlpha(T.brown || T.primaryDeep, isDark ? 0.28 : 0.18), self.dp(1), self.dp(26))); } catch(eBg1) {}
try { b.setElevation(self.dp(1)); } catch(eElev) {}
} else {
// 与设置页按钮一致:次级也用大圆角按钮,不再做小 chip。
b.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 15);
// 次级按钮走设置页里的奶油/描边感,不再用薄荷绿底。
b.setTextColor(T.brown || T.sub);
b.setPadding(self.dp(16), 0, self.dp(16), 0);
try { b.setMinHeight(self.dp(52)); } catch(eMinH2) {}
try { b.setBackground(self.ui.createStrokeDrawable(T.card2 || T.card, self.withAlpha(T.stroke || T.brown, isDark ? 0.42 : 0.55), self.dp(1), self.dp(26))); } catch(eBg2) {}
try { b.setElevation(self.dp(1)); } catch(eElev2) {}
}
try { b.setClickable(true); b.setFocusable(true); } catch(eClickable) {}
b.setOnClickListener(new android.view.View.OnClickListener({
onClick: function(v) {
self.touchActivity();
try { if (onClick) onClick(v); } catch(eBtn) { safeLog(self.L, 'e', "color panel action err=" + String(eBtn)); }
}
}));
return b;
}
var actionRow = new android.widget.LinearLayout(context);
actionRow.setOrientation(android.widget.LinearLayout.HORIZONTAL);
actionRow.setGravity(android.view.Gravity.CENTER_VERTICAL);
actionRow.setPadding(self.dp(10), self.dp(10), self.dp(10), self.dp(12));
var btnClear = createColorPanelActionButton("恢复默认", false, function() {
isFollowTheme = true;
selectedColor = "";
updatePreview();
updateValueTv();
refreshRecentGrid();
refreshCommonGrid();
syncRgbSeeks();
alphaSeek.setProgress(255);
alphaValTv.setText("255");
currentAlphaByte = 255;
});
var clearLp = new android.widget.LinearLayout.LayoutParams(0, self.dp(52));
clearLp.weight = 1;
clearLp.setMargins(0, 0, self.dp(6), 0);
actionRow.addView(btnClear, clearLp);
var btnOk = createColorPanelActionButton("保存颜色", true, function() {
try {
var finalColor = isFollowTheme ? "" : String(selectedColor || "");
if (!isFollowTheme && selectedColor) {
pushRecentColor(selectedColor);
}
if (typeof onSelect === "function") {
try { onSelect(finalColor); } catch(eOnSelect) {
safeLog(self.L, 'e', "colorPicker onSelect err=" + String(eOnSelect));
}
}
} catch(e) {
safeLog(self.L, 'e', "colorPicker confirm err=" + String(e));
}
closePopup();
});
var okLp = new android.widget.LinearLayout.LayoutParams(0, self.dp(52));
okLp.weight = 1;
okLp.setMargins(self.dp(6), 0, 0, 0);
actionRow.addView(btnOk, okLp);
content.addView(actionRow, new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
));
}
});
return popupResult;
};

509
code/th_14_icon_picker.js Normal file
View File

@@ -0,0 +1,509 @@
// @version 1.0.0
// ToolHub - ShortX 图标选择器模块
//
// 阶段 1承载 showShortXIconPickerPopup。
// 目的在新增模块下载、验签、eval 链路已稳定后,迁移 ShortX 图标选择器主体。
// 注意:加载顺序必须位于 th_14_panels.js 之后、th_15_extra.js 之前。
FloatBallAppWM.prototype.showShortXIconPickerPopup = function(opts) {
var self = this;
var opt = opts || {};
var currentName = String(opt.currentName || "");
var onSelect = (typeof opt.onSelect === "function") ? opt.onSelect : null;
var onDismissCb = (typeof opt.onDismiss === "function") ? opt.onDismiss : null;
var catalog = [];
try { catalog = self.getShortXIconCatalog() || []; } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
if (!catalog.length) {
try { catalog = self.getShortXIconCatalog(true) || []; } catch(e2) { safeLog(null, 'e', "catch " + String(e2)); }
}
if (!catalog.length) {
self.toast("图标库未加载");
return null;
}
var selectedName = currentName;
var popupState = { currentPage: 0, filter: "全部" };
var PT = self.getIslandPickerTheme ? self.getIslandPickerTheme() : null;
var isDark = PT ? PT.isDark : self.isDarkTheme();
var C = self.ui.colors;
var T = PT ? PT.T : self.getAnimalIslandTheme();
var textColor = PT ? PT.text : (isDark ? C.textPriDark : C.textPriLight);
var subTextColor = PT ? PT.sub : (isDark ? C.textSecDark : C.textSecLight);
var cardColor = PT ? PT.card2 : (isDark ? C.cardDark : C.cardLight);
var wm = self.state.wm;
var filterTags = ["全部", "常用", "最近", "收藏", "线框", "实心"];
var filterViews = [];
var FAVORITE_ICONS_KEY = "shortx_icon_favorites";
var favoriteIcons = [];
var favoriteMap = {};
function rebuildFavoriteMap() {
favoriteMap = {};
for (var fi = 0; fi < favoriteIcons.length; fi++) {
var fn = String(favoriteIcons[fi] || "");
if (fn) favoriteMap[fn] = true;
}
}
function loadFavoriteIcons() {
favoriteIcons = [];
try {
var saved = self.loadPanelState ? self.loadPanelState(FAVORITE_ICONS_KEY) : null;
var arr = saved && saved.icons ? saved.icons : [];
for (var li = 0; li < arr.length && favoriteIcons.length < 300; li++) {
var name = String(arr[li] || "");
if (name && !favoriteMap[name]) {
favoriteIcons.push(name);
favoriteMap[name] = true;
}
}
} catch(eFavLoad) { safeLog(null, 'e', "catch " + String(eFavLoad)); }
rebuildFavoriteMap();
}
function saveFavoriteIcons() {
try {
if (self.savePanelState) self.savePanelState(FAVORITE_ICONS_KEY, { icons: favoriteIcons.slice(0, 300) });
} catch(eFavSave) { safeLog(null, 'e', "catch " + String(eFavSave)); }
}
function isFavoriteIcon(name) {
return !!favoriteMap[String(name || "")];
}
function toggleFavoriteIcon(name) {
name = String(name || "");
if (!name) return false;
var next = [];
var existed = false;
for (var ti = 0; ti < favoriteIcons.length; ti++) {
var oldName = String(favoriteIcons[ti] || "");
if (!oldName) continue;
if (oldName === name) { existed = true; continue; }
next.push(oldName);
}
if (!existed) next.unshift(name);
favoriteIcons = next.slice(0, 300);
rebuildFavoriteMap();
saveFavoriteIcons();
return !existed;
}
loadFavoriteIcons();
function matchesFilter(entry, f) {
if (!entry) return false;
if (!f || f === "全部") return true;
var n = String(entry.shortName || entry.name || "").toLowerCase();
if (f === "常用") return n.indexOf("home") >= 0 || n.indexOf("share") >= 0 || n.indexOf("search") >= 0 || n.indexOf("settings") >= 0 || n.indexOf("add") >= 0 || n.indexOf("back") >= 0 || n.indexOf("close") >= 0;
if (f === "最近") return selectedName && String(entry.name) === String(selectedName);
if (f === "收藏") return isFavoriteIcon(entry.name);
if (f === "线框") return n.indexOf("outline") >= 0 || n.indexOf("line") >= 0 || n.indexOf("stroke") >= 0 || n.indexOf("border") >= 0;
if (f === "实心") return n.indexOf("fill") >= 0 || n.indexOf("solid") >= 0 || n.indexOf("round") >= 0;
return true;
}
function filterCatalog(q) {
var qLower = String(q || "").toLowerCase();
var out = [];
for (var i = 0; i < catalog.length; i++) {
var entry = catalog[i];
if (!entry) continue;
if (!matchesFilter(entry, popupState.filter)) continue;
if (qLower) {
var n = String(entry.shortName || entry.name).toLowerCase();
if (n.indexOf(qLower) < 0) continue;
}
out.push(entry);
}
return out;
}
var dm = context.getResources().getDisplayMetrics();
var sw = dm.widthPixels;
var sh = dm.heightPixels;
var panelWidth = Math.round(sw * 0.92);
var panelHeight = Math.round(sh * 0.90);
try {
if (self.calculateToolAppLayout) {
var toolLayout = self.calculateToolAppLayout(null);
if (toolLayout && toolLayout.width > 0) panelWidth = toolLayout.width;
if (toolLayout && toolLayout.height > 0) panelHeight = toolLayout.height;
}
} catch(eLayout) { safeLog(null, 'e', "catch " + String(eLayout)); }
if (panelWidth > self.dp(560)) panelWidth = self.dp(560);
if (panelWidth < self.dp(320)) panelWidth = Math.min(sw - self.dp(16), self.dp(320));
if (panelHeight > sh - self.dp(24)) panelHeight = sh - self.dp(24);
if (panelHeight < self.dp(460)) panelHeight = Math.min(sh - self.dp(16), self.dp(460));
var padH = self.dp(14);
var padV = self.dp(12);
var gap = self.dp(8);
var colCount = 5;
var availW = panelWidth - padH * 2 - self.dp(10) * 2;
var cellW = Math.floor((availW - gap * (colCount - 1)) / colCount);
if (cellW < self.dp(46)) cellW = self.dp(46);
var iconSize = Math.max(self.dp(23), Math.min(self.dp(30), Math.floor(cellW * 0.46)));
var cellH = self.dp(70);
var headerH = self.dp(176);
var bottomH = self.dp(58);
var maxGridH = Math.max(self.dp(250), panelHeight - headerH - bottomH);
var rowCount = Math.max(3, Math.floor(maxGridH / (cellH + gap)));
var pageSize = colCount * rowCount;
var rootOverlay = new android.widget.FrameLayout(context);
try { rootOverlay.setBackgroundColor(self.withAlpha(isDark ? 0xFF000000 : 0xFFFFFFFF, isDark ? 0.58 : 0.42)); }
catch(eOverlayBg) { rootOverlay.setBackgroundColor(0x33000000); }
rootOverlay.setClickable(true);
var card = new android.widget.LinearLayout(context);
card.setOrientation(android.widget.LinearLayout.VERTICAL);
card.setPadding(padH, padV, padH, padV);
card.setBackground(self.ui.createStrokeDrawable(T.card, self.withAlpha(T.primaryDeep, isDark ? 0.28 : 0.22), self.dp(1), self.dp(24)));
try { card.setElevation(self.dp(10)); } catch(eCardElev) { safeLog(null, 'e', "catch " + String(eCardElev)); }
var cardLp = new android.widget.FrameLayout.LayoutParams(panelWidth, panelHeight);
cardLp.gravity = android.view.Gravity.CENTER;
card.setLayoutParams(cardLp);
rootOverlay.addView(card);
var overlayLp = 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_LAYOUT_NO_LIMITS,
android.graphics.PixelFormat.TRANSLUCENT
);
overlayLp.softInputMode = android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING
| android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;
try { wm.addView(rootOverlay, overlayLp); } catch(eAdd) {
safeLog(self.L, 'e', "icon picker addView fail: " + String(eAdd));
return null;
}
var isDismissed = false;
function dismiss() {
if (isDismissed) return;
isDismissed = true;
try { wm.removeView(rootOverlay); } catch(e) { safeLog(null, 'e', "catch " + String(e)); }
if (typeof onDismissCb === "function") {
try { onDismissCb(); } catch(eD) { safeLog(null, 'e', "catch " + String(eD)); }
}
}
rootOverlay.setOnClickListener(new android.view.View.OnClickListener({ onClick: function() { dismiss(); } }));
card.setOnClickListener(new android.view.View.OnClickListener({ onClick: function() { } }));
var header = new android.widget.LinearLayout(context);
header.setOrientation(android.widget.LinearLayout.HORIZONTAL);
header.setGravity(android.view.Gravity.CENTER_VERTICAL);
var titleBox = new android.widget.LinearLayout(context);
titleBox.setOrientation(android.widget.LinearLayout.VERTICAL);
titleBox.setGravity(android.view.Gravity.CENTER_VERTICAL);
titleBox.setLayoutParams(new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, 1));
var titleTv = new android.widget.TextView(context);
titleTv.setText("岛上图标库");
titleTv.setTextColor(textColor);
titleTv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 17);
titleTv.setTypeface(null, android.graphics.Typeface.BOLD);
titleBox.addView(titleTv);
var countTv = new android.widget.TextView(context);
countTv.setText("共 " + catalog.length + " 个图标");
countTv.setTextColor(subTextColor);
countTv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 11);
titleBox.addView(countTv);
header.addView(titleBox);
var closeBtn = self.ui.createFlatButton(self, "✕", T.primaryDeep, function() { dismiss(); });
closeBtn.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 18);
closeBtn.setTypeface(null, android.graphics.Typeface.BOLD);
closeBtn.setPadding(self.dp(8), 0, self.dp(8), 0);
try { closeBtn.setBackground(self.ui.createStrokeDrawable(T.primarySoft, self.withAlpha(T.primaryDeep, isDark ? 0.30 : 0.22), self.dp(1), self.dp(18))); } catch(eCloseBg) {}
header.addView(closeBtn, new android.widget.LinearLayout.LayoutParams(self.dp(42), self.dp(38)));
card.addView(header);
var searchEt = new android.widget.EditText(context);
searchEt.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
searchEt.setTextColor(textColor);
try { searchEt.setHintTextColor(subTextColor); } catch(eHint) { safeLog(null, 'e', "catch " + String(eHint)); }
searchEt.setHint("寻找岛上图标,如 share / home");
searchEt.setSingleLine(true);
searchEt.setFocusable(true);
searchEt.setFocusableInTouchMode(true);
searchEt.setPadding(self.dp(14), self.dp(10), self.dp(14), self.dp(10));
searchEt.setBackground(self.ui.createStrokeDrawable(T.card2, self.withAlpha(T.primaryDeep, isDark ? 0.24 : 0.18), self.dp(1), self.dp(20)));
var searchLp = new android.widget.LinearLayout.LayoutParams(android.widget.LinearLayout.LayoutParams.MATCH_PARENT, self.dp(46));
searchLp.setMargins(0, self.dp(10), 0, self.dp(8));
card.addView(searchEt, searchLp);
searchEt.setOnClickListener(new android.view.View.OnClickListener({
onClick: function(v) {
self.touchActivity();
try {
v.requestFocus();
var imm = context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE);
if (imm) imm.showSoftInput(v, android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT);
} catch(eIme) { safeLog(null, 'e', "catch " + String(eIme)); }
}
}));
var filterScroll = new android.widget.HorizontalScrollView(context);
filterScroll.setHorizontalScrollBarEnabled(false);
var filterRow = new android.widget.LinearLayout(context);
filterRow.setOrientation(android.widget.LinearLayout.HORIZONTAL);
filterRow.setGravity(android.view.Gravity.CENTER_VERTICAL);
filterRow.setPadding(0, 0, 0, 0);
filterScroll.addView(filterRow);
var filterLp = new android.widget.LinearLayout.LayoutParams(android.widget.LinearLayout.LayoutParams.MATCH_PARENT, self.dp(36));
filterLp.setMargins(0, 0, 0, self.dp(4));
card.addView(filterScroll, filterLp);
function refreshFilterTags() {
try {
for (var i = 0; i < filterViews.length; i++) {
var item = filterViews[i];
if (!item || !item.view) continue;
var active = item.name === popupState.filter;
item.view.setTextColor(active ? T.onPrimary : T.primaryDeep);
item.view.setTypeface(null, active ? android.graphics.Typeface.BOLD : android.graphics.Typeface.NORMAL);
item.view.setBackground(self.ui.createStrokeDrawable(active ? T.primary : T.primarySoft, self.withAlpha(T.primaryDeep, active ? 0.36 : 0.18), self.dp(1), self.dp(16)));
}
} catch(eTags) { safeLog(null, 'e', "catch " + String(eTags)); }
}
for (var ft = 0; ft < filterTags.length; ft++) {
(function(tag) {
var chip = new android.widget.TextView(context);
chip.setText(tag);
chip.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 12);
chip.setGravity(android.view.Gravity.CENTER);
chip.setPadding(self.dp(12), 0, self.dp(12), 0);
chip.setSingleLine(true);
chip.setClickable(true);
chip.setOnClickListener(new android.view.View.OnClickListener({
onClick: function() {
self.touchActivity();
popupState.filter = tag;
popupState.currentPage = 0;
refreshFilterTags();
renderGrid();
}
}));
var chipLp = new android.widget.LinearLayout.LayoutParams(android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, self.dp(30));
chipLp.setMargins(0, 0, self.dp(7), 0);
filterRow.addView(chip, chipLp);
filterViews.push({ name: tag, view: chip });
})(filterTags[ft]);
}
refreshFilterTags();
var pageBar = new android.widget.LinearLayout(context);
pageBar.setOrientation(android.widget.LinearLayout.HORIZONTAL);
pageBar.setGravity(android.view.Gravity.CENTER_VERTICAL);
pageBar.setPadding(self.dp(2), 0, self.dp(2), self.dp(4));
var pageBarLp = new android.widget.LinearLayout.LayoutParams(android.widget.LinearLayout.LayoutParams.MATCH_PARENT, self.dp(34));
card.addView(pageBar, pageBarLp);
var btnPrev = self.ui.createFlatButton(self, "上一页", self.withAlpha(T.primaryDeep, 0.72), function() {
if (popupState.currentPage > 0) { popupState.currentPage--; renderGrid(); }
});
pageBar.addView(btnPrev, new android.widget.LinearLayout.LayoutParams(self.dp(78), self.dp(30)));
var pageInfo = new android.widget.TextView(context);
pageInfo.setTextColor(self.withAlpha(textColor, 0.76));
pageInfo.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 12);
pageInfo.setGravity(android.view.Gravity.CENTER);
pageInfo.setLayoutParams(new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, 1));
pageBar.addView(pageInfo);
var btnNext = self.ui.createFlatButton(self, "下一页", self.withAlpha(T.primaryDeep, 0.72), function() {
popupState.currentPage++;
renderGrid();
});
pageBar.addView(btnNext, new android.widget.LinearLayout.LayoutParams(self.dp(78), self.dp(30)));
var gridScroll = new android.widget.ScrollView(context);
gridScroll.setVerticalScrollBarEnabled(false);
gridScroll.setOverScrollMode(android.view.View.OVER_SCROLL_NEVER);
gridScroll.setLayoutParams(new android.widget.LinearLayout.LayoutParams(android.widget.LinearLayout.LayoutParams.MATCH_PARENT, 0, 1));
card.addView(gridScroll);
var grid = new android.widget.GridLayout(context);
grid.setColumnCount(colCount);
grid.setPadding(self.dp(10), self.dp(6), self.dp(10), self.dp(8));
gridScroll.addView(grid);
var selectRow = new android.widget.LinearLayout(context);
selectRow.setOrientation(android.widget.LinearLayout.HORIZONTAL);
selectRow.setGravity(android.view.Gravity.CENTER_VERTICAL);
selectRow.setPadding(self.dp(12), self.dp(8), self.dp(12), self.dp(8));
selectRow.setBackground(self.ui.createStrokeDrawable(T.card2, self.withAlpha(T.primaryDeep, isDark ? 0.22 : 0.16), self.dp(1), self.dp(22)));
var selectRowLp = new android.widget.LinearLayout.LayoutParams(android.widget.LinearLayout.LayoutParams.MATCH_PARENT, self.dp(58));
selectRowLp.setMargins(0, self.dp(6), 0, 0);
card.addView(selectRow, selectRowLp);
var selectNameTv = new android.widget.TextView(context);
selectNameTv.setTextColor(textColor);
selectNameTv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 13);
selectNameTv.setSingleLine(true);
try { selectNameTv.setEllipsize(android.text.TextUtils.TruncateAt.END); } catch(eEll) {}
selectNameTv.setLayoutParams(new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, 1));
selectRow.addView(selectNameTv);
var selectConfirm = self.ui.createSolidButton(self, "带回小岛", T.primary, T.onPrimary, function() {
self.touchActivity();
try {
if (typeof onSelect === "function") onSelect(selectedName);
} catch(eSelect) {
safeLog(self.L, 'e', "icon onSelect err=" + String(eSelect));
}
dismiss();
});
var confirmLp = new android.widget.LinearLayout.LayoutParams(self.dp(104), self.dp(42));
confirmLp.setMargins(self.dp(10), 0, 0, 0);
selectRow.addView(selectConfirm, confirmLp);
function updateSelectedLabel() {
try {
selectNameTv.setText(selectedName ? ("已选:" + String(selectedName)) : "还没选图标");
} catch(eLabel) { safeLog(null, 'e', "catch " + String(eLabel)); }
}
updateSelectedLabel();
function renderGrid() {
try {
grid.removeAllViews();
var q = String(searchEt.getText() || "");
var matched = filterCatalog(q);
var totalPages = Math.max(1, Math.ceil(matched.length / pageSize));
if (popupState.currentPage >= totalPages) popupState.currentPage = totalPages - 1;
if (popupState.currentPage < 0) popupState.currentPage = 0;
var start = popupState.currentPage * pageSize;
var pageItems = matched.slice(start, start + pageSize);
pageInfo.setText("第 " + (popupState.currentPage + 1) + " / " + totalPages + " 页");
btnPrev.setEnabled(popupState.currentPage > 0);
btnNext.setEnabled(popupState.currentPage < totalPages - 1);
if (pageItems.length === 0) {
var emptyTv = new android.widget.TextView(context);
emptyTv.setText(popupState.filter === "收藏" ? "收藏夹还空着,点图标左上角 ☆ 收藏" : "没有找到这枚小图标");
emptyTv.setTextColor(subTextColor);
emptyTv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
emptyTv.setGravity(android.view.Gravity.CENTER);
emptyTv.setPadding(0, self.dp(60), 0, self.dp(60));
grid.addView(emptyTv);
return;
}
grid.setColumnCount(colCount);
for (var idx = 0; idx < pageItems.length; idx++) {
(function(item) {
var frame = new android.widget.FrameLayout(context);
frame.setClickable(true);
var isSelected = selectedName === item.name;
var frameBg = isSelected ? T.primarySoft : self.withAlpha(cardColor, 0.96);
var frameStroke = isSelected ? self.withAlpha(T.primaryDeep, isDark ? 0.50 : 0.42) : self.withAlpha(T.primaryDeep, isDark ? 0.18 : 0.12);
frame.setBackground(self.ui.createStrokeDrawable(frameBg, frameStroke, isSelected ? self.dp(2) : self.dp(1), self.dp(15)));
var cell = new android.widget.LinearLayout(context);
cell.setOrientation(android.widget.LinearLayout.VERTICAL);
cell.setGravity(android.view.Gravity.CENTER);
cell.setPadding(self.dp(4), self.dp(6), self.dp(4), self.dp(5));
frame.addView(cell, new android.widget.FrameLayout.LayoutParams(android.widget.FrameLayout.LayoutParams.MATCH_PARENT, android.widget.FrameLayout.LayoutParams.MATCH_PARENT));
var iv = new android.widget.ImageView(context);
iv.setLayoutParams(new android.widget.LinearLayout.LayoutParams(iconSize, iconSize));
iv.setScaleType(android.widget.ImageView.ScaleType.FIT_CENTER);
try {
var dr = self.getShortXIconDrawable(item.name);
if (dr) { iv.setImageDrawable(dr); }
} catch(eIcon) { safeLog(null, 'e', "catch " + String(eIcon)); }
cell.addView(iv);
var tv = new android.widget.TextView(context);
tv.setText(String(item.shortName || item.name));
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 9);
tv.setGravity(android.view.Gravity.CENTER);
tv.setMaxLines(1);
try { tv.setEllipsize(android.text.TextUtils.TruncateAt.END); } catch(eTvEll) {}
tv.setPadding(self.dp(2), self.dp(5), self.dp(2), 0);
tv.setTextColor(isSelected ? T.primaryDeep : subTextColor);
cell.addView(tv, new android.widget.LinearLayout.LayoutParams(android.widget.LinearLayout.LayoutParams.MATCH_PARENT, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT));
var favBtn = new android.widget.TextView(context);
favBtn.setText(isFavoriteIcon(item.name) ? "★" : "☆");
favBtn.setTextColor(isFavoriteIcon(item.name) ? T.primaryDeep : self.withAlpha(T.primaryDeep, 0.52));
favBtn.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 13);
favBtn.setGravity(android.view.Gravity.CENTER);
favBtn.setTypeface(null, android.graphics.Typeface.BOLD);
favBtn.setBackground(self.ui.createRoundDrawable(isFavoriteIcon(item.name) ? T.primarySoft : self.withAlpha(T.card, 0.88), self.dp(9)));
favBtn.setClickable(true);
favBtn.setOnClickListener(new android.view.View.OnClickListener({
onClick: function() {
self.touchActivity();
var added = toggleFavoriteIcon(item.name);
try { self.toast(added ? "已收藏到小岛" : "已取消收藏"); } catch(eFavToast) {}
if (popupState.filter === "收藏") popupState.currentPage = 0;
renderGrid();
}
}));
var favLp = new android.widget.FrameLayout.LayoutParams(self.dp(18), self.dp(18));
favLp.gravity = android.view.Gravity.TOP | android.view.Gravity.LEFT;
favLp.setMargins(self.dp(4), self.dp(4), 0, 0);
frame.addView(favBtn, favLp);
if (isSelected) {
var badge = new android.widget.TextView(context);
badge.setText("✓");
badge.setTextColor(T.onPrimary);
badge.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 10);
badge.setGravity(android.view.Gravity.CENTER);
badge.setTypeface(null, android.graphics.Typeface.BOLD);
badge.setBackground(self.ui.createRoundDrawable(T.primary, self.dp(9)));
var badgeLp = new android.widget.FrameLayout.LayoutParams(self.dp(18), self.dp(18));
badgeLp.gravity = android.view.Gravity.TOP | android.view.Gravity.RIGHT;
badgeLp.setMargins(0, self.dp(4), self.dp(4), 0);
frame.addView(badge, badgeLp);
}
frame.setOnClickListener(new android.view.View.OnClickListener({
onClick: function() {
self.touchActivity();
selectedName = item.name;
updateSelectedLabel();
renderGrid();
}
}));
var cellLp = new android.widget.GridLayout.LayoutParams();
cellLp.width = cellW;
cellLp.height = cellH;
var col = idx % colCount;
var mr = (col === colCount - 1) ? 0 : gap;
cellLp.setMargins(0, 0, mr, gap);
frame.setLayoutParams(cellLp);
grid.addView(frame);
})(pageItems[idx]);
}
} catch(eRender) {
safeLog(self.L, 'e', "renderShortXIconGrid err=" + String(eRender));
}
}
searchEt.addTextChangedListener(new android.text.TextWatcher({
beforeTextChanged: function() {},
onTextChanged: function() {
self.touchActivity();
popupState.currentPage = 0;
renderGrid();
},
afterTextChanged: function() {}
}));
renderGrid();
return { close: dismiss };
};

File diff suppressed because it is too large Load Diff

392
code/th_14_schema_editor.js Normal file
View File

@@ -0,0 +1,392 @@
// ToolHub - 高级蓝图编辑器模块
// 依赖th_14_panels.js 的设置页主题/基础 UIth_05_persistence.js 的 ConfigManager。
// 加载顺序th_14_panels.js 之后th_15_extra.js 之前。
FloatBallAppWM.prototype.buildSchemaEditorPanelView = function() {
var self = this;
if (this.state.editingSchemaIndex === undefined) this.state.editingSchemaIndex = null;
if (!this.state.keepSchemaEditorState || !this.state.tempSchema) {
var current = ConfigManager.loadSchema();
this.state.tempSchema = JSON.parse(JSON.stringify(current));
}
this.state.keepSchemaEditorState = false;
var schema = this.state.tempSchema || [];
var isEditing = (this.state.editingSchemaIndex !== null && this.state.editingSchemaIndex !== undefined);
var isDark = this.isDarkTheme();
var C = this.ui.colors;
var T = this.getAnimalIslandTheme ? this.getAnimalIslandTheme() : null;
try { if (this.applySettingsTheme) this.applySettingsTheme(T, isDark, C, this.state.pendingUserCfg || this.config); } catch(eTheme) {}
var bgColor = T && T.bg ? T.bg : (isDark ? C.bgDark : C.bgLight);
var cardColor = T && T.card ? T.card : (isDark ? C.cardDark : C.cardLight);
var textColor = T && T.text ? T.text : (isDark ? C.textPriDark : C.textPriLight);
var subTextColor = T && T.sub ? T.sub : (isDark ? C.textSecDark : C.textSecLight);
var primaryColor = T && T.primary ? T.primary : C.primary;
var primaryDeep = T && T.primaryDeep ? T.primaryDeep : C.primary;
var primarySoft = T && T.primarySoft ? T.primarySoft : self.withAlpha(primaryColor, isDark ? 0.22 : 0.12);
var strokeColor = T && T.stroke ? T.stroke : (isDark ? C.dividerDark : C.dividerLight);
var inputBgColor = isDark ? C.inputBgDark : C.inputBgLight;
var dangerColor = C.danger || C.error || 0xffd32f2f;
function lpFullWrap() {
return new android.widget.LinearLayout.LayoutParams(android.widget.LinearLayout.LayoutParams.MATCH_PARENT, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT);
}
function addWithMargins(parent, view, l, t, r, b) {
var lp = lpFullWrap();
lp.setMargins(self.dp(l || 0), self.dp(t || 0), self.dp(r || 0), self.dp(b || 0));
parent.addView(view, lp);
}
function makeText(txt, sp, color, bold) {
var tv = new android.widget.TextView(context);
tv.setText(String(txt || ""));
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, sp || 13);
tv.setTextColor(color || textColor);
if (bold) tv.setTypeface(null, android.graphics.Typeface.BOLD);
return tv;
}
function makeChip(txt, color, onClick) {
var tv = new android.widget.TextView(context);
tv.setText(String(txt || ""));
tv.setTextColor(color || primaryDeep);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 12);
tv.setGravity(android.view.Gravity.CENTER);
tv.setTypeface(null, android.graphics.Typeface.BOLD);
tv.setMinHeight(self.dp(32));
tv.setPadding(self.dp(10), 0, self.dp(10), 0);
try { tv.setBackground(self.ui.createStrokeDrawable(self.withAlpha(color || primaryColor, isDark ? 0.16 : 0.08), self.withAlpha(color || primaryColor, isDark ? 0.45 : 0.28), self.dp(1), self.dp(16))); } catch(eBg) {}
if (onClick) {
tv.setOnClickListener(new android.view.View.OnClickListener({ onClick: function(v) {
try { self.touchActivity(); } catch(eTouch) {}
try { onClick(v); } catch(eClick) { safeLog(null, 'e', "schema chip click err=" + String(eClick)); }
}}));
}
return tv;
}
function showToast(msg) { try { self.toast(String(msg || "")); } catch(eToast) {} }
function refreshPanel() {
self.state.keepSchemaEditorState = true;
self.showPanelAvoidBall("schema_editor");
}
function clearDraft() {
self.state.schemaEditingDraft = null;
self.state.schemaEditingDraftIndex = null;
}
function normalizeItem(item) {
var it = item || {};
it.type = String(it.type || "bool");
if (it.type !== "section") {
it.key = String(it.key || "").trim();
}
it.name = String(it.name || it.key || "").trim();
if (it.type === "section") {
delete it.key; delete it.min; delete it.max; delete it.step; delete it.action;
} else if (it.type === "bool" || it.type === "text") {
delete it.min; delete it.max; delete it.step; delete it.action;
} else if (it.type === "int" || it.type === "float") {
delete it.action;
if (it.min === "" || isNaN(Number(it.min))) delete it.min; else it.min = Number(it.min);
if (it.max === "" || isNaN(Number(it.max))) delete it.max; else it.max = Number(it.max);
if (it.step === "" || isNaN(Number(it.step))) delete it.step; else it.step = Number(it.step);
} else if (it.type === "action") {
delete it.min; delete it.max; delete it.step;
it.action = String(it.action || "").trim();
}
return it;
}
function validateItem(item) {
if (!item.name) return "请填写显示名字";
if (item.type !== "section" && !item.key) return "请填写配置键";
if (item.type === "action" && !item.action) return "请填写动作 ID";
return "";
}
var panel = this.ui.createStyledPanel(this, 16);
try { panel.setBackground(this.ui.createRoundDrawable(bgColor, this.dp(20))); } catch(ePanelBg) {}
if (!isEditing) {
var topCard = new android.widget.LinearLayout(context);
topCard.setOrientation(android.widget.LinearLayout.VERTICAL);
topCard.setPadding(self.dp(14), self.dp(12), self.dp(14), self.dp(12));
try { topCard.setBackground(self.ui.createStrokeDrawable(cardColor, self.withAlpha(strokeColor, isDark ? 0.32 : 0.42), self.dp(1), self.dp(20))); } catch(eTopBg) {}
var titleRow = new android.widget.LinearLayout(context);
titleRow.setOrientation(android.widget.LinearLayout.HORIZONTAL);
titleRow.setGravity(android.view.Gravity.CENTER_VERTICAL);
var titleBox = new android.widget.LinearLayout(context);
titleBox.setOrientation(android.widget.LinearLayout.VERTICAL);
titleBox.addView(makeText("岛屿蓝图", 17, textColor, true));
titleBox.addView(makeText("这里会改变设置页结构,建议只在需要整理入口时使用", 12, subTextColor, false));
titleRow.addView(titleBox, new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, 1));
titleRow.addView(makeChip("添加", primaryDeep, function() {
self.state.editingSchemaIndex = -1;
clearDraft();
if (self.state.toolAppActive && self.pushToolAppPage) self.pushToolAppPage("schema_editor");
else refreshPanel();
}));
topCard.addView(titleRow);
var stat = makeText("共 " + schema.length + " 个蓝图项 · 保存后才会生效", 12, subTextColor, false);
stat.setPadding(0, self.dp(8), 0, 0);
topCard.addView(stat);
addWithMargins(panel, topCard, 0, 0, 0, 8);
var scroll = new android.widget.ScrollView(context);
try { scroll.setVerticalScrollBarEnabled(false); scroll.setOverScrollMode(android.view.View.OVER_SCROLL_NEVER); } catch(eScroll) {}
var list = new android.widget.LinearLayout(context);
list.setOrientation(android.widget.LinearLayout.VERTICAL);
list.setPadding(0, self.dp(2), 0, self.dp(10));
if (!schema.length) {
var empty = makeText("蓝图列表为空,可点上方“添加”创建新项。", 13, subTextColor, false);
empty.setGravity(android.view.Gravity.CENTER);
empty.setPadding(self.dp(12), self.dp(24), self.dp(12), self.dp(24));
list.addView(empty, lpFullWrap());
}
for (var i = 0; i < schema.length; i++) {
(function(idx) {
var item = schema[idx] || {};
var card = new android.widget.LinearLayout(context);
card.setOrientation(android.widget.LinearLayout.VERTICAL);
card.setPadding(self.dp(13), self.dp(11), self.dp(13), self.dp(10));
try { card.setBackground(self.ui.createStrokeDrawable(cardColor, self.withAlpha(strokeColor, isDark ? 0.28 : 0.36), self.dp(1), self.dp(18))); card.setElevation(self.dp(1)); } catch(eCardBg) {}
var row = new android.widget.LinearLayout(context);
row.setOrientation(android.widget.LinearLayout.HORIZONTAL);
row.setGravity(android.view.Gravity.CENTER_VERTICAL);
var badge = makeText(item.type === "section" ? "分组" : String(item.type || "项"), 11, primaryDeep, true);
badge.setGravity(android.view.Gravity.CENTER);
badge.setPadding(self.dp(8), self.dp(3), self.dp(8), self.dp(3));
try { badge.setBackground(self.ui.createStrokeDrawable(primarySoft, self.withAlpha(primaryDeep, 0.25), self.dp(1), self.dp(12))); } catch(eBadge) {}
row.addView(badge);
var info = new android.widget.LinearLayout(context);
info.setOrientation(android.widget.LinearLayout.VERTICAL);
info.setPadding(self.dp(10), 0, 0, 0);
var nameTv = makeText(String(item.name || item.key || "未命名蓝图项"), 14, textColor, true);
info.addView(nameTv);
var sub = item.type === "section" ? "分组标题" : ("配置键:" + String(item.key || "未填写"));
info.addView(makeText(sub, 11, subTextColor, false));
row.addView(info, new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, 1));
card.addView(row);
var actions = new android.widget.LinearLayout(context);
actions.setOrientation(android.widget.LinearLayout.HORIZONTAL);
actions.setGravity(android.view.Gravity.RIGHT | android.view.Gravity.CENTER_VERTICAL);
actions.setPadding(0, self.dp(9), 0, 0);
if (idx > 0) actions.addView(makeChip("上搬", subTextColor, function() { var tmp = schema[idx]; schema[idx] = schema[idx - 1]; schema[idx - 1] = tmp; refreshPanel(); }));
if (idx < schema.length - 1) actions.addView(makeChip("下搬", subTextColor, function() { var tmp = schema[idx]; schema[idx] = schema[idx + 1]; schema[idx + 1] = tmp; refreshPanel(); }));
actions.addView(makeChip("编辑", primaryDeep, function() {
self.state.editingSchemaIndex = idx;
clearDraft();
if (self.state.toolAppActive && self.pushToolAppPage) self.pushToolAppPage("schema_editor");
else refreshPanel();
}));
actions.addView(makeChip("移除", dangerColor, function() { schema.splice(idx, 1); refreshPanel(); }));
card.addView(actions);
addWithMargins(list, card, 2, 4, 2, 6);
})(i);
}
scroll.addView(list);
panel.addView(scroll, new android.widget.LinearLayout.LayoutParams(android.widget.LinearLayout.LayoutParams.MATCH_PARENT, 0, 1));
var bottom = new android.widget.LinearLayout(context);
bottom.setOrientation(android.widget.LinearLayout.HORIZONTAL);
bottom.setGravity(android.view.Gravity.CENTER_VERTICAL);
bottom.setPadding(0, self.dp(8), 0, 0);
var resetBtn = makeChip("恢复默认蓝图", dangerColor, function() {
ConfigManager.resetSchema();
self.state.tempSchema = null;
clearDraft();
showToast("已恢复默认蓝图");
refreshPanel();
});
bottom.addView(resetBtn, new android.widget.LinearLayout.LayoutParams(0, self.dp(42), 1));
var saveBtn = self.ui.createSolidButton(self, "保存蓝图", primaryColor, T && T.onPrimary ? T.onPrimary : android.graphics.Color.WHITE, function() {
ConfigManager.saveSchema(schema);
self.state.tempSchema = null;
clearDraft();
showToast("蓝图已保存");
if (self.state.toolAppActive && self.popToolAppPage) {
self.state.editingSchemaIndex = null;
self.popToolAppPage("schema_save_all");
} else {
self.hideAllPanels();
self.showPanelAvoidBall("settings");
}
});
var saveLp = new android.widget.LinearLayout.LayoutParams(0, self.dp(42), 1);
saveLp.setMargins(self.dp(8), 0, 0, 0);
bottom.addView(saveBtn, saveLp);
panel.addView(bottom, lpFullWrap());
return panel;
}
var editIdx = this.state.editingSchemaIndex;
if (this.state.schemaEditingDraftIndex !== editIdx || !this.state.schemaEditingDraft) {
var baseItem = (editIdx === -1) ? { type: "bool", name: "", key: "" } : (schema[editIdx] ? JSON.parse(JSON.stringify(schema[editIdx])) : { type: "bool", name: "", key: "" });
this.state.schemaEditingDraft = baseItem;
this.state.schemaEditingDraftIndex = editIdx;
}
var editItem = this.state.schemaEditingDraft;
if (!editItem.type) editItem.type = "bool";
var formCard = new android.widget.LinearLayout(context);
formCard.setOrientation(android.widget.LinearLayout.VERTICAL);
formCard.setPadding(self.dp(14), self.dp(12), self.dp(14), self.dp(12));
try { formCard.setBackground(self.ui.createStrokeDrawable(cardColor, self.withAlpha(strokeColor, isDark ? 0.32 : 0.42), self.dp(1), self.dp(20))); } catch(eFormBg) {}
var editTitle = makeText(editIdx === -1 ? "添加蓝图项" : "整理蓝图项", 17, textColor, true);
formCard.addView(editTitle);
var editHint = makeText("修改后先“暂存”,回到列表再统一保存蓝图。", 12, subTextColor, false);
editHint.setPadding(0, self.dp(4), 0, self.dp(10));
formCard.addView(editHint);
var schemaInlineNotice = makeText("", 12, primaryDeep, false);
schemaInlineNotice.setPadding(self.dp(12), self.dp(8), self.dp(12), self.dp(8));
schemaInlineNotice.setVisibility(android.view.View.GONE);
function updateSchemaInlineNotice(msg, kind) {
try {
var color = String(kind || "info") === "error" ? dangerColor : primaryDeep;
schemaInlineNotice.setText(String(msg || ""));
schemaInlineNotice.setTextColor(color);
schemaInlineNotice.setBackground(self.ui.createStrokeDrawable(self.withAlpha(color, isDark ? 0.18 : 0.08), self.withAlpha(color, isDark ? 0.42 : 0.25), self.dp(1), self.dp(14)));
schemaInlineNotice.setVisibility(android.view.View.VISIBLE);
} catch(eNotice) {}
}
formCard.addView(schemaInlineNotice, lpFullWrap());
var scroll2 = new android.widget.ScrollView(context);
try { scroll2.setVerticalScrollBarEnabled(false); scroll2.setOverScrollMode(android.view.View.OVER_SCROLL_NEVER); } catch(eScroll2) {}
var form = new android.widget.LinearLayout(context);
form.setOrientation(android.widget.LinearLayout.VERTICAL);
scroll2.addView(form);
function createInput(label, key, inputType, hint) {
var box = new android.widget.LinearLayout(context);
box.setOrientation(android.widget.LinearLayout.VERTICAL);
box.setPadding(0, 0, 0, self.dp(10));
box.addView(makeText(label, 12, subTextColor, false));
var et = new android.widget.EditText(context);
et.setText(String(editItem[key] !== undefined && editItem[key] !== null ? editItem[key] : ""));
et.setTextColor(textColor);
et.setHint(String(hint || ""));
et.setHintTextColor(self.withAlpha(subTextColor, 0.55));
et.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
et.setSingleLine(true);
et.setBackground(self.ui.createStrokeDrawable(inputBgColor, self.withAlpha(strokeColor, 0.55), self.dp(1), self.dp(12)));
et.setPadding(self.dp(12), self.dp(7), self.dp(12), self.dp(7));
if (inputType) et.setInputType(inputType);
box.addView(et, lpFullWrap());
form.addView(box, lpFullWrap());
return { input: et, getValue: function() { return String(et.getText()); } };
}
var inputName = null;
var inputKey = null;
var inputMin = null;
var inputMax = null;
var inputStep = null;
var inputAction = null;
function syncDraftFromInputs() {
try {
if (inputName) editItem.name = inputName.getValue();
if (inputKey) editItem.key = inputKey.getValue();
if (inputMin) editItem.min = inputMin.getValue();
if (inputMax) editItem.max = inputMax.getValue();
if (inputStep) editItem.step = inputStep.getValue();
if (inputAction) editItem.action = inputAction.getValue();
self.state.schemaEditingDraft = editItem;
self.state.schemaEditingDraftIndex = editIdx;
} catch(eSync) { safeLog(null, 'e', "schema draft sync err=" + String(eSync)); }
}
var typeRow = new android.widget.LinearLayout(context);
typeRow.setOrientation(android.widget.LinearLayout.HORIZONTAL);
typeRow.setGravity(android.view.Gravity.CENTER_VERTICAL);
typeRow.setPadding(0, 0, 0, self.dp(10));
typeRow.addView(makeText("项目类型", 12, subTextColor, false), new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, 1));
var types = ["section", "bool", "int", "float", "text", "action"];
var typeNames = { section: "分组标题", bool: "开关", int: "整数", float: "小数", text: "文本", action: "动作入口" };
typeRow.addView(makeChip(typeNames[String(editItem.type)] || String(editItem.type), primaryDeep, function() {
syncDraftFromInputs();
var currIdx = types.indexOf(String(editItem.type || "bool"));
if (currIdx < 0) currIdx = 1;
editItem.type = types[(currIdx + 1) % types.length];
self.state.schemaEditingDraft = editItem;
self.state.schemaEditingDraftIndex = editIdx;
refreshPanel();
}));
form.addView(typeRow, lpFullWrap());
inputName = createInput("显示名字", "name", android.text.InputType.TYPE_CLASS_TEXT, "例如:漂浮气球");
if (editItem.type !== "section") {
inputKey = createInput("配置键", "key", android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS, "例如BALL_SIZE_DP");
}
if (editItem.type === "int" || editItem.type === "float") {
inputMin = createInput("最小值", "min", android.text.InputType.TYPE_CLASS_NUMBER | android.text.InputType.TYPE_NUMBER_FLAG_DECIMAL | android.text.InputType.TYPE_NUMBER_FLAG_SIGNED, "可留空");
inputMax = createInput("最大值", "max", android.text.InputType.TYPE_CLASS_NUMBER | android.text.InputType.TYPE_NUMBER_FLAG_DECIMAL | android.text.InputType.TYPE_NUMBER_FLAG_SIGNED, "可留空");
inputStep = createInput("步进", "step", android.text.InputType.TYPE_CLASS_NUMBER | android.text.InputType.TYPE_NUMBER_FLAG_DECIMAL, "可留空");
}
if (editItem.type === "action") {
inputAction = createInput("动作 ID", "action", android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS, "例如open_btn_mgr");
}
formCard.addView(scroll2, new android.widget.LinearLayout.LayoutParams(android.widget.LinearLayout.LayoutParams.MATCH_PARENT, 0, 1));
panel.addView(formCard, new android.widget.LinearLayout.LayoutParams(android.widget.LinearLayout.LayoutParams.MATCH_PARENT, 0, 1));
var editBottom = new android.widget.LinearLayout(context);
editBottom.setOrientation(android.widget.LinearLayout.HORIZONTAL);
editBottom.setGravity(android.view.Gravity.CENTER_VERTICAL);
editBottom.setPadding(0, self.dp(8), 0, 0);
editBottom.addView(makeChip("不改了", subTextColor, function() {
clearDraft();
self.state.editingSchemaIndex = null;
if (self.state.toolAppActive && self.popToolAppPage) {
self.state.keepSchemaEditorState = true;
self.popToolAppPage("schema_edit_back");
} else refreshPanel();
}), new android.widget.LinearLayout.LayoutParams(0, self.dp(42), 1));
var saveDraftBtn = self.ui.createSolidButton(self, "暂存蓝图项", primaryColor, T && T.onPrimary ? T.onPrimary : android.graphics.Color.WHITE, function() {
try {
syncDraftFromInputs();
normalizeItem(editItem);
var err = validateItem(editItem);
if (err) {
updateSchemaInlineNotice(err, "error");
try { scroll2.fullScroll(android.view.View.FOCUS_UP); } catch(eFocusUp) {}
return;
}
if (editIdx === -1) schema.push(JSON.parse(JSON.stringify(editItem)));
else schema[editIdx] = JSON.parse(JSON.stringify(editItem));
clearDraft();
self.state.editingSchemaIndex = null;
if (self.state.toolAppActive && self.popToolAppPage) {
self.state.keepSchemaEditorState = true;
self.popToolAppPage("schema_edit_save");
} else refreshPanel();
} catch(eSave) {
updateSchemaInlineNotice("暂存失败:" + String(eSave), "error");
try { scroll2.fullScroll(android.view.View.FOCUS_UP); } catch(eFocusUp2) {}
}
});
var saveDraftLp = new android.widget.LinearLayout.LayoutParams(0, self.dp(42), 1);
saveDraftLp.setMargins(self.dp(8), 0, 0, 0);
editBottom.addView(saveDraftBtn, saveDraftLp);
panel.addView(editBottom, lpFullWrap());
return panel;
};

File diff suppressed because it is too large Load Diff

View File

@@ -128,17 +128,6 @@ FloatBallAppWM.prototype.close = function() {
safeLog(this.L, 'i', "close done");
// # 清理日志定时器
try {
if (this.L) {
try { this.L._flushBuffer(); } catch (eFlushLog0) { safeLog(this.L, 'e', "logger flush fail: " + String(eFlushLog0)); }
if (this.L._flushTimer) {
this.L._flushTimer.cancel();
this.L._flushTimer = null;
}
}
} catch (eLog) {}
// # 清空缓存
try {
this._iconLru = null;
@@ -229,7 +218,7 @@ FloatBallAppWM.prototype.startAsync = function(entryProcInfo, closeRule) {
if (act === android.content.Intent.ACTION_CONFIGURATION_CHANGED) {
self.cancelDockTimer();
self.onScreenChangedReflow();
self.scheduleScreenReflow("configuration_changed");
self.touchActivity();
}

View File

@@ -2,71 +2,83 @@
"alg": "SHA256withRSA",
"files": {
"th_01_base.js": {
"sha256": "30c121902ef5a0006a5daabca8443ed6cbf6e2508209604c48f89855d0690034",
"size": 52546
"sha256": "e465a92b21f6819079255719bfe41c28a42ff3716f1d29b399897bc299ef92ec",
"size": 54633
},
"th_02_core.js": {
"sha256": "f44f88f0ce3f44f0d1675e55a9f0b66d831caa7819dda54aef6e308ec27faaeb",
"size": 3934
"sha256": "7bfd21df21c137595c3e2a8724b3eb3f0cce82ef0dd18b732de08e9be30b2ba3",
"size": 4631
},
"th_03_icon.js": {
"sha256": "717f7f37474d3616c2cd944581455f600020a850ec8812100d0546ec1302c987",
"size": 5598
},
"th_04_theme.js": {
"sha256": "b815acfee30e56458e61c033ab3fddfe5d8d0c52ec172c419bad7187fcb341ba",
"size": 36083
"sha256": "16a7121c734fccae17b9f8a23c1f0758d3a765541d1de4d6159747fc92bc26a4",
"size": 42568
},
"th_05_persistence.js": {
"sha256": "d80787c2810839ebbe499e93db3df33d6e8d2d6b6ae71644ce351db0f36e4d3e",
"size": 14077
"sha256": "7fd2a62275bd26fcd940202057480fca7a8a800eaeb1fcb9d52003e255d1ef60",
"size": 14763
},
"th_06_icon_parser.js": {
"sha256": "25b95a5df634a7ee359f3ab798e4d3154a71c24016f7b4bf8a658096644b2484",
"size": 22909
},
"th_07_shortcut.js": {
"sha256": "7b2dbd1e35c636cca4ccce335dfb9e0b972342972ce012116ff4bbcfc438caa1",
"size": 72992
"sha256": "8e70adda0d73930e2afc1eba488e95016fc2f739ba199834a30bec015fa6156c",
"size": 73011
},
"th_08_content.js": {
"sha256": "8a76f15dfd1292081cba4b2dd218424be66540350e2807065421a6176a86c2db",
"size": 7938
},
"th_09_animation.js": {
"sha256": "7120d208910955a2a163c4ad535b2eca674f7a0c2c462ef2f03bad11c9511933",
"size": 27541
"sha256": "7c4b673869978f9015b8641bd2266fc7bbd2131f96338b4bb9c12fab11c50468",
"size": 41346
},
"th_10_shell.js": {
"sha256": "0ed793079c2f6ba7d29f4c0d411705cb72419f45f572cbe37ed32ac16527a8bc",
"size": 1094
},
"th_11_action.js": {
"sha256": "a0142d26621f3d076bd1b749f2885af2c0806c9f206e362a3b3680a5d2312b31",
"size": 13545
"sha256": "1ba348415e891093bcf49619d950e9653e1c7323c3d35f74f6f71bec14b7c9e6",
"size": 13838
},
"th_12_rebuild.js": {
"sha256": "7b820e813d2dd8866778fefe8bfeb6aca227bb1a32a89d318de830178f19824f",
"size": 2362
},
"th_13_panel_ui.js": {
"sha256": "19e5e1c346051aafdda6253ed7c25ef5fbc4f883de66f656c9575d97e9d6ffc8",
"size": 20386
"sha256": "9052e01ccdc0af29615ef8e72550d7d312240f9ab36ebfb66e673c92ab2e4b9c",
"size": 22308
},
"th_14_color_picker.js": {
"sha256": "d3ca89146e31e611e17d703ae9834d82794321e651b6ef156ade006227d231e7",
"size": 23587
},
"th_14_icon_picker.js": {
"sha256": "1df5c4bf82e68de24d7b89c852a4d9878768451effe086e8ddb86620ecc98c35",
"size": 23906
},
"th_14_panels.js": {
"sha256": "cf18cb06a9e221b671360f94040542c1c27e8776bb5037abe4f4b0f3de3e1073",
"size": 217347
"sha256": "1af01633720865a83a10a1e5261e3738d91be310195b014e17e579893eae90ee",
"size": 267207
},
"th_14_schema_editor.js": {
"sha256": "5669d0b5a16f770bed24eedee24203df57f7cbc7910c840931e533adac1ef146",
"size": 20484
},
"th_15_extra.js": {
"sha256": "ed56b19a5a798a785c024eb931140ded69d16921d2e87ad4ccd861df1c1907d8",
"size": 62936
"sha256": "5b39db203b748f9d9919b6e19fc9fae5b5b5dcce6f46ade0f8e5cd2729f223aa",
"size": 123635
},
"th_16_entry.js": {
"sha256": "e7c99c3dfbd6aedab05551426955081ae6cae034754f2f557cefa01dc75dc001",
"size": 12777
"sha256": "652aa70214a9419923785e528a067d3828094fde48fc9c8c57cfda1e08206e25",
"size": 12479
}
},
"keyId": "toolhub-targets-2026-rsa3072",
"schema": 2,
"version": 20260512022403
"version": 20260522191219
}

View File

@@ -1 +1 @@
mNKpW6JxqzeRVFYnEjJcxIogH2bMLbR5rExVdGHMQcOXhtAbamkl6ZLEtUG534wweU95Amku8lxWO+Pe5p2Bg7ufExHqtBgm3eFsDtftGlG7KYOakPvCluGcWZivGMrxFdUq4CsF/Y4dhi9UG9Dtr2X8UdnKQRXLIMtPW+EpDvZvheg4G+7hsEKOYyRzkX1HK8/KEUaaiDogr7Hp+rT0jPa0iCMBn3fiLAIODYMWj0mzjML7WqtBOwXBoLGSeHxJ7BwJd1USxQh+pMo0/jxf/Uas7oTue67bYpyub9v2ESAfiLIg+qijP9oQRnptvtSA99c0Qj/8BJLiXIl6AD1Qp+PhnJwmGgQg62OtJdp7ly0zPz51UbULXfPwqj9djguac1yN7qVGQroT2oo93brZMpV3iRwQw2ov2E/efFf4iXSXnd/aMbzozUNQtqINnsWfZsrjBNxuDLLHvuSUh5h19/Yok+5EczPL5iZDl/W1GNmurrDvOhnTCZjuVS7t97Zt
c7CEARIR2JCXq4UkAGC9hQ2L7q9fM1CbSAZY5RazdoIijLBSNuXEmEgYVmlJNuJ4+hfU5vTCXgU8AkTL/NG4Q17fm27uFqQLkzqwb/9stQHvs3I4By9fWm11w9pcr17620vHe8alqjLslmLijUy0GSKy3r+nVQKFYWmveem5tdZNm6dXxbMG+2ePuPkc+CP02/vOQz9kx8C2eCFBjlcS+x/lOlpVYWXAqGGV7SseymOLe4RkGozz84p3fl8snFNOjA9F7CThAdEbd/UrC6UAM4VhVoaRuwHblWyxjTiRL4+OluIHWRnFDMpB7R3u/kp+mnon0v5gNlN4QIIVo4/jgVbWirsRAzw5+UM6Zft7T1P/j8MXAv8BrQzmMElU71vzReWyEmWvu6BjUi88R55IlZ8Fu5xjm6ttXCW0fB2miZ0Je52E3PdYKGzLMULtv2M07X71oSdqjd60g25R1ulGph6izuWKwxnabcSRLnvjgdhGl2qJUvMAn0LjCeARPFNC

View File

@@ -28,7 +28,8 @@ MODULES = [
"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",
"th_14_panels.js", "th_14_color_picker.js", "th_14_icon_picker.js",
"th_14_schema_editor.js", "th_15_extra.js", "th_16_entry.js",
]

View File

@@ -0,0 +1,29 @@
#!/usr/bin/env python3
import pathlib
import re
import sys
root = pathlib.Path(__file__).resolve().parents[1]
text = (root / "code" / "th_14_panels.js").read_text(encoding="utf-8")
checks = [
("manager has action chip helper for search", "createButtonManagerActionChip" in text),
("manager has text action helper for card actions", "createButtonManagerTextAction" in text),
("manager card has two-row layout marker", "按钮管理列表卡片:上信息、下操作" in text),
("manager has search surface marker", "按钮管理搜索卡片" in text),
("manager no false long-press sort text", "长按卡片排序" not in text),
("manager no homepage block text", "按钮管理首页" not in text),
("manager header count only", '" + buttons.length + " 个按钮' in text or '" + buttons.length + " 个工具伙伴' in text),
("manager list footer uses remaining-height scroll", "避免“取消更改/保存所有”被挤到屏幕外" in text and "scrollLp.weight = 1" in text),
("manager list footer has equal buttons and bottom gap", "btnListCancelLp.weight = 1" in text and "btnListSaveLp.weight = 1" in text and "listBottomLp.setMargins(0, self.dp(6), 0, self.dp(12))" in text),
("editor no useless workbench", "按钮编辑工作台" not in text and "createButtonEditorHeroCard" not in text),
("editor has field spacing helper", "addButtonEditorField" in text),
("editor fixed footer has equal buttons", "btnCancelLp.weight = 1" in text and "btnSaveLp.weight = 1" in text and "self.dp(44)" in text),
("editor section descriptions are concise", "按钮名称" in text and "图标和颜色" in text and "点击后做什么" in text and "先填写名称,便于在按钮管理列表中识别" not in text and "选择点击后执行的动作类型与参数" not in text),
]
failed = [name for name, ok in checks if not ok]
if failed:
print("Button manager/editor layout verification FAILED:")
for name in failed:
print(" - " + name)
sys.exit(1)
print("Button manager/editor layout verification OK")

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env python3
import pathlib
import re
import sys
root = pathlib.Path(__file__).resolve().parents[1]
extra = (root / "code" / "th_15_extra.js").read_text(encoding="utf-8")
checks = []
def check(name, ok):
checks.append((name, bool(ok)))
check("has calculateToolAppLayout", "FloatBallAppWM.prototype.calculateToolAppLayout" in extra)
check("layout uses screen orientation", "isLandscape" in extra and "shortSide" in extra and "longSide" in extra)
check("layout has tiny screen branch", "shortSide < this.dp(420)" in extra)
check("layout has tablet/large screen branch", "shortSide >= this.dp(720)" in extra)
check("layout applies margins", "marginX" in extra and "marginTop" in extra and "marginBottom" in extra)
show_match = re.search(r"FloatBallAppWM\.prototype\.showToolApp\s*=\s*function\([^)]*\)\s*\{(?P<body>.*?)\n\};", extra, re.S)
show_body = show_match.group("body") if show_match else ""
check("showToolApp exists", bool(show_match))
check("showToolApp uses calculateToolAppLayout", "this.calculateToolAppLayout" in show_body)
check("showToolApp applies adaptive width", "layout.width" in show_body and "layout.height" in show_body)
check("showToolApp updates x/y when reusing root", "viewerPanelLp.x = layout.x" in show_body and "viewerPanelLp.y = layout.y" in show_body)
check("showToolApp addPanel uses adaptive x/y", "this.addPanel(shell, layout.x, layout.y" in show_body)
check("showToolApp no fixed 0.92/0.82 sizing", "0.92" not in show_body and "0.82" not in show_body)
failed = [name for name, ok in checks if not ok]
if failed:
print("ToolApp adaptive-size verification FAILED:")
for name in failed:
print(" - " + name)
sys.exit(1)
print("ToolApp adaptive-size verification OK")

View File

@@ -0,0 +1,37 @@
#!/usr/bin/env python3
import pathlib
import re
import sys
root = pathlib.Path(__file__).resolve().parents[1]
core = (root / "code" / "th_02_core.js").read_text(encoding="utf-8")
extra = (root / "code" / "th_15_extra.js").read_text(encoding="utf-8")
checks = []
def check(name, ok):
checks.append((name, bool(ok)))
check("state keeps a single ToolApp root view", "toolAppRoot" in core)
check("state keeps ToolApp content host", "toolAppContentHost" in core)
check("state keeps ToolApp title view", "toolAppTitleView" in core)
check("state keeps ToolApp back button", "toolAppBackButton" in core)
check("has ensureToolAppShell", "FloatBallAppWM.prototype.ensureToolAppShell" in extra)
check("has updateToolAppShellChrome", "FloatBallAppWM.prototype.updateToolAppShellChrome" in extra)
check("has setToolAppContent", "FloatBallAppWM.prototype.setToolAppContent" in extra)
show_match = re.search(r"FloatBallAppWM\.prototype\.showToolApp\s*=\s*function\([^)]*\)\s*\{(?P<body>.*?)\n\};", extra, re.S)
show_body = show_match.group("body") if show_match else ""
check("showToolApp exists", bool(show_match))
check("showToolApp does not remove viewer on page switch", "this.hideViewerPanel();" not in show_body)
check("showToolApp ensures shell once", "this.ensureToolAppShell" in show_body)
check("showToolApp swaps content host", "this.setToolAppContent" in show_body)
check("showToolApp only addPanel when shell not added", "!this.state.addedViewer" in show_body and "this.addPanel" in show_body)
failed = [name for name, ok in checks if not ok]
if failed:
print("ToolApp single-root verification FAILED:")
for name in failed:
print(" - " + name)
sys.exit(1)
print("ToolApp single-root verification OK")