refactor: split th_2_core.js into 12 modules, rename all files to 2-digit numbering
- Split th_2_core.js (4715 lines, 177KB) into: th_02_core.js, th_03_icon.js, th_04_theme.js, th_05_persistence.js, th_06_icon_parser.js, th_07_shortcut.js, th_08_content.js, th_09_animation.js, th_10_shell.js, th_11_action.js, th_12_rebuild.js, th_13_panel_ui.js - Rename existing: th_1_base→th_01_base, th_3_panels→th_14_panels, th_4_extra→th_15_extra, th_5_entry→th_16_entry - Update ToolHub.js MODULE_MANIFEST, modules array, and critical module check
This commit is contained in:
28
ToolHub.js
28
ToolHub.js
@@ -2,11 +2,22 @@
|
||||
// 将本文件放入 ShortX 任务,th_*.js 放入 ShortX 数据根目录/ToolHub/code/ 文件夹
|
||||
|
||||
var MODULE_MANIFEST = {
|
||||
"th_1_base.js": "1.0.0",
|
||||
"th_2_core.js": "1.0.0",
|
||||
"th_3_panels.js": "1.0.0",
|
||||
"th_4_extra.js": "1.0.0",
|
||||
"th_5_entry.js": "1.0.0"
|
||||
"th_01_base.js": "1.0.0",
|
||||
"th_02_core.js": "1.0.0",
|
||||
"th_03_icon.js": "1.0.0",
|
||||
"th_04_theme.js": "1.0.0",
|
||||
"th_05_persistence.js": "1.0.0",
|
||||
"th_06_icon_parser.js": "1.0.0",
|
||||
"th_07_shortcut.js": "1.0.0",
|
||||
"th_08_content.js": "1.0.0",
|
||||
"th_09_animation.js": "1.0.0",
|
||||
"th_10_shell.js": "1.0.0",
|
||||
"th_11_action.js": "1.0.0",
|
||||
"th_12_rebuild.js": "1.0.0",
|
||||
"th_13_panel_ui.js": "1.0.0",
|
||||
"th_14_panels.js": "1.0.0",
|
||||
"th_15_extra.js": "1.0.0",
|
||||
"th_16_entry.js": "1.0.0"
|
||||
};
|
||||
|
||||
var GIT_BASE = "https://git.xin-blog.com/linshenjianlu/ShortX_ToolHub/raw/branch/main/code/";
|
||||
@@ -187,7 +198,10 @@ function loadScript(relPath) {
|
||||
}
|
||||
}
|
||||
|
||||
var modules = ["th_1_base.js", "th_2_core.js", "th_3_panels.js", "th_4_extra.js", "th_5_entry.js"];
|
||||
var modules = ["th_01_base.js", "th_02_core.js", "th_03_icon.js", "th_04_theme.js", "th_05_persistence.js",
|
||||
"th_06_icon_parser.js", "th_07_shortcut.js", "th_08_content.js", "th_09_animation.js",
|
||||
"th_10_shell.js", "th_11_action.js", "th_12_rebuild.js", "th_13_panel_ui.js",
|
||||
"th_14_panels.js", "th_15_extra.js", "th_16_entry.js"];
|
||||
var loadErrors = [];
|
||||
for (var i = 0; i < modules.length; i++) {
|
||||
try {
|
||||
@@ -195,7 +209,7 @@ for (var i = 0; i < modules.length; i++) {
|
||||
} catch (e) {
|
||||
writeLog("Module load failed: " + modules[i] + " -> " + String(e));
|
||||
loadErrors.push({ module: modules[i], err: String(e) });
|
||||
if (modules[i] === "th_5_entry.js") {
|
||||
if (modules[i] === "th_16_entry.js") {
|
||||
throw "Critical module failed: " + modules[i];
|
||||
}
|
||||
}
|
||||
|
||||
123
code/th_02_core.js
Normal file
123
code/th_02_core.js
Normal file
@@ -0,0 +1,123 @@
|
||||
// @version 1.0.0
|
||||
function FloatBallAppWM(logger) {
|
||||
this.L = logger || null;
|
||||
|
||||
// # 加载配置
|
||||
this.config = ConfigManager.loadSettings();
|
||||
this.currentPanelKey = "main";
|
||||
this.panels = { main: ConfigManager.loadButtons() };
|
||||
|
||||
// # 更新 Logger 配置(因为 Logger 初始化时是默认值)
|
||||
if (this.L) this.L.updateConfig(this.config);
|
||||
|
||||
this.state = {
|
||||
receivers: [], // 存储广播接收器引用,用于 close 时注销
|
||||
wm: null,
|
||||
dm: null,
|
||||
density: 1.0,
|
||||
|
||||
screen: { w: 0, h: 0 },
|
||||
lastRotation: -1,
|
||||
lastMonitorTs: 0,
|
||||
|
||||
ht: null,
|
||||
h: null,
|
||||
|
||||
addedBall: false,
|
||||
addedPanel: false,
|
||||
addedSettings: false,
|
||||
addedViewer: false,
|
||||
addedMask: false,
|
||||
|
||||
ballRoot: null,
|
||||
ballContent: null,
|
||||
ballLp: null,
|
||||
|
||||
panel: null,
|
||||
panelLp: null,
|
||||
|
||||
settingsPanel: null,
|
||||
settingsPanelLp: null,
|
||||
|
||||
viewerPanel: null,
|
||||
viewerPanelLp: null,
|
||||
|
||||
mask: null,
|
||||
maskLp: null,
|
||||
|
||||
loadedPos: null,
|
||||
lastSaveTs: 0,
|
||||
|
||||
dragging: false,
|
||||
rawX: 0,
|
||||
rawY: 0,
|
||||
downX: 0,
|
||||
downY: 0,
|
||||
|
||||
docked: false,
|
||||
dockSide: null,
|
||||
|
||||
lastMotionTs: 0,
|
||||
idleDockRunnable: null,
|
||||
|
||||
longPressArmed: false,
|
||||
longPressTriggered: false,
|
||||
longPressRunnable: null,
|
||||
|
||||
displayListener: null,
|
||||
|
||||
// # 设置面板:临时编辑缓存
|
||||
pendingUserCfg: null,
|
||||
pendingDirty: false,
|
||||
|
||||
closing: false
|
||||
};
|
||||
|
||||
// # 创建实例独立的 UI 工具对象,避免多实例共享颜色状态
|
||||
this.ui = {};
|
||||
var protoUi = FloatBallAppWM.prototype.ui;
|
||||
for (var _uiKey in protoUi) {
|
||||
this.ui[_uiKey] = protoUi[_uiKey];
|
||||
}
|
||||
this.ui.colors = {};
|
||||
|
||||
// # 初始化莫奈动态配色(传入当前主题避免重复检测)
|
||||
try { this.refreshMonetColors(this.isDarkTheme()); } catch (eM) {}
|
||||
}
|
||||
|
||||
// =======================【工具:dp/now/clamp】======================
|
||||
FloatBallAppWM.prototype.dp = function(v) { return Math.floor(Number(v) * this.state.density); };
|
||||
FloatBallAppWM.prototype.sp = function(v) { try { return Math.floor(Number(v) * context.getResources().getDisplayMetrics().scaledDensity); } catch (e) { return Math.floor(Number(v) * this.state.density); } };
|
||||
FloatBallAppWM.prototype.now = function() { return new Date().getTime(); };
|
||||
FloatBallAppWM.prototype.clamp = function(v, min, max) { if (v < min) return min; if (v > max) return max; return v; };
|
||||
FloatBallAppWM.prototype.rectIntersect = function(ax, ay, aw, ah, bx, by, bw, bh) {
|
||||
return !(ax + aw <= bx || bx + bw <= ax || ay + ah <= by || by + bh <= ay);
|
||||
};
|
||||
|
||||
// # 这段代码的主要内容/用途:安全地在 UI 线程执行代码(用于后台线程回调更新 UI)
|
||||
FloatBallAppWM.prototype.runOnUiThreadSafe = function(fn) {
|
||||
try {
|
||||
if (!fn) return;
|
||||
var self = this;
|
||||
// 优先使用 Activity 的 runOnUiThread,否则使用 View.post
|
||||
if (this.state && this.state.ballRoot) {
|
||||
this.state.ballRoot.post(new java.lang.Runnable({
|
||||
run: function() { try { fn.call(self); } catch(e) {} }
|
||||
}));
|
||||
} else {
|
||||
// 兜底:直接执行(如果已经在 UI 线程)或尝试使用 Handler
|
||||
try {
|
||||
var h = new android.os.Handler(android.os.Looper.getMainLooper());
|
||||
h.post(new java.lang.Runnable({
|
||||
run: function() { try { fn.call(self); } catch(e) {} }
|
||||
}));
|
||||
} catch(e) {
|
||||
// 最后兜底:直接执行
|
||||
try { fn.call(self); } catch(e2) {}
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
};
|
||||
|
||||
// # 这段代码的主要内容/用途:统一图标缓存(LRU),减少反复解码/反复走 PackageManager,降低卡顿与内存波动(不改变 UI 与功能)
|
||||
// 优化后的图标缓存(带 Bitmap 回收,防止内存泄漏)
|
||||
170
code/th_03_icon.js
Normal file
170
code/th_03_icon.js
Normal file
@@ -0,0 +1,170 @@
|
||||
// @version 1.0.0
|
||||
FloatBallAppWM.prototype._iconCache = {
|
||||
map: {},
|
||||
keys: [],
|
||||
max: 80, // 减少缓存数量,降低内存压力
|
||||
|
||||
get: function(key) {
|
||||
var item = this.map[key];
|
||||
if (!item) return null;
|
||||
// 移动到末尾(最近使用)
|
||||
var idx = this.keys.indexOf(key);
|
||||
if (idx > -1) {
|
||||
this.keys.splice(idx, 1);
|
||||
this.keys.push(key);
|
||||
}
|
||||
return item.dr;
|
||||
},
|
||||
|
||||
put: function(key, drawable) {
|
||||
// 清理旧的
|
||||
if (this.map[key]) {
|
||||
this._remove(key);
|
||||
}
|
||||
|
||||
// 空间检查:超过 80% 时批量清理 20%
|
||||
if (this.keys.length >= this.max * 0.8) {
|
||||
var removeCount = Math.floor(this.max * 0.2);
|
||||
for (var i = 0; i < removeCount && this.keys.length > 0; i++) {
|
||||
var oldKey = this.keys.shift();
|
||||
this._remove(oldKey);
|
||||
}
|
||||
}
|
||||
|
||||
this.keys.push(key);
|
||||
this.map[key] = {dr: drawable, ts: Date.now()};
|
||||
},
|
||||
|
||||
_remove: function(key) {
|
||||
var item = this.map[key];
|
||||
if (item && item.dr) {
|
||||
// 关键:回收 Bitmap,防止内存泄漏
|
||||
try {
|
||||
if (item.dr instanceof android.graphics.drawable.BitmapDrawable) {
|
||||
var bmp = item.dr.getBitmap();
|
||||
if (bmp && !bmp.isRecycled()) bmp.recycle();
|
||||
}
|
||||
} catch(e) {}
|
||||
delete this.map[key];
|
||||
}
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
for (var i = 0; i < this.keys.length; i++) {
|
||||
this._remove(this.keys[i]);
|
||||
}
|
||||
this.keys = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 兼容性封装(保持原有调用方式不变)
|
||||
FloatBallAppWM.prototype._iconLruEnsure = function() {};
|
||||
FloatBallAppWM.prototype._iconLruGet = function(key) {
|
||||
return this._iconCache.get(key);
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype._iconLruPut = function(key, val) {
|
||||
try {
|
||||
this._iconLruEnsure(120);
|
||||
var k = String(key || "");
|
||||
if (!k) return;
|
||||
if (val == null) return;
|
||||
|
||||
// # 若已存在,先移除旧顺序位置
|
||||
try {
|
||||
var ord = this._iconLru.order;
|
||||
for (var i = ord.length - 1; i >= 0; i--) {
|
||||
if (ord[i] === k) { ord.splice(i, 1); break; }
|
||||
}
|
||||
ord.push(k);
|
||||
} catch (eLru3) {}
|
||||
|
||||
this._iconLru.map[k] = val;
|
||||
|
||||
// # 超限清理:按最久未使用淘汰
|
||||
try {
|
||||
var maxN = Math.max(20, Math.floor(Number(this._iconLru.max || 120)));
|
||||
var ord2 = this._iconLru.order;
|
||||
while (ord2.length > maxN) {
|
||||
var oldK = ord2.shift();
|
||||
if (oldK != null) {
|
||||
try { delete this._iconLru.map[oldK]; } catch (eDel) {}
|
||||
}
|
||||
}
|
||||
} catch (eLru4) {}
|
||||
} catch (eLru5) {}
|
||||
};
|
||||
|
||||
|
||||
// =======================【工具:悬浮球图标(PNG 文件)】======================
|
||||
// # 这段代码的主要内容/用途:从指定路径加载透明 PNG 作为悬浮球图标;带"文件大小/像素上限"保护;按目标尺寸采样解码,避免 system_server OOM。
|
||||
FloatBallAppWM.prototype.loadBallIconDrawableFromFile = function(path, targetPx, maxBytes, maxPx) {
|
||||
try {
|
||||
var p = String(path || "");
|
||||
if (!p) return null;
|
||||
|
||||
// # 统一 LRU 缓存:文件图标(按 path + targetPx + mtime + size 复用 Drawable,避免反复解码)
|
||||
var f = new java.io.File(p);
|
||||
if (!f.exists() || !f.isFile()) return null;
|
||||
|
||||
var ckLru = null;
|
||||
try {
|
||||
ckLru = "file|" + p + "@" + String(targetPx == null ? "" : targetPx) + "@" + String(f.lastModified()) + "@" + String(f.length());
|
||||
var hitLru = this._iconLruGet(ckLru);
|
||||
if (hitLru) return hitLru;
|
||||
} catch (eLruF0) {}
|
||||
|
||||
// # 文件大小限制(字节)
|
||||
var limitBytes = Math.max(0, Math.floor(Number(maxBytes || 0)));
|
||||
if (limitBytes > 0) {
|
||||
try {
|
||||
var sz = Number(f.length());
|
||||
if (sz > limitBytes) return null;
|
||||
} catch (eSz) { return null; }
|
||||
}
|
||||
|
||||
// # 先只读尺寸(不解码)
|
||||
var opt = new android.graphics.BitmapFactory.Options();
|
||||
opt.inJustDecodeBounds = true;
|
||||
try { android.graphics.BitmapFactory.decodeFile(p, opt); } catch (eB0) { return null; }
|
||||
|
||||
var w = Number(opt.outWidth || 0);
|
||||
var h = Number(opt.outHeight || 0);
|
||||
if (w <= 0 || h <= 0) return null;
|
||||
|
||||
// # 像素边长上限(宽/高任意一边超限则拒绝)
|
||||
var limitPx = Math.max(0, Math.floor(Number(maxPx || 0)));
|
||||
if (limitPx > 0) {
|
||||
if (w > limitPx || h > limitPx) return null;
|
||||
}
|
||||
|
||||
// # 计算采样倍率:按目标尺寸(一般为 iconSizePx)采样
|
||||
var tp = Math.max(1, Math.floor(Number(targetPx || 1)));
|
||||
// # 允许解码到目标的 2 倍以内,减少锯齿又不浪费内存
|
||||
var desired = Math.max(tp * 2, tp);
|
||||
|
||||
var sample = 1;
|
||||
while ((w / sample) > desired || (h / sample) > desired) sample = sample * 2;
|
||||
if (sample < 1) sample = 1;
|
||||
|
||||
var opt2 = new android.graphics.BitmapFactory.Options();
|
||||
opt2.inJustDecodeBounds = false;
|
||||
opt2.inSampleSize = sample;
|
||||
opt2.inPreferredConfig = android.graphics.Bitmap.Config.ARGB_8888;
|
||||
|
||||
var bmp = null;
|
||||
try { bmp = android.graphics.BitmapFactory.decodeFile(p, opt2); } catch (eB1) { bmp = null; }
|
||||
if (bmp == null) return null;
|
||||
|
||||
var d = new android.graphics.drawable.BitmapDrawable(context.getResources(), bmp);
|
||||
|
||||
// # 写入统一 LRU 缓存
|
||||
try {
|
||||
if (ckLru) this._iconLruPut(ckLru, d);
|
||||
} catch (eLruF1) {}
|
||||
return d;
|
||||
} catch (e0) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
800
code/th_04_theme.js
Normal file
800
code/th_04_theme.js
Normal file
@@ -0,0 +1,800 @@
|
||||
// @version 1.0.0
|
||||
// =======================【工具:屏幕/旋转】======================
|
||||
FloatBallAppWM.prototype.getScreenSizePx = function() {
|
||||
var m = new android.util.DisplayMetrics();
|
||||
try { this.state.wm.getDefaultDisplay().getRealMetrics(m); } catch (e) { this.state.wm.getDefaultDisplay().getMetrics(m); }
|
||||
return { w: m.widthPixels, h: m.heightPixels };
|
||||
};
|
||||
FloatBallAppWM.prototype.getRotation = function() { try { return this.state.wm.getDefaultDisplay().getRotation(); } catch (e) {} return -1; };
|
||||
|
||||
// =======================【工具:alpha/toast/vibrate】======================
|
||||
FloatBallAppWM.prototype.withAlpha = function(colorInt, alpha01) { var a = Math.floor(Number(alpha01) * 255); return (colorInt & 0x00FFFFFF) | (a << 24); };
|
||||
FloatBallAppWM.prototype.toast = function(msg) { try { android.widget.Toast.makeText(context, String(msg), 0).show(); } catch (e) {} };
|
||||
FloatBallAppWM.prototype.vibrateOnce = function(ms) {
|
||||
if (!this.config.LONG_PRESS_HAPTIC_ENABLE) return;
|
||||
try {
|
||||
var vib = context.getSystemService(android.content.Context.VIBRATOR_SERVICE);
|
||||
if (!vib) return;
|
||||
var dur = Math.max(1, Math.floor(ms));
|
||||
if (android.os.Build.VERSION.SDK_INT >= 26) {
|
||||
var ve = android.os.VibrationEffect.createOneShot(dur, android.os.VibrationEffect.DEFAULT_AMPLITUDE);
|
||||
vib.vibrate(ve);
|
||||
} else {
|
||||
vib.vibrate(dur);
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
// =======================【工具:UI样式辅助】======================
|
||||
FloatBallAppWM.prototype.ui = {
|
||||
// 基础颜色
|
||||
colors: {
|
||||
// 以下为默认回退值,实例化时会被 refreshMonetColors() 覆盖为系统莫奈色
|
||||
primary: android.graphics.Color.parseColor("#005BC0"),
|
||||
primaryDark: android.graphics.Color.parseColor("#041E49"),
|
||||
accent: android.graphics.Color.parseColor("#00639B"),
|
||||
danger: android.graphics.Color.parseColor("#BA1A1A"),
|
||||
success: android.graphics.Color.parseColor("#15803d"),
|
||||
warning: android.graphics.Color.parseColor("#b45309"),
|
||||
|
||||
bgLight: android.graphics.Color.parseColor("#F8F9FA"),
|
||||
bgDark: android.graphics.Color.parseColor("#131314"),
|
||||
|
||||
cardLight: android.graphics.Color.parseColor("#E1E3E1"),
|
||||
cardDark: android.graphics.Color.parseColor("#49454F"),
|
||||
|
||||
textPriLight: android.graphics.Color.parseColor("#1F1F1F"),
|
||||
textPriDark: android.graphics.Color.parseColor("#E3E3E3"),
|
||||
|
||||
textSecLight: android.graphics.Color.parseColor("#5F6368"),
|
||||
textSecDark: android.graphics.Color.parseColor("#C4C7C5"),
|
||||
|
||||
dividerLight: android.graphics.Color.parseColor("#747775"),
|
||||
dividerDark: android.graphics.Color.parseColor("#8E918F"),
|
||||
|
||||
inputBgLight: android.graphics.Color.parseColor("#F8F9FA"),
|
||||
inputBgDark: android.graphics.Color.parseColor("#131314"),
|
||||
|
||||
// Monet 扩展字段(供面板直接使用)
|
||||
_monetSurface: android.graphics.Color.parseColor("#F8F9FA"),
|
||||
_monetOnSurface: android.graphics.Color.parseColor("#1F1F1F"),
|
||||
_monetOutline: android.graphics.Color.parseColor("#747775"),
|
||||
_monetOnPrimary: android.graphics.Color.parseColor("#FFFFFF"),
|
||||
_monetPrimaryContainer: android.graphics.Color.parseColor("#D3E3FD"),
|
||||
_monetOnPrimaryContainer: android.graphics.Color.parseColor("#041E49"),
|
||||
_monetSecondary: android.graphics.Color.parseColor("#00639B"),
|
||||
_monetTertiary: android.graphics.Color.parseColor("#5C5891")
|
||||
},
|
||||
|
||||
// 创建圆角背景 (Solid)
|
||||
createRoundDrawable: function(color, radiusPx) {
|
||||
var d = new android.graphics.drawable.GradientDrawable();
|
||||
d.setShape(android.graphics.drawable.GradientDrawable.RECTANGLE);
|
||||
d.setColor(color);
|
||||
d.setCornerRadius(radiusPx);
|
||||
return d;
|
||||
},
|
||||
|
||||
// 创建圆角描边背景 (Stroke)
|
||||
createStrokeDrawable: function(fillColor, strokeColor, strokeWidthPx, radiusPx) {
|
||||
var d = new android.graphics.drawable.GradientDrawable();
|
||||
d.setShape(android.graphics.drawable.GradientDrawable.RECTANGLE);
|
||||
if (fillColor) d.setColor(fillColor);
|
||||
d.setCornerRadius(radiusPx);
|
||||
d.setStroke(strokeWidthPx, strokeColor);
|
||||
return d;
|
||||
},
|
||||
|
||||
// 创建按压反馈背景 (StateList)
|
||||
createRippleDrawable: function(normalColor, pressedColor, radiusPx) {
|
||||
var sd = new android.graphics.drawable.StateListDrawable();
|
||||
var p = this.createRoundDrawable(pressedColor, radiusPx);
|
||||
var n = this.createRoundDrawable(normalColor, radiusPx);
|
||||
sd.addState([android.R.attr.state_pressed], p);
|
||||
sd.addState([], n);
|
||||
return sd;
|
||||
},
|
||||
|
||||
// 创建纯色按压反馈 (StateList) - 用于透明背景按钮
|
||||
createTransparentRippleDrawable: function(pressedColor, radiusPx) {
|
||||
var sd = new android.graphics.drawable.StateListDrawable();
|
||||
var p = this.createRoundDrawable(pressedColor, radiusPx);
|
||||
var n = new android.graphics.drawable.ColorDrawable(android.graphics.Color.TRANSPARENT);
|
||||
sd.addState([android.R.attr.state_pressed], p);
|
||||
sd.addState([], n);
|
||||
return sd;
|
||||
},
|
||||
|
||||
// 辅助:创建扁平按钮
|
||||
createFlatButton: function(app, txt, txtColor, onClick) {
|
||||
var btn = new android.widget.TextView(context);
|
||||
btn.setText(txt);
|
||||
btn.setTextColor(txtColor);
|
||||
btn.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
|
||||
btn.setPadding(app.dp(12), app.dp(6), app.dp(12), app.dp(6));
|
||||
btn.setGravity(android.view.Gravity.CENTER);
|
||||
// use divider color or just low alpha text color for ripple
|
||||
var rippleColor = app.withAlpha ? app.withAlpha(txtColor, 0.1) : 0x22888888;
|
||||
btn.setBackground(this.createTransparentRippleDrawable(rippleColor, app.dp(8)));
|
||||
btn.setOnClickListener(new android.view.View.OnClickListener({
|
||||
onClick: function(v) { app.touchActivity(); app.guardClick("ui_btn", INTERACTION_CONSTANTS.CLICK_COOLDOWN_MS, function(){ if(onClick) onClick(v); }); }
|
||||
}));
|
||||
return btn;
|
||||
},
|
||||
|
||||
// 辅助:创建实心按钮
|
||||
createSolidButton: function(app, txt, bgColor, txtColor, onClick) {
|
||||
var btn = new android.widget.TextView(context);
|
||||
btn.setText(txt);
|
||||
btn.setTextColor(txtColor);
|
||||
btn.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
|
||||
btn.setTypeface(null, android.graphics.Typeface.BOLD);
|
||||
btn.setPadding(app.dp(16), app.dp(8), app.dp(16), app.dp(8));
|
||||
btn.setGravity(android.view.Gravity.CENTER);
|
||||
var pressedColor = app.withAlpha ? app.withAlpha(bgColor, 0.8) : bgColor;
|
||||
btn.setBackground(this.createRippleDrawable(bgColor, pressedColor, app.dp(24)));
|
||||
try { btn.setElevation(app.dp(2)); } catch(e){}
|
||||
btn.setOnClickListener(new android.view.View.OnClickListener({
|
||||
onClick: function(v) { app.touchActivity(); app.guardClick("ui_btn", INTERACTION_CONSTANTS.CLICK_COOLDOWN_MS, function(){ if(onClick) onClick(v); }); }
|
||||
}));
|
||||
return btn;
|
||||
},
|
||||
|
||||
// 辅助:创建带标签的输入组(支持粘贴)
|
||||
createInputGroup: function(app, label, initVal, isMultiLine, hint) {
|
||||
var box = new android.widget.LinearLayout(context);
|
||||
box.setOrientation(android.widget.LinearLayout.VERTICAL);
|
||||
box.setPadding(0, 0, 0, app.dp(12));
|
||||
|
||||
var topLine = new android.widget.LinearLayout(context);
|
||||
topLine.setOrientation(android.widget.LinearLayout.HORIZONTAL);
|
||||
topLine.setGravity(android.view.Gravity.CENTER_VERTICAL);
|
||||
box.addView(topLine);
|
||||
|
||||
var lb = new android.widget.TextView(context);
|
||||
lb.setText(label);
|
||||
lb.setTextColor(this.colors.textSecLight); // 默认用浅色主题副文本色,外部可覆盖
|
||||
try { if (app.isDarkTheme && app.isDarkTheme()) lb.setTextColor(this.colors.textSecDark); } catch(e){}
|
||||
lb.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 12);
|
||||
var lpLb = new android.widget.LinearLayout.LayoutParams(0, -2);
|
||||
lpLb.weight = 1;
|
||||
topLine.addView(lb, lpLb);
|
||||
|
||||
var et = new android.widget.EditText(context);
|
||||
et.setText(initVal ? String(initVal) : "");
|
||||
et.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
|
||||
et.setTextColor(this.colors.textPriLight);
|
||||
try { if (app.isDarkTheme && app.isDarkTheme()) et.setTextColor(this.colors.textPriDark); } catch(e){}
|
||||
|
||||
// 输入框背景优化
|
||||
var strokeColor = this.colors.dividerLight;
|
||||
try { if (app.isDarkTheme && app.isDarkTheme()) strokeColor = this.colors.dividerDark; } catch(e){}
|
||||
|
||||
var bg = this.createStrokeDrawable(this.colors.inputBgLight, strokeColor, app.dp(1), app.dp(8));
|
||||
try { if (app.isDarkTheme && app.isDarkTheme()) bg = this.createStrokeDrawable(this.colors.inputBgDark, strokeColor, app.dp(1), app.dp(8)); } catch(e){}
|
||||
et.setBackground(bg);
|
||||
|
||||
et.setPadding(app.dp(8), app.dp(8), app.dp(8), app.dp(8));
|
||||
if (hint) et.setHint(hint);
|
||||
|
||||
if (isMultiLine) {
|
||||
et.setSingleLine(false);
|
||||
et.setMaxLines(4);
|
||||
} else {
|
||||
et.setSingleLine(true);
|
||||
}
|
||||
|
||||
// 粘贴功能
|
||||
var pasteBtn = this.createFlatButton(app, "粘贴", this.colors.accent, function() {
|
||||
try {
|
||||
var cb = context.getSystemService(android.content.Context.CLIPBOARD_SERVICE);
|
||||
if (cb.hasPrimaryClip()) {
|
||||
var item = cb.getPrimaryClip().getItemAt(0);
|
||||
if (item) {
|
||||
var txt = item.getText();
|
||||
if (txt) {
|
||||
var st = String(txt);
|
||||
var old = String(et.getText());
|
||||
if (old.length > 0) et.setText(old + st);
|
||||
else et.setText(st);
|
||||
et.setSelection(et.getText().length());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
app.toast("剪贴板为空");
|
||||
}
|
||||
} catch (eP) {
|
||||
app.toast("粘贴失败: " + eP);
|
||||
}
|
||||
});
|
||||
// 调整粘贴按钮样式使其更紧凑
|
||||
pasteBtn.setPadding(app.dp(8), app.dp(2), app.dp(8), app.dp(2));
|
||||
pasteBtn.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 12);
|
||||
topLine.addView(pasteBtn);
|
||||
|
||||
box.addView(et);
|
||||
|
||||
// 错误提示
|
||||
var errTv = new android.widget.TextView(context);
|
||||
errTv.setTextColor(this.colors.danger);
|
||||
errTv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 10);
|
||||
errTv.setVisibility(android.view.View.GONE);
|
||||
box.addView(errTv);
|
||||
|
||||
var self = this;
|
||||
return {
|
||||
view: box,
|
||||
input: et,
|
||||
getValue: function() { return String(et.getText()); },
|
||||
setError: function(msg) {
|
||||
if (msg) {
|
||||
errTv.setText(msg);
|
||||
errTv.setVisibility(android.view.View.VISIBLE);
|
||||
et.setBackground(self.createRoundDrawable(app.withAlpha(self.colors.danger, 0.2), app.dp(4)));
|
||||
} else {
|
||||
errTv.setVisibility(android.view.View.GONE);
|
||||
var strokeColor = self.colors.dividerLight;
|
||||
try { if (app.isDarkTheme && app.isDarkTheme()) strokeColor = self.colors.dividerDark; } catch(e){}
|
||||
|
||||
var normalBg = self.createStrokeDrawable(self.colors.inputBgLight, strokeColor, app.dp(1), app.dp(8));
|
||||
try { if (app.isDarkTheme && app.isDarkTheme()) normalBg = self.createStrokeDrawable(self.colors.inputBgDark, strokeColor, app.dp(1), app.dp(8)); } catch(e){}
|
||||
et.setBackground(normalBg);
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
// 辅助:创建标准面板容器
|
||||
createStyledPanel: function(app, paddingDp) {
|
||||
var isDark = app.isDarkTheme();
|
||||
var bgColor = isDark ? this.colors.bgDark : this.colors.bgLight;
|
||||
|
||||
var panel = new android.widget.LinearLayout(context);
|
||||
panel.setOrientation(android.widget.LinearLayout.VERTICAL);
|
||||
|
||||
var bgDr = new android.graphics.drawable.GradientDrawable();
|
||||
bgDr.setColor(bgColor);
|
||||
bgDr.setCornerRadius(app.dp(16));
|
||||
panel.setBackground(bgDr);
|
||||
try { panel.setElevation(app.dp(8)); } catch(e){}
|
||||
|
||||
var p = (paddingDp !== undefined) ? app.dp(paddingDp) : app.dp(16);
|
||||
panel.setPadding(p, p, p, p);
|
||||
|
||||
return panel;
|
||||
},
|
||||
|
||||
// 辅助:创建标准标题栏容器
|
||||
createStyledHeader: function(app, paddingBottomDp) {
|
||||
var header = new android.widget.LinearLayout(context);
|
||||
header.setOrientation(android.widget.LinearLayout.HORIZONTAL);
|
||||
header.setGravity(android.view.Gravity.CENTER_VERTICAL);
|
||||
var pb = (paddingBottomDp !== undefined) ? app.dp(paddingBottomDp) : app.dp(8);
|
||||
header.setPadding(0, 0, 0, pb);
|
||||
return header;
|
||||
},
|
||||
|
||||
// 辅助:创建占位符(撑开空间)
|
||||
createSpacer: function(app) {
|
||||
var dummy = new android.view.View(context);
|
||||
var dummyLp = new android.widget.LinearLayout.LayoutParams(0, 1);
|
||||
dummyLp.weight = 1;
|
||||
dummy.setLayoutParams(dummyLp);
|
||||
return dummy;
|
||||
}
|
||||
};
|
||||
|
||||
// =======================【工具:主题/类莫奈颜色】======================
|
||||
// # 主题调试日志工具(仅打印,不改变逻辑)
|
||||
function _th_hex(c) {
|
||||
try {
|
||||
var Integer = Packages.java.lang.Integer;
|
||||
var s = Integer.toHexString(c);
|
||||
while (s.length < 8) s = "0" + s;
|
||||
return "0x" + s;
|
||||
} catch (e) {
|
||||
try { return String(c); } catch (e2) { return "<?>"; }
|
||||
}
|
||||
}
|
||||
|
||||
function _th_argb(c) {
|
||||
try {
|
||||
var Color = Packages.android.graphics.Color;
|
||||
var ci = Math.floor(Number(c));
|
||||
if (isNaN(ci)) return "NaN";
|
||||
return "a=" + Color.alpha(ci) + " r=" + Color.red(ci) + " g=" + Color.green(ci) + " b=" + Color.blue(ci);
|
||||
} catch (e) {
|
||||
return "argb_err";
|
||||
}
|
||||
}
|
||||
|
||||
function _th_log(L, level, msg) {
|
||||
try {
|
||||
if (!L) return;
|
||||
if (level === "e" && L.e) { L.e(msg); return; }
|
||||
if (level === "w" && L.w) { L.w(msg); return; }
|
||||
if (level === "i" && L.i) { L.i(msg); return; }
|
||||
if (L.d) { L.d(msg); return; }
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// =======================【莫奈动态取色工具】======================
|
||||
var MonetColorProvider = {
|
||||
_cacheLight: null,
|
||||
_cacheDark: null,
|
||||
|
||||
_getResColor: function(resName, fallbackHex) {
|
||||
try {
|
||||
var res = android.content.res.Resources.getSystem();
|
||||
var id = res.getIdentifier(resName, "color", "android");
|
||||
if (id > 0) return res.getColor(id, null);
|
||||
} catch (e) {}
|
||||
try { return android.graphics.Color.parseColor(fallbackHex); } catch (e) { return 0; }
|
||||
},
|
||||
|
||||
_getFallbackLight: function() {
|
||||
return {
|
||||
// 使用更接近 AOSP Monet 标准值的 fallback,日间 primary 更饱和、对比度更高
|
||||
primary: android.graphics.Color.parseColor("#005BC0"),
|
||||
onPrimary: android.graphics.Color.parseColor("#FFFFFF"),
|
||||
primaryContainer: android.graphics.Color.parseColor("#D3E3FD"),
|
||||
onPrimaryContainer: android.graphics.Color.parseColor("#041E49"),
|
||||
secondary: android.graphics.Color.parseColor("#00639B"),
|
||||
secondaryContainer: android.graphics.Color.parseColor("#C2E4FF"),
|
||||
tertiary: android.graphics.Color.parseColor("#5C5891"),
|
||||
surface: android.graphics.Color.parseColor("#F8F9FA"),
|
||||
onSurface: android.graphics.Color.parseColor("#1F1F1F"),
|
||||
surfaceVariant: android.graphics.Color.parseColor("#E1E3E1"),
|
||||
onSurfaceVariant: android.graphics.Color.parseColor("#5F6368"),
|
||||
outline: android.graphics.Color.parseColor("#747775"),
|
||||
outlineVariant: android.graphics.Color.parseColor("#C4C7C5"),
|
||||
error: android.graphics.Color.parseColor("#BA1A1A"),
|
||||
errorContainer: android.graphics.Color.parseColor("#F9DEDC"),
|
||||
onErrorContainer: android.graphics.Color.parseColor("#410E0B")
|
||||
};
|
||||
},
|
||||
|
||||
_getFallbackDark: function() {
|
||||
return {
|
||||
primary: android.graphics.Color.parseColor("#A8C7FA"),
|
||||
onPrimary: android.graphics.Color.parseColor("#062E6F"),
|
||||
primaryContainer: android.graphics.Color.parseColor("#0842A0"),
|
||||
onPrimaryContainer: android.graphics.Color.parseColor("#D3E3FD"),
|
||||
secondary: android.graphics.Color.parseColor("#7FCFFF"),
|
||||
secondaryContainer: android.graphics.Color.parseColor("#004A77"),
|
||||
tertiary: android.graphics.Color.parseColor("#C2C5DD"),
|
||||
surface: android.graphics.Color.parseColor("#131314"),
|
||||
onSurface: android.graphics.Color.parseColor("#E3E3E3"),
|
||||
surfaceVariant: android.graphics.Color.parseColor("#49454F"),
|
||||
onSurfaceVariant: android.graphics.Color.parseColor("#C4C7C5"),
|
||||
outline: android.graphics.Color.parseColor("#8E918F"),
|
||||
outlineVariant: android.graphics.Color.parseColor("#49454F"),
|
||||
error: android.graphics.Color.parseColor("#F2B8B5"),
|
||||
errorContainer: android.graphics.Color.parseColor("#8C1D18"),
|
||||
onErrorContainer: android.graphics.Color.parseColor("#F9DEDC")
|
||||
};
|
||||
},
|
||||
|
||||
_loadMonet: function(isDark) {
|
||||
var c = isDark ? this._getFallbackDark() : this._getFallbackLight();
|
||||
try {
|
||||
var res = android.content.res.Resources.getSystem();
|
||||
var map = isDark ? {
|
||||
primary: "system_accent1_200",
|
||||
onPrimary: "system_accent1_800",
|
||||
primaryContainer: "system_accent1_700",
|
||||
onPrimaryContainer: "system_accent1_100",
|
||||
secondary: "system_accent2_200",
|
||||
secondaryContainer: "system_accent2_700",
|
||||
tertiary: "system_accent3_200",
|
||||
surface: "system_neutral1_900",
|
||||
onSurface: "system_neutral1_100",
|
||||
surfaceVariant: "system_neutral2_700",
|
||||
onSurfaceVariant: "system_neutral2_200",
|
||||
outline: "system_neutral2_400",
|
||||
outlineVariant: "system_neutral2_700",
|
||||
error: "system_accent3_200",
|
||||
errorContainer: "system_accent3_800",
|
||||
onErrorContainer: "system_accent3_100"
|
||||
} : {
|
||||
primary: "system_accent1_600",
|
||||
onPrimary: "system_accent1_0",
|
||||
primaryContainer: "system_accent1_100",
|
||||
onPrimaryContainer: "system_accent1_900",
|
||||
secondary: "system_accent2_600",
|
||||
secondaryContainer: "system_accent2_100",
|
||||
tertiary: "system_accent3_600",
|
||||
surface: "system_neutral1_10",
|
||||
onSurface: "system_neutral1_900",
|
||||
surfaceVariant: "system_neutral2_100",
|
||||
onSurfaceVariant: "system_neutral2_700",
|
||||
outline: "system_neutral2_500",
|
||||
outlineVariant: "system_neutral2_200",
|
||||
error: "system_accent3_600",
|
||||
errorContainer: "system_accent3_100",
|
||||
onErrorContainer: "system_accent3_900"
|
||||
};
|
||||
for (var name in map) {
|
||||
try {
|
||||
var id = res.getIdentifier(map[name], "color", "android");
|
||||
if (id > 0) c[name] = res.getColor(id, null);
|
||||
} catch (e) {}
|
||||
}
|
||||
} catch (e) {}
|
||||
return c;
|
||||
},
|
||||
|
||||
getColors: function(isDark) {
|
||||
if (isDark) {
|
||||
if (!this._cacheDark) this._cacheDark = this._loadMonet(true);
|
||||
return this._cacheDark;
|
||||
} else {
|
||||
if (!this._cacheLight) this._cacheLight = this._loadMonet(false);
|
||||
return this._cacheLight;
|
||||
}
|
||||
},
|
||||
|
||||
invalidate: function() {
|
||||
this._cacheLight = null;
|
||||
this._cacheDark = null;
|
||||
}
|
||||
};
|
||||
|
||||
// =======================【兼容兜底:themeTextInt/themeBgInt】======================
|
||||
// 这段代码的主要内容/用途:兼容旧代码或异步回调里误引用 themeTextInt/themeBgInt 导致 ReferenceError 崩溃。
|
||||
// 说明:当前版本文字色应通过 getPanelTextColorInt(bgInt) 获取;这里仅作为"兜底全局变量",避免回调炸线程。
|
||||
|
||||
// 声明全局变量(避免 ReferenceError)
|
||||
var themeBgInt = 0;
|
||||
var themeTextInt = 0;
|
||||
|
||||
// =======================【API 兼容性辅助函数】======================
|
||||
// 这段代码的主要内容/用途:处理 Android API 级别差异,避免在旧版本上崩溃
|
||||
|
||||
/**
|
||||
* 安全创建 UserHandle(兼容 API 17 以下)
|
||||
* @param {number} userId - 用户 ID
|
||||
* @returns {android.os.UserHandle} UserHandle 对象或 null
|
||||
*/
|
||||
function createUserHandle(userId) {
|
||||
try {
|
||||
// UserHandle.of 在 API 17+ 可用
|
||||
if (android.os.Build.VERSION.SDK_INT >= 17) {
|
||||
return android.os.UserHandle.of(userId);
|
||||
}
|
||||
// API 17 以下返回当前用户句柄
|
||||
return android.os.Process.myUserHandle();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全启动 Activity 跨用户(兼容 API 17 以下)
|
||||
* @param {Context} ctx - Context
|
||||
* @param {Intent} intent - Intent
|
||||
* @param {number} userId - 用户 ID(API 17+ 有效)
|
||||
*/
|
||||
function startActivityAsUserSafe(ctx, intent, userId) {
|
||||
try {
|
||||
if (android.os.Build.VERSION.SDK_INT >= 17 && userId !== 0) {
|
||||
var uh = android.os.UserHandle.of(userId);
|
||||
ctx.startActivityAsUser(intent, uh);
|
||||
} else {
|
||||
ctx.startActivity(intent);
|
||||
}
|
||||
} catch (e) {
|
||||
// 降级到普通启动
|
||||
try {
|
||||
ctx.startActivity(intent);
|
||||
} catch (e2) {}
|
||||
}
|
||||
}
|
||||
|
||||
FloatBallAppWM.prototype.isDarkTheme = function() {
|
||||
// 0) 优先检查用户强制设置 (0=跟随系统, 1=白天, 2=黑夜)
|
||||
var mode = 0;
|
||||
try { mode = Math.floor(Number(this.config.THEME_MODE || 0)); } catch(e){}
|
||||
|
||||
if (mode === 2) return true;
|
||||
if (mode === 1) return false;
|
||||
|
||||
// mode === 0 (or others) -> Fallback to system detection
|
||||
// 这段代码的主要内容/用途:更稳健地判断当前是否处于"夜间/暗色"模式(并打印调试日志)。
|
||||
// 说明:system_server 场景下 Configuration.uiMode 偶发不一致,因此再用 UiModeManager 兜底交叉验证。
|
||||
var result = false;
|
||||
var from = "unknown";
|
||||
|
||||
try {
|
||||
// # 1) 优先用 Configuration(最快)
|
||||
var uiMode = context.getResources().getConfiguration().uiMode;
|
||||
var nightMask = (uiMode & android.content.res.Configuration.UI_MODE_NIGHT_MASK);
|
||||
if (nightMask === android.content.res.Configuration.UI_MODE_NIGHT_YES) { result = true; from = "Configuration(UI_MODE_NIGHT_YES)"; }
|
||||
else if (nightMask === android.content.res.Configuration.UI_MODE_NIGHT_NO) { result = false; from = "Configuration(UI_MODE_NIGHT_NO)"; }
|
||||
} catch (e1) {}
|
||||
|
||||
if (from === "unknown") {
|
||||
try {
|
||||
// # 2) 再用 UiModeManager(更"系统态")
|
||||
var um = context.getSystemService(android.content.Context.UI_MODE_SERVICE);
|
||||
if (um) {
|
||||
var nm = um.getNightMode();
|
||||
if (nm === android.app.UiModeManager.MODE_NIGHT_YES) { result = true; from = "UiModeManager(MODE_NIGHT_YES)"; }
|
||||
else if (nm === android.app.UiModeManager.MODE_NIGHT_NO) { result = false; from = "UiModeManager(MODE_NIGHT_NO)"; }
|
||||
else { from = "UiModeManager(mode=" + String(nm) + ")"; }
|
||||
}
|
||||
} catch (e2) {}
|
||||
}
|
||||
|
||||
// # 3) 实在判断不了,就按"非暗色"处理,避免自动主题背景黑成一片
|
||||
if (from === "unknown") { result = false; from = "fallback(false)"; }
|
||||
|
||||
// 仅在状态改变时打印日志,避免刷屏
|
||||
var logKey = String(result) + "|" + from + "|" + mode;
|
||||
if (this._lastDarkThemeLog !== logKey) {
|
||||
this._lastDarkThemeLog = logKey;
|
||||
try { _th_log(this.L, "d", "[theme] isDarkTheme=" + String(result) + " via=" + from + " mode=" + mode); } catch (e3) {}
|
||||
}
|
||||
|
||||
// # 主题切换时刷新莫奈配色(传入 result 避免递归)
|
||||
// 注:构造函数中会初始化,这里只在构造完成后的切换时触发
|
||||
if (this._lastDarkResult !== undefined && this._lastDarkResult !== result) {
|
||||
this._lastDarkResult = result;
|
||||
try { this.refreshMonetColors(result); } catch (eM) {}
|
||||
} else if (this._lastDarkResult === undefined) {
|
||||
this._lastDarkResult = result;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.refreshMonetColors = function(forceDark) {
|
||||
try {
|
||||
var isDark = (forceDark !== undefined) ? forceDark : this.isDarkTheme();
|
||||
var m = MonetColorProvider.getColors(isDark);
|
||||
var ml = MonetColorProvider.getColors(false);
|
||||
var md = MonetColorProvider.getColors(true);
|
||||
var c = this.ui.colors;
|
||||
|
||||
// 浅色配色
|
||||
c.bgLight = ml.surface;
|
||||
c.cardLight = ml.surfaceVariant;
|
||||
c.textPriLight = ml.onSurface;
|
||||
c.textSecLight = ml.onSurfaceVariant;
|
||||
c.dividerLight = ml.outline;
|
||||
c.inputBgLight = ml.surface;
|
||||
|
||||
// 深色配色
|
||||
c.bgDark = md.surface;
|
||||
c.cardDark = md.surfaceVariant;
|
||||
c.textPriDark = md.onSurface;
|
||||
c.textSecDark = md.onSurfaceVariant;
|
||||
c.dividerDark = md.outline;
|
||||
c.inputBgDark = md.surface;
|
||||
|
||||
// 当前主题配色(随主题切换)
|
||||
c.primary = m.primary;
|
||||
// primaryDark 修正为 onPrimaryContainer:日间为深蓝(#041E49),夜间为浅蓝(#D3E3FD),符合"深色变体"语义
|
||||
c.primaryDark = m.onPrimaryContainer;
|
||||
c.accent = m.secondary;
|
||||
c.danger = m.error;
|
||||
// success/warning 优化对比度:日间更深确保可见,夜间保持适度亮度不刺眼
|
||||
c.success = isDark ? android.graphics.Color.parseColor("#4ade80") : android.graphics.Color.parseColor("#15803d");
|
||||
c.warning = isDark ? android.graphics.Color.parseColor("#fbbf24") : android.graphics.Color.parseColor("#b45309");
|
||||
|
||||
// 扩展:完整 Monet 语义字段(供面板方法直接使用)
|
||||
c._monetSurface = m.surface;
|
||||
c._monetOnSurface = m.onSurface;
|
||||
c._monetOutline = m.outline;
|
||||
c._monetOnPrimary = m.onPrimary;
|
||||
c._monetPrimaryContainer = m.primaryContainer;
|
||||
c._monetOnPrimaryContainer = m.onPrimaryContainer;
|
||||
c._monetSecondary = m.secondary;
|
||||
c._monetTertiary = m.tertiary;
|
||||
|
||||
try { _th_log(this.L, "d", "[monet] refreshed isDark=" + isDark + " primary=" + _th_hex(c.primary) + " primaryDark=" + _th_hex(c.primaryDark) + " accent=" + _th_hex(c.accent)); } catch (e) {}
|
||||
} catch (e) {
|
||||
try { _th_log(this.L, "e", "[monet] refresh err=" + String(e)); } catch (e2) {}
|
||||
}
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.getMonetAccentForBall = function() {
|
||||
// 这段代码的主要内容/用途:尝试读取系统动态强调色(Monet/accent);失败则使用兜底颜色;并打印命中信息。
|
||||
// 优化点:
|
||||
// 1) 日间使用 500/600 档(更深、对比度更高),夜间使用 400/300 档(柔和、不刺眼)
|
||||
// 2) 移除 system_neutral1_* 中性灰色(不是强调色)
|
||||
var res = context.getResources();
|
||||
var dark = this.isDarkTheme();
|
||||
var names = dark
|
||||
? ["system_accent1_400", "system_accent1_300", "system_accent2_400"]
|
||||
: ["system_accent1_500", "system_accent1_600", "system_accent2_500"];
|
||||
|
||||
var i;
|
||||
for (i = 0; i < names.length; i++) {
|
||||
try {
|
||||
var id = res.getIdentifier(names[i], "color", "android");
|
||||
if (id > 0) {
|
||||
var c = res.getColor(id, null);
|
||||
var logKey = "hit|" + names[i] + "|" + c;
|
||||
if (this._lastAccentLog !== logKey) {
|
||||
this._lastAccentLog = logKey;
|
||||
try { _th_log(this.L, "d", "[theme] hit accent=" + names[i] + " id=" + String(id) + " c=" + _th_hex(c) + " " + _th_argb(c)); } catch (eL0) {}
|
||||
}
|
||||
return c;
|
||||
}
|
||||
} catch (e) {
|
||||
try { _th_log(this.L, "w", "[theme] err accent=" + names[i] + " e=" + String(e)); } catch (eL2) {}
|
||||
}
|
||||
}
|
||||
|
||||
var fbHex = dark
|
||||
? (this.config.BALL_FALLBACK_DARK || CONST_BALL_FALLBACK_DARK)
|
||||
: (this.config.BALL_FALLBACK_LIGHT || CONST_BALL_FALLBACK_LIGHT);
|
||||
var fb = android.graphics.Color.parseColor(fbHex);
|
||||
var logKeyFb = "miss_all|" + fb;
|
||||
if (this._lastAccentLog !== logKeyFb) {
|
||||
this._lastAccentLog = logKeyFb;
|
||||
try { _th_log(this.L, "w", "[theme] accent miss all, fallback=" + _th_hex(fb) + " " + _th_argb(fb)); } catch (eL3) {}
|
||||
}
|
||||
return fb;
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.updateBallContentBackground = function(contentView) {
|
||||
try {
|
||||
var ballColor = this.getMonetAccentForBall();
|
||||
var dark = this.isDarkTheme();
|
||||
var alpha01 = dark ? this.config.BALL_RIPPLE_ALPHA_DARK : this.config.BALL_RIPPLE_ALPHA_LIGHT;
|
||||
var rippleColor = this.withAlpha(ballColor, alpha01);
|
||||
|
||||
// # 自定义 PNG/APP 模式下:背景透明
|
||||
var fillColor = ballColor;
|
||||
var _usedKind = "none";
|
||||
try { _usedKind = this.state.usedIconKind || "none"; } catch(eK){}
|
||||
|
||||
try {
|
||||
var _pngModeBg = Number(this.config.BALL_PNG_MODE || 0);
|
||||
if ((_pngModeBg === 1 && _usedKind === "file") || _usedKind === "app") {
|
||||
fillColor = android.graphics.Color.TRANSPARENT;
|
||||
}
|
||||
} catch (eBg) {}
|
||||
|
||||
var content = new android.graphics.drawable.GradientDrawable();
|
||||
content.setShape(android.graphics.drawable.GradientDrawable.OVAL);
|
||||
content.setColor(fillColor);
|
||||
|
||||
// # 描边:根据球体颜色亮度自动选择白/黑描边,确保任何背景下都可见
|
||||
if (_usedKind !== "file" && _usedKind !== "app") {
|
||||
try {
|
||||
var Color = android.graphics.Color;
|
||||
var lum = (Color.red(fillColor)*0.299 + Color.green(fillColor)*0.587 + Color.blue(fillColor)*0.114) / 255.0;
|
||||
var strokeInt = lum > 0.55
|
||||
? Color.parseColor("#33000000") // 浅球用半透明黑边
|
||||
: Color.parseColor("#55FFFFFF"); // 深球用半透明白边
|
||||
content.setStroke(this.dp(1), strokeInt);
|
||||
} catch(eS){}
|
||||
}
|
||||
|
||||
var mask = new android.graphics.drawable.GradientDrawable();
|
||||
mask.setShape(android.graphics.drawable.GradientDrawable.OVAL);
|
||||
mask.setColor(android.graphics.Color.WHITE);
|
||||
|
||||
contentView.setBackground(new android.graphics.drawable.RippleDrawable(
|
||||
android.content.res.ColorStateList.valueOf(rippleColor),
|
||||
content,
|
||||
mask
|
||||
));
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
|
||||
FloatBallAppWM.prototype.safeParseColor = function(hex, fallbackInt) {
|
||||
// 这段代码的主要内容/用途:安全解析 #RRGGBB/#AARRGGBB,解析失败直接回退,避免 system_server 抛异常。
|
||||
try { return android.graphics.Color.parseColor(String(hex)); } catch (e) { return fallbackInt; }
|
||||
};
|
||||
|
||||
|
||||
|
||||
FloatBallAppWM.prototype.getPanelBgColorInt = function() {
|
||||
// 这段代码的主要内容/用途:配合"白天/夜晚"两档主题,返回统一的背景颜色(不再依赖自动亮度推断)。
|
||||
var isDark = this.isDarkTheme();
|
||||
|
||||
var dayBgHex = (this.config.THEME_DAY_BG_HEX != null) ? String(this.config.THEME_DAY_BG_HEX) : null;
|
||||
var nightBgHex = (this.config.THEME_NIGHT_BG_HEX != null) ? String(this.config.THEME_NIGHT_BG_HEX) : null;
|
||||
|
||||
// # 兼容旧版默认配色:若仍为旧默认值,自动回退到莫奈色
|
||||
if (dayBgHex === "#FAF4E3") dayBgHex = null;
|
||||
if (nightBgHex === "#191928") nightBgHex = null;
|
||||
|
||||
// # 未配置时使用莫奈 surface 色作为回退
|
||||
var dayFallback = this.ui.colors.bgLight || android.graphics.Color.parseColor("#F8F9FA");
|
||||
var nightFallback = this.ui.colors.bgDark || android.graphics.Color.parseColor("#131314");
|
||||
|
||||
var base = isDark
|
||||
? (nightBgHex ? this.safeParseColor(nightBgHex, nightFallback) : nightFallback)
|
||||
: (dayBgHex ? this.safeParseColor(dayBgHex, dayFallback) : dayFallback);
|
||||
|
||||
// # 继承原配置:面板背景透明度(0~1)
|
||||
var a = 1.0;
|
||||
try { a = Number(this.config.PANEL_BG_ALPHA); } catch (e1) { a = 0.85; }
|
||||
if (!(a >= 0.0 && a <= 1.0)) a = 0.85;
|
||||
|
||||
var out = this.withAlpha(base, a);
|
||||
|
||||
try { _th_log(this.L, "d", "[t]bg isDark=" + isDark + " o=" + _th_hex(out)); } catch (e2) {}
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.getPanelTextColorInt = function(bgInt) {
|
||||
// 这段代码的主要内容/用途:配合"白天/夜晚"两档主题,返回统一的文字颜色(不再依赖自动亮度推断)。
|
||||
var isDark = this.isDarkTheme();
|
||||
|
||||
var dayTextHex = (this.config.THEME_DAY_TEXT_HEX != null) ? String(this.config.THEME_DAY_TEXT_HEX) : null;
|
||||
var nightTextHex = (this.config.THEME_NIGHT_TEXT_HEX != null) ? String(this.config.THEME_NIGHT_TEXT_HEX) : null;
|
||||
|
||||
// # 兼容旧版默认配色:若仍为旧默认值,自动回退到莫奈色
|
||||
if (dayTextHex === "#333333") dayTextHex = null;
|
||||
if (nightTextHex === "#E6E6F0") nightTextHex = null;
|
||||
|
||||
// # 未配置时使用莫奈 onSurface 色作为回退
|
||||
var dayFallback = this.ui.colors.textPriLight || android.graphics.Color.parseColor("#1F1F1F");
|
||||
var nightFallback = this.ui.colors.textPriDark || android.graphics.Color.parseColor("#E3E3E3");
|
||||
|
||||
if (!isDark) return dayTextHex ? this.safeParseColor(dayTextHex, dayFallback) : dayFallback;
|
||||
return nightTextHex ? this.safeParseColor(nightTextHex, nightFallback) : nightFallback;
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.applyTextColorRecursive = function(v, colorInt) {
|
||||
// 这段代码的主要内容/用途:递归设置面板内所有 TextView 的文字颜色(标题/按钮文字/描述/设置项等)。
|
||||
try {
|
||||
if (!v) return;
|
||||
if (v instanceof android.widget.TextView) {
|
||||
v.setTextColor(colorInt);
|
||||
}
|
||||
if (v instanceof android.view.ViewGroup) {
|
||||
var i, n = v.getChildCount();
|
||||
for (i = 0; i < n; i++) {
|
||||
this.applyTextColorRecursive(v.getChildAt(i), colorInt);
|
||||
}
|
||||
}
|
||||
} catch (e0) {}
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.updatePanelBackground = function(panelView) {
|
||||
// 这段代码的主要内容/用途:统一为"主面板/设置面板/查看器面板"应用背景与文字颜色(自动/亮/暗三档),并输出调试日志(命中哪个颜色)。
|
||||
try {
|
||||
var bg = new android.graphics.drawable.GradientDrawable();
|
||||
bg.setCornerRadius(this.dp(22));
|
||||
|
||||
var bgInt = this.getPanelBgColorInt();
|
||||
bg.setColor(bgInt);
|
||||
|
||||
// 轻量描边:亮色时更明显,暗色时也保留一点边界(不提供自定义输入,避免设置页复杂化)
|
||||
var sw = this.dp(1);
|
||||
var isDark = this.isDarkTheme();
|
||||
var outlineColor = this.ui.colors._monetOutline || (isDark ? android.graphics.Color.parseColor("#8E918F") : android.graphics.Color.parseColor("#747775"));
|
||||
var stroke = this.withAlpha(outlineColor, isDark ? 0.26 : 0.20);
|
||||
try { bg.setStroke(sw, stroke); } catch (eS) {}
|
||||
|
||||
panelView.setBackground(bg);
|
||||
|
||||
var tc = this.getPanelTextColorInt(bgInt);
|
||||
try { themeBgInt = bgInt; themeTextInt = tc; } catch (eT) {}
|
||||
this.applyTextColorRecursive(panelView, tc);
|
||||
|
||||
try { _th_log(this.L, "d", "[t]apply bg=" + _th_hex(bgInt) + " tx=" + _th_hex(tc)); } catch (e) {}
|
||||
|
||||
|
||||
try {
|
||||
_th_log(this.L, "d",
|
||||
"[theme:apply] isDark=" + isDark +
|
||||
" bg=" + _th_hex(bgInt) + " " + _th_argb(bgInt) +
|
||||
" text=" + _th_hex(tc) + " " + _th_argb(tc) +
|
||||
" stroke=" + _th_hex(stroke)
|
||||
);
|
||||
} catch (eL0) {}
|
||||
} catch (e) {
|
||||
try { _th_log(this.L, "e", "[theme:apply] err=" + String(e)); } catch (eL1) {}
|
||||
}
|
||||
};
|
||||
|
||||
298
code/th_05_persistence.js
Normal file
298
code/th_05_persistence.js
Normal file
@@ -0,0 +1,298 @@
|
||||
// @version 1.0.0
|
||||
// =======================【工具:面板位置持久化】======================
|
||||
FloatBallAppWM.prototype.savePanelState = function(key, state) {
|
||||
if (!key || !state) return;
|
||||
try {
|
||||
if (!this.config.PANEL_STATES) this.config.PANEL_STATES = {};
|
||||
this.config.PANEL_STATES[key] = state;
|
||||
// 节流或立即保存? 面板拖动结束通常不频繁,立即保存即可
|
||||
// 但为了避免连续事件,还是可以复用 savePos 的节流逻辑,或者直接保存
|
||||
ConfigManager.saveSettings(this.config);
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.loadPanelState = function(key) {
|
||||
if (!key || !this.config.PANEL_STATES) return null;
|
||||
return this.config.PANEL_STATES[key];
|
||||
};
|
||||
|
||||
// =======================【工具:位置持久化】======================
|
||||
FloatBallAppWM.prototype.savePos = function(x, y) {
|
||||
try {
|
||||
this.config.BALL_INIT_X = Math.floor(x);
|
||||
this.config.BALL_INIT_Y_DP = Math.floor(y / this.state.density);
|
||||
// # 节流保存
|
||||
return ConfigManager.saveSettings(this.config);
|
||||
} catch (e) { return false; }
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.loadSavedPos = function() {
|
||||
// # 直接从 config 返回,因为 config 已经是持久化的
|
||||
var x = Number(this.config.BALL_INIT_X || 0);
|
||||
var y = this.dp(Number(this.config.BALL_INIT_Y_DP || 100));
|
||||
return { x: x, y: y };
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.trySavePosThrottled = function(x, y) {
|
||||
var t = this.now();
|
||||
if (t - this.state.lastSaveTs < this.config.SAVE_THROTTLE_MS) return false;
|
||||
this.state.lastSaveTs = t;
|
||||
return this.savePos(x, y);
|
||||
};
|
||||
|
||||
// =======================【工具:配置持久化】======================
|
||||
FloatBallAppWM.prototype.saveConfig = function(obj) {
|
||||
try {
|
||||
for (var k in obj) {
|
||||
if (obj.hasOwnProperty(k)) {
|
||||
this.config[k] = obj[k];
|
||||
}
|
||||
}
|
||||
if (this.L) this.L.updateConfig(this.config);
|
||||
return ConfigManager.saveSettings(this.config);
|
||||
} catch (e) { return false; }
|
||||
};
|
||||
|
||||
// =======================【设置面板:schema】======================
|
||||
FloatBallAppWM.prototype.getConfigSchema = function() {
|
||||
return ConfigManager.loadSchema();
|
||||
};
|
||||
|
||||
// =======================【设置面板:临时编辑缓存】======================
|
||||
FloatBallAppWM.prototype.beginEditConfig = function() {
|
||||
try {
|
||||
var schema = this.getConfigSchema();
|
||||
var p = {};
|
||||
var i;
|
||||
for (i = 0; i < schema.length; i++) {
|
||||
if (!schema[i] || !schema[i].key) continue;
|
||||
var k = String(schema[i].key);
|
||||
p[k] = this.config[k];
|
||||
}
|
||||
this.state.pendingUserCfg = p;
|
||||
this.state.pendingDirty = false;
|
||||
return true;
|
||||
} catch (e0) {
|
||||
this.state.pendingUserCfg = null;
|
||||
this.state.pendingDirty = false;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
FloatBallAppWM.prototype.getPendingValue = function(k) {
|
||||
if (this.state.pendingUserCfg && this.state.pendingUserCfg.hasOwnProperty(k)) return this.state.pendingUserCfg[k];
|
||||
return this.config[k];
|
||||
};
|
||||
FloatBallAppWM.prototype.setPendingValue = function(k, v) {
|
||||
if (!this.state.pendingUserCfg) this.beginEditConfig();
|
||||
this.state.pendingUserCfg[k] = v;
|
||||
this.state.pendingDirty = true;
|
||||
if (this.state.previewMode) {
|
||||
this.refreshPreview(k);
|
||||
}
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.getEffectiveConfig = function() {
|
||||
if (!this.state.previewMode || !this.state.pendingUserCfg) return this.config;
|
||||
var cfg = {};
|
||||
for (var k in this.config) { cfg[k] = this.config[k]; }
|
||||
for (var k in this.state.pendingUserCfg) { cfg[k] = this.state.pendingUserCfg[k]; }
|
||||
return cfg;
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.refreshPreview = function(changedKey) {
|
||||
if (this.state.closing) return;
|
||||
var self = this;
|
||||
// Post to next tick to avoid destroying view during event dispatch (fixes crash on switch toggle)
|
||||
if (this.state.h) {
|
||||
this.state.h.post(new JavaAdapter(java.lang.Runnable, {
|
||||
run: function() { self._refreshPreviewInternal(changedKey); }
|
||||
}));
|
||||
} else {
|
||||
self._refreshPreviewInternal(changedKey);
|
||||
}
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype._refreshPreviewInternal = function(changedKey) {
|
||||
if (this.state.closing) return;
|
||||
var originalConfig = this.config;
|
||||
try {
|
||||
// 使用临时配置
|
||||
this.config = this.getEffectiveConfig();
|
||||
|
||||
var needBall = false;
|
||||
var needPanel = false;
|
||||
|
||||
if (!changedKey) {
|
||||
needBall = true;
|
||||
needPanel = true;
|
||||
} else {
|
||||
// 根据修改的 key 判断需要刷新什么,避免全量刷新导致闪烁
|
||||
if (changedKey.indexOf("BALL_") === 0) needBall = true;
|
||||
if (changedKey.indexOf("PANEL_") === 0) needPanel = true;
|
||||
// 球大小改变会影响面板位置
|
||||
if (changedKey === "BALL_SIZE_DP") needPanel = true;
|
||||
}
|
||||
|
||||
// 1. 刷新悬浮球 (保持面板不关闭)
|
||||
if (needBall) {
|
||||
this.rebuildBallForNewSize(true);
|
||||
}
|
||||
|
||||
// 2. 刷新主面板预览
|
||||
if (needPanel) {
|
||||
// 如果当前没有显示主面板,则创建并显示;如果已显示,则替换
|
||||
|
||||
var panel = this.buildPanelView("main");
|
||||
|
||||
// 计算位置 (使用当前球的位置)
|
||||
var maxH = Math.floor(this.state.screen.h * 0.75);
|
||||
panel.measure(
|
||||
android.view.View.MeasureSpec.makeMeasureSpec(this.state.screen.w, android.view.View.MeasureSpec.AT_MOST),
|
||||
android.view.View.MeasureSpec.makeMeasureSpec(maxH, android.view.View.MeasureSpec.AT_MOST)
|
||||
);
|
||||
var pw = panel.getMeasuredWidth();
|
||||
var ph = panel.getMeasuredHeight();
|
||||
if (ph > maxH) ph = maxH;
|
||||
|
||||
var bx = this.state.ballLp.x;
|
||||
var by = this.state.ballLp.y;
|
||||
var px = this.computePanelX(bx, pw);
|
||||
var py = by;
|
||||
|
||||
// 尝试调整 Y
|
||||
var r = this.tryAdjustPanelY(px, py, pw, ph, bx, by);
|
||||
var finalX = r.ok ? r.x : px;
|
||||
var finalY = r.ok ? r.y : this.clamp(py, 0, this.state.screen.h - ph);
|
||||
|
||||
// 优化闪烁:先添加新面板,再移除旧面板 (这样新面板会在最上层,符合预览需求)
|
||||
var oldPanel = this.state.panel;
|
||||
var oldAdded = this.state.addedPanel;
|
||||
|
||||
// 添加新面板 (addPanel 会更新 this.state.panel)
|
||||
// 注意:addPanel 中已为 main 添加 FLAG_NOT_FOCUSABLE,所以即使在最上层也不会抢走 Settings 的输入焦点
|
||||
this.addPanel(panel, finalX, finalY, "main");
|
||||
|
||||
// 移除旧面板
|
||||
if (oldAdded && oldPanel) {
|
||||
try { this.state.wm.removeView(oldPanel); } catch(e) {}
|
||||
}
|
||||
}
|
||||
|
||||
} catch(e) {
|
||||
safeLog(this.L, 'e', "refreshPreview err=" + e);
|
||||
} finally {
|
||||
this.config = originalConfig;
|
||||
}
|
||||
};
|
||||
FloatBallAppWM.prototype.persistUserCfgFromObject = function(obj) {
|
||||
// # 这段代码的主要内容/用途:从临时编辑对象里按 schema 白名单抽取并保存(跳过 section 标题等无 key 项)
|
||||
try {
|
||||
var schema = this.getConfigSchema();
|
||||
var out = {};
|
||||
var i;
|
||||
for (i = 0; i < schema.length; i++) {
|
||||
if (!schema[i] || !schema[i].key) continue;
|
||||
var k = String(schema[i].key);
|
||||
out[k] = obj[k];
|
||||
}
|
||||
return this.saveConfig(out);
|
||||
} catch (e0) { return false; }
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.applyImmediateEffectsForKey = function(k) {
|
||||
try {
|
||||
if (k === "LOG_ENABLE") {
|
||||
try {
|
||||
if (this.L) {
|
||||
this.L.enable = !!this.config.LOG_ENABLE;
|
||||
this.L.cfg.LOG_ENABLE = !!this.config.LOG_ENABLE;
|
||||
this.L.i("apply LOG_ENABLE=" + String(this.config.LOG_ENABLE));
|
||||
}
|
||||
} catch (eLE) {}
|
||||
return;
|
||||
}
|
||||
if (k === "LOG_DEBUG") {
|
||||
try {
|
||||
if (this.L) {
|
||||
this.L.debug = !!this.config.LOG_DEBUG;
|
||||
this.L.cfg.LOG_DEBUG = !!this.config.LOG_DEBUG;
|
||||
this.L.i("apply LOG_DEBUG=" + String(this.config.LOG_DEBUG));
|
||||
}
|
||||
} catch (eLD) {}
|
||||
return;
|
||||
}
|
||||
if (k === "LOG_KEEP_DAYS") {
|
||||
try {
|
||||
var n = Math.max(1, Math.floor(Number(this.config.LOG_KEEP_DAYS || 3)));
|
||||
this.config.LOG_KEEP_DAYS = n;
|
||||
if (this.L) {
|
||||
this.L.keepDays = n;
|
||||
this.L.cfg.LOG_KEEP_DAYS = n;
|
||||
this.L.i("apply LOG_KEEP_DAYS=" + String(n));
|
||||
this.L.cleanupOldFiles();
|
||||
}
|
||||
} catch (eLK) {}
|
||||
return;
|
||||
}
|
||||
if (k === "BALL_SIZE_DP" || k === "BALL_PNG_MODE" || k === "BALL_ICON_TYPE" || k === "BALL_ICON_PKG" || 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_TEXT" || k === "BALL_TEXT_SIZE_SP" || k === "BALL_TEXT_COLOR_HEX" || k === "BALL_ICON_TEXT_GAP_DP") { this.rebuildBallForNewSize(); return; }
|
||||
|
||||
if (k === "PANEL_ROWS" || k === "PANEL_COLS" ||
|
||||
k === "PANEL_ITEM_SIZE_DP" || k === "PANEL_GAP_DP" ||
|
||||
k === "PANEL_PADDING_DP" || k === "PANEL_ICON_SIZE_DP" ||
|
||||
k === "PANEL_LABEL_ENABLED" || k === "PANEL_LABEL_TEXT_SIZE_SP" ||
|
||||
k === "PANEL_LABEL_TOP_MARGIN_DP") {
|
||||
|
||||
if (this.state.addedPanel) this.hideMainPanel();
|
||||
if (this.state.addedSettings) this.hideSettingsPanel();
|
||||
if (this.state.addedViewer) this.hideViewerPanel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (k === "EDGE_VISIBLE_RATIO") {
|
||||
if (this.state.addedBall && this.state.docked) {
|
||||
this.state.docked = false;
|
||||
this.snapToEdgeDocked(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (e0) {}
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.commitPendingUserCfg = function() {
|
||||
try {
|
||||
if (!this.state.pendingUserCfg) return { ok: false, reason: "no_pending" };
|
||||
|
||||
var schema = this.getConfigSchema();
|
||||
var changedKeys = [];
|
||||
var i;
|
||||
|
||||
for (i = 0; i < schema.length; i++) {
|
||||
if (!schema[i] || !schema[i].key) continue;
|
||||
var k = String(schema[i].key);
|
||||
var oldV = this.config[k];
|
||||
var newV = this.state.pendingUserCfg[k];
|
||||
if (String(oldV) !== String(newV)) {
|
||||
this.config[k] = newV;
|
||||
changedKeys.push(k);
|
||||
}
|
||||
}
|
||||
|
||||
this.persistUserCfgFromObject(this.state.pendingUserCfg);
|
||||
|
||||
var j;
|
||||
for (j = 0; j < changedKeys.length; j++) {
|
||||
if (changedKeys[j] === "BALL_SIZE_DP") { this.applyImmediateEffectsForKey("BALL_SIZE_DP"); break; }
|
||||
}
|
||||
for (j = 0; j < changedKeys.length; j++) {
|
||||
if (changedKeys[j] !== "BALL_SIZE_DP") this.applyImmediateEffectsForKey(changedKeys[j]);
|
||||
}
|
||||
|
||||
this.state.pendingDirty = false;
|
||||
safeLog(this.L, 'i', "commit settings changed=" + JSON.stringify(changedKeys));
|
||||
return { ok: true, changed: changedKeys };
|
||||
} catch (e0) {
|
||||
safeLog(this.L, 'e', "commitPendingUserCfg err=" + String(e0));
|
||||
return { ok: false, err: String(e0) };
|
||||
}
|
||||
};
|
||||
|
||||
485
code/th_06_icon_parser.js
Normal file
485
code/th_06_icon_parser.js
Normal file
@@ -0,0 +1,485 @@
|
||||
// @version 1.0.0
|
||||
// =======================【工具:吸边数据】======================
|
||||
FloatBallAppWM.prototype.getDockInfo = function() {
|
||||
var ballSize = this.dp(this.config.BALL_SIZE_DP);
|
||||
var visible = Math.max(1, Math.round(ballSize * this.config.EDGE_VISIBLE_RATIO));
|
||||
var hidden = ballSize - visible;
|
||||
return { ballSize: ballSize, visiblePx: visible, hiddenPx: hidden };
|
||||
};
|
||||
|
||||
// =======================【工具:图标解析】======================
|
||||
|
||||
// =======================【工具:快捷方式图标文件路径】======================
|
||||
FloatBallAppWM.prototype.getShortcutIconFilePath = function(pkg, shortcutId, userId) {
|
||||
// # 主要用途:把快捷方式图标持久化到 shortcut_icons 目录,供按钮页/按钮管理页稳定显示(桌面移除后仍可显示)
|
||||
try {
|
||||
var p = (pkg == null) ? "" : String(pkg);
|
||||
var s = (shortcutId == null) ? "" : String(shortcutId);
|
||||
var u = (userId == null) ? "0" : String(userId);
|
||||
|
||||
// # 文件名去非法字符,避免路径注入或创建失败
|
||||
function _sn(v) {
|
||||
try {
|
||||
var t = String(v == null ? "" : v);
|
||||
t = t.replace(/[^a-zA-Z0-9._-]+/g, "_");
|
||||
if (t.length > 120) t = t.substring(0, 120);
|
||||
return t;
|
||||
} catch(e) { return ""; }
|
||||
}
|
||||
|
||||
var dir = String(APP_ROOT_DIR) + "/shortcut_icons";
|
||||
var fn = _sn(p) + "__" + _sn(s) + "__u" + _sn(u) + ".png";
|
||||
return dir + "/" + fn;
|
||||
} catch (e0) {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.resolveIconDrawable = function(btn) {
|
||||
// # 主要用途:解析面板按钮图标(优先 app 包名图标,其次自定义 resId,最后兜底)
|
||||
try {
|
||||
if (!btn) return context.getResources().getDrawable(android.R.drawable.ic_menu_help, null);
|
||||
|
||||
// # 0) 优先检查 iconPath (绝对路径)
|
||||
// # 引用优化:复用 loadBallIconDrawableFromFile 安全加载逻辑
|
||||
if (btn.iconPath) {
|
||||
try {
|
||||
var path = String(btn.iconPath);
|
||||
if (path) {
|
||||
// targetPx: 面板图标大小; Limit: 1MB, 1024px
|
||||
var sizeDp = this.config.PANEL_ICON_SIZE_DP || 32;
|
||||
var dr = this.loadBallIconDrawableFromFile(path, this.dp(sizeDp), 1048576, 1024);
|
||||
if (dr) return dr;
|
||||
}
|
||||
} catch (ePath) {}
|
||||
}
|
||||
|
||||
|
||||
// # 1) type=app 且配置了 pkg:自动取应用图标
|
||||
try {
|
||||
var t = (btn.type == null) ? "" : String(btn.type);
|
||||
if (t === "app") {
|
||||
var pkg = (btn.pkg == null) ? "" : String(btn.pkg);
|
||||
if (pkg.length > 0) {
|
||||
// # 统一 LRU 缓存:避免频繁走 PackageManager(Drawable 可复用);并带容量上限,防止无限增长
|
||||
var kApp = "app|" + pkg;
|
||||
var hitApp = this._iconLruGet(kApp);
|
||||
if (hitApp) return hitApp;
|
||||
|
||||
var pm = context.getPackageManager();
|
||||
var drApp = pm.getApplicationIcon(pkg);
|
||||
if (drApp != null) {
|
||||
this._iconLruPut(kApp, drApp);
|
||||
return drApp;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (eApp) {}
|
||||
|
||||
|
||||
// # 1.5) type=shortcut:尝试取 Shortcuts 快捷方式图标(显示与 shortcuts.js 页面一致)
|
||||
// # 说明:btn 需要包含 pkg + shortcutId;图标获取可能较重,因此做简单缓存,失败则回退到应用图标。
|
||||
try {
|
||||
var t2 = (btn.type == null) ? "" : String(btn.type);
|
||||
if (t2 === "shortcut") {
|
||||
var pkg2 = (btn.pkg == null) ? "" : String(btn.pkg);
|
||||
var sid2 = (btn.shortcutId == null) ? "" : String(btn.shortcutId);
|
||||
if (pkg2.length > 0 && sid2.length > 0) {
|
||||
// # 1.5.1) 优先从 shortcut_icons 持久化目录读取(桌面移除后仍可显示正确图标)
|
||||
try {
|
||||
var iconFilePath0 = this.getShortcutIconFilePath(pkg2, sid2, (btn.userId != null ? String(btn.userId) : "0"));
|
||||
if (iconFilePath0) {
|
||||
try {
|
||||
var f0 = new java.io.File(iconFilePath0);
|
||||
if (f0.exists() && f0.isFile()) {
|
||||
var sizeDp0 = this.config.PANEL_ICON_SIZE_DP || 32;
|
||||
var iconSizePx0 = this.dp(sizeDp0);
|
||||
var dr0 = this.loadBallIconDrawableFromFile(iconFilePath0, iconSizePx0, 1048576, 1024);
|
||||
if (dr0) {
|
||||
// # 写入统一 LRU 缓存:同一个 shortcut 复用 Drawable,避免反复解码 PNG
|
||||
try {
|
||||
var sk0 = pkg2 + "@" + sid2 + "@" + (btn.userId != null ? String(btn.userId) : "");
|
||||
this._iconLruPut("sc|" + sk0, dr0);
|
||||
} catch (eSc0) {}
|
||||
return dr0;
|
||||
}
|
||||
}
|
||||
} catch(eF0) {}
|
||||
}
|
||||
} catch(eFile0) {}
|
||||
var skey = pkg2 + "@" + sid2 + "@" + (btn.userId != null ? String(btn.userId) : "");
|
||||
var kSc = "sc|" + skey;
|
||||
var hitSc = this._iconLruGet(kSc);
|
||||
if (hitSc) return hitSc;
|
||||
// # 失败冷却:某些 ROM/桌面在短时间内反复查询 Shortcuts 会很慢或直接抛异常。
|
||||
// # 这里对同一个 shortcut key 做短暂冷却(默认 10 秒),避免面板频繁刷新导致卡顿/ANR 风险。
|
||||
if (!this._shortcutIconFailTs) this._shortcutIconFailTs = {};
|
||||
var nowTs = 0;
|
||||
try { nowTs = new Date().getTime(); } catch(eNow) { nowTs = 0; }
|
||||
var lastFailTs = this._shortcutIconFailTs[skey];
|
||||
if (lastFailTs && nowTs > 0 && (nowTs - lastFailTs) < 10000) {
|
||||
// # 冷却期内直接跳过 shortcut icon 查询,走回退逻辑(应用图标)
|
||||
} else {
|
||||
|
||||
var la = context.getSystemService(android.content.Context.LAUNCHER_APPS_SERVICE);
|
||||
if (la) {
|
||||
|
||||
// # 修复:按钮管理页/按钮面板中,微信小程序等 shortcut 图标显示成宿主 App 图标
|
||||
// # 根因(工作假设):用 setShortcutIds() 精确查询时,部分 ROM 的实现会返回"退化"的 ShortcutInfo,
|
||||
// # getShortcutIconDrawable 结果被降级为宿主 App 图标。
|
||||
// # 方案:完全复用 shortcuts.js 的做法--先按 package + flags 拉取该包的 shortcuts 列表,再在列表里按 id 过滤命中项取图标。
|
||||
// # 注意:不依赖外部 buildShortcutItemsIndex(),避免主程序环境下未加载 shortcuts.js 导致直接回退。
|
||||
// # 方案:完全复用"选择快捷方式列表"的取数路径,确保微信小程序等 pinned shortcut 能拿到正确的 ShortcutInfo
|
||||
// # 1) 优先直连 shortcut service(选择器同款:信息更完整)
|
||||
// # 2) 失败再回退 LauncherApps.getShortcuts
|
||||
var list = null;
|
||||
|
||||
// # 1) shortcut service 直连(与选择器保持一致)
|
||||
try {
|
||||
var userIdIntForSvc = 0;
|
||||
try {
|
||||
var buid2 = null;
|
||||
if (btn.userId != null) buid2 = btn.userId;
|
||||
if (buid2 == null && btn.user != null) buid2 = btn.user;
|
||||
if (buid2 != null) {
|
||||
var tmpUid2 = parseInt(String(buid2), 10);
|
||||
if (!isNaN(tmpUid2)) userIdIntForSvc = tmpUid2;
|
||||
}
|
||||
} catch(eUidSvc) {}
|
||||
|
||||
var sm2 = android.os.ServiceManager;
|
||||
var shortcutSvc = null;
|
||||
try { shortcutSvc = sm2.getService("shortcut"); } catch(eSm2) { shortcutSvc = null; }
|
||||
if (shortcutSvc) {
|
||||
var CFG_MATCH_ALL2 = 0x0000000F;
|
||||
var slice2 = null;
|
||||
try { slice2 = shortcutSvc.getShortcuts(String(pkg2), CFG_MATCH_ALL2, userIdIntForSvc); } catch(eS0b) { slice2 = null; }
|
||||
if (slice2) {
|
||||
var listObj2 = null;
|
||||
try { listObj2 = slice2.getList(); } catch(eS1b) { listObj2 = null; }
|
||||
if (listObj2) list = listObj2;
|
||||
}
|
||||
}
|
||||
} catch(eSvc2) {}
|
||||
|
||||
// # 2) LauncherApps 回退(当 shortcut service 不可用或返回空时)
|
||||
if (list == null) {
|
||||
var q = new android.content.pm.LauncherApps.ShortcutQuery();
|
||||
try { q.setPackage(pkg2); } catch(eSP) {}
|
||||
|
||||
// # 重要:必须设置 QueryFlags,否则 getShortcuts 可能返回空(默认 flags=0)
|
||||
// # 兼容性:不同 Android/ROM 可能缺少部分 FLAG,逐个 try 叠加
|
||||
try {
|
||||
var qFlags = 0;
|
||||
try { qFlags = qFlags | android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC; } catch(eF1) {}
|
||||
try { qFlags = qFlags | android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED; } catch(eF2) {}
|
||||
try { qFlags = qFlags | android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST; } catch(eF3) {}
|
||||
try { qFlags = qFlags | android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED; } catch(eF4) {}
|
||||
try { q.setQueryFlags(qFlags); } catch(eSF) {}
|
||||
} catch(eQF) {}
|
||||
|
||||
// # 重要:用户句柄优先用按钮携带的 userId(如有),否则使用当前用户
|
||||
var uh = android.os.Process.myUserHandle();
|
||||
try {
|
||||
var buid = null;
|
||||
if (btn.userId != null) buid = btn.userId;
|
||||
if (buid == null && btn.user != null) buid = btn.user;
|
||||
if (buid != null) {
|
||||
var uidInt = parseInt(String(buid), 10);
|
||||
if (!isNaN(uidInt)) {
|
||||
uh = android.os.UserHandle.of(uidInt);
|
||||
}
|
||||
}
|
||||
} catch(eUH) {}
|
||||
|
||||
try { list = la.getShortcuts(q, uh); } catch(eGS) { list = null; }
|
||||
}
|
||||
|
||||
if (list && list.size && list.size() > 0) {
|
||||
// # 在返回列表中按 shortcutId 精确命中
|
||||
var si = null;
|
||||
try {
|
||||
for (var kk = 0; kk < list.size(); kk++) {
|
||||
var s0 = list.get(kk);
|
||||
if (s0 != null) {
|
||||
var id0 = "";
|
||||
try { id0 = String(s0.getId()); } catch(eId0) { id0 = ""; }
|
||||
if (id0 === sid2) { si = s0; break; }
|
||||
}
|
||||
}
|
||||
} catch(eFind) { si = null; }
|
||||
if (si != null) {
|
||||
// # 与 shortcuts.js 一致:优先 la.getShortcutIconDrawable(shortcutInfo, 0),再兜底 Icon.loadDrawable
|
||||
var drSc = null;
|
||||
try { drSc = la.getShortcutIconDrawable(si, 0); } catch(eIcon0) { drSc = null; }
|
||||
if (drSc == null) {
|
||||
try {
|
||||
var ic = si.getIcon();
|
||||
if (ic != null) {
|
||||
var d2 = ic.loadDrawable(context);
|
||||
if (d2 != null) drSc = d2;
|
||||
}
|
||||
} catch(eIcon1) {}
|
||||
}
|
||||
if (drSc != null) {
|
||||
try { this._iconLruPut("sc|" + skey, drSc); } catch (eSc1) {}
|
||||
return drSc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// # 如果没拿到 shortcut 图标,记录一次失败时间,触发冷却
|
||||
try {
|
||||
if (nowTs > 0) this._shortcutIconFailTs[skey] = nowTs;
|
||||
else this._shortcutIconFailTs[skey] = new Date().getTime();
|
||||
} catch(eFT) {}
|
||||
}
|
||||
|
||||
|
||||
// # 回退:取应用图标,至少保证按钮有图标可见
|
||||
try {
|
||||
var pm2 = context.getPackageManager();
|
||||
var drApp2 = pm2.getApplicationIcon(pkg2);
|
||||
if (drApp2 != null) return drApp2;
|
||||
} catch(eFall) {}
|
||||
}
|
||||
}
|
||||
} catch (eSc) {}
|
||||
// # 2) 显式指定 iconResName (String) 或 iconResId (int)
|
||||
try {
|
||||
if (btn.iconResName) {
|
||||
var drShortx = this.resolveShortXDrawable(btn.iconResName, btn && btn.iconTint ? String(btn.iconTint) : "");
|
||||
if (drShortx != null) return drShortx;
|
||||
var name = this.normalizeShortXIconName(btn.iconResName, true);
|
||||
// # 回退到 android 系统图标
|
||||
var id = context.getResources().getIdentifier(name, "drawable", "android");
|
||||
if (id > 0) return context.getResources().getDrawable(id, null);
|
||||
}
|
||||
if (btn.iconResId) return context.getResources().getDrawable(btn.iconResId, null);
|
||||
} catch (e1) {}
|
||||
|
||||
// # 3) 兜底
|
||||
return context.getResources().getDrawable(android.R.drawable.ic_menu_help, null);
|
||||
} catch (e2) {}
|
||||
return null;
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.getShortXResHandle = function() {
|
||||
if (this._shortxResHandle) return this._shortxResHandle;
|
||||
try {
|
||||
var flags = 0;
|
||||
try { flags = android.content.Context.CONTEXT_INCLUDE_CODE | android.content.Context.CONTEXT_IGNORE_SECURITY; } catch (eF) { flags = android.content.Context.CONTEXT_RESTRICTED; }
|
||||
var sxCtx = context.createPackageContext(CONST_SHORTX_PACKAGE, flags);
|
||||
this._shortxResHandle = {
|
||||
ctx: sxCtx,
|
||||
res: sxCtx.getResources(),
|
||||
cl: sxCtx.getClassLoader(),
|
||||
pkg: CONST_SHORTX_PACKAGE
|
||||
};
|
||||
return this._shortxResHandle;
|
||||
} catch (e) {
|
||||
safeLog(this.L, 'w', "getShortXResHandle failed: " + String(e));
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.normalizeShortXIconName = function(name, keepPrefix) {
|
||||
try {
|
||||
var s = String(name == null ? "" : name).replace(/^\s+|\s+$/g, "");
|
||||
if (!s) return "";
|
||||
if (s.indexOf("@drawable/") === 0) s = s.substring(10);
|
||||
if (s.indexOf(".") > 0) {
|
||||
var parts = s.split(".");
|
||||
s = parts[parts.length - 1];
|
||||
}
|
||||
if (s.indexOf("ic_remix_") === 0) {
|
||||
return keepPrefix ? s : s.substring("ic_remix_".length);
|
||||
}
|
||||
if (s.indexOf("ic_") === 0) {
|
||||
return s;
|
||||
}
|
||||
return keepPrefix ? ("ic_remix_" + s) : s;
|
||||
} catch (e) {}
|
||||
return "";
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.getShortXApkPaths = function() {
|
||||
var out = [];
|
||||
try {
|
||||
var handle = this.getShortXResHandle();
|
||||
var ai = null;
|
||||
try { if (handle && handle.ctx) ai = handle.ctx.getApplicationInfo(); } catch (eAi0) { ai = null; }
|
||||
if (ai == null) {
|
||||
try { ai = context.getPackageManager().getApplicationInfo(CONST_SHORTX_PACKAGE, 0); } catch (eAi1) { ai = null; }
|
||||
}
|
||||
function pushPath(p) {
|
||||
try {
|
||||
p = String(p || "");
|
||||
if (!p) return;
|
||||
if (out.indexOf(p) < 0) out.push(p);
|
||||
} catch (eP) {}
|
||||
}
|
||||
if (ai) {
|
||||
try { pushPath(ai.sourceDir); } catch (e0) {}
|
||||
try { pushPath(ai.publicSourceDir); } catch (e1) {}
|
||||
try {
|
||||
var ss = ai.splitSourceDirs;
|
||||
if (ss) {
|
||||
var i;
|
||||
for (i = 0; i < ss.length; i++) pushPath(ss[i]);
|
||||
}
|
||||
} catch (e2) {}
|
||||
}
|
||||
} catch (e) {}
|
||||
return out;
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.scanShortXIconsFromApk = function() {
|
||||
var out = [];
|
||||
var seen = {};
|
||||
var paths = this.getShortXApkPaths();
|
||||
var regex = /^res\/drawable[^\/]*\/(ic_remix_[a-z0-9_]+|ic_shortx|ic_launcher|ic_menu_preferences)\.(xml|png|webp|jpg|jpeg)$/;
|
||||
var lastErr = "";
|
||||
var pi;
|
||||
for (pi = 0; pi < paths.length; pi++) {
|
||||
var zip = null;
|
||||
try {
|
||||
zip = new java.util.zip.ZipFile(String(paths[pi]));
|
||||
var en = zip.entries();
|
||||
while (en.hasMoreElements()) {
|
||||
var ze = en.nextElement();
|
||||
var name = String(ze.getName());
|
||||
var m = regex.exec(name);
|
||||
if (!m) continue;
|
||||
var fullName = String(m[1]);
|
||||
if (seen[fullName]) continue;
|
||||
seen[fullName] = true;
|
||||
out.push({
|
||||
name: fullName,
|
||||
shortName: this.normalizeShortXIconName(fullName, false),
|
||||
id: 0
|
||||
});
|
||||
}
|
||||
} catch (eZip) {
|
||||
lastErr = String(eZip);
|
||||
} finally {
|
||||
try { if (zip) zip.close(); } catch (eClose) {}
|
||||
}
|
||||
}
|
||||
if ((!out || out.length === 0) && lastErr) this._shortxIconCatalogError = lastErr;
|
||||
return out;
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.getShortXIconLookupNames = function(name) {
|
||||
var out = [];
|
||||
try {
|
||||
var s = String(name == null ? "" : name).replace(/^\s+|\s+$/g, "");
|
||||
if (!s) return out;
|
||||
if (s.indexOf("@drawable/") === 0) s = s.substring(10);
|
||||
if (s.indexOf(".") > 0) {
|
||||
var parts = s.split(".");
|
||||
s = parts[parts.length - 1];
|
||||
}
|
||||
function add(v) {
|
||||
if (!v) return;
|
||||
if (out.indexOf(v) < 0) out.push(v);
|
||||
}
|
||||
add(s);
|
||||
if (s.indexOf("ic_remix_") === 0) {
|
||||
add(s.substring("ic_remix_".length));
|
||||
} else if (s.indexOf("ic_") !== 0) {
|
||||
add("ic_remix_" + s);
|
||||
add("ic_" + s);
|
||||
}
|
||||
} catch (e) {}
|
||||
return out;
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.resolveShortXDrawableResId = function(name) {
|
||||
try {
|
||||
var handle = this.getShortXResHandle();
|
||||
if (!handle || !handle.res) return 0;
|
||||
var cands = this.getShortXIconLookupNames(name);
|
||||
var i;
|
||||
for (i = 0; i < cands.length; i++) {
|
||||
var resId = 0;
|
||||
try { resId = handle.res.getIdentifier(String(cands[i]), "drawable", handle.pkg); } catch (e1) { resId = 0; }
|
||||
if (resId > 0) return resId;
|
||||
}
|
||||
} catch (e) {}
|
||||
return 0;
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.resolveShortXDrawable = function(name, tintHex) {
|
||||
try {
|
||||
var handle = this.getShortXResHandle();
|
||||
if (!handle || !handle.res) return null;
|
||||
var resId = this.resolveShortXDrawableResId(name);
|
||||
if (resId <= 0) return null;
|
||||
var dr = handle.res.getDrawable(resId, null);
|
||||
if (dr && tintHex) {
|
||||
try {
|
||||
dr = dr.mutate();
|
||||
dr.setColorFilter(android.graphics.Color.parseColor(String(tintHex)), android.graphics.PorterDuff.Mode.SRC_IN);
|
||||
} catch (eTint) {}
|
||||
}
|
||||
return dr;
|
||||
} catch (e) {}
|
||||
return null;
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.getShortXIconCatalog = function(forceReload) {
|
||||
if (!forceReload && this._shortxIconCatalog) return this._shortxIconCatalog;
|
||||
var out = [];
|
||||
this._shortxIconCatalogError = "";
|
||||
try {
|
||||
var handle = this.getShortXResHandle();
|
||||
if (handle && handle.cl) {
|
||||
try {
|
||||
var clz = handle.cl.loadClass(CONST_SHORTX_PACKAGE + ".R$drawable");
|
||||
var fields = clz.getFields();
|
||||
var i;
|
||||
for (i = 0; i < fields.length; i++) {
|
||||
try {
|
||||
var f = fields[i];
|
||||
var fname = String(f.getName());
|
||||
if (fname.indexOf("ic_remix_") !== 0 && fname !== "ic_shortx" && fname !== "ic_launcher" && fname !== "ic_menu_preferences") continue;
|
||||
out.push({
|
||||
name: fname,
|
||||
shortName: (fname.indexOf("ic_remix_") === 0) ? fname.substring("ic_remix_".length) : fname,
|
||||
id: f.getInt(null)
|
||||
});
|
||||
} catch (eField) {}
|
||||
}
|
||||
} catch (eClz) {
|
||||
this._shortxIconCatalogError = String(eClz);
|
||||
safeLog(this.L, 'w', "getShortXIconCatalog reflect failed: " + String(eClz));
|
||||
}
|
||||
}
|
||||
if (!out || out.length === 0) {
|
||||
out = this.scanShortXIconsFromApk();
|
||||
}
|
||||
out.sort(function(a, b) {
|
||||
var aw = a.name.indexOf("ic_remix_") === 0 ? 1 : 0;
|
||||
var bw = b.name.indexOf("ic_remix_") === 0 ? 1 : 0;
|
||||
if (aw !== bw) return aw - bw;
|
||||
var as = String(a.shortName || a.name);
|
||||
var bs = String(b.shortName || b.name);
|
||||
return as < bs ? -1 : (as > bs ? 1 : 0);
|
||||
});
|
||||
if (!out || out.length === 0) {
|
||||
if (!this._shortxIconCatalogError) this._shortxIconCatalogError = "反射与 APK 扫描均未获取到图标";
|
||||
} else {
|
||||
this._shortxIconCatalogError = "";
|
||||
}
|
||||
} catch (e) {
|
||||
this._shortxIconCatalogError = String(e);
|
||||
safeLog(this.L, 'w', "getShortXIconCatalog failed: " + String(e));
|
||||
}
|
||||
this._shortxIconCatalog = out;
|
||||
return out;
|
||||
};
|
||||
|
||||
1175
code/th_07_shortcut.js
Normal file
1175
code/th_07_shortcut.js
Normal file
File diff suppressed because it is too large
Load Diff
209
code/th_08_content.js
Normal file
209
code/th_08_content.js
Normal file
@@ -0,0 +1,209 @@
|
||||
// @version 1.0.0
|
||||
// =======================【Content:解析 settings URI】======================
|
||||
// 这段代码的主要内容/用途:识别 content://settings/(system|secure|global)/KEY 并用 Settings.* get/put 更稳
|
||||
FloatBallAppWM.prototype.parseSettingsUri = function(uriStr) {
|
||||
try {
|
||||
var s = String(uriStr || "");
|
||||
if (s.indexOf("content://settings/") !== 0) return null;
|
||||
|
||||
// content://settings/system/accelerometer_rotation
|
||||
var rest = s.substring("content://settings/".length);
|
||||
var parts = rest.split("/");
|
||||
if (!parts || parts.length < 1) return null;
|
||||
|
||||
var table = String(parts[0] || "");
|
||||
var key = "";
|
||||
if (parts.length >= 2) key = String(parts[1] || "");
|
||||
|
||||
if (table !== "system" && table !== "secure" && table !== "global") return null;
|
||||
return { table: table, key: key };
|
||||
} catch (e) { return null; }
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.settingsGetStringByTable = function(table, key) {
|
||||
try {
|
||||
var cr = context.getContentResolver();
|
||||
if (table === "system") return android.provider.Settings.System.getString(cr, String(key));
|
||||
if (table === "secure") return android.provider.Settings.Secure.getString(cr, String(key));
|
||||
if (table === "global") return android.provider.Settings.Global.getString(cr, String(key));
|
||||
return null;
|
||||
} catch (e) { return null; }
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.settingsPutStringByTable = function(table, key, value) {
|
||||
try {
|
||||
var cr = context.getContentResolver();
|
||||
if (table === "system") return android.provider.Settings.System.putString(cr, String(key), String(value));
|
||||
if (table === "secure") return android.provider.Settings.Secure.putString(cr, String(key), String(value));
|
||||
if (table === "global") return android.provider.Settings.Global.putString(cr, String(key), String(value));
|
||||
return false;
|
||||
} catch (e) { return false; }
|
||||
};
|
||||
|
||||
// =======================【Content:通用 query】======================
|
||||
// 这段代码的主要内容/用途:ContentResolver.query 并把 Cursor 转成文本(用于查看器面板)
|
||||
FloatBallAppWM.prototype.contentQueryToText = function(uriStr, projection, selection, selectionArgs, sortOrder, maxRows) {
|
||||
var out = { ok: false, uri: String(uriStr || ""), rows: 0, text: "", err: "" };
|
||||
var cr = null;
|
||||
var cur = null;
|
||||
|
||||
try {
|
||||
cr = context.getContentResolver();
|
||||
var uri = android.net.Uri.parse(String(uriStr));
|
||||
|
||||
var projArr = null;
|
||||
if (projection && projection.length) {
|
||||
projArr = java.lang.reflect.Array.newInstance(java.lang.String, projection.length);
|
||||
var i0;
|
||||
for (i0 = 0; i0 < projection.length; i0++) projArr[i0] = String(projection[i0]);
|
||||
}
|
||||
|
||||
var sel = (selection === undefined || selection === null) ? null : String(selection);
|
||||
|
||||
var selArgsArr = null;
|
||||
if (selectionArgs && selectionArgs.length) {
|
||||
selArgsArr = java.lang.reflect.Array.newInstance(java.lang.String, selectionArgs.length);
|
||||
var i1;
|
||||
for (i1 = 0; i1 < selectionArgs.length; i1++) selArgsArr[i1] = String(selectionArgs[i1]);
|
||||
}
|
||||
|
||||
var so = (sortOrder === undefined || sortOrder === null) ? null : String(sortOrder);
|
||||
|
||||
cur = cr.query(uri, projArr, sel, selArgsArr, so);
|
||||
if (!cur) {
|
||||
out.err = "query return null cursor";
|
||||
return out;
|
||||
}
|
||||
|
||||
var colCount = cur.getColumnCount();
|
||||
var cols = [];
|
||||
var ci;
|
||||
for (ci = 0; ci < colCount; ci++) cols.push(String(cur.getColumnName(ci)));
|
||||
|
||||
var sb = [];
|
||||
sb.push("URI: " + String(uriStr));
|
||||
sb.push("Columns(" + String(colCount) + "): " + cols.join(", "));
|
||||
sb.push("");
|
||||
|
||||
var limit = Math.max(1, Math.floor(Number(maxRows || this.config.CONTENT_MAX_ROWS || 20)));
|
||||
var row = 0;
|
||||
|
||||
while (cur.moveToNext()) {
|
||||
row++;
|
||||
sb.push("#" + String(row));
|
||||
var cj;
|
||||
for (cj = 0; cj < colCount; cj++) {
|
||||
var v = "";
|
||||
try {
|
||||
if (cur.isNull(cj)) v = "null";
|
||||
else v = String(cur.getString(cj));
|
||||
} catch (eV) {
|
||||
try { v = String(cur.getLong(cj)); } catch (eV2) { v = "<??>"; }
|
||||
}
|
||||
sb.push(" " + cols[cj] + " = " + v);
|
||||
}
|
||||
sb.push("");
|
||||
if (row >= limit) break;
|
||||
}
|
||||
|
||||
out.ok = true;
|
||||
out.rows = row;
|
||||
out.text = sb.join("\n");
|
||||
return out;
|
||||
} catch (e) {
|
||||
out.err = String(e);
|
||||
return out;
|
||||
} finally {
|
||||
try { if (cur) cur.close(); } catch (eC) {}
|
||||
}
|
||||
};
|
||||
|
||||
// =======================【Content:统一入口】======================
|
||||
// 这段代码的主要内容/用途:处理按钮里的 type:"content"
|
||||
FloatBallAppWM.prototype.execContentAction = function(btn) {
|
||||
var mode = btn.mode ? String(btn.mode) : ((btn.value !== undefined && btn.value !== null) ? "put" : "get");
|
||||
var uri = btn.uri ? String(btn.uri) : "";
|
||||
if (!uri) return { ok: false, err: "missing uri" };
|
||||
|
||||
// settings uri 优先走 Settings API
|
||||
var su = this.parseSettingsUri(uri);
|
||||
|
||||
if (mode === "get") {
|
||||
if (su && su.key) {
|
||||
var v = this.settingsGetStringByTable(su.table, su.key);
|
||||
return { ok: true, mode: "get", kind: "settings", table: su.table, key: su.key, value: (v === null ? "null" : String(v)) };
|
||||
}
|
||||
|
||||
// 非 settings:尝试 query 一行
|
||||
var q1 = this.contentQueryToText(uri, btn.projection, btn.selection, btn.selectionArgs, btn.sortOrder, 1);
|
||||
if (!q1.ok) return { ok: false, mode: "get", kind: "query", err: q1.err };
|
||||
return { ok: true, mode: "get", kind: "query", text: q1.text, rows: q1.rows };
|
||||
}
|
||||
|
||||
if (mode === "put") {
|
||||
var val = (btn.value === undefined || btn.value === null) ? "" : String(btn.value);
|
||||
|
||||
if (su && su.key) {
|
||||
var ok = this.settingsPutStringByTable(su.table, su.key, val);
|
||||
return { ok: !!ok, mode: "put", kind: "settings", table: su.table, key: su.key, value: val };
|
||||
}
|
||||
|
||||
// 非 settings:尽力走 update(ContentValues)
|
||||
try {
|
||||
var cr = context.getContentResolver();
|
||||
var u = android.net.Uri.parse(uri);
|
||||
var cv = new android.content.ContentValues();
|
||||
|
||||
// 支持 btn.values = {col1: "...", col2: "..."};否则写 value 列
|
||||
if (btn.values) {
|
||||
var k;
|
||||
for (k in btn.values) {
|
||||
if (!btn.values.hasOwnProperty(k)) continue;
|
||||
cv.put(String(k), String(btn.values[k]));
|
||||
}
|
||||
} else {
|
||||
cv.put("value", val);
|
||||
}
|
||||
|
||||
var where = (btn.selection === undefined || btn.selection === null) ? null : String(btn.selection);
|
||||
|
||||
var whereArgs = null;
|
||||
if (btn.selectionArgs && btn.selectionArgs.length) {
|
||||
whereArgs = java.lang.reflect.Array.newInstance(java.lang.String, btn.selectionArgs.length);
|
||||
var i2;
|
||||
for (i2 = 0; i2 < btn.selectionArgs.length; i2++) whereArgs[i2] = String(btn.selectionArgs[i2]);
|
||||
}
|
||||
|
||||
var n = cr.update(u, cv, where, whereArgs);
|
||||
return { ok: true, mode: "put", kind: "update", updated: Number(n) };
|
||||
} catch (eU) {
|
||||
return { ok: false, mode: "put", kind: "update", err: String(eU) };
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === "query") {
|
||||
var maxRows = (btn.maxRows === undefined || btn.maxRows === null) ? this.config.CONTENT_MAX_ROWS : Number(btn.maxRows);
|
||||
var q = this.contentQueryToText(uri, btn.projection, btn.selection, btn.selectionArgs, btn.sortOrder, maxRows);
|
||||
if (!q.ok) return { ok: false, mode: "query", err: q.err };
|
||||
return { ok: true, mode: "query", rows: q.rows, text: q.text };
|
||||
}
|
||||
|
||||
if (mode === "view") {
|
||||
try {
|
||||
var it = new android.content.Intent(android.content.Intent.ACTION_VIEW);
|
||||
it.setData(android.net.Uri.parse(uri));
|
||||
it.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(it);
|
||||
return { ok: true, mode: "view" };
|
||||
} catch (eV) {
|
||||
return { ok: false, mode: "view", err: String(eV) };
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: false, err: "unknown mode=" + mode };
|
||||
};
|
||||
|
||||
/* =======================
|
||||
下面开始:WM 动画、面板、触摸、启动、输出
|
||||
======================= */
|
||||
|
||||
662
code/th_09_animation.js
Normal file
662
code/th_09_animation.js
Normal file
@@ -0,0 +1,662 @@
|
||||
// @version 1.0.0
|
||||
FloatBallAppWM.prototype.animateBallLayout = function(toX, toY, toW, durMs, endCb) {
|
||||
var st = this.state;
|
||||
if (!st.addedBall || !st.ballRoot || !st.ballLp) { if (endCb) endCb(); return; }
|
||||
|
||||
var fromX = st.ballLp.x;
|
||||
var fromY = st.ballLp.y;
|
||||
var fromW = st.ballLp.width;
|
||||
|
||||
try {
|
||||
var va = android.animation.ValueAnimator.ofFloat(0.0, 1.0);
|
||||
va.setDuration(durMs);
|
||||
try {
|
||||
// 使用 OvershootInterpolator 产生轻微的回弹效果,更加生动
|
||||
// 0.7 的张力适中,不会过于夸张
|
||||
va.setInterpolator(new android.view.animation.OvershootInterpolator(0.7));
|
||||
} catch (eI) {
|
||||
try { va.setInterpolator(new android.view.animation.DecelerateInterpolator()); } catch (eI2) {}
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
va.addUpdateListener(new android.animation.ValueAnimator.AnimatorUpdateListener({
|
||||
onAnimationUpdate: function(anim) {
|
||||
try {
|
||||
if (self.state.closing) return;
|
||||
if (!self.state.addedBall) return;
|
||||
|
||||
var f = anim.getAnimatedValue();
|
||||
var nx = Math.round(fromX + (toX - fromX) * f);
|
||||
var ny = Math.round(fromY + (toY - fromY) * f);
|
||||
var nw = Math.round(fromW + (toW - fromW) * f);
|
||||
|
||||
// 性能优化:只有坐标真正变化时才请求 WindowManager 更新
|
||||
if (nx !== self.state.ballLp.x || ny !== self.state.ballLp.y || nw !== self.state.ballLp.width) {
|
||||
self.state.ballLp.x = nx;
|
||||
self.state.ballLp.y = ny;
|
||||
self.state.ballLp.width = nw;
|
||||
// # 关键操作使用 safeOperation 封装
|
||||
safeOperation("dockAnimation.updateViewLayout", function() {
|
||||
self.state.wm.updateViewLayout(self.state.ballRoot, self.state.ballLp);
|
||||
}, true, self.L);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}));
|
||||
|
||||
va.addListener(new android.animation.Animator.AnimatorListener({
|
||||
onAnimationStart: function() {},
|
||||
onAnimationRepeat: function() {},
|
||||
onAnimationCancel: function() {},
|
||||
onAnimationEnd: function() {
|
||||
try {
|
||||
if (!self.state.closing && self.state.addedBall) {
|
||||
self.state.ballLp.x = toX;
|
||||
self.state.ballLp.y = toY;
|
||||
self.state.ballLp.width = toW;
|
||||
self.state.wm.updateViewLayout(self.state.ballRoot, self.state.ballLp);
|
||||
self.savePos(self.state.ballLp.x, self.state.ballLp.y);
|
||||
}
|
||||
} catch (e2) {}
|
||||
try { if (endCb) endCb(); } catch (eCb) { try { if (self && self.L && self.L.e) self.L.e("animateBallLayout endCb err=" + String(eCb)); } catch (eLog) {} }
|
||||
}
|
||||
}));
|
||||
|
||||
va.start();
|
||||
} catch (e0) {
|
||||
try {
|
||||
st.ballLp.x = toX;
|
||||
st.ballLp.y = toY;
|
||||
st.ballLp.width = toW;
|
||||
st.wm.updateViewLayout(st.ballRoot, st.ballLp);
|
||||
this.savePos(st.ballLp.x, st.ballLp.y);
|
||||
} catch (e1) {}
|
||||
try { if (endCb) endCb(); } catch (eCb2) { try { if (this && this.L && this.L.e) this.L.e("animateBallLayout endCb err=" + String(eCb2)); } catch (eLog2) {} }
|
||||
}
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.playBounce = function(v) {
|
||||
if (!this.config.ENABLE_BOUNCE) return;
|
||||
if (!this.config.ENABLE_ANIMATIONS) return;
|
||||
|
||||
try { v.animate().cancel(); } catch (e0) {}
|
||||
|
||||
var self = this;
|
||||
var i = 0;
|
||||
|
||||
function step() {
|
||||
if (self.state.closing) return;
|
||||
|
||||
if (i >= self.config.BOUNCE_TIMES) {
|
||||
try { v.setScaleX(1); v.setScaleY(1); } catch (e2) {}
|
||||
return;
|
||||
}
|
||||
|
||||
var amp = (self.config.BOUNCE_MAX_SCALE - 1) * Math.pow(self.config.BOUNCE_DECAY, i);
|
||||
var s = 1 + amp;
|
||||
|
||||
v.animate()
|
||||
.scaleX(s)
|
||||
.scaleY(s)
|
||||
.setDuration(self.config.BOUNCE_STEP_MS)
|
||||
.setInterpolator(new android.view.animation.OvershootInterpolator())
|
||||
.withEndAction(new JavaAdapter(java.lang.Runnable, {
|
||||
run: function() {
|
||||
v.animate()
|
||||
.scaleX(1)
|
||||
.scaleY(1)
|
||||
.setDuration(self.config.BOUNCE_STEP_MS)
|
||||
.setInterpolator(new android.view.animation.AccelerateDecelerateInterpolator())
|
||||
.withEndAction(new JavaAdapter(java.lang.Runnable, {
|
||||
run: function() { i++; step(); }
|
||||
}))
|
||||
.start();
|
||||
}
|
||||
}))
|
||||
.start();
|
||||
}
|
||||
|
||||
step();
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.safeRemoveView = function(v, whichName) {
|
||||
try {
|
||||
if (!v) return { ok: true, skipped: true };
|
||||
this.state.wm.removeView(v);
|
||||
return { ok: true };
|
||||
} catch (e) {
|
||||
safeLog(this.L, 'w', "removeView fail which=" + String(whichName || "") + " err=" + String(e));
|
||||
return { ok: false, err: String(e), where: whichName || "" };
|
||||
}
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.hideMask = function() {
|
||||
if (!this.state.addedMask) return;
|
||||
if (!this.state.mask) return;
|
||||
|
||||
this.safeRemoveView(this.state.mask, "mask");
|
||||
this.state.mask = null;
|
||||
this.state.maskLp = null;
|
||||
this.state.addedMask = false;
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.hideMainPanel = function() {
|
||||
if (!this.state.addedPanel) return;
|
||||
if (!this.state.panel) return;
|
||||
|
||||
this.safeRemoveView(this.state.panel, "panel");
|
||||
this.state.panel = null;
|
||||
this.state.panelLp = null;
|
||||
this.state.addedPanel = false;
|
||||
|
||||
this.hideMask();
|
||||
this.touchActivity();
|
||||
|
||||
this._clearHeavyCachesIfAllHidden("hideMainPanel");
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.hideSettingsPanel = function() {
|
||||
if (!this.state.addedSettings) return;
|
||||
if (!this.state.settingsPanel) return;
|
||||
|
||||
this.safeRemoveView(this.state.settingsPanel, "settingsPanel");
|
||||
this.state.settingsPanel = null;
|
||||
this.state.settingsPanelLp = null;
|
||||
this.state.addedSettings = false;
|
||||
|
||||
this.state.pendingUserCfg = null;
|
||||
this.state.pendingDirty = false;
|
||||
|
||||
this.hideMask();
|
||||
this.touchActivity();
|
||||
|
||||
this._clearHeavyCachesIfAllHidden("hideSettingsPanel");
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.hideViewerPanel = function() {
|
||||
if (!this.state.addedViewer) return;
|
||||
if (!this.state.viewerPanel) return;
|
||||
|
||||
this.safeRemoveView(this.state.viewerPanel, "viewerPanel");
|
||||
this.state.viewerPanel = null;
|
||||
this.state.viewerPanelLp = null;
|
||||
this.state.addedViewer = false;
|
||||
|
||||
this.hideMask();
|
||||
this.touchActivity();
|
||||
|
||||
this._clearHeavyCachesIfAllHidden("hideViewerPanel");
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.clearHeavyCaches = function(reason) {
|
||||
// 这段代码的主要内容/用途:在所有面板都关闭后,主动清理"图标/快捷方式"等重缓存,降低 system_server 常驻内存。
|
||||
// 说明:仅清理缓存引用,不强行 recycle Bitmap,避免误伤仍被使用的 Drawable。
|
||||
|
||||
// # 防抖:5秒内相同 reason 不重复清理
|
||||
var now = Date.now();
|
||||
var cacheKey = "_lastClear_" + (reason || "default");
|
||||
var lastClear = this.state[cacheKey] || 0;
|
||||
if (now - lastClear < 5000) {
|
||||
return; // 5秒内已清理过,跳过
|
||||
}
|
||||
this.state[cacheKey] = now;
|
||||
|
||||
try { this._iconLru = null; } catch (eLruClr) {}
|
||||
try { this._shortcutIconFailTs = {}; } catch (e2) {}
|
||||
|
||||
// # Shortcuts 相关全局缓存(按钮编辑页/快捷方式选择器可能会创建)
|
||||
try { if (typeof __scIconCache !== "undefined") __scIconCache = {}; } catch (e3) {}
|
||||
try { if (typeof __scAppLabelCache !== "undefined") __scAppLabelCache = {}; } catch (e4) {}
|
||||
|
||||
// # 记录一次清理日志(精简:只记录关键 reason,且 5秒防抖)
|
||||
var keyReasons = ["memory_pressure", "screen_changed", "close"];
|
||||
var isKeyReason = keyReasons.indexOf(reason) >= 0;
|
||||
try {
|
||||
if (isKeyReason && this.L && this.L.i) {
|
||||
this.L.i("clearHeavyCaches reason=" + String(reason));
|
||||
}
|
||||
} catch (e5) {}
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype._clearHeavyCachesIfAllHidden = function(reason) {
|
||||
// 这段代码的主要内容/用途:只在"主面板/设置/查看器"全部关闭后清理缓存,避免页面切换时反复重建导致卡顿。
|
||||
try {
|
||||
if (!this.state.addedPanel && !this.state.addedSettings && !this.state.addedViewer) {
|
||||
this.clearHeavyCaches(reason || "all_hidden");
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.hideAllPanels = function() {
|
||||
this.hideMainPanel();
|
||||
this.hideSettingsPanel();
|
||||
this.hideViewerPanel();
|
||||
this.hideMask();
|
||||
|
||||
this._clearHeavyCachesIfAllHidden("hideAllPanels");
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.showMask = function() {
|
||||
if (this.state.addedMask) return;
|
||||
if (this.state.closing) return;
|
||||
|
||||
var self = this;
|
||||
var mask = new android.widget.FrameLayout(context);
|
||||
|
||||
// 遮罩层背景:轻微的黑色半透明,提升层次感
|
||||
try { mask.setBackgroundColor(android.graphics.Color.parseColor("#33000000")); } catch (e0) {
|
||||
mask.setBackgroundColor(0x33000000);
|
||||
}
|
||||
|
||||
mask.setOnTouchListener(new JavaAdapter(android.view.View.OnTouchListener, {
|
||||
onTouch: function(v, e) {
|
||||
var a = e.getAction();
|
||||
if (a === android.view.MotionEvent.ACTION_DOWN) {
|
||||
self.touchActivity();
|
||||
self.hideAllPanels();
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}));
|
||||
|
||||
var lp = new android.view.WindowManager.LayoutParams(
|
||||
android.view.WindowManager.LayoutParams.MATCH_PARENT,
|
||||
android.view.WindowManager.LayoutParams.MATCH_PARENT,
|
||||
android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
|
||||
android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
android.graphics.PixelFormat.TRANSLUCENT
|
||||
);
|
||||
|
||||
lp.gravity = android.view.Gravity.TOP | android.view.Gravity.START;
|
||||
lp.x = 0;
|
||||
lp.y = 0;
|
||||
|
||||
try {
|
||||
this.state.wm.addView(mask, lp);
|
||||
this.state.mask = mask;
|
||||
this.state.maskLp = lp;
|
||||
this.state.addedMask = true;
|
||||
|
||||
// 简单的淡入动画
|
||||
try {
|
||||
if (this.config.ENABLE_ANIMATIONS) {
|
||||
mask.setAlpha(0);
|
||||
mask.animate().alpha(1).setDuration(200).start();
|
||||
} else {
|
||||
mask.setAlpha(1);
|
||||
}
|
||||
} catch(eAnim){}
|
||||
|
||||
} catch (e1) {
|
||||
safeLog(this.L, 'e', "add mask fail err=" + String(e1));
|
||||
this.state.addedMask = false;
|
||||
}
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.snapToEdgeDocked = function(withAnim, forceSide) {
|
||||
if (this.state.closing) return;
|
||||
// 移除对面板/Mask的检查,允许在任何情况下强制吸边(如果调用方逻辑正确)
|
||||
// 如果需要保护,调用方自己判断
|
||||
if (this.state.dragging) return;
|
||||
|
||||
var di = this.getDockInfo();
|
||||
var ballSize = di.ballSize;
|
||||
var visible = di.visiblePx;
|
||||
var hidden = di.hiddenPx;
|
||||
|
||||
var snapLeft;
|
||||
if (forceSide === "left") snapLeft = true;
|
||||
else if (forceSide === "right") snapLeft = false;
|
||||
else {
|
||||
// 默认根据中心点判断
|
||||
var centerX = this.state.ballLp.x + Math.round(ballSize / 2);
|
||||
snapLeft = centerX < Math.round(this.state.screen.w / 2);
|
||||
}
|
||||
|
||||
var targetW = visible;
|
||||
var targetY = this.clamp(this.state.ballLp.y, 0, this.state.screen.h - ballSize);
|
||||
|
||||
if (snapLeft) {
|
||||
this.state.dockSide = "left";
|
||||
this.state.docked = true;
|
||||
|
||||
try { this.state.ballContent.setX(-hidden); } catch (eL) {}
|
||||
|
||||
if (withAnim) {
|
||||
this.animateBallLayout(0, targetY, targetW, this.config.DOCK_ANIM_MS, null);
|
||||
} else {
|
||||
this.state.ballLp.x = 0;
|
||||
this.state.ballLp.y = targetY;
|
||||
this.state.ballLp.width = targetW;
|
||||
try { this.state.wm.updateViewLayout(this.state.ballRoot, this.state.ballLp); } catch (eU1) {}
|
||||
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
|
||||
}
|
||||
|
||||
safeLog(this.L, 'd', "dock left x=0 y=" + String(targetY) + " w=" + String(targetW));
|
||||
|
||||
// 闲置变暗
|
||||
try {
|
||||
if (this.config.ENABLE_ANIMATIONS) {
|
||||
this.state.ballContent.animate().alpha(this.config.BALL_IDLE_ALPHA).setDuration(300).start();
|
||||
} else {
|
||||
this.state.ballContent.setAlpha(this.config.BALL_IDLE_ALPHA);
|
||||
}
|
||||
} catch(eA) {}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.dockSide = "right";
|
||||
this.state.docked = true;
|
||||
|
||||
try { this.state.ballContent.setX(0); } catch (eR) {}
|
||||
|
||||
var x2 = this.state.screen.w - visible;
|
||||
|
||||
if (withAnim) {
|
||||
this.animateBallLayout(x2, targetY, targetW, this.config.DOCK_ANIM_MS, null);
|
||||
} else {
|
||||
this.state.ballLp.x = x2;
|
||||
this.state.ballLp.y = targetY;
|
||||
this.state.ballLp.width = targetW;
|
||||
try { this.state.wm.updateViewLayout(this.state.ballRoot, this.state.ballLp); } catch (eU2) {}
|
||||
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
|
||||
}
|
||||
|
||||
// # 日志精简:dock 事件添加防抖(10秒内不重复记录相同边)
|
||||
var dockNow = Date.now();
|
||||
var lastDock = this.state._lastDockLog || 0;
|
||||
if (dockNow - lastDock > 10000) {
|
||||
safeLog(this.L, 'i', "dock right x=" + String(x2) + " y=" + String(targetY));
|
||||
this.state._lastDockLog = dockNow;
|
||||
}
|
||||
|
||||
// 闲置变暗
|
||||
try {
|
||||
if (this.config.ENABLE_ANIMATIONS) {
|
||||
this.state.ballContent.animate().alpha(this.config.BALL_IDLE_ALPHA).setDuration(300).start();
|
||||
} else {
|
||||
this.state.ballContent.setAlpha(this.config.BALL_IDLE_ALPHA);
|
||||
}
|
||||
} catch(eA) {}
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.undockToFull = function(withAnim, endCb) {
|
||||
if (this.state.closing) { if (endCb) endCb(); return; }
|
||||
if (!this.state.docked) { if (endCb) endCb(); return; }
|
||||
if (!this.state.addedBall) { if (endCb) endCb(); return; }
|
||||
|
||||
var di = this.getDockInfo();
|
||||
var ballSize = di.ballSize;
|
||||
var targetW = ballSize;
|
||||
var targetY = this.clamp(this.state.ballLp.y, 0, this.state.screen.h - ballSize);
|
||||
|
||||
try { this.state.ballContent.setX(0); } catch (e0) {}
|
||||
|
||||
if (this.state.dockSide === "left") {
|
||||
this.state.docked = false;
|
||||
this.state.dockSide = null;
|
||||
|
||||
if (withAnim) this.animateBallLayout(0, targetY, targetW, this.config.UNDOCK_ANIM_MS, endCb);
|
||||
else {
|
||||
this.state.ballLp.x = 0;
|
||||
this.state.ballLp.y = targetY;
|
||||
this.state.ballLp.width = targetW;
|
||||
try { this.state.wm.updateViewLayout(this.state.ballRoot, this.state.ballLp); } catch (eU1) {}
|
||||
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
|
||||
if (endCb) endCb();
|
||||
}
|
||||
|
||||
// 恢复不透明度
|
||||
try {
|
||||
if (withAnim && this.config.ENABLE_ANIMATIONS) {
|
||||
this.state.ballContent.animate().alpha(1.0).setDuration(150).start();
|
||||
} else {
|
||||
this.state.ballContent.setAlpha(1.0);
|
||||
}
|
||||
} catch(eA) {}
|
||||
|
||||
safeLog(this.L, 'i', "undock from left");
|
||||
return;
|
||||
}
|
||||
|
||||
var x = this.state.screen.w - ballSize;
|
||||
|
||||
this.state.docked = false;
|
||||
this.state.dockSide = null;
|
||||
|
||||
if (withAnim) this.animateBallLayout(x, targetY, targetW, this.config.UNDOCK_ANIM_MS, endCb);
|
||||
else {
|
||||
this.state.ballLp.x = x;
|
||||
this.state.ballLp.y = targetY;
|
||||
this.state.ballLp.width = targetW;
|
||||
try { this.state.wm.updateViewLayout(this.state.ballRoot, this.state.ballLp); } catch (eU2) {}
|
||||
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
|
||||
if (endCb) endCb();
|
||||
}
|
||||
|
||||
// 恢复不透明度
|
||||
try {
|
||||
if (withAnim && this.config.ENABLE_ANIMATIONS) {
|
||||
this.state.ballContent.animate().alpha(1.0).setDuration(150).start();
|
||||
} else {
|
||||
this.state.ballContent.setAlpha(1.0);
|
||||
}
|
||||
} catch(eA) {}
|
||||
|
||||
// # 日志精简:undock 事件改为 INFO 级别,且记录方向
|
||||
var undockSide = this.state.dockSide || "right";
|
||||
safeLog(this.L, 'i', "undock from " + undockSide);
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.cancelDockTimer = function() {
|
||||
try { if (this.state.idleDockRunnable && this.state.h) this.state.h.removeCallbacks(this.state.idleDockRunnable); } catch (e) {}
|
||||
this.state.idleDockRunnable = null;
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.armDockTimer = function() {
|
||||
if (this.state.closing) return;
|
||||
if (!this.state.h) return;
|
||||
if (!this.state.addedBall) return;
|
||||
if (this.state.docked) return;
|
||||
|
||||
this.cancelDockTimer();
|
||||
|
||||
var hasPanel = (this.state.addedPanel || this.state.addedSettings || this.state.addedViewer || this.state.addedMask);
|
||||
var targetMs = hasPanel ? this.config.PANEL_IDLE_CLOSE_AND_DOCK_MS : this.config.DOCK_AFTER_IDLE_MS;
|
||||
|
||||
var self = this;
|
||||
|
||||
this.state.idleDockRunnable = new java.lang.Runnable({
|
||||
run: function() {
|
||||
try {
|
||||
if (self.state.closing) return;
|
||||
if (self.state.docked) return;
|
||||
if (self.state.dragging) return;
|
||||
|
||||
var hasPanel2 = (self.state.addedPanel || self.state.addedSettings || self.state.addedViewer || self.state.addedMask);
|
||||
var needMs = hasPanel2 ? self.config.PANEL_IDLE_CLOSE_AND_DOCK_MS : self.config.DOCK_AFTER_IDLE_MS;
|
||||
|
||||
var idle = self.now() - self.state.lastMotionTs;
|
||||
if (idle < needMs) { self.armDockTimer(); return; }
|
||||
|
||||
// if (hasPanel2) self.hideAllPanels(); // 用户要求不再自动关闭面板
|
||||
if (self.config.ENABLE_SNAP_TO_EDGE) {
|
||||
self.snapToEdgeDocked(true);
|
||||
}
|
||||
} catch (e0) {
|
||||
if (self.L) self.L.e("dockTimer run err=" + String(e0));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.state.h.postDelayed(this.state.idleDockRunnable, targetMs);
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.touchActivity = function() {
|
||||
this.state.lastMotionTs = this.now();
|
||||
this.armDockTimer();
|
||||
}
|
||||
|
||||
// # 点击防抖与安全执行
|
||||
// 这段代码的主要内容/用途:防止在悬浮面板上快速/乱点导致重复 add/remove、状态机被打穿,从而引发 system_server 异常重启。
|
||||
FloatBallAppWM.prototype.guardClick = function(key, cooldownMs, fn) {
|
||||
try {
|
||||
var now = android.os.SystemClock.uptimeMillis();
|
||||
if (!this.state._clickGuards) this.state._clickGuards = {};
|
||||
var last = this.state._clickGuards[key] || 0;
|
||||
var cd = (cooldownMs != null ? cooldownMs : INTERACTION_CONSTANTS.CLICK_COOLDOWN_MS);
|
||||
if (now - last < cd) return false;
|
||||
this.state._clickGuards[key] = now;
|
||||
try {
|
||||
fn && fn();
|
||||
} catch (e1) {
|
||||
safeLog(this.L, 'e', "guardClick err key=" + String(key) + " err=" + String(e1));
|
||||
}
|
||||
return true;
|
||||
} catch (e0) {
|
||||
// 兜底:绝不让点击回调异常冒泡到 system_server
|
||||
try { fn && fn(); } catch (e2) {}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.safeUiCall = function(tag, fn) {
|
||||
try {
|
||||
fn && fn();
|
||||
} catch (e) {
|
||||
safeLog(this.L, 'e', "safeUiCall err tag=" + String(tag || "") + " err=" + String(e));
|
||||
}
|
||||
};
|
||||
|
||||
;
|
||||
|
||||
FloatBallAppWM.prototype.onScreenChangedReflow = function() {
|
||||
if (this.state.closing) return;
|
||||
if (!this.state.addedBall) return;
|
||||
|
||||
var di = this.getDockInfo();
|
||||
|
||||
var oldW = this.state.screen.w;
|
||||
var oldH = this.state.screen.h;
|
||||
|
||||
var newScreen = this.getScreenSizePx();
|
||||
var newW = newScreen.w;
|
||||
var newH = newScreen.h;
|
||||
|
||||
if (newW <= 0 || newH <= 0) return;
|
||||
if (oldW <= 0) oldW = newW;
|
||||
if (oldH <= 0) oldH = newH;
|
||||
|
||||
this.state.screen = { w: newW, h: newH };
|
||||
|
||||
var ballSize = di.ballSize;
|
||||
var visible = di.visiblePx;
|
||||
var hidden = di.hiddenPx;
|
||||
|
||||
var oldMaxX = Math.max(1, oldW - ballSize);
|
||||
var oldMaxY = Math.max(1, oldH - ballSize);
|
||||
var newMaxX = Math.max(1, newW - ballSize);
|
||||
var newMaxY = Math.max(1, newH - ballSize);
|
||||
|
||||
var xRatio = this.state.ballLp.x / oldMaxX;
|
||||
var yRatio = this.state.ballLp.y / oldMaxY;
|
||||
|
||||
var mappedX = Math.round(xRatio * newMaxX);
|
||||
var mappedY = Math.round(yRatio * newMaxY);
|
||||
|
||||
mappedX = this.clamp(mappedX, 0, newMaxX);
|
||||
mappedY = this.clamp(mappedY, 0, newMaxY);
|
||||
|
||||
if (this.state.docked) {
|
||||
this.state.ballLp.y = mappedY;
|
||||
this.state.ballLp.width = visible;
|
||||
|
||||
if (this.state.dockSide === "left") {
|
||||
this.state.ballLp.x = 0;
|
||||
try { this.state.ballContent.setX(-hidden); } catch (eL) {}
|
||||
} else {
|
||||
this.state.ballLp.x = newW - visible;
|
||||
try { this.state.ballContent.setX(0); } catch (eR) {}
|
||||
}
|
||||
// 重新进入闲置变暗逻辑(如果需要)
|
||||
try { this.state.ballContent.setAlpha(this.config.BALL_IDLE_ALPHA); } catch(eA) {}
|
||||
} else {
|
||||
this.state.ballLp.x = mappedX;
|
||||
this.state.ballLp.y = mappedY;
|
||||
this.state.ballLp.width = ballSize;
|
||||
try { this.state.ballContent.setX(0); } catch (e0) {}
|
||||
try { this.state.ballContent.setAlpha(1.0); } catch(eA) {}
|
||||
}
|
||||
|
||||
try { this.state.wm.updateViewLayout(this.state.ballRoot, this.state.ballLp); } catch (eU) {}
|
||||
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
|
||||
|
||||
safeLog(this.L, 'i', "screen reflow w=" + String(newW) + " h=" + String(newH) + " x=" + String(this.state.ballLp.x) + " y=" + String(this.state.ballLp.y));
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.setupDisplayMonitor = function() {
|
||||
if (this.state.closing) return;
|
||||
|
||||
try {
|
||||
var dm = context.getSystemService(android.content.Context.DISPLAY_SERVICE);
|
||||
if (!dm) return;
|
||||
|
||||
this.state.dm = dm;
|
||||
this.state.lastRotation = this.getRotation();
|
||||
|
||||
var self = this;
|
||||
|
||||
var listener = new JavaAdapter(android.hardware.display.DisplayManager.DisplayListener, {
|
||||
onDisplayAdded: function(displayId) {},
|
||||
onDisplayRemoved: function(displayId) {},
|
||||
onDisplayChanged: function(displayId) {
|
||||
try {
|
||||
if (self.state.closing) return;
|
||||
var nowTs = self.now();
|
||||
if (nowTs - self.state.lastMonitorTs < self.config.SCREEN_MONITOR_THROTTLE_MS) return;
|
||||
self.state.lastMonitorTs = nowTs;
|
||||
|
||||
self.state.h.post(new JavaAdapter(java.lang.Runnable, {
|
||||
run: function() {
|
||||
try {
|
||||
if (self.state.closing) return;
|
||||
if (!self.state.addedBall) return;
|
||||
|
||||
var rot = self.getRotation();
|
||||
var sz = self.getScreenSizePx();
|
||||
|
||||
var changed = false;
|
||||
if (rot !== self.state.lastRotation) { self.state.lastRotation = rot; changed = true; }
|
||||
if (sz.w !== self.state.screen.w || sz.h !== self.state.screen.h) changed = true;
|
||||
|
||||
if (changed) {
|
||||
self.cancelDockTimer();
|
||||
self.onScreenChangedReflow();
|
||||
self.touchActivity();
|
||||
}
|
||||
} catch (e1) {
|
||||
if (self.L) self.L.e("displayChanged run err=" + String(e1));
|
||||
}
|
||||
}
|
||||
}));
|
||||
} catch (e0) {}
|
||||
}
|
||||
});
|
||||
|
||||
this.state.displayListener = listener;
|
||||
dm.registerDisplayListener(listener, this.state.h);
|
||||
safeLog(this.L, 'i', "display monitor registered");
|
||||
} catch (e2) {
|
||||
safeLog(this.L, 'e', "setupDisplayMonitor err=" + String(e2));
|
||||
}
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.stopDisplayMonitor = function() {
|
||||
try { if (this.state.dm && this.state.displayListener) this.state.dm.unregisterDisplayListener(this.state.displayListener); } catch (e) {}
|
||||
this.state.displayListener = null;
|
||||
this.state.dm = null;
|
||||
};
|
||||
|
||||
33
code/th_10_shell.js
Normal file
33
code/th_10_shell.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// @version 1.0.0
|
||||
// =======================【Shell:智能执行(Action优先 + 广播桥兜底)】======================
|
||||
// 这段代码的主要内容/用途:执行 Shell 按钮动作时,优先尝试 ShortX 的 ShellCommand Action(如可用);失败则走自定义广播桥(由外部接收器实际执行)。
|
||||
// 注意:system_server 进程本身不直接执行 shell;这里只负责"触发执行"。
|
||||
// 这段代码的主要内容/用途:通过广播桥触发 Shell 执行(仅广播桥,不再使用 ShellCommand Action)。
|
||||
// 注意:system_server 进程本身不直接执行 shell;外部接收器负责实际执行。
|
||||
FloatBallAppWM.prototype.execShellSmart = function(cmdB64, needRoot) {
|
||||
var ret = { ok: false, via: "", err: "" };
|
||||
|
||||
try {
|
||||
var action = String(this.config.SHELL_BRIDGE_ACTION || CONST_SHELL_BRIDGE_ACTION || "shortx.toolhub.SHELL");
|
||||
var it = new android.content.Intent(action);
|
||||
|
||||
// # 固定广播协议:cmd_b64 + root + from(不再发送明文 cmd,避免协议漂移)
|
||||
it.putExtra(CONST_SHELL_BRIDGE_EXTRA_CMD, String(cmdB64));
|
||||
it.putExtra(CONST_SHELL_BRIDGE_EXTRA_ROOT, !!needRoot);
|
||||
// # from:来源标记,仅用于接收端识别/日志,不参与权限判断
|
||||
it.putExtra(CONST_SHELL_BRIDGE_EXTRA_FROM, "ToolHub");
|
||||
|
||||
context.sendBroadcast(it);
|
||||
|
||||
ret.ok = true;
|
||||
ret.via = "BroadcastBridge";
|
||||
safeLog(this.L, 'i', "shell via broadcast ok action=" + action + " root=" + String(!!needRoot));
|
||||
} catch (eB) {
|
||||
ret.err = "Broadcast err=" + String(eB);
|
||||
safeLog(this.L, 'e', "shell via broadcast fail err=" + String(eB));
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
|
||||
320
code/th_11_action.js
Normal file
320
code/th_11_action.js
Normal file
@@ -0,0 +1,320 @@
|
||||
// @version 1.0.0
|
||||
// =======================【WM 线程:按钮动作执行】======================
|
||||
FloatBallAppWM.prototype.execButtonAction = function(btn, idx) {
|
||||
// # 点击防抖
|
||||
// 这段代码的主要内容/用途:防止在按钮面板上连续/乱点导致重复执行与 UI 状态机冲突(可能触发 system_server 异常重启)。
|
||||
if (!this.guardClick("btn_exec_" + String(idx), 380, null)) return;
|
||||
|
||||
try {
|
||||
if (!btn || !btn.type) {
|
||||
this.toast("按钮#" + idx + " 未配置");
|
||||
safeLog(this.L, 'w', "btn#" + String(idx) + " no type");
|
||||
return;
|
||||
}
|
||||
|
||||
var t = String(btn.type);
|
||||
safeLog(this.L, 'i', "btn click idx=" + String(idx) + " type=" + t + " title=" + String(btn.title || ""));
|
||||
|
||||
if (t === "open_settings") {
|
||||
this.showPanelAvoidBall("settings");
|
||||
return;
|
||||
}
|
||||
|
||||
if (t === "open_viewer") {
|
||||
var logPath = (this.L && this.L._filePathForToday) ? this.L._filePathForToday() : "";
|
||||
if (!logPath) logPath = PATH_LOG_DIR + "/ShortX_ToolHub_" + (new java.text.SimpleDateFormat("yyyyMMdd").format(new java.util.Date())) + ".log";
|
||||
|
||||
var content = FileIO.readText(logPath);
|
||||
if (!content) content = "(日志文件不存在或为空: " + logPath + ")";
|
||||
|
||||
if (content.length > 30000) {
|
||||
content = "[...前略...]\n" + content.substring(content.length - 30000);
|
||||
}
|
||||
|
||||
// 简单的按行倒序,方便查看最新日志
|
||||
try {
|
||||
var lines = content.split("\n");
|
||||
if (lines.length > 1) {
|
||||
content = lines.reverse().join("\n");
|
||||
}
|
||||
} catch(eRev) {}
|
||||
|
||||
this.showViewerPanel("今日日志 (倒序)", content);
|
||||
return;
|
||||
}
|
||||
|
||||
if (t === "toast") {
|
||||
var msg = "";
|
||||
if (btn.text !== undefined && btn.text !== null) msg = String(btn.text);
|
||||
else if (btn.title) msg = String(btn.title);
|
||||
else msg = "按钮#" + idx;
|
||||
this.toast(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (t === "app") {
|
||||
var pkg = btn.pkg ? String(btn.pkg) : "";
|
||||
if (!pkg) { this.toast("按钮#" + idx + " 缺少 pkg"); return; }
|
||||
|
||||
var it = context.getPackageManager().getLaunchIntentForPackage(pkg);
|
||||
if (!it) { this.toast("无法启动 " + pkg); return; }
|
||||
|
||||
it.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
// # 系统级跨用户启动:Context.startActivityAsUser
|
||||
// 这段代码的主要内容/用途:支持"主应用/分身应用"选择,避免弹出选择器或误启动到另一用户。
|
||||
// 说明:当未配置 launchUserId 时,默认使用 0(主用户);失败则回退 startActivity。
|
||||
var launchUid = 0;
|
||||
try {
|
||||
if (btn.launchUserId != null && String(btn.launchUserId).length > 0) launchUid = parseInt(String(btn.launchUserId), 10);
|
||||
} catch(eLU0) { launchUid = 0; }
|
||||
if (isNaN(launchUid)) launchUid = 0;
|
||||
|
||||
try {
|
||||
// 运行日志:记录跨用户启动参数(便于定位分身启动失败原因)
|
||||
safeLog(this.L, 'i', "startAsUser(app) idx=" + idx + " pkg=" + pkg + " launchUserId=" + launchUid);
|
||||
if (launchUid !== 0) {
|
||||
context.startActivityAsUser(it, android.os.UserHandle.of(launchUid));
|
||||
} else {
|
||||
context.startActivity(it);
|
||||
}
|
||||
} catch (eA) {
|
||||
// # 兜底:某些 ROM/权限限制下 startActivityAsUser 可能抛异常,回退普通启动
|
||||
try { context.startActivity(it); } catch (eA2) {}
|
||||
this.toast("启动失败");
|
||||
safeLog(this.L, 'e', "start app fail pkg=" + pkg + " uid=" + String(launchUid) + " err=" + String(eA));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (t === "shell") {
|
||||
// # 这段代码的主要内容/用途:执行 shell(支持 cmd 明文 与 cmd_b64;最终会确保发送/执行的是"真正的 base64")
|
||||
// # 修复点:历史配置里有些按钮把"明文命令"误存进 cmd_b64(或 b64 被破坏),会导致广播接收端解码失败→看起来"没效果"。
|
||||
var cmdB64 = (btn.cmd_b64 !== undefined && btn.cmd_b64 !== null) ? String(btn.cmd_b64) : "";
|
||||
var cmdPlain = (btn.cmd !== undefined && btn.cmd !== null) ? String(btn.cmd) : "";
|
||||
|
||||
// # 1) 只有明文但没有 b64:自动补齐 b64(避免特殊字符在多层字符串传递中被破坏)
|
||||
if ((!cmdB64 || cmdB64.length === 0) && cmdPlain && cmdPlain.length > 0) {
|
||||
try {
|
||||
var b64x = encodeBase64Utf8(cmdPlain);
|
||||
if (b64x && b64x.length > 0) cmdB64 = String(b64x);
|
||||
} catch (eB64a) {}
|
||||
}
|
||||
|
||||
// # 2) cmd_b64 非空但无法解码:把它当作"明文命令"重新编码(保证广播桥/Action 都能吃到正确命令)
|
||||
// # 说明:decodeBase64Utf8 返回空串通常意味着 b64 非法或被破坏;而真实命令不太可能是空串。
|
||||
if (cmdB64 && cmdB64.length > 0) {
|
||||
try {
|
||||
var testPlain = decodeBase64Utf8(cmdB64);
|
||||
if ((!testPlain || testPlain.length === 0) && (!cmdPlain || cmdPlain.length === 0)) {
|
||||
cmdPlain = String(cmdB64);
|
||||
cmdB64 = "";
|
||||
}
|
||||
} catch (eB64b) {}
|
||||
}
|
||||
if ((!cmdB64 || cmdB64.length === 0) && cmdPlain && cmdPlain.length > 0) {
|
||||
try {
|
||||
var b64y = encodeBase64Utf8(cmdPlain);
|
||||
if (b64y && b64y.length > 0) cmdB64 = String(b64y);
|
||||
} catch (eB64c) {}
|
||||
}
|
||||
|
||||
if (!cmdB64 || cmdB64.length === 0) {
|
||||
this.toast("按钮#" + idx + " 缺少 cmd/cmd_b64");
|
||||
safeLog(this.L, 'e', "shell missing cmd idx=" + String(idx));
|
||||
return;
|
||||
}
|
||||
|
||||
// # 广播桥接收端默认以 root 执行,强制使用 root
|
||||
var needRoot = true;
|
||||
|
||||
var r = this.execShellSmart(cmdB64, needRoot);
|
||||
if (r && r.ok) return;
|
||||
|
||||
this.toast("shell 失败(Action + 广播桥均失败)");
|
||||
safeLog(this.L, 'e', "shell all failed cmd_b64=" + cmdB64 + " ret=" + JSON.stringify(r || {}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (t === "broadcast") {
|
||||
// 这段代码的主要内容/用途:发送自定义广播(兼容 btn.extra / btn.extras),并对 Shell 广播桥(shortx.toolhub.SHELL)做额外兼容(cmd/cmd_b64/root)。
|
||||
var action = btn.action ? String(btn.action) : "";
|
||||
if (!action) { this.toast("按钮#" + idx + " 缺少 action"); return; }
|
||||
|
||||
var it2 = new android.content.Intent(action);
|
||||
|
||||
// # 1) 兼容字段:extra / extras(两种都认)
|
||||
var ex = null;
|
||||
try {
|
||||
if (btn.extras) ex = btn.extras;
|
||||
else if (btn.extra) ex = btn.extra;
|
||||
} catch (eEx0) { ex = null; }
|
||||
|
||||
// # 2) 写入 extras(支持 number / boolean / string;其他类型一律转字符串)
|
||||
if (ex) {
|
||||
try {
|
||||
var k;
|
||||
for (k in ex) {
|
||||
if (!ex.hasOwnProperty(k)) continue;
|
||||
var v = ex[k];
|
||||
|
||||
if (typeof v === "number") it2.putExtra(String(k), Number(v));
|
||||
else if (typeof v === "boolean") it2.putExtra(String(k), !!v);
|
||||
else it2.putExtra(String(k), String(v));
|
||||
}
|
||||
} catch (eE) {}
|
||||
}
|
||||
|
||||
// # 3) 对"Shell 广播桥"做额外兼容:
|
||||
// - 你可以在 cfg 里写 extra.cmd(明文)或 extra.cmd_b64(Base64)
|
||||
// - 同时会补齐 root/from,并且把 cmd 明文也塞一份,方便外部 MVEL 直接读取 cmd 进行验证
|
||||
try {
|
||||
var bridgeAction = String(this.config.SHELL_BRIDGE_ACTION || "shortx.toolhub.SHELL");
|
||||
if (action === bridgeAction) {
|
||||
var kCmdB64 = String(this.config.SHELL_BRIDGE_EXTRA_CMD || "cmd_b64");
|
||||
var kFrom = String(this.config.SHELL_BRIDGE_EXTRA_FROM || "from");
|
||||
var kRoot = String(this.config.SHELL_BRIDGE_EXTRA_ROOT || "root");
|
||||
|
||||
var cmdPlain = "";
|
||||
var cmdB64 = "";
|
||||
|
||||
try { cmdB64 = String(it2.getStringExtra(kCmdB64) || ""); } catch (eC0) { cmdB64 = ""; }
|
||||
try { cmdPlain = String(it2.getStringExtra("cmd") || ""); } catch (eC1) { cmdPlain = ""; }
|
||||
|
||||
// # 有明文但没 b64:自动补 b64
|
||||
if ((!cmdB64 || cmdB64.length === 0) && cmdPlain && cmdPlain.length > 0) {
|
||||
try {
|
||||
var b64x = encodeBase64Utf8(cmdPlain);
|
||||
if (b64x && b64x.length > 0) {
|
||||
cmdB64 = b64x;
|
||||
it2.putExtra(kCmdB64, String(cmdB64));
|
||||
}
|
||||
} catch (eC2) {}
|
||||
}
|
||||
|
||||
// # 有 b64 但没明文:也补一份明文(便于外部规则验证;真正执行仍建议用 cmd_b64)
|
||||
if ((!cmdPlain || cmdPlain.length === 0) && cmdB64 && cmdB64.length > 0) {
|
||||
try {
|
||||
var decoded = decodeBase64Utf8(cmdB64);
|
||||
if (decoded && decoded.length > 0) {
|
||||
cmdPlain = decoded;
|
||||
it2.putExtra("cmd", String(cmdPlain));
|
||||
}
|
||||
} catch (eC3) {}
|
||||
}
|
||||
|
||||
// # root:广播桥接收端默认以 root 执行,强制传递 true
|
||||
try {
|
||||
if (!it2.hasExtra(kRoot)) {
|
||||
it2.putExtra(kRoot, true);
|
||||
}
|
||||
} catch (eR0) {
|
||||
try {
|
||||
it2.putExtra(kRoot, true);
|
||||
} catch (eR1) {}
|
||||
}
|
||||
|
||||
|
||||
// # root 类型纠正:如果外部 cfg 用了字符串 "true"/"false",这里纠正为 boolean,避免外部 getBooleanExtra 读不到
|
||||
try {
|
||||
if (it2.hasExtra(kRoot)) {
|
||||
var bdl = it2.getExtras();
|
||||
if (bdl) {
|
||||
var raw = bdl.get(kRoot);
|
||||
if (raw != null) {
|
||||
var rawStr = String(raw);
|
||||
if (rawStr === "true" || rawStr === "false") {
|
||||
it2.removeExtra(kRoot);
|
||||
it2.putExtra(kRoot, rawStr === "true");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (eRB) {}
|
||||
|
||||
// # from:标识来源(便于外部执行器做白名单/审计)
|
||||
try {
|
||||
if (!it2.hasExtra(kFrom)) it2.putExtra(kFrom, "ToolHub@system_server");
|
||||
} catch (eF0) { try { it2.putExtra(kFrom, "ToolHub@system_server"); } catch (eF1) {} }
|
||||
|
||||
if (this.L) {
|
||||
try {
|
||||
this.L.i("broadcast(shell_bridge) action=" + action + " cmd_len=" + String(cmdPlain ? cmdPlain.length : 0) +
|
||||
" cmd_b64_len=" + String(cmdB64 ? cmdB64.length : 0) + " root=" + String(it2.getBooleanExtra(kRoot, false)));
|
||||
} catch (eLg) {}
|
||||
}
|
||||
}
|
||||
} catch (eSB) {}
|
||||
|
||||
try { context.sendBroadcast(it2); } catch (eB) { this.toast("广播失败"); safeLog(this.L, 'e', "broadcast fail action=" + action + " err=" + String(eB)); }
|
||||
return;
|
||||
}
|
||||
|
||||
if (t === "shortcut") {
|
||||
// 这段代码的主要内容/用途:仅使用 JavaScript(startActivityAsUser) 执行快捷方式,取消 Shell 与所有兜底,避免弹出主/分身选择器。
|
||||
// 说明:
|
||||
// 1) 运行时只执行按钮字段 shortcutJsCode(由"选择快捷方式列表"点选自动生成,可手动微调)
|
||||
// 2) 不再调用 am start,不再回退 LauncherApps.startShortcut(用户要求:取消 shell、取消兜底)
|
||||
// 3) 目标 userId:launchUserId > userId(用于锁定主/分身)
|
||||
|
||||
var spkg = btn.pkg ? String(btn.pkg) : "";
|
||||
var sid = btn.shortcutId ? String(btn.shortcutId) : "";
|
||||
var iu = (btn.intentUri != null) ? String(btn.intentUri) : "";
|
||||
|
||||
var uid = 0;
|
||||
try { uid = (btn.userId != null) ? parseInt(String(btn.userId), 10) : 0; } catch(eUid0) { uid = 0; }
|
||||
if (isNaN(uid)) uid = 0;
|
||||
|
||||
// # 启动 userId 优先级:launchUserId > userId
|
||||
try {
|
||||
if (btn.launchUserId != null && String(btn.launchUserId).length > 0) {
|
||||
var lu0 = parseInt(String(btn.launchUserId), 10);
|
||||
if (!isNaN(lu0)) uid = lu0;
|
||||
}
|
||||
} catch(eLu0) {}
|
||||
|
||||
if (!spkg) { this.toast("按钮#" + idx + " 缺少 pkg"); return; }
|
||||
if (!sid) { this.toast("按钮#" + idx + " 缺少 shortcutId"); return; }
|
||||
|
||||
// # JavaScript 执行:只执行 shortcutJsCode
|
||||
var jsCode = (btn.shortcutJsCode != null) ? String(btn.shortcutJsCode) : "";
|
||||
if (!jsCode || jsCode.length === 0) {
|
||||
this.toast("按钮#" + idx + " 未配置 JS 启动代码");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// # 提供少量上下文变量给脚本使用(可选)
|
||||
// - __sc_intentUri: 当前按钮 intentUri
|
||||
// - __sc_userId: 当前目标 userId(已合并 launchUserId)
|
||||
var __sc_intentUri = iu;
|
||||
var __sc_userId = uid;
|
||||
|
||||
var rjs = eval(jsCode);
|
||||
|
||||
// # 约定:返回值以 ok 开头视为成功;以 err 开头视为失败(失败也不兜底)
|
||||
var sret = (rjs == null) ? "" : String(rjs);
|
||||
if (sret.indexOf("ok") === 0) {
|
||||
safeLog(this.L, 'i', "shortcut(js-only) ok pkg=" + spkg + " id=" + sid + " user=" + String(uid));
|
||||
return;
|
||||
}
|
||||
|
||||
safeLog(this.L, 'e', "shortcut(js-only) fail pkg=" + spkg + " id=" + sid + " user=" + String(uid) + " ret=" + sret);
|
||||
this.toast("快捷方式 JS 启动失败: " + sret);
|
||||
return;
|
||||
} catch (eJsSc) {
|
||||
safeLog(this.L, 'e', "shortcut(js-only) exception pkg=" + spkg + " id=" + sid + " err=" + eJsSc);
|
||||
this.toast("快捷方式 JS 异常: " + String(eJsSc));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.toast("未知 type=" + t);
|
||||
safeLog(this.L, 'w', "unknown btn type=" + t);
|
||||
} catch (eBtn) {
|
||||
try { this.toast("按钮执行异常"); } catch (e0) {}
|
||||
safeLog(this.L, 'e', "execButtonAction crash idx=" + String(idx) + " err=" + String(eBtn));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
76
code/th_12_rebuild.js
Normal file
76
code/th_12_rebuild.js
Normal file
@@ -0,0 +1,76 @@
|
||||
// @version 1.0.0
|
||||
// =======================【新增:改大小后安全重建悬浮球】======================
|
||||
FloatBallAppWM.prototype.rebuildBallForNewSize = function(keepPanels) {
|
||||
if (this.state.closing) return false;
|
||||
if (!this.state.wm) return false;
|
||||
if (!this.state.addedBall) return false;
|
||||
if (!this.state.ballRoot) return false;
|
||||
if (!this.state.ballLp) return false;
|
||||
if (this.state.dragging) return false;
|
||||
|
||||
var oldSize = this.state.ballLp.height;
|
||||
if (!oldSize || oldSize <= 0) oldSize = this.getDockInfo().ballSize;
|
||||
|
||||
var oldX = this.state.ballLp.x;
|
||||
var oldY = this.state.ballLp.y;
|
||||
|
||||
var oldCenterX = oldX + Math.round(oldSize / 2);
|
||||
var oldCenterY = oldY + Math.round(oldSize / 2);
|
||||
|
||||
if (!keepPanels) {
|
||||
this.hideAllPanels();
|
||||
}
|
||||
this.cancelDockTimer();
|
||||
|
||||
this.state.docked = false;
|
||||
this.state.dockSide = null;
|
||||
|
||||
this.safeRemoveView(this.state.ballRoot, "ballRoot-rebuild");
|
||||
|
||||
this.state.ballRoot = null;
|
||||
this.state.ballContent = null;
|
||||
this.state.ballLp = null;
|
||||
this.state.addedBall = false;
|
||||
|
||||
this.createBallViews();
|
||||
|
||||
var di = this.getDockInfo();
|
||||
var newSize = di.ballSize;
|
||||
|
||||
var newX = oldCenterX - Math.round(newSize / 2);
|
||||
var newY = oldCenterY - Math.round(newSize / 2);
|
||||
|
||||
var maxX = Math.max(0, this.state.screen.w - newSize);
|
||||
var maxY = Math.max(0, this.state.screen.h - newSize);
|
||||
|
||||
newX = this.clamp(newX, 0, maxX);
|
||||
newY = this.clamp(newY, 0, maxY);
|
||||
|
||||
var lp = new android.view.WindowManager.LayoutParams(
|
||||
newSize,
|
||||
newSize,
|
||||
android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
|
||||
android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
android.graphics.PixelFormat.TRANSLUCENT
|
||||
);
|
||||
|
||||
lp.gravity = android.view.Gravity.TOP | android.view.Gravity.START;
|
||||
lp.x = newX;
|
||||
lp.y = newY;
|
||||
|
||||
try {
|
||||
this.state.wm.addView(this.state.ballRoot, lp);
|
||||
this.state.ballLp = lp;
|
||||
this.state.addedBall = true;
|
||||
} catch (eAdd) {
|
||||
try { this.toast("重建悬浮球失败: " + String(eAdd)); } catch (eT) {}
|
||||
safeLog(this.L, 'e', "rebuildBall add fail err=" + String(eAdd));
|
||||
return false;
|
||||
}
|
||||
|
||||
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
|
||||
this.touchActivity();
|
||||
safeLog(this.L, 'i', "rebuildBall ok size=" + String(newSize) + " x=" + String(newX) + " y=" + String(newY));
|
||||
return true;
|
||||
};
|
||||
|
||||
375
code/th_13_panel_ui.js
Normal file
375
code/th_13_panel_ui.js
Normal file
@@ -0,0 +1,375 @@
|
||||
// @version 1.0.0
|
||||
// =======================【设置面板:UI(右上角确认)】======================
|
||||
FloatBallAppWM.prototype.createSectionHeader = function(item, parent) {
|
||||
var isDark = this.isDarkTheme();
|
||||
var C = this.ui.colors;
|
||||
var color = C.primary;
|
||||
|
||||
var h = new android.widget.TextView(context);
|
||||
h.setText(String(item.name || ""));
|
||||
h.setTextColor(color);
|
||||
h.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 15);
|
||||
h.setTypeface(null, android.graphics.Typeface.BOLD);
|
||||
h.setPadding(this.dp(16), this.dp(24), this.dp(16), this.dp(8));
|
||||
parent.addView(h);
|
||||
};
|
||||
|
||||
FloatBallAppWM.prototype.createSettingItemView = function(item, parent, needDivider) {
|
||||
var isDark = this.isDarkTheme();
|
||||
var C = this.ui.colors;
|
||||
var textColor = isDark ? C.textPriDark : C.textPriLight;
|
||||
var secColor = isDark ? C.textSecDark : C.textSecLight;
|
||||
var dividerColor = isDark ? C.dividerDark : C.dividerLight;
|
||||
var primary = C.primary;
|
||||
var switchOff = isDark ? (0xFF555555 | 0) : (0xFFCCCCCC | 0);
|
||||
|
||||
// 增加内边距
|
||||
var padH = this.dp(16);
|
||||
var padV = this.dp(16);
|
||||
|
||||
// 分割线 (顶部)
|
||||
if (needDivider) {
|
||||
var line = new android.view.View(context);
|
||||
var lineLp = new android.widget.LinearLayout.LayoutParams(
|
||||
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
1 // 1px
|
||||
);
|
||||
lineLp.setMargins(padH, 0, padH, 0);
|
||||
line.setLayoutParams(lineLp);
|
||||
line.setBackgroundColor(dividerColor);
|
||||
parent.addView(line);
|
||||
}
|
||||
|
||||
// 容器
|
||||
var row = new android.widget.LinearLayout(context);
|
||||
row.setOrientation(android.widget.LinearLayout.VERTICAL);
|
||||
// 增加点击波纹反馈
|
||||
try {
|
||||
var outValue = new android.util.TypedValue();
|
||||
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true);
|
||||
row.setBackgroundResource(outValue.resourceId);
|
||||
} catch(e) {}
|
||||
|
||||
row.setPadding(padH, padV, padH, padV);
|
||||
|
||||
var self = this;
|
||||
|
||||
if (item.type === "bool") {
|
||||
// === 开关类型 ===
|
||||
row.setOrientation(android.widget.LinearLayout.HORIZONTAL);
|
||||
row.setGravity(android.view.Gravity.CENTER_VERTICAL);
|
||||
row.setClickable(true);
|
||||
|
||||
var tv = new android.widget.TextView(context);
|
||||
tv.setText(String(item.name));
|
||||
tv.setTextColor(textColor);
|
||||
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16);
|
||||
var tvLp = new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
tvLp.weight = 1;
|
||||
tv.setLayoutParams(tvLp);
|
||||
row.addView(tv);
|
||||
|
||||
var sw = new android.widget.Switch(context);
|
||||
try { sw.setTextOn(""); sw.setTextOff(""); } catch (eT) {}
|
||||
|
||||
// 优化开关颜色
|
||||
try {
|
||||
var states = [
|
||||
[android.R.attr.state_checked],
|
||||
[-android.R.attr.state_checked]
|
||||
];
|
||||
var thumbColors = [primary, switchOff];
|
||||
var trackColors = [self.withAlpha(primary, 0.5), self.withAlpha(switchOff, 0.5)];
|
||||
|
||||
var thumbList = new android.content.res.ColorStateList(states, thumbColors);
|
||||
var trackList = new android.content.res.ColorStateList(states, trackColors);
|
||||
|
||||
sw.setThumbTintList(thumbList);
|
||||
sw.setTrackTintList(trackList);
|
||||
} catch(eColor) {}
|
||||
|
||||
var cur = !!self.getPendingValue(item.key);
|
||||
sw.setChecked(cur);
|
||||
|
||||
// 监听器
|
||||
sw.setOnCheckedChangeListener(new android.widget.CompoundButton.OnCheckedChangeListener({
|
||||
onCheckedChanged: function(btn, checked) {
|
||||
try {
|
||||
self.touchActivity();
|
||||
self.setPendingValue(item.key, !!checked);
|
||||
if (self.L) self.L.d("pending " + String(item.key) + "=" + String(!!checked));
|
||||
} catch (e0) {}
|
||||
}
|
||||
}));
|
||||
|
||||
// 点击行也触发开关
|
||||
row.setOnClickListener(new android.view.View.OnClickListener({
|
||||
onClick: function(v) {
|
||||
sw.setChecked(!sw.isChecked());
|
||||
}
|
||||
}));
|
||||
|
||||
row.addView(sw);
|
||||
parent.addView(row);
|
||||
|
||||
} else if (item.type === "int" || item.type === "float") {
|
||||
// === 数值类型 (SeekBar) ===
|
||||
// 垂直布局:上面是 标题+数值,下面是 SeekBar
|
||||
|
||||
// 第一行:标题 + 数值
|
||||
var line1 = new android.widget.LinearLayout(context);
|
||||
line1.setOrientation(android.widget.LinearLayout.HORIZONTAL);
|
||||
line1.setLayoutParams(new android.widget.LinearLayout.LayoutParams(
|
||||
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
));
|
||||
|
||||
var tv = new android.widget.TextView(context);
|
||||
tv.setText(String(item.name));
|
||||
tv.setTextColor(textColor);
|
||||
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16);
|
||||
var tvLp = new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
tvLp.weight = 1;
|
||||
tv.setLayoutParams(tvLp);
|
||||
line1.addView(tv);
|
||||
|
||||
var valTv = new android.widget.TextView(context);
|
||||
valTv.setTextColor(primary);
|
||||
valTv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
|
||||
valTv.setTypeface(null, android.graphics.Typeface.BOLD);
|
||||
line1.addView(valTv);
|
||||
|
||||
row.addView(line1);
|
||||
|
||||
// 第二行:SeekBar
|
||||
var sb = new android.widget.SeekBar(context);
|
||||
var sbLp = new android.widget.LinearLayout.LayoutParams(
|
||||
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
sbLp.topMargin = self.dp(16); // 增加间距
|
||||
sb.setLayoutParams(sbLp);
|
||||
|
||||
// 优化 SeekBar 颜色
|
||||
try {
|
||||
sb.getThumb().setTint(primary);
|
||||
sb.getProgressDrawable().setTint(primary);
|
||||
} catch(eColor) {}
|
||||
|
||||
// 配置 SeekBar
|
||||
var min = (item.min !== undefined) ? Number(item.min) : 0;
|
||||
var max = (item.max !== undefined) ? Number(item.max) : 100;
|
||||
var step = (item.step !== undefined) ? Number(item.step) : 1;
|
||||
|
||||
var curV = Number(self.getPendingValue(item.key));
|
||||
if (isNaN(curV)) curV = min;
|
||||
curV = self.clamp(curV, min, max);
|
||||
|
||||
var maxP = Math.floor((max - min) / step);
|
||||
if (maxP < 1) maxP = 1;
|
||||
sb.setMax(maxP);
|
||||
|
||||
var curP = Math.floor((curV - min) / step);
|
||||
if (curP < 0) curP = 0;
|
||||
if (curP > maxP) curP = maxP;
|
||||
sb.setProgress(curP);
|
||||
|
||||
function formatVal(v) {
|
||||
if (item.type === "float") return String(Math.round(v * 1000) / 1000);
|
||||
return String(Math.round(v));
|
||||
}
|
||||
function computeValByProgress(p) {
|
||||
var v = min + p * step;
|
||||
v = self.clamp(v, min, max);
|
||||
if (item.type === "int") v = Math.round(v);
|
||||
if (item.type === "float") v = Math.round(v * 1000) / 1000;
|
||||
return v;
|
||||
}
|
||||
|
||||
valTv.setText(formatVal(curV));
|
||||
|
||||
sb.setOnSeekBarChangeListener(new android.widget.SeekBar.OnSeekBarChangeListener({
|
||||
onProgressChanged: function(seek, progress, fromUser) {
|
||||
try {
|
||||
self.touchActivity();
|
||||
var v = computeValByProgress(progress);
|
||||
valTv.setText(formatVal(v));
|
||||
if (fromUser) {
|
||||
self.setPendingValue(item.key, v);
|
||||
}
|
||||
} catch (e1) {}
|
||||
},
|
||||
onStartTrackingTouch: function() { try { self.touchActivity(); } catch (e2) {} },
|
||||
onStopTrackingTouch: function() { try { self.touchActivity(); } catch (e3) {} }
|
||||
}));
|
||||
|
||||
row.addView(sb);
|
||||
parent.addView(row);
|
||||
|
||||
} else if (item.type === "action") {
|
||||
// === 动作按钮 ===
|
||||
row.setOrientation(android.widget.LinearLayout.HORIZONTAL);
|
||||
row.setGravity(android.view.Gravity.CENTER_VERTICAL);
|
||||
row.setClickable(true);
|
||||
|
||||
var tv = new android.widget.TextView(context);
|
||||
tv.setText(String(item.name));
|
||||
tv.setTextColor(textColor);
|
||||
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16);
|
||||
var tvLp = new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
tvLp.weight = 1;
|
||||
tv.setLayoutParams(tvLp);
|
||||
row.addView(tv);
|
||||
|
||||
// 样式化文本按钮
|
||||
var btn = new android.widget.TextView(context);
|
||||
btn.setText("打开");
|
||||
btn.setTextColor(primary);
|
||||
btn.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
|
||||
btn.setTypeface(null, android.graphics.Typeface.BOLD);
|
||||
btn.setGravity(android.view.Gravity.CENTER);
|
||||
btn.setPadding(self.dp(16), self.dp(8), self.dp(16), self.dp(8));
|
||||
// 透明波纹背景
|
||||
btn.setBackground(self.ui.createTransparentRippleDrawable(primary, self.dp(16)));
|
||||
|
||||
btn.setOnClickListener(new android.view.View.OnClickListener({
|
||||
onClick: function(v) {
|
||||
try {
|
||||
self.touchActivity();
|
||||
if (item.action === "open_btn_mgr") {
|
||||
self.showPanelAvoidBall("btn_editor");
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
}));
|
||||
row.addView(btn);
|
||||
|
||||
// 行点击也触发
|
||||
row.setOnClickListener(new android.view.View.OnClickListener({
|
||||
onClick: function(v) {
|
||||
try {
|
||||
self.touchActivity();
|
||||
if (item.action === "open_btn_mgr") {
|
||||
self.showPanelAvoidBall("btn_editor");
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
}));
|
||||
|
||||
parent.addView(row);
|
||||
} else if (item.type === "text") {
|
||||
// === 文本输入 ===
|
||||
var tv = new android.widget.TextView(context);
|
||||
tv.setText(String(item.name));
|
||||
tv.setTextColor(textColor);
|
||||
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16);
|
||||
row.addView(tv);
|
||||
|
||||
var et = new android.widget.EditText(context);
|
||||
var curVal = self.getPendingValue(item.key);
|
||||
if (curVal === undefined || curVal === null) curVal = "";
|
||||
et.setText(String(curVal));
|
||||
et.setTextColor(textColor);
|
||||
et.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
|
||||
et.setBackground(self.ui.createRoundDrawable(isDark ? C.inputBgDark : C.inputBgLight, self.dp(6)));
|
||||
et.setPadding(self.dp(8), self.dp(8), self.dp(8), self.dp(8));
|
||||
et.setSingleLine(true);
|
||||
|
||||
var etLp = new android.widget.LinearLayout.LayoutParams(
|
||||
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
etLp.topMargin = self.dp(8);
|
||||
et.setLayoutParams(etLp);
|
||||
|
||||
// Explicitly request keyboard on click
|
||||
et.setOnClickListener(new android.view.View.OnClickListener({
|
||||
onClick: function(v) {
|
||||
try {
|
||||
v.requestFocus();
|
||||
var imm = context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE);
|
||||
imm.showSoftInput(v, 0);
|
||||
} catch(e) {}
|
||||
}
|
||||
}));
|
||||
|
||||
et.addTextChangedListener(new android.text.TextWatcher({
|
||||
beforeTextChanged: function(s, start, count, after) {},
|
||||
onTextChanged: function(s, start, before, count) {},
|
||||
afterTextChanged: function(s) {
|
||||
try {
|
||||
self.touchActivity();
|
||||
self.setPendingValue(item.key, String(s));
|
||||
} catch (e) {}
|
||||
}
|
||||
}));
|
||||
|
||||
row.addView(et);
|
||||
parent.addView(row);
|
||||
} else if (item.type === "single_choice") {
|
||||
// === 单选类型 (RadioGroup) ===
|
||||
var tv = new android.widget.TextView(context);
|
||||
tv.setText(String(item.name));
|
||||
tv.setTextColor(textColor);
|
||||
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16);
|
||||
row.addView(tv);
|
||||
|
||||
var rg = new android.widget.RadioGroup(context);
|
||||
rg.setOrientation(android.widget.RadioGroup.VERTICAL);
|
||||
var rgLp = new android.widget.LinearLayout.LayoutParams(
|
||||
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
rgLp.topMargin = self.dp(8);
|
||||
rg.setLayoutParams(rgLp);
|
||||
|
||||
var curVal = String(self.getPendingValue(item.key) || "");
|
||||
if (!curVal) curVal = "auto"; // default
|
||||
|
||||
var options = item.options || [];
|
||||
for (var i = 0; i < options.length; i++) {
|
||||
(function(opt) {
|
||||
var rb = new android.widget.RadioButton(context);
|
||||
rb.setText(String(opt.label));
|
||||
rb.setTextColor(textColor);
|
||||
rb.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
|
||||
// 颜色优化
|
||||
try {
|
||||
var states = [[android.R.attr.state_checked], [-android.R.attr.state_checked]];
|
||||
var colors = [primary, secColor];
|
||||
rb.setButtonTintList(new android.content.res.ColorStateList(states, colors));
|
||||
} catch(eC){}
|
||||
|
||||
rb.setId(android.view.View.generateViewId ? android.view.View.generateViewId() : i);
|
||||
|
||||
// Check state
|
||||
if (String(opt.value) === curVal) {
|
||||
rb.setChecked(true);
|
||||
}
|
||||
|
||||
rb.setOnCheckedChangeListener(new android.widget.CompoundButton.OnCheckedChangeListener({
|
||||
onCheckedChanged: function(btn, checked) {
|
||||
if (checked) {
|
||||
try {
|
||||
self.touchActivity();
|
||||
self.setPendingValue(item.key, String(opt.value));
|
||||
} catch(e){}
|
||||
}
|
||||
}
|
||||
}));
|
||||
rg.addView(rb);
|
||||
})(options[i]);
|
||||
}
|
||||
|
||||
row.addView(rg);
|
||||
parent.addView(row);
|
||||
} else {
|
||||
// 兜底文本
|
||||
var tv = new android.widget.TextView(context);
|
||||
tv.setText(String(item.name));
|
||||
tv.setTextColor(secColor);
|
||||
row.addView(tv);
|
||||
parent.addView(row);
|
||||
}
|
||||
};
|
||||
|
||||
4715
code/th_2_core.js
4715
code/th_2_core.js
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user