{ "facts": [{ "@type": "type.googleapis.com/ClipboardContentChanged", "customContextDataKey": { }, "id": "F-fcd6857b-edf4-452e-b2c7-a0229501a3b6" }], "actions": [{ "@type": "type.googleapis.com/CreateLocalVar", "localVar": { "name": "剪贴板", "type": { "@type": "type.googleapis.com/StringVar" } }, "customContextDataKey": { }, "id": "A-673d78c7-d6b5-4ede-8036-16ac3d5c95ab" }, { "@type": "type.googleapis.com/WriteLocalVar", "varName": "剪贴板", "valueAsString": "{clipboardContent}", "customContextDataKey": { }, "id": "A-860602e0-1ba4-456f-9158-50206d9d0f71" }, { "@type": "type.googleapis.com/ShowOverlayButton", "buttonSettings": [{ "actions": [{ "@type": "type.googleapis.com/ExecuteJS", "expression": "// \u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\n// 拾字 - 文字选择工具\n// 如拾贝壳,收集文字\n// \u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\n// 来源: 阿然博客 xin-blog.com\n// \u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\n\n(function() {\n \u0027use strict\u0027;\n \n var appContext;\n try {\n if (typeof context \u003d\u003d\u003d \u0027undefined\u0027 || context \u003d\u003d null) {\n return;\n }\n appContext \u003d context.getApplicationContext ? context.getApplicationContext() : context;\n } catch (e) {\n return;\n }\n \n var LayoutParams \u003d android.view.WindowManager.LayoutParams;\n var LinearLayout \u003d android.widget.LinearLayout;\n var TextView \u003d android.widget.TextView;\n var Button \u003d android.widget.Button;\n var ScrollView \u003d android.widget.ScrollView;\n var SeekBar \u003d android.widget.SeekBar;\n var GradientDrawable \u003d android.graphics.drawable.GradientDrawable;\n var Color \u003d android.graphics.Color;\n var MotionEvent \u003d android.view.MotionEvent;\n var Gravity \u003d android.view.Gravity;\n var TypedValue \u003d android.util.TypedValue;\n var View \u003d android.view.View;\n var Handler \u003d android.os.Handler;\n var Looper \u003d android.os.Looper;\n var SpannableString \u003d android.text.SpannableString;\n var BackgroundColorSpan \u003d android.text.style.BackgroundColorSpan;\n var ForegroundColorSpan \u003d android.text.style.ForegroundColorSpan;\n\n var mainHandler \u003d new Handler(Looper.getMainLooper());\n var keepAliveTimer \u003d null;\n\n var PREFS_NAME \u003d \"拾字Prefs\";\n var KEY_FONT_SIZE \u003d \"fontSize\";\n var MIN_FONT_SIZE \u003d 12;\n var MAX_FONT_SIZE \u003d 32;\n var DEFAULT_FONT_SIZE \u003d 20;\n var currentFontSize \u003d DEFAULT_FONT_SIZE;\n\n var windowManager \u003d null;\n var mainLayout \u003d null;\n var layoutParams \u003d null;\n var textView \u003d null;\n var previewTextView \u003d null;\n var fontSizePanel \u003d null;\n var seekBar \u003d null;\n var fontSizeLabel \u003d null;\n var scrollView \u003d null;\n var countLabelView \u003d null; // 计数标签引用\n var translateHeaderView \u003d null; // 翻译结果标题\n var translateScrollView \u003d null; // 翻译结果滚动区域\n var translateTextView \u003d null; // 翻译结果文本\n var translateResultRow \u003d null; // 翻译结果行(包含复制按钮)\n var fullText \u003d \"\";\n var spannable \u003d null;\n var selectedIndices \u003d [];\n \n // 简化拖动状态\n var isDragging \u003d false;\n var dragStartIndex \u003d -1;\n var lastDragIndex \u003d -1;\n var isLongPress \u003d false;\n var dragSnapshot \u003d []; // 拖动开始时的选择快照\n var longPressHandler \u003d null;\n var touchDownTime \u003d 0;\n var touchDownX \u003d 0;\n var touchDownY \u003d 0;\n var isShowing \u003d false;\n var selectedSet \u003d {}; // 使用对象模拟 Set,键为索引,值为 true\n \n // 性能优化相关变量\n var cachedLayout \u003d null; // 缓存 TextView layout\n var pendingUpdate \u003d false; // 是否有待处理的 UI 更新\n \n // 拖动更新节流控制\n var lastDragUpdateTime \u003d 0; // 上次 UI 更新时间\n var DRAG_UPDATE_INTERVAL \u003d 50; // 拖动 UI 更新间隔(ms)\n var pendingDragUpdate \u003d null; // 待处理的拖动更新\n var lastDragEnd \u003d -1; // 上次拖动结束位置\n \n // 自动滚动相关变量\n var autoScrollRunnable \u003d null; // 自动滚动任务\n var lastTouchY \u003d 0; // 最后触摸位置\n var isAutoScrolling \u003d false; // 是否正在自动滚动\n var SCROLL_EDGE_TOP \u003d 0.15; // 顶部触发区域比例(15%)\n var SCROLL_EDGE_BOTTOM \u003d 0.15; // 底部触发区域比例(15%)\n var SCROLL_MIN_SPEED \u003d 5; // 最小滚动速度(dp)\n var SCROLL_MAX_SPEED \u003d 25; // 最大滚动速度(dp)\n var SCROLL_DELAY \u003d 16; // 滚动间隔(ms),约60fps\n \n // 启用 Java HTTP Keep-Alive 连接池\n try {\n java.lang.System.setProperty(\"http.keepAlive\", \"true\");\n java.lang.System.setProperty(\"http.maxConnections\", \"10\");\n } catch (e) {}\n \n // 全局翻译连接状态\n var connLastUsed \u003d 0;\n var CONN_MAX_IDLE \u003d 55000; // 55秒\n var heartbeatTimer \u003d null;\n var HEARTBEAT_INTERVAL \u003d 15000; // 15秒心跳一次,保持连接更稳定\n \n // 启动心跳保活\n function startHeartbeat() {\n if (heartbeatTimer) {\n mainHandler.removeCallbacks(heartbeatTimer);\n }\n heartbeatTimer \u003d new java.lang.Runnable({\n run: function() {\n // 后台持续保活,不依赖UI显示状态\n warmUpConnection();\n mainHandler.postDelayed(heartbeatTimer, HEARTBEAT_INTERVAL);\n }\n });\n mainHandler.postDelayed(heartbeatTimer, HEARTBEAT_INTERVAL);\n }\n \n // 停止心跳\n function stopHeartbeat() {\n if (heartbeatTimer) {\n mainHandler.removeCallbacks(heartbeatTimer);\n heartbeatTimer \u003d null;\n }\n }\n \n // API Key 配置\n var API_KEY \u003d \"翻译api\";\n var API_BASE \u003d \"https://libretranslate.heji233.com\";\n \n // 翻译连接预热 + 模型预加载(方案C)\n function warmUpConnection() {\n new java.lang.Thread(new java.lang.Runnable({\n run: function() {\n try {\n // 如果近期已预热(5秒内),跳过\n if ((Date.now() - connLastUsed) \u003c 5000) {\n return;\n }\n \n // 方案C:直接调用翻译API预加载模型\n var url \u003d new java.net.URL(API_BASE + \"/translate\");\n var conn \u003d url.openConnection();\n conn.setRequestMethod(\"POST\");\n conn.setRequestProperty(\"Content-Type\", \"application/json\");\n conn.setRequestProperty(\"Authorization\", \"Bearer \" + API_KEY);\n conn.setRequestProperty(\"Connection\", \"keep-alive\");\n conn.setDoOutput(true);\n conn.setConnectTimeout(10000);\n conn.setReadTimeout(30000);\n \n // 预加载:翻译 \"warmup\"\n var jsonBody \u003d JSON.stringify({\n q: \"warmup\",\n source: \"en\",\n target: \"zh\"\n });\n var os \u003d conn.getOutputStream();\n os.write(new java.lang.String(jsonBody).getBytes(\"UTF-8\"));\n os.flush();\n os.close();\n \n // 读取响应(预加载模型)\n conn.getResponseCode();\n conn.getInputStream().close();\n \n connLastUsed \u003d Date.now();\n } catch (e) {}\n }\n })).start();\n }\n \n // 辅助函数:将 selectedSet 转换为排序后的数组\n function setToArray(set) {\n var result \u003d [];\n for (var key in set) {\n if (set.hasOwnProperty(key) \u0026\u0026 set[key] \u003d\u003d\u003d true) {\n result.push(parseInt(key, 10));\n }\n }\n return result.sort(function(a, b) { return a - b; });\n }\n \n // 辅助函数:数组查找(兼容老 Rhino)\n function arrayIndexOf(arr, val) {\n for (var i \u003d 0; i \u003c arr.length; i++) {\n if (arr[i] \u003d\u003d\u003d val) return i;\n }\n return -1;\n }\n \n // 辅助函数:批量设置对象属性\n function batchSetProps(obj, indices, value) {\n for (var i \u003d 0; i \u003c indices.length; i++) {\n obj[indices[i]] \u003d value;\n }\n }\n \n var LONG_PRESS_TIME \u003d 300; // 缩短长按时间\n var TOUCH_SLOP \u003d 12;\n\n var isDark \u003d false;\n try {\n var uiMode \u003d appContext.getResources().getConfiguration().uiMode;\n isDark \u003d (uiMode \u0026 android.content.res.Configuration.UI_MODE_NIGHT_MASK) \u003d\u003d\u003d \n android.content.res.Configuration.UI_MODE_NIGHT_YES;\n } catch (e) {\n isDark \u003d false;\n }\n\n // \u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\n // UI 配色方案 - Material You 风格\n // \u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\n var Colors \u003d {\n // 背景层级\n bg: isDark ? Color.parseColor(\"#0f172a\") : Color.parseColor(\"#ffffff\"),\n surface: isDark ? Color.parseColor(\"#1e293b\") : Color.parseColor(\"#f8fafc\"),\n surfaceVariant: isDark ? Color.parseColor(\"#334155\") : Color.parseColor(\"#f1f5f9\"),\n \n // 文字颜色\n text: isDark ? Color.parseColor(\"#f8fafc\") : Color.parseColor(\"#0f172a\"),\n textSecondary: isDark ? Color.parseColor(\"#94a3b8\") : Color.parseColor(\"#64748b\"),\n textTertiary: isDark ? Color.parseColor(\"#64748b\") : Color.parseColor(\"#94a3b8\"),\n \n // 主色调 - Indigo\n primary: Color.parseColor(\"#6366f1\"),\n primaryLight: isDark ? Color.parseColor(\"#312e81\") : Color.parseColor(\"#e0e7ff\"),\n onPrimary: Color.WHITE,\n \n // 选中状态\n selectionBg: Color.parseColor(\"#6366f1\"),\n selectionText: Color.WHITE,\n selectionGlow: Color.parseColor(\"#818cf8\"),\n \n // 语义色\n success: Color.parseColor(\"#22c55e\"),\n warning: Color.parseColor(\"#f59e0b\"),\n \n // 按钮状态\n btnPrimaryBg: Color.parseColor(\"#6366f1\"),\n btnPrimaryPressed: Color.parseColor(\"#4f46e5\"),\n btnSecondaryBg: isDark ? Color.parseColor(\"#334155\") : Color.parseColor(\"#f1f5f9\"),\n btnSecondaryPressed: isDark ? Color.parseColor(\"#475569\") : Color.parseColor(\"#e2e8f0\")\n };\n\n function dp(value) {\n return TypedValue.applyDimension(\n TypedValue.COMPLEX_UNIT_DIP, \n value, \n appContext.getResources().getDisplayMetrics()\n );\n }\n \n // \u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\n // 响应式布局 - 根据屏幕尺寸自适应\n // \u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\n var screenWidth \u003d 0;\n var screenHeight \u003d 0;\n var isTablet \u003d false;\n var windowWidth \u003d 0;\n var maxWindowHeight \u003d 0;\n var textAreaHeight \u003d 0;\n \n function detectScreenSize() {\n try {\n var wm \u003d appContext.getSystemService(appContext.WINDOW_SERVICE);\n var display \u003d wm.getDefaultDisplay();\n var metrics \u003d new android.util.DisplayMetrics();\n display.getMetrics(metrics);\n \n screenWidth \u003d metrics.widthPixels;\n screenHeight \u003d metrics.heightPixels;\n \n // 计算dp值\n var widthDp \u003d screenWidth / metrics.density;\n var heightDp \u003d screenHeight / metrics.density;\n \n // 判断是否为平板(最短边 \u003e\u003d 600dp)\n var smallestWidth \u003d Math.min(widthDp, heightDp);\n isTablet \u003d smallestWidth \u003e\u003d 600;\n \n // 根据屏幕尺寸计算窗口大小\n if (isTablet) {\n // 平板:窗口占屏幕 75% 宽度,更宽更协调\n windowWidth \u003d screenWidth * 0.75;\n maxWindowHeight \u003d screenHeight * 0.8;\n textAreaHeight \u003d dp(400); // 更大的文字区域\n } else if (widthDp \u003e\u003d 400) {\n // 大屏手机\n windowWidth \u003d screenWidth * 0.9;\n maxWindowHeight \u003d screenHeight * 0.8;\n textAreaHeight \u003d dp(280);\n } else {\n // 小屏手机\n windowWidth \u003d screenWidth * 0.95;\n maxWindowHeight \u003d screenHeight * 0.85;\n textAreaHeight \u003d dp(240);\n }\n } catch (e) {\n // 默认 fallback\n windowWidth \u003d dp(360);\n maxWindowHeight \u003d dp(600);\n textAreaHeight \u003d dp(280);\n }\n }\n \n function createRoundRectDrawable(color, radiusDp) {\n var drawable \u003d new GradientDrawable();\n drawable.setShape(GradientDrawable.RECTANGLE);\n drawable.setColor(color);\n drawable.setCornerRadius(dp(radiusDp));\n return drawable;\n }\n \n // \u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\n // UI 辅助函数 - 水波纹和动效\n // \u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\n \n // 创建带按压效果的背景\n function createPressableDrawable(normalColor, pressedColor, radiusDp) {\n var StateListDrawable \u003d android.graphics.drawable.StateListDrawable;\n var drawable \u003d new StateListDrawable();\n \n var pressed \u003d new GradientDrawable();\n pressed.setShape(GradientDrawable.RECTANGLE);\n pressed.setColor(pressedColor);\n pressed.setCornerRadius(dp(radiusDp));\n \n var normal \u003d new GradientDrawable();\n normal.setShape(GradientDrawable.RECTANGLE);\n normal.setColor(normalColor);\n normal.setCornerRadius(dp(radiusDp));\n \n drawable.addState([android.R.attr.state_pressed], pressed);\n drawable.addState([], normal);\n return drawable;\n }\n \n // 窗口进入动画\n function animateWindowEnter(view) {\n view.setScaleX(0.9);\n view.setScaleY(0.9);\n view.setAlpha(0);\n view.animate()\n .scaleX(1)\n .scaleY(1)\n .alpha(1)\n .setDuration(200)\n .setInterpolator(new android.view.animation.DecelerateInterpolator())\n .start();\n }\n \n // 按钮点击缩放动画\n function applyButtonAnimation(btn) {\n btn.setOnTouchListener(new View.OnTouchListener({\n onTouch: function(v, event) {\n switch(event.getAction()) {\n case MotionEvent.ACTION_DOWN:\n v.animate()\n .scaleX(0.95)\n .scaleY(0.95)\n .setDuration(80)\n .start();\n break;\n case MotionEvent.ACTION_UP:\n case MotionEvent.ACTION_CANCEL:\n v.animate()\n .scaleX(1)\n .scaleY(1)\n .setDuration(80)\n .start();\n break;\n }\n return false;\n }\n }));\n }\n \n // 震动反馈\n function hapticFeedback(view) {\n try {\n view.performHapticFeedback(android.view.HapticFeedbackConstants.VIRTUAL_KEY);\n } catch (e) {}\n }\n\n function showToast(msg) {\n if (!mainHandler || !appContext) return;\n mainHandler.post(new java.lang.Runnable({\n run: function() {\n android.widget.Toast.makeText(appContext, String(msg), \n android.widget.Toast.LENGTH_SHORT).show();\n }\n }));\n }\n\n function setClipboard(text) {\n try {\n if (typeof setClip \u003d\u003d\u003d \u0027function\u0027) {\n setClip(text);\n return true;\n } else {\n var cm \u003d appContext.getSystemService(appContext.CLIPBOARD_SERVICE);\n if (cm) {\n var clip \u003d android.content.ClipData.newPlainText(\"拾字\", String(text));\n cm.setPrimaryClip(clip);\n return true;\n }\n }\n } catch (e) {\n showToast(\"复制失败: \" + e.message);\n }\n return false;\n }\n\n function runUi(action) {\n mainHandler.post(new java.lang.Runnable({\n run: action\n }));\n }\n\n function getSharedPrefs() {\n return appContext.getSharedPreferences(PREFS_NAME, appContext.MODE_PRIVATE);\n }\n\n function loadFontSize() {\n try {\n var prefs \u003d getSharedPrefs();\n var savedSize \u003d prefs.getInt(KEY_FONT_SIZE, DEFAULT_FONT_SIZE);\n if (savedSize \u003e\u003d MIN_FONT_SIZE \u0026\u0026 savedSize \u003c\u003d MAX_FONT_SIZE) {\n currentFontSize \u003d savedSize;\n } else {\n currentFontSize \u003d DEFAULT_FONT_SIZE;\n }\n } catch (e) {\n currentFontSize \u003d DEFAULT_FONT_SIZE;\n }\n return currentFontSize;\n }\n\n function saveFontSize(size) {\n try {\n var prefs \u003d getSharedPrefs();\n var editor \u003d prefs.edit();\n editor.putInt(KEY_FONT_SIZE, size);\n editor.apply();\n } catch (e) {}\n }\n\n var 拾字Floaty \u003d {\n show: function(text) {\n // \u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d 第一步:立即预热连接(在UI显示前)\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\n warmUpConnection();\n \n // 如果窗口已存在(只是隐藏了),直接显示\n if (mainLayout !\u003d\u003d null) {\n isShowing \u003d true;\n fullText \u003d (typeof text \u003d\u003d\u003d \u0027string\u0027) ? text : String(text || \"\");\n selectedIndices \u003d [];\n selectedSet \u003d {};\n cachedLayout \u003d null;\n pendingUpdate \u003d false;\n isDragging \u003d false;\n dragStartIndex \u003d -1;\n lastDragIndex \u003d -1;\n isLongPress \u003d false;\n isAutoScrolling \u003d false;\n lastTouchY \u003d 0;\n lastDragEnd \u003d -1;\n \n var self \u003d this;\n runUi(function() {\n try {\n // 重置翻译结果显示\n if (translateHeaderView) {\n translateHeaderView.setVisibility(View.GONE);\n translateScrollView.setVisibility(View.GONE);\n translateTextView.setText(\"\");\n }\n // 更新内容\n self.updateTextView();\n self.adjustScrollViewHeight();\n // 显示窗口\n mainLayout.setVisibility(View.VISIBLE);\n animateWindowEnter(mainLayout);\n } catch (e) {\n showToast(\"显示窗口失败: \" + e.message);\n isShowing \u003d false;\n }\n });\n // 启动心跳保活(窗口复用时)\n startHeartbeat();\n return;\n }\n \n if (isShowing) {\n showToast(\"拾字已在运行\");\n return;\n }\n \n loadFontSize();\n \n if (android.os.Build.VERSION.SDK_INT \u003e\u003d 23) {\n if (!android.provider.Settings.canDrawOverlays(appContext)) {\n showToast(\"请先授予悬浮窗权限\");\n var intent \u003d new android.content.Intent(\n android.provider.Settings.ACTION_MANAGE_OVERLAY_PERMISSION);\n intent.setData(android.net.Uri.parse(\"package:\" + appContext.getPackageName()));\n intent.setFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK);\n appContext.startActivity(intent);\n return;\n }\n }\n \n isShowing \u003d true;\n fullText \u003d (typeof text \u003d\u003d\u003d \u0027string\u0027) ? text : String(text || \"\");\n selectedIndices \u003d [];\n selectedSet \u003d {};\n cachedLayout \u003d null;\n pendingUpdate \u003d false;\n isDragging \u003d false;\n dragStartIndex \u003d -1;\n lastDragIndex \u003d -1;\n isLongPress \u003d false;\n isAutoScrolling \u003d false;\n lastTouchY \u003d 0;\n lastDragEnd \u003d -1;\n \n // 预热翻译连接\n warmUpConnection();\n \n // 启动心跳保活(每30秒访问一次 /languages 保持连接热状态)\n startHeartbeat();\n \n var self \u003d this;\n runUi(function() {\n try {\n self.createWindow();\n animateWindowEnter(mainLayout); // 添加进入动画\n self.updateTextView();\n mainHandler.postDelayed(new java.lang.Runnable({\n run: function() {\n self.adjustScrollViewHeight();\n }\n }), 100);\n } catch (e) {\n showToast(\"创建窗口失败: \" + e.message);\n isShowing \u003d false;\n }\n });\n },\n \n hide: function() {\n if (windowManager !\u003d\u003d null \u0026\u0026 mainLayout !\u003d\u003d null) {\n runUi(function() {\n try {\n if (longPressHandler) {\n mainHandler.removeCallbacks(longPressHandler);\n longPressHandler \u003d null;\n }\n if (autoScrollRunnable) {\n mainHandler.removeCallbacks(autoScrollRunnable);\n autoScrollRunnable \u003d null;\n }\n // 不停止心跳,保持后台连接保活\n // stopHeartbeat();\n // 只隐藏,不销毁\n mainLayout.setVisibility(View.GONE);\n } catch (e) {}\n isShowing \u003d false;\n });\n }\n },\n \n createWindow: function() {\n // 检测屏幕尺寸\n detectScreenSize();\n \n windowManager \u003d appContext.getSystemService(appContext.WINDOW_SERVICE);\n \n layoutParams \u003d new LayoutParams(\n windowWidth,\n LayoutParams.WRAP_CONTENT,\n LayoutParams.TYPE_APPLICATION_OVERLAY,\n LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_DIM_BEHIND,\n android.graphics.PixelFormat.TRANSLUCENT\n );\n \n layoutParams.gravity \u003d Gravity.CENTER | Gravity.TOP;\n layoutParams.x \u003d 0;\n layoutParams.y \u003d isTablet ? dp(60) : dp(60); // 统一位置\n layoutParams.dimAmount \u003d 0.4; // 背景变暗程度\n \n mainLayout \u003d new LinearLayout(appContext);\n mainLayout.setOrientation(LinearLayout.VERTICAL);\n mainLayout.setBackground(createRoundRectDrawable(Colors.bg, isTablet ? 20 : 16));\n mainLayout.setElevation(dp(isTablet ? 8 : 6)); // 阴影效果\n mainLayout.setPadding(\n isTablet ? dp(32) : dp(16), \n isTablet ? dp(24) : dp(16), \n isTablet ? dp(32) : dp(16), \n isTablet ? dp(24) : dp(16)\n );\n \n var titleBar \u003d this.createTitleBar();\n mainLayout.addView(titleBar);\n \n fontSizePanel \u003d this.createFontSizePanel();\n fontSizePanel.setVisibility(View.GONE);\n mainLayout.addView(fontSizePanel);\n \n scrollView \u003d new ScrollView(appContext);\n var scrollParams \u003d new LinearLayout.LayoutParams(\n LayoutParams.MATCH_PARENT, textAreaHeight);\n scrollParams.setMargins(0, dp(isTablet ? 16 : 12), 0, dp(isTablet ? 12 : 8));\n scrollView.setLayoutParams(scrollParams);\n \n textView \u003d new TextView(appContext);\n textView.setTextColor(Colors.text);\n textView.setTextSize(currentFontSize);\n textView.setLineSpacing(dp(isTablet ? 8 : 6), 1.1);\n textView.setPadding(\n dp(isTablet ? 16 : 12), \n dp(isTablet ? 16 : 12), \n dp(isTablet ? 16 : 12), \n dp(isTablet ? 16 : 12)\n );\n textView.setBackground(createRoundRectDrawable(Colors.surface, isTablet ? 12 : 8));\n \n // 关键:禁用 TextView 的点击,让触摸事件传递到 OnTouchListener\n textView.setClickable(false);\n textView.setLongClickable(false);\n textView.setFocusable(false);\n \n this.setupTextViewTouch();\n \n scrollView.addView(textView);\n mainLayout.addView(scrollView);\n \n var hint \u003d new TextView(appContext);\n hint.setText(\"👆 单击选字 · 👆 长按拖动多选\");\n hint.setTextColor(Colors.textTertiary);\n hint.setTextSize(isTablet ? 13 : 11);\n hint.setGravity(Gravity.CENTER);\n hint.setPadding(0, isTablet ? dp(12) : dp(8), 0, isTablet ? dp(8) : dp(4));\n mainLayout.addView(hint);\n \n var previewBox \u003d this.createPreviewBox();\n mainLayout.addView(previewBox);\n \n var self \u003d this;\n var actionBar \u003d new LinearLayout(appContext);\n actionBar.setOrientation(LinearLayout.HORIZONTAL);\n actionBar.setPadding(0, isTablet ? dp(32) : dp(16), 0, 0);\n // 主次按钮布局:复制(主) + 翻译 + 全选/清空(次)\n actionBar.addView(this.createPrimaryBtn(\"📋 复制\", function() { self.doCopy(); }));\n actionBar.addView(this.createIconBtn(\"🌐 译\", function() { self.doTranslate(); }));\n actionBar.addView(this.createIconBtn(\"全选\", function() { self.selectAll(); }));\n actionBar.addView(this.createIconBtn(\"清空\", function() { self.clear(); }));\n mainLayout.addView(actionBar);\n \n windowManager.addView(mainLayout, layoutParams);\n },\n \n createTitleBar: function() {\n var titleBar \u003d new LinearLayout(appContext);\n titleBar.setOrientation(LinearLayout.HORIZONTAL);\n titleBar.setGravity(Gravity.CENTER_VERTICAL);\n \n // 标题 + 图标\n var titleContainer \u003d new LinearLayout(appContext);\n titleContainer.setOrientation(LinearLayout.HORIZONTAL);\n titleContainer.setGravity(Gravity.CENTER_VERTICAL);\n \n var iconText \u003d new TextView(appContext);\n iconText.setText(\"✦\");\n iconText.setTextColor(Colors.primary);\n iconText.setTextSize(isTablet ? 22 : 18);\n iconText.setPadding(0, 0, dp(isTablet ? 8 : 6), 0);\n \n var titleText \u003d new TextView(appContext);\n titleText.setText(\"拾字\");\n titleText.setTextColor(Colors.text);\n titleText.setTextSize(isTablet ? 22 : 18);\n titleText.setTypeface(null, android.graphics.Typeface.BOLD);\n \n titleContainer.addView(iconText);\n titleContainer.addView(titleText);\n \n var titleParams \u003d new LinearLayout.LayoutParams(0, LayoutParams.WRAP_CONTENT, 1);\n titleContainer.setLayoutParams(titleParams);\n \n // 设置按钮 - 使用 TextView 代替 Button 更轻量\n var settingsBtn \u003d new TextView(appContext);\n settingsBtn.setText(\"⚙\");\n settingsBtn.setTextColor(Colors.textSecondary);\n settingsBtn.setTextSize(isTablet ? 22 : 18);\n settingsBtn.setPadding(dp(isTablet ? 16 : 12), dp(isTablet ? 12 : 8), dp(isTablet ? 16 : 12), dp(isTablet ? 12 : 8));\n settingsBtn.setBackground(createPressableDrawable(\n Color.TRANSPARENT, \n isDark ? Color.parseColor(\"#334155\") : Color.parseColor(\"#e2e8f0\"), \n isTablet ? 16 : 12\n ));\n \n var self \u003d this;\n settingsBtn.setOnClickListener(new View.OnClickListener({\n onClick: function(v) {\n hapticFeedback(v);\n self.toggleFontSizePanel();\n }\n }));\n applyButtonAnimation(settingsBtn);\n \n // 关闭按钮\n var closeBtn \u003d new TextView(appContext);\n closeBtn.setText(\"✕\");\n closeBtn.setTextColor(Colors.textSecondary);\n closeBtn.setTextSize(isTablet ? 20 : 16);\n closeBtn.setPadding(dp(isTablet ? 16 : 12), dp(isTablet ? 12 : 8), dp(isTablet ? 8 : 4), dp(isTablet ? 12 : 8));\n closeBtn.setBackground(createPressableDrawable(\n Color.TRANSPARENT,\n isDark ? Color.parseColor(\"#334155\") : Color.parseColor(\"#e2e8f0\"),\n isTablet ? 16 : 12\n ));\n \n closeBtn.setOnClickListener(new View.OnClickListener({\n onClick: function(v) { \n hapticFeedback(v);\n self.hide(); \n }\n }));\n applyButtonAnimation(closeBtn);\n \n titleBar.addView(titleContainer);\n titleBar.addView(settingsBtn);\n titleBar.addView(closeBtn);\n \n var touchStartX \u003d 0, touchStartY \u003d 0;\n var layoutStartX \u003d 0, layoutStartY \u003d 0;\n var isDraggingWindow \u003d false;\n \n titleBar.setOnTouchListener(new View.OnTouchListener({\n onTouch: function(v, event) {\n var action \u003d event.getAction();\n if (action \u003d\u003d\u003d MotionEvent.ACTION_DOWN) {\n touchStartX \u003d event.getRawX();\n touchStartY \u003d event.getRawY();\n layoutStartX \u003d layoutParams.x;\n layoutStartY \u003d layoutParams.y;\n isDraggingWindow \u003d true;\n return true;\n } else if (action \u003d\u003d\u003d MotionEvent.ACTION_MOVE \u0026\u0026 isDraggingWindow) {\n var dx \u003d event.getRawX() - touchStartX;\n var dy \u003d event.getRawY() - touchStartY;\n layoutParams.x \u003d layoutStartX + dx;\n layoutParams.y \u003d layoutStartY + dy;\n windowManager.updateViewLayout(mainLayout, layoutParams);\n return true;\n } else if (action \u003d\u003d\u003d MotionEvent.ACTION_UP || action \u003d\u003d\u003d MotionEvent.ACTION_CANCEL) {\n isDraggingWindow \u003d false;\n return true;\n }\n return false;\n }\n }));\n \n return titleBar;\n },\n \n createFontSizePanel: function() {\n var panel \u003d new LinearLayout(appContext);\n panel.setOrientation(LinearLayout.VERTICAL);\n panel.setBackground(createRoundRectDrawable(Colors.surfaceVariant, isTablet ? 16 : 12));\n panel.setPadding(dp(isTablet ? 20 : 16), dp(isTablet ? 16 : 12), dp(isTablet ? 20 : 16), dp(isTablet ? 16 : 12));\n var params \u003d new LinearLayout.LayoutParams(\n LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);\n params.setMargins(0, dp(isTablet ? 16 : 12), 0, 0);\n panel.setLayoutParams(params);\n \n // A- A+ 样式的字体调节\n var sliderRow \u003d new LinearLayout(appContext);\n sliderRow.setOrientation(LinearLayout.HORIZONTAL);\n sliderRow.setGravity(Gravity.CENTER_VERTICAL);\n \n // A- 小字体图标\n var smallA \u003d new TextView(appContext);\n smallA.setText(\"A\");\n smallA.setTextSize(isTablet ? 14 : 12);\n smallA.setTextColor(Colors.textSecondary);\n smallA.setPadding(0, 0, dp(isTablet ? 12 : 8), 0);\n \n // SeekBar\n seekBar \u003d new SeekBar(appContext);\n var seekParams \u003d new LinearLayout.LayoutParams(\n 0, LayoutParams.WRAP_CONTENT, 1);\n seekBar.setLayoutParams(seekParams);\n seekBar.setMax(MAX_FONT_SIZE - MIN_FONT_SIZE);\n seekBar.setProgress(currentFontSize - MIN_FONT_SIZE);\n \n // A+ 大字体图标\n var largeA \u003d new TextView(appContext);\n largeA.setText(\"A\");\n largeA.setTextSize(isTablet ? 22 : 18);\n largeA.setTextColor(Colors.textSecondary);\n largeA.setPadding(dp(isTablet ? 12 : 8), 0, 0, 0);\n \n // 当前大小标签\n fontSizeLabel \u003d new TextView(appContext);\n fontSizeLabel.setText(currentFontSize + \"sp\");\n fontSizeLabel.setTextColor(Colors.primary);\n fontSizeLabel.setTextSize(isTablet ? 14 : 12);\n fontSizeLabel.setPadding(dp(isTablet ? 16 : 12), 0, 0, 0);\n \n var self \u003d this;\n seekBar.setOnSeekBarChangeListener(new android.widget.SeekBar.OnSeekBarChangeListener({\n onProgressChanged: function(seekBar, progress, fromUser) {\n var newSize \u003d MIN_FONT_SIZE + progress;\n self.updateFontSize(newSize);\n },\n onStartTrackingTouch: function(seekBar) {},\n onStopTrackingTouch: function(seekBar) {\n var newSize \u003d MIN_FONT_SIZE + seekBar.getProgress();\n saveFontSize(newSize);\n }\n }));\n \n sliderRow.addView(smallA);\n sliderRow.addView(seekBar);\n sliderRow.addView(largeA);\n sliderRow.addView(fontSizeLabel);\n panel.addView(sliderRow);\n return panel;\n },\n \n toggleFontSizePanel: function() {\n if (fontSizePanel.getVisibility() \u003d\u003d\u003d View.VISIBLE) {\n fontSizePanel.setVisibility(View.GONE);\n this.adjustScrollViewHeight();\n } else {\n fontSizePanel.setVisibility(View.VISIBLE);\n this.adjustScrollViewHeight();\n seekBar.setProgress(currentFontSize - MIN_FONT_SIZE);\n fontSizeLabel.setText(currentFontSize + \"sp\");\n }\n },\n \n updateFontSize: function(size) {\n currentFontSize \u003d size;\n if (fontSizeLabel) {\n fontSizeLabel.setText(size + \"sp\");\n }\n if (textView) {\n textView.setTextSize(size);\n this.adjustScrollViewHeight();\n }\n },\n \n adjustScrollViewHeight: function() {\n if (!scrollView || !textView) return;\n \n var self \u003d this;\n mainHandler.postDelayed(new java.lang.Runnable({\n run: function() {\n try {\n var layout \u003d textView.getLayout();\n if (layout) {\n var lineCount \u003d layout.getLineCount();\n var lineHeight \u003d textView.getLineHeight();\n var padding \u003d textView.getPaddingTop() + textView.getPaddingBottom();\n var contentHeight \u003d lineCount * lineHeight + padding;\n \n var isPanelVisible \u003d fontSizePanel \u0026\u0026 fontSizePanel.getVisibility() \u003d\u003d\u003d View.VISIBLE;\n // 根据设备类型调整最大高度\n var panelHeight \u003d isPanelVisible ? (isTablet ? dp(280) : dp(200)) : textAreaHeight;\n var minHeight \u003d dp(isTablet ? 80 : 60);\n \n var newHeight \u003d Math.max(minHeight, Math.min(contentHeight + dp(8), panelHeight));\n \n var params \u003d scrollView.getLayoutParams();\n if (params.height !\u003d\u003d newHeight) {\n params.height \u003d newHeight;\n scrollView.setLayoutParams(params);\n }\n }\n } catch (e) {}\n }\n }), 50);\n },\n \n createPreviewBox: function() {\n var previewBox \u003d new LinearLayout(appContext);\n previewBox.setOrientation(LinearLayout.VERTICAL);\n previewBox.setBackground(createRoundRectDrawable(Colors.primaryLight, isTablet ? 16 : 12));\n previewBox.setPadding(\n isTablet ? dp(16) : dp(12), \n isTablet ? dp(14) : dp(10), \n isTablet ? dp(16) : dp(12), \n isTablet ? dp(14) : dp(10)\n );\n var params \u003d new LinearLayout.LayoutParams(\n LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);\n params.setMargins(0, isTablet ? dp(16) : dp(12), 0, 0);\n previewBox.setLayoutParams(params);\n \n // 头部:计数标签\n var header \u003d new LinearLayout(appContext);\n header.setOrientation(LinearLayout.HORIZONTAL);\n \n countLabelView \u003d new TextView(appContext);\n countLabelView.setText(\"已选 0 字\");\n countLabelView.setTextColor(Colors.primary);\n countLabelView.setTextSize(isTablet ? 13 : 11);\n \n var headerParams \u003d new LinearLayout.LayoutParams(0, LayoutParams.WRAP_CONTENT, 1);\n countLabelView.setLayoutParams(headerParams);\n \n header.addView(countLabelView);\n previewBox.addView(header);\n \n // 内容区:带滚动条的预览\n var previewScroll \u003d new ScrollView(appContext);\n var scrollParams \u003d new LinearLayout.LayoutParams(\n LayoutParams.MATCH_PARENT, isTablet ? dp(100) : dp(60));\n previewScroll.setLayoutParams(scrollParams);\n \n previewTextView \u003d new TextView(appContext);\n previewTextView.setText(\"点击选择文字...\");\n previewTextView.setTextColor(Colors.textSecondary);\n previewTextView.setTextSize(isTablet ? 18 : 14);\n previewTextView.setLineSpacing(dp(isTablet ? 6 : 2), 1);\n \n previewScroll.addView(previewTextView);\n previewBox.addView(previewScroll);\n \n // 翻译结果区域(默认隐藏)\n translateHeaderView \u003d new TextView(appContext);\n translateHeaderView.setText(\"翻译结果\");\n translateHeaderView.setTextColor(Colors.primary);\n translateHeaderView.setTextSize(isTablet ? 16 : 11);\n translateHeaderView.setPadding(0, dp(isTablet ? 16 : 8), 0, dp(isTablet ? 12 : 4));\n translateHeaderView.setVisibility(View.GONE);\n previewBox.addView(translateHeaderView);\n \n translateScrollView \u003d new ScrollView(appContext);\n var translateScrollParams \u003d new LinearLayout.LayoutParams(\n 0, isTablet ? dp(100) : dp(60), 1); // 权重1,占据大部分空间\n translateScrollView.setLayoutParams(translateScrollParams);\n translateScrollView.setVisibility(View.GONE);\n \n translateTextView \u003d new TextView(appContext);\n translateTextView.setText(\"\");\n translateTextView.setTextColor(Colors.text);\n translateTextView.setTextSize(isTablet ? 18 : 14);\n translateTextView.setLineSpacing(dp(isTablet ? 6 : 2), 1);\n \n translateScrollView.addView(translateTextView);\n \n // 复制翻译结果按钮\n var copyResultBtn \u003d new TextView(appContext);\n copyResultBtn.setText(\"📋\");\n copyResultBtn.setTextSize(isTablet ? 20 : 16);\n copyResultBtn.setGravity(Gravity.CENTER);\n copyResultBtn.setPadding(dp(isTablet ? 12 : 8), 0, dp(isTablet ? 12 : 8), 0);\n copyResultBtn.setBackground(createPressableDrawable(\n Color.TRANSPARENT,\n isDark ? Color.parseColor(\"#334155\") : Color.parseColor(\"#e2e8f0\"),\n isTablet ? 12 : 8\n ));\n var copyBtnParams \u003d new LinearLayout.LayoutParams(\n isTablet ? dp(48) : dp(40), isTablet ? dp(48) : dp(40));\n copyBtnParams.setMargins(dp(isTablet ? 12 : 8), 0, 0, 0);\n copyResultBtn.setLayoutParams(copyBtnParams);\n \n var self \u003d this;\n copyResultBtn.setOnClickListener(new View.OnClickListener({\n onClick: function(v) {\n try {\n var text \u003d String(translateTextView.getText());\n if (text \u0026\u0026 text.length \u003e 0) {\n setClipboard(text);\n showToast(\"已复制\");\n }\n } catch (e) {\n showToast(\"复制失败\");\n }\n }\n }));\n applyButtonAnimation(copyResultBtn);\n \n // 翻译结果行:文本 + 复制按钮\n translateResultRow \u003d new LinearLayout(appContext);\n translateResultRow.setOrientation(LinearLayout.HORIZONTAL);\n translateResultRow.setGravity(Gravity.CENTER_VERTICAL);\n translateResultRow.setVisibility(View.GONE); // 初始隐藏\n translateResultRow.addView(translateScrollView);\n translateResultRow.addView(copyResultBtn);\n \n previewBox.addView(translateResultRow);\n \n return previewBox;\n },\n \n // 创建主操作按钮(填充色)\n createPrimaryBtn: function(text, callback) {\n var btn \u003d new Button(appContext);\n btn.setText(text);\n btn.setTextColor(Colors.onPrimary);\n btn.setTextSize(isTablet ? 18 : 14);\n btn.setBackground(createPressableDrawable(Colors.btnPrimaryBg, Colors.btnPrimaryPressed, isTablet ? 12 : 8));\n btn.setAllCaps(false);\n \n var params \u003d new LinearLayout.LayoutParams(0, isTablet ? dp(56) : dp(40), 2); // 2x 宽度权重\n params.setMargins(dp(isTablet ? 8 : 4), 0, dp(isTablet ? 8 : 4), 0);\n btn.setLayoutParams(params);\n \n var self \u003d this;\n btn.setOnClickListener(new View.OnClickListener({\n onClick: function(v) {\n hapticFeedback(v);\n callback();\n }\n }));\n applyButtonAnimation(btn);\n return btn;\n },\n \n // 创建次要操作按钮(图标样式)\n createIconBtn: function(text, callback) {\n var btn \u003d new Button(appContext);\n btn.setText(text);\n btn.setTextColor(Colors.textSecondary);\n btn.setTextSize(isTablet ? 16 : 12);\n btn.setBackground(createPressableDrawable(Colors.btnSecondaryBg, Colors.btnSecondaryPressed, isTablet ? 12 : 8));\n btn.setAllCaps(false);\n \n var params \u003d new LinearLayout.LayoutParams(0, isTablet ? dp(56) : dp(40), 1);\n params.setMargins(dp(isTablet ? 8 : 4), 0, dp(isTablet ? 8 : 4), 0);\n btn.setLayoutParams(params);\n \n btn.setOnClickListener(new View.OnClickListener({\n onClick: function(v) {\n hapticFeedback(v);\n callback();\n }\n }));\n applyButtonAnimation(btn);\n return btn;\n },\n \n // 旧版按钮(兼容)\n createBtn: function(text, callback) {\n return this.createIconBtn(text, callback);\n },\n \n setupTextViewTouch: function() {\n var self \u003d this;\n var longPressRunnable \u003d null;\n var isPressed \u003d false;\n var lastValidIndex \u003d -1; // 记录最后一个有效位置\n \n var onTouch \u003d new View.OnTouchListener({\n onTouch: function(v, event) {\n var action \u003d event.getAction();\n var x \u003d event.getX();\n var y \u003d event.getY();\n \n // 获取当前字符索引(简单直接)\n var currentIndex \u003d self.getCharIndexAtPosition(x, y);\n \n switch(action) {\n case MotionEvent.ACTION_DOWN:\n isPressed \u003d true;\n isDragging \u003d false;\n dragStartIndex \u003d -1;\n dragSnapshot \u003d [];\n lastValidIndex \u003d -1;\n touchDownTime \u003d +new Date();\n touchDownX \u003d x;\n touchDownY \u003d y;\n \n // 重置 ScrollView 拦截\n if (scrollView) {\n scrollView.requestDisallowInterceptTouchEvent(true);\n }\n \n // 取消之前的长按检测\n if (longPressRunnable) {\n mainHandler.removeCallbacks(longPressRunnable);\n }\n \n // 保存视图引用,避免匿名类中捕获问题\n var textViewRef \u003d textView;\n \n // 设置长按检测(300ms后开始拖动模式)\n longPressRunnable \u003d new java.lang.Runnable({\n run: function() {\n if (!isPressed || !textViewRef) return;\n \n // 长按触发时重新获取当前位置\n var longPressX \u003d touchDownX;\n var longPressY \u003d touchDownY;\n var indexAtLongPress \u003d self.getCharIndexAtPosition(longPressX, longPressY);\n \n if (indexAtLongPress \u003c 0) {\n // 如果位置无效,尝试附近位置\n for (var offset \u003d 10; offset \u003c\u003d 50; offset +\u003d 10) {\n indexAtLongPress \u003d self.getCharIndexAtPosition(longPressX + offset, longPressY);\n if (indexAtLongPress \u003e\u003d 0) break;\n indexAtLongPress \u003d self.getCharIndexAtPosition(longPressX - offset, longPressY);\n if (indexAtLongPress \u003e\u003d 0) break;\n indexAtLongPress \u003d self.getCharIndexAtPosition(longPressX, longPressY + offset);\n if (indexAtLongPress \u003e\u003d 0) break;\n indexAtLongPress \u003d self.getCharIndexAtPosition(longPressX, longPressY - offset);\n if (indexAtLongPress \u003e\u003d 0) break;\n }\n }\n \n if (indexAtLongPress \u003c 0) return; // 仍然无效则取消\n \n isDragging \u003d true;\n lastValidIndex \u003d indexAtLongPress;\n dragStartIndex \u003d indexAtLongPress;\n dragSnapshot \u003d setToArray(selectedSet);\n \n // 缓存 layout 提高拖动性能\n cachedLayout \u003d textViewRef.getLayout();\n \n // 震动反馈\n try {\n textViewRef.performHapticFeedback(android.view.HapticFeedbackConstants.LONG_PRESS);\n } catch (e) {}\n \n // 切换起始位置的选择状态\n if (dragStartIndex \u003e\u003d 0) {\n if (selectedSet[dragStartIndex]) {\n delete selectedSet[dragStartIndex];\n } else {\n selectedSet[dragStartIndex] \u003d true;\n }\n selectedIndices \u003d setToArray(selectedSet);\n self.updateSelectionSpans();\n self.updatePreview();\n }\n }\n });\n mainHandler.postDelayed(longPressRunnable, LONG_PRESS_TIME);\n return true;\n \n case MotionEvent.ACTION_MOVE:\n if (!isPressed) return true;\n \n // 拖动期间使用缓存的 layout\n var moveIndex \u003d self.getCharIndexAtPosition(x, y, isDragging);\n \n // 如果当前位置无效,尝试使用上一个有效位置或附近位置\n if (moveIndex \u003c 0 \u0026\u0026 isDragging) {\n // 先尝试使用上一个有效位置\n if (lastValidIndex \u003e\u003d 0) {\n moveIndex \u003d lastValidIndex;\n }\n // 如果还不行,尝试附近位置(使用缓存 layout)\n if (moveIndex \u003c 0) {\n for (var offset \u003d 5; offset \u003c\u003d 40; offset +\u003d 5) {\n moveIndex \u003d self.getCharIndexAtPosition(x + offset, y, true);\n if (moveIndex \u003e\u003d 0) break;\n moveIndex \u003d self.getCharIndexAtPosition(x - offset, y, true);\n if (moveIndex \u003e\u003d 0) break;\n moveIndex \u003d self.getCharIndexAtPosition(x, y + offset, true);\n if (moveIndex \u003e\u003d 0) break;\n moveIndex \u003d self.getCharIndexAtPosition(x, y - offset, true);\n if (moveIndex \u003e\u003d 0) break;\n }\n }\n }\n \n // 更新最后一个有效位置\n if (moveIndex \u003e\u003d 0) {\n lastValidIndex \u003d moveIndex;\n }\n \n var dx \u003d Math.abs(x - touchDownX);\n var dy \u003d Math.abs(y - touchDownY);\n \n // 如果移动距离超过阈值,取消长按检测(还未进入拖动模式时)\n if (!isDragging \u0026\u0026 (dx \u003e dp(TOUCH_SLOP) || dy \u003e dp(TOUCH_SLOP))) {\n if (longPressRunnable) {\n mainHandler.removeCallbacks(longPressRunnable);\n longPressRunnable \u003d null;\n }\n // 允许 ScrollView 拦截\n if (scrollView) {\n scrollView.requestDisallowInterceptTouchEvent(false);\n }\n }\n \n // 拖动模式:实时更新选择范围\n if (isDragging \u0026\u0026 moveIndex \u003e\u003d 0) {\n self.updateDragSelection(dragStartIndex, moveIndex, dragSnapshot);\n \n // 简单的自动滚动检测\n self.checkAndScroll(y);\n }\n return true;\n \n case MotionEvent.ACTION_UP:\n isPressed \u003d false;\n \n // 恢复 ScrollView 拦截\n if (scrollView) {\n scrollView.requestDisallowInterceptTouchEvent(false);\n }\n \n if (longPressRunnable) {\n mainHandler.removeCallbacks(longPressRunnable);\n longPressRunnable \u003d null;\n }\n \n var pressDuration \u003d +new Date() - touchDownTime;\n var totalDx \u003d Math.abs(x - touchDownX);\n var totalDy \u003d Math.abs(y - touchDownY);\n \n // 单击处理(未触发拖动,且移动距离小,时间短)\n if (!isDragging \u0026\u0026 pressDuration \u003c LONG_PRESS_TIME \u0026\u0026 \n totalDx \u003c dp(TOUCH_SLOP) \u0026\u0026 totalDy \u003c dp(TOUCH_SLOP)) {\n if (currentIndex \u003e\u003d 0) {\n self.toggleSelection(currentIndex);\n }\n }\n \n // 拖动结束,更新预览\n if (isDragging) {\n // 取消待处理的延迟更新,立即执行\n if (pendingDragUpdate) {\n mainHandler.removeCallbacks(pendingDragUpdate);\n pendingDragUpdate \u003d null;\n }\n self.updatePreview();\n }\n \n // 停止自动滚动\n self.stopAutoScroll();\n \n // 重置状态\n isDragging \u003d false;\n dragStartIndex \u003d -1;\n dragSnapshot \u003d [];\n lastValidIndex \u003d -1;\n cachedLayout \u003d null;\n lastTouchY \u003d 0;\n lastDragEnd \u003d -1;\n lastDragUpdateTime \u003d 0;\n return true;\n \n case MotionEvent.ACTION_CANCEL:\n isPressed \u003d false;\n \n // 恢复 ScrollView 拦截\n if (scrollView) {\n scrollView.requestDisallowInterceptTouchEvent(false);\n }\n \n if (longPressRunnable) {\n mainHandler.removeCallbacks(longPressRunnable);\n }\n \n // 停止自动滚动\n self.stopAutoScroll();\n \n isDragging \u003d false;\n dragStartIndex \u003d -1;\n dragSnapshot \u003d [];\n lastValidIndex \u003d -1;\n cachedLayout \u003d null;\n lastTouchY \u003d 0;\n lastDragEnd \u003d -1;\n lastDragUpdateTime \u003d 0;\n return true;\n }\n \n return false;\n }\n });\n \n textView.setOnTouchListener(onTouch);\n },\n \n updateDragSelection: function(start, end, snapshot) {\n // 如果位置没有变化,跳过更新\n if (end \u003d\u003d\u003d lastDragEnd) return;\n lastDragEnd \u003d end;\n \n var currentMin \u003d Math.min(start, end);\n var currentMax \u003d Math.max(start, end);\n var startWasSelected \u003d arrayIndexOf(snapshot, start) \u003e\u003d 0;\n \n // 先恢复为快照状态\n selectedSet \u003d {};\n for (var k \u003d 0; k \u003c snapshot.length; k++) {\n selectedSet[snapshot[k]] \u003d true;\n }\n \n // 然后应用当前范围的状态切换\n for (var i \u003d currentMin; i \u003c\u003d currentMax; i++) {\n if (startWasSelected) {\n // 起始是选中状态 → 范围内取消选中\n delete selectedSet[i];\n } else {\n // 起始是未选中状态 → 范围内选中\n selectedSet[i] \u003d true;\n }\n }\n \n // 节流:限制 UI 更新频率\n var now \u003d +new Date();\n if (now - lastDragUpdateTime \u003c DRAG_UPDATE_INTERVAL) {\n // 延迟更新,取消之前的待处理更新\n if (pendingDragUpdate) {\n mainHandler.removeCallbacks(pendingDragUpdate);\n }\n var self \u003d this;\n pendingDragUpdate \u003d new java.lang.Runnable({\n run: function() {\n pendingDragUpdate \u003d null;\n selectedIndices \u003d setToArray(selectedSet);\n self.updateSelectionSpans();\n self.updatePreview();\n }\n });\n mainHandler.postDelayed(pendingDragUpdate, DRAG_UPDATE_INTERVAL);\n } else {\n // 立即更新\n lastDragUpdateTime \u003d now;\n selectedIndices \u003d setToArray(selectedSet);\n this.updateSelectionSpans();\n this.updatePreview();\n }\n },\n \n // 跟手的自动滚动检测 - 手指在边缘时持续滚动,移开时停止\n checkAndScroll: function(touchY) {\n if (!scrollView || !isDragging || !textView) {\n this.stopAutoScroll();\n return;\n }\n \n lastTouchY \u003d touchY;\n \n // 获取 TextView 在 ScrollView 中的位置\n var textViewTop \u003d textView.getTop();\n // 计算手指相对于 ScrollView 的 Y 坐标\n var relativeY \u003d touchY + textViewTop - scrollView.getScrollY();\n \n var scrollViewHeight \u003d scrollView.getHeight();\n \n // 计算触发区域高度\n var topZone \u003d scrollViewHeight * SCROLL_EDGE_TOP;\n var bottomZone \u003d scrollViewHeight * (1 - SCROLL_EDGE_BOTTOM);\n \n var scrollDirection \u003d 0; // 0: 停止, -1: 向上, 1: 向下\n var scrollSpeed \u003d 0;\n \n if (relativeY \u003c topZone) {\n // 手指在顶部区域,向上滚动\n scrollDirection \u003d -1;\n // 越靠近边缘速度越快\n var ratio \u003d (topZone - relativeY) / topZone;\n scrollSpeed \u003d SCROLL_MIN_SPEED + (SCROLL_MAX_SPEED - SCROLL_MIN_SPEED) * ratio;\n } else if (relativeY \u003e bottomZone) {\n // 手指在底部区域,向下滚动\n scrollDirection \u003d 1;\n // 越靠近边缘速度越快\n var ratio \u003d (relativeY - bottomZone) / (scrollViewHeight - bottomZone);\n scrollSpeed \u003d SCROLL_MIN_SPEED + (SCROLL_MAX_SPEED - SCROLL_MIN_SPEED) * ratio;\n }\n \n if (scrollDirection !\u003d\u003d 0) {\n this.startAutoScroll(scrollDirection, scrollSpeed);\n } else {\n this.stopAutoScroll();\n }\n },\n \n // 开始自动滚动\n startAutoScroll: function(direction, speed) {\n if (isAutoScrolling) return;\n isAutoScrolling \u003d true;\n \n var self \u003d this;\n autoScrollRunnable \u003d new java.lang.Runnable({\n run: function() {\n if (!isDragging || !isAutoScrolling || !scrollView) {\n isAutoScrolling \u003d false;\n return;\n }\n \n var scrollY \u003d scrollView.getScrollY();\n var textHeight \u003d textView.getHeight();\n var scrollViewHeight \u003d scrollView.getHeight();\n var maxScroll \u003d Math.max(0, textHeight - scrollViewHeight);\n var scrollAmount \u003d dp(speed);\n \n var newScrollY;\n if (direction \u003c 0) {\n // 向上滚动\n newScrollY \u003d Math.max(0, scrollY - scrollAmount);\n } else {\n // 向下滚动\n newScrollY \u003d Math.min(maxScroll, scrollY + scrollAmount);\n }\n \n if (newScrollY !\u003d\u003d scrollY) {\n scrollView.scrollTo(0, newScrollY);\n \n // 滚动后更新选择(手指位置不变,但文字移动了)\n var moveIndex \u003d self.getCharIndexAtPosition(0, lastTouchY, true);\n if (moveIndex \u003e\u003d 0 \u0026\u0026 dragStartIndex \u003e\u003d 0) {\n self.updateDragSelection(dragStartIndex, moveIndex, dragSnapshot);\n }\n \n // 继续滚动\n mainHandler.postDelayed(autoScrollRunnable, SCROLL_DELAY);\n } else {\n // 到达边界,停止滚动\n isAutoScrolling \u003d false;\n }\n }\n });\n \n mainHandler.postDelayed(autoScrollRunnable, SCROLL_DELAY);\n },\n \n // 停止自动滚动\n stopAutoScroll: function() {\n isAutoScrolling \u003d false;\n if (autoScrollRunnable) {\n mainHandler.removeCallbacks(autoScrollRunnable);\n autoScrollRunnable \u003d null;\n }\n },\n \n getCharIndexAtPosition: function(x, y, useCachedLayout) {\n if (!textView || !fullText || fullText.length \u003d\u003d\u003d 0) return -1;\n\n try {\n // 使用缓存的 layout 或重新获取\n var layout \u003d useCachedLayout \u0026\u0026 cachedLayout ? cachedLayout : textView.getLayout();\n if (!layout) return -1;\n\n var paddingLeft \u003d textView.getPaddingLeft();\n var paddingTop \u003d textView.getPaddingTop();\n \n // 转换到文本坐标系\n x -\u003d paddingLeft;\n y -\u003d paddingTop;\n \n // 获取布局信息\n var lineCount \u003d layout.getLineCount();\n var layoutHeight \u003d layout.getHeight();\n \n // Y 轴边界处理 - 允许一定的溢出容差\n if (y \u003c -dp(20)) return -1; // 上方超出太多\n if (y \u003e layoutHeight + dp(20)) return -1; // 下方超出太多\n \n // 限制在有效范围内\n if (y \u003c 0) y \u003d 0;\n if (y \u003e layoutHeight) y \u003d layoutHeight - 1;\n \n // 获取行号\n var line \u003d layout.getLineForVertical(y);\n \n // 确保行号有效\n if (line \u003c 0) line \u003d 0;\n if (line \u003e\u003d lineCount) line \u003d lineCount - 1;\n \n // 获取该行的字符范围\n var lineStart \u003d layout.getLineStart(line);\n var lineEnd \u003d layout.getLineEnd(line);\n \n // 获取水平位置对应的字符偏移\n var offset \u003d layout.getOffsetForHorizontal(line, x);\n \n // 确保偏移在有效范围内\n if (offset \u003c lineStart) offset \u003d lineStart;\n if (offset \u003e\u003d lineEnd) offset \u003d Math.max(0, lineEnd - 1);\n if (offset \u003c 0) offset \u003d 0;\n if (offset \u003e\u003d fullText.length) offset \u003d fullText.length - 1;\n \n return offset;\n } catch (e) {\n return -1;\n }\n },\n \n toggleSelection: function(index) {\n if (textView) {\n hapticFeedback(textView);\n }\n if (selectedSet[index]) {\n this.removeFromSelection(index);\n } else {\n this.addToSelection(index);\n }\n },\n \n addToSelection: function(index) {\n if (index \u003c 0 || index \u003e\u003d fullText.length) return;\n if (selectedSet[index]) return;\n \n selectedSet[index] \u003d true;\n selectedIndices \u003d setToArray(selectedSet);\n this.updateSelectionSpans();\n this.updatePreview();\n },\n \n removeFromSelection: function(index) {\n if (index \u003c 0 || index \u003e\u003d fullText.length) return;\n if (!selectedSet[index]) return;\n \n delete selectedSet[index];\n selectedIndices \u003d setToArray(selectedSet);\n this.updateSelectionSpans();\n this.updatePreview();\n },\n \n updateSelectionSpans: function() {\n if (!spannable || !textView) return;\n \n try {\n // 创建新的 Spannable 从头构建,避免清除 span 的问题\n var newSpannable \u003d new SpannableString(fullText);\n \n // 应用所有选中状态\n for (var i \u003d 0; i \u003c selectedIndices.length; i++) {\n var idx \u003d selectedIndices[i];\n if (idx \u003e\u003d 0 \u0026\u0026 idx \u003c fullText.length) {\n newSpannable.setSpan(\n new BackgroundColorSpan(Colors.selectionBg),\n idx, idx + 1,\n android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE\n );\n newSpannable.setSpan(\n new ForegroundColorSpan(Colors.selectionText),\n idx, idx + 1,\n android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE\n );\n }\n }\n \n spannable \u003d newSpannable;\n textView.setText(spannable);\n } catch (e) {\n // 出错时静默处理\n }\n },\n \n updateTextView: function() {\n spannable \u003d new SpannableString(fullText);\n textView.setText(spannable);\n this.updatePreview();\n this.adjustScrollViewHeight();\n },\n \n updatePreview: function() {\n var count \u003d selectedIndices.length;\n \n // 直接更新计数标签\n if (countLabelView) {\n countLabelView.setText(\"已选 \" + count + \" 字\");\n }\n \n // 更新预览文本\n if (count \u003d\u003d\u003d 0) {\n previewTextView.setText(\"点击选择文字...\");\n previewTextView.setTextColor(Colors.textSecondary);\n return;\n }\n \n // 使用数组拼接优化性能(比字符串拼接快)\n var chars \u003d [];\n for (var i \u003d 0; i \u003c selectedIndices.length; i++) {\n chars.push(fullText.charAt(selectedIndices[i]));\n }\n var result \u003d chars.join(\u0027\u0027);\n \n previewTextView.setText(result);\n previewTextView.setTextColor(Colors.text);\n },\n \n selectAll: function() {\n selectedSet \u003d {};\n selectedIndices \u003d [];\n for (var i \u003d 0; i \u003c fullText.length; i++) {\n selectedSet[i] \u003d true;\n selectedIndices.push(i);\n }\n this.updateSelectionSpans();\n this.updatePreview();\n showToast(\"已全选 \" + selectedIndices.length + \" 个字\");\n },\n \n clear: function() {\n selectedIndices \u003d [];\n selectedSet \u003d {};\n this.updateSelectionSpans();\n this.updatePreview();\n this.hideTranslateResult();\n },\n \n // 判断文本是否包含中文字符\n isChinese: function(text) {\n for (var i \u003d 0; i \u003c text.length; i++) {\n var code \u003d text.charCodeAt(i);\n if (code \u003e\u003d 0x4E00 \u0026\u0026 code \u003c\u003d 0x9FA5) {\n return true;\n }\n }\n return false;\n },\n \n // 显示翻译结果\n showTranslateResult: function(result) {\n var self \u003d this;\n runUi(function() {\n try {\n if (translateHeaderView) translateHeaderView.setVisibility(View.VISIBLE);\n if (translateResultRow) translateResultRow.setVisibility(View.VISIBLE);\n if (translateScrollView) translateScrollView.setVisibility(View.VISIBLE);\n if (translateTextView) translateTextView.setText(result);\n \n self.adjustScrollViewHeight();\n } catch (e) {\n showToast(\"显示翻译结果失败: \" + e.message);\n }\n });\n },\n \n // 隐藏翻译结果\n hideTranslateResult: function() {\n var self \u003d this;\n runUi(function() {\n try {\n if (translateHeaderView) translateHeaderView.setVisibility(View.GONE);\n if (translateResultRow) translateResultRow.setVisibility(View.GONE);\n if (translateScrollView) translateScrollView.setVisibility(View.GONE);\n if (translateTextView) translateTextView.setText(\"\");\n \n self.adjustScrollViewHeight();\n } catch (e) {}\n });\n },\n \n // 翻译功能\n doTranslate: function() {\n if (selectedIndices.length \u003d\u003d\u003d 0) {\n showToast(\"请先选择文字\");\n return;\n }\n \n // 获取选中的文字\n var chars \u003d [];\n for (var i \u003d 0; i \u003c selectedIndices.length; i++) {\n chars.push(fullText.charAt(selectedIndices[i]));\n }\n var text \u003d chars.join(\u0027\u0027);\n \n if (text.length \u003e 5000) {\n showToast(\"文本过长,最多支持5000字符\");\n return;\n }\n \n // 判断源语言和目标语言\n var source \u003d this.isChinese(text) ? \"zh\" : \"en\";\n var target \u003d this.isChinese(text) ? \"en\" : \"zh\";\n \n showToast(\"正在翻译...\");\n \n var self \u003d this;\n // 在后台线程执行翻译请求\n new java.lang.Thread(new java.lang.Runnable({\n run: function() {\n try {\n // 使用 API Key 认证 + JSON 格式\n var url \u003d new java.net.URL(API_BASE + \"/translate\");\n var conn \u003d url.openConnection();\n conn.setRequestMethod(\"POST\");\n conn.setRequestProperty(\"Content-Type\", \"application/json\");\n conn.setRequestProperty(\"Authorization\", \"Bearer \" + API_KEY);\n conn.setRequestProperty(\"Connection\", \"keep-alive\");\n conn.setDoOutput(true);\n conn.setConnectTimeout(10000);\n conn.setReadTimeout(30000);\n \n // 构建 JSON 请求体\n var jsonBody \u003d JSON.stringify({\n q: text,\n source: source,\n target: target\n });\n \n // 发送请求\n var os \u003d conn.getOutputStream();\n os.write(new java.lang.String(jsonBody).getBytes(\"UTF-8\"));\n os.flush();\n os.close();\n \n // 读取响应\n var responseCode \u003d conn.getResponseCode();\n if (responseCode \u003d\u003d\u003d 200) {\n var reader \u003d new java.io.BufferedReader(\n new java.io.InputStreamReader(conn.getInputStream(), \"UTF-8\")\n );\n var line;\n var response \u003d \"\";\n while ((line \u003d reader.readLine()) !\u003d null) {\n response +\u003d line;\n }\n reader.close();\n \n // 解析 JSON 响应\n var json \u003d JSON.parse(response);\n if (json.translatedText) {\n self.showTranslateResult(json.translatedText);\n showToast(\"翻译完成\");\n } else {\n showToast(\"翻译失败: 无效响应\");\n }\n } else {\n showToast(\"翻译失败: HTTP \" + responseCode);\n }\n \n // 不主动 disconnect,让连接池复用\n // 如果服务器返回 Connection: close,Java 会自动关闭\n } catch (e) {\n showToast(\"翻译出错: \" + e.message);\n }\n }\n })).start();\n },\n \n doCopy: function() {\n if (selectedIndices.length \u003d\u003d\u003d 0) {\n showToast(\"请先选择文字\");\n return;\n }\n \n // 使用数组拼接优化性能\n var chars \u003d [];\n for (var i \u003d 0; i \u003c selectedIndices.length; i++) {\n chars.push(fullText.charAt(selectedIndices[i]));\n }\n var text \u003d chars.join(\u0027\u0027);\n \n setClipboard(text);\n showToast(\"已复制: \" + text.substring(0, 15) + (text.length \u003e 15 ? \"...\" : \"\"));\n this.hide();\n }\n };\n\n function startBigBang(text) {\n try {\n // 确保 text 是字符串\n if (typeof text \u003d\u003d\u003d \u0027function\u0027) {\n text \u003d text();\n }\n if (typeof text !\u003d\u003d \u0027string\u0027) {\n text \u003d String(text || \"\");\n }\n \n if (!text || text.length \u003d\u003d\u003d 0) {\n var cm \u003d appContext.getSystemService(appContext.CLIPBOARD_SERVICE);\n if (cm \u0026\u0026 cm.hasPrimaryClip \u0026\u0026 cm.getPrimaryClip() \u0026\u0026 \n cm.getPrimaryClip().getItemCount() \u003e 0) {\n var item \u003d cm.getPrimaryClip().getItemAt(0);\n text \u003d item.getText() ? item.getText().toString() : \"\";\n }\n }\n \n if (!text || text.length \u003d\u003d\u003d 0) {\n showToast(\"剪贴板为空\");\n return;\n }\n \n if (text.length \u003e 1000) {\n text \u003d text.substring(0, 1000);\n showToast(\"文本过长,已截取前1000字\");\n }\n \n 拾字Floaty.show(text);\n \n // 安全的心跳定时器,使用命名引用避免循环引用\n if (keepAliveTimer) {\n mainHandler.removeCallbacks(keepAliveTimer);\n }\n keepAliveTimer \u003d new java.lang.Runnable({\n run: function() {\n if (isShowing \u0026\u0026 keepAliveTimer) {\n mainHandler.postDelayed(keepAliveTimer, 5000);\n }\n }\n });\n mainHandler.postDelayed(keepAliveTimer, 5000);\n \n } catch (e) {\n showToast(\"启动失败: \" + e.message);\n }\n }\n\n var closeBigBang \u003d function() {\n try {\n 拾字Floaty.hide();\n return true;\n } catch (e) {\n return false;\n }\n };\n\n var inputText \u003d typeof localVarOf$剪贴板 !\u003d\u003d \u0027undefined\u0027 ? localVarOf$剪贴板 : null;\n startBigBang(inputText);\n \n})();\n", "context": "CoroutineContext_UI", "customContextDataKey": { }, "id": "A-71f9e0f9-5bdb-4a5d-b369-b8429e4ad863" }], "icon": "edit-2-line", "label": "拾字", "id": "BTN-924910e4-bcb1-4a86-9f87-57c3a00dd0d0" }], "tag": "shizi", "maxHeightInDp": 500, "maxWidthInDp": 20, "backgroundAlpha": 0.8, "buttonMinWidth": 64, "enableGlobalDrag": true, "disableAutoEdge": true, "closeOnAction": true, "customContextDataKey": { }, "id": "A-0f1375dd-b503-43fc-b7e7-e306cdf82957" }, { "@type": "type.googleapis.com/Delay", "timeString": "5", "timeUnit": "TimeUnit_S", "customContextDataKey": { }, "id": "A-6054f6d7-1a92-4bfe-ad14-7e01cb7ac995" }, { "@type": "type.googleapis.com/HideOverlayButton", "overlayTags": ["shizi"], "customContextDataKey": { }, "id": "A-0e4eb332-022c-40f5-97d3-90d8defdbf66" }], "id": "SHARE-rule-10509ffd-de11-4271-8e76-4d5d1cbd3536", "lastUpdateTime": "1773498603537", "createTime": "1773232307721", "author": { "name": "ShortX" }, "title": "拾字", "description": "文字选择工具", "isEnabled": true, "hook": { }, "quit": { }, "versionCode": "1" } ###------### {"type":"rule"}