frida-float-menu 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +404 -0
- package/lib/api.d.ts +50 -0
- package/lib/api.js +149 -0
- package/lib/component/button.d.ts +11 -0
- package/lib/component/button.js +57 -0
- package/lib/component/category.d.ts +8 -0
- package/lib/component/category.js +37 -0
- package/lib/component/checkBox.d.ts +28 -0
- package/lib/component/checkBox.js +199 -0
- package/lib/component/collapsible.d.ts +19 -0
- package/lib/component/collapsible.js +177 -0
- package/lib/component/image.d.ts +18 -0
- package/lib/component/image.js +87 -0
- package/lib/component/input.d.ts +33 -0
- package/lib/component/input.js +346 -0
- package/lib/component/selector.d.ts +22 -0
- package/lib/component/selector.js +95 -0
- package/lib/component/slider.d.ts +18 -0
- package/lib/component/slider.js +155 -0
- package/lib/component/style/style.d.ts +5 -0
- package/lib/component/style/style.js +261 -0
- package/lib/component/style/theme.d.ts +29 -0
- package/lib/component/style/theme.js +23 -0
- package/lib/component/switch.d.ts +12 -0
- package/lib/component/switch.js +77 -0
- package/lib/component/text.d.ts +9 -0
- package/lib/component/text.js +36 -0
- package/lib/component/ui-components.d.ts +24 -0
- package/lib/component/ui-components.js +49 -0
- package/lib/component/views/log-view.d.ts +25 -0
- package/lib/component/views/log-view.js +231 -0
- package/lib/component/views/tabs-view.d.ts +35 -0
- package/lib/component/views/tabs-view.js +296 -0
- package/lib/event-emitter.d.ts +10 -0
- package/lib/event-emitter.js +52 -0
- package/lib/float-menu.d.ts +63 -0
- package/lib/float-menu.js +568 -0
- package/lib/index.d.ts +12 -0
- package/lib/index.js +28 -0
- package/lib/logger.d.ts +43 -0
- package/lib/logger.js +183 -0
- package/lib/utils.d.ts +8 -0
- package/lib/utils.js +16 -0
- package/package.json +36 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FloatMenu = void 0;
|
|
4
|
+
const event_emitter_1 = require("./event-emitter");
|
|
5
|
+
const logger_1 = require("./logger");
|
|
6
|
+
const api_1 = require("./api");
|
|
7
|
+
const style_1 = require("./component/style/style");
|
|
8
|
+
const theme_1 = require("./component/style/theme");
|
|
9
|
+
const log_view_1 = require("./component/views/log-view");
|
|
10
|
+
const utils_1 = require("./utils");
|
|
11
|
+
const tabs_view_1 = require("./component/views/tabs-view");
|
|
12
|
+
class FloatMenu {
|
|
13
|
+
get context() {
|
|
14
|
+
if (this._context === null) {
|
|
15
|
+
this._context = Java.use("android.app.ActivityThread")
|
|
16
|
+
.currentApplication()
|
|
17
|
+
.getApplicationContext();
|
|
18
|
+
}
|
|
19
|
+
return this._context;
|
|
20
|
+
}
|
|
21
|
+
get windowManager() {
|
|
22
|
+
if (this._windowManager === null) {
|
|
23
|
+
const Context = api_1.API.Context;
|
|
24
|
+
this._windowManager = Java.cast(this.context.getSystemService(Context.WINDOW_SERVICE.value), api_1.API.ViewManager);
|
|
25
|
+
}
|
|
26
|
+
return this._windowManager;
|
|
27
|
+
}
|
|
28
|
+
constructor(options = {}) {
|
|
29
|
+
this.uiComponents = new Map();
|
|
30
|
+
this.pendingComponents = [];
|
|
31
|
+
this.eventEmitter = new event_emitter_1.EventEmitter();
|
|
32
|
+
this.isIconMode = true;
|
|
33
|
+
this._context = null;
|
|
34
|
+
this._windowManager = null;
|
|
35
|
+
this.options = {
|
|
36
|
+
width: 1000,
|
|
37
|
+
height: 900,
|
|
38
|
+
x: 0,
|
|
39
|
+
y: 0,
|
|
40
|
+
iconWidth: 200,
|
|
41
|
+
iconHeight: 200,
|
|
42
|
+
logMaxLines: 100,
|
|
43
|
+
title: "Frida Float Menu",
|
|
44
|
+
theme: theme_1.DarkNeonTheme,
|
|
45
|
+
tabs: [],
|
|
46
|
+
activeTab: undefined,
|
|
47
|
+
...options,
|
|
48
|
+
};
|
|
49
|
+
this.logger = logger_1.Logger.instance;
|
|
50
|
+
Java.perform(() => {
|
|
51
|
+
const resources = this.context.getResources();
|
|
52
|
+
const metrics = resources.getDisplayMetrics();
|
|
53
|
+
this.screenWidth = metrics.widthPixels.value;
|
|
54
|
+
this.screenHeight = metrics.heightPixels.value;
|
|
55
|
+
this.options.height = Math.min(this.options.height, this.screenHeight - 80);
|
|
56
|
+
});
|
|
57
|
+
this.logger.info("屏幕尺寸:", this.screenWidth, this.screenHeight);
|
|
58
|
+
this.tabsView = new tabs_view_1.TabsView(this.context, this.options.theme, this.options.tabs, this.options.activeTab);
|
|
59
|
+
this.tabsView.initializeTabs();
|
|
60
|
+
this.logger.info("悬浮窗初始化完成,等待显示");
|
|
61
|
+
}
|
|
62
|
+
addDragListener(targetView, window, winParams) {
|
|
63
|
+
const OnTouchListener = api_1.API.OnTouchListener;
|
|
64
|
+
const MotionEvent = api_1.API.MotionEvent;
|
|
65
|
+
targetView.setClickable(true);
|
|
66
|
+
const getBounds = () => {
|
|
67
|
+
const w = this.isIconMode ? this.options.iconWidth : this.options.width;
|
|
68
|
+
const h = this.isIconMode
|
|
69
|
+
? this.options.iconHeight
|
|
70
|
+
: this.options.height;
|
|
71
|
+
return {
|
|
72
|
+
left: 0,
|
|
73
|
+
top: -40,
|
|
74
|
+
right: this.screenWidth - w,
|
|
75
|
+
bottom: this.screenHeight - h,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
const self = this;
|
|
79
|
+
let isDragging = false;
|
|
80
|
+
const DRAG_THRESHOLD = 5;
|
|
81
|
+
let touchOffsetX = 0;
|
|
82
|
+
let touchOffsetY = 0;
|
|
83
|
+
const touchListener = Java.registerClass({
|
|
84
|
+
name: "com.frida.FloatDragListener" +
|
|
85
|
+
Date.now() +
|
|
86
|
+
Math.random().toString(36).substring(6),
|
|
87
|
+
implements: [OnTouchListener],
|
|
88
|
+
methods: {
|
|
89
|
+
onTouch: function (v, event) {
|
|
90
|
+
const action = event.getAction();
|
|
91
|
+
switch (action) {
|
|
92
|
+
case MotionEvent.ACTION_DOWN.value: {
|
|
93
|
+
isDragging = false;
|
|
94
|
+
const rawX = event.getRawX();
|
|
95
|
+
const rawY = event.getRawY();
|
|
96
|
+
const startWx = winParams.x.value;
|
|
97
|
+
const startWy = winParams.y.value;
|
|
98
|
+
touchOffsetX = rawX - startWx;
|
|
99
|
+
touchOffsetY = rawY - startWy;
|
|
100
|
+
self.lastTouchX = rawX;
|
|
101
|
+
self.lastTouchY = rawY;
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
case MotionEvent.ACTION_MOVE.value: {
|
|
105
|
+
const rawX = event.getRawX();
|
|
106
|
+
const rawY = event.getRawY();
|
|
107
|
+
const dx = rawX - self.lastTouchX;
|
|
108
|
+
const dy = rawY - self.lastTouchY;
|
|
109
|
+
if (!isDragging &&
|
|
110
|
+
(Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD)) {
|
|
111
|
+
isDragging = true;
|
|
112
|
+
}
|
|
113
|
+
if (isDragging) {
|
|
114
|
+
let wx = rawX - touchOffsetX;
|
|
115
|
+
let wy = rawY - touchOffsetY;
|
|
116
|
+
const p = (0, utils_1.windowToLogical)(wx, wy, self.screenWidth, self.screenHeight, self.isIconMode
|
|
117
|
+
? self.options.iconWidth
|
|
118
|
+
: self.options.width, self.isIconMode
|
|
119
|
+
? self.options.iconHeight
|
|
120
|
+
: self.options.height);
|
|
121
|
+
let newX = p.x;
|
|
122
|
+
let newY = p.y;
|
|
123
|
+
const bounds = getBounds();
|
|
124
|
+
newX = Math.max(bounds.left, Math.min(bounds.right, newX));
|
|
125
|
+
newY = Math.max(bounds.top, Math.min(bounds.bottom, newY));
|
|
126
|
+
self.updatePosition(window, winParams, { x: newX, y: newY });
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
case MotionEvent.ACTION_UP.value:
|
|
131
|
+
case MotionEvent.ACTION_CANCEL.value: {
|
|
132
|
+
if (!isDragging) {
|
|
133
|
+
try {
|
|
134
|
+
self.isIconMode = false;
|
|
135
|
+
self.iconContainerWin.setAlpha(1);
|
|
136
|
+
self.toggleView();
|
|
137
|
+
}
|
|
138
|
+
catch { }
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
targetView.setOnTouchListener(touchListener.$new());
|
|
147
|
+
}
|
|
148
|
+
createMenuContainerWindow() {
|
|
149
|
+
const FrameLayout = api_1.API.FrameLayout;
|
|
150
|
+
const LinearLayout = api_1.API.LinearLayout;
|
|
151
|
+
const FrameLayoutParams = api_1.API.FrameLayoutParams;
|
|
152
|
+
const ViewGroupLayoutParams = api_1.API.ViewGroupLayoutParams;
|
|
153
|
+
const View = api_1.API.View;
|
|
154
|
+
const LayoutParams = api_1.API.LayoutParams;
|
|
155
|
+
this.menuContainerWin = FrameLayout.$new(this.context);
|
|
156
|
+
const rootLp = FrameLayoutParams.$new(ViewGroupLayoutParams.MATCH_PARENT.value, ViewGroupLayoutParams.MATCH_PARENT.value);
|
|
157
|
+
rootLp.gravity = api_1.API.Gravity.TOP.value | api_1.API.Gravity.START.value;
|
|
158
|
+
this.menuContainerWin.setLayoutParams(rootLp);
|
|
159
|
+
try {
|
|
160
|
+
this.menuContainerWin.setBackgroundColor(0x00000000);
|
|
161
|
+
}
|
|
162
|
+
catch (e) { }
|
|
163
|
+
const panel = LinearLayout.$new(this.context);
|
|
164
|
+
panel.setOrientation(LinearLayout.VERTICAL.value);
|
|
165
|
+
panel.setLayoutParams(ViewGroupLayoutParams.$new(ViewGroupLayoutParams.MATCH_PARENT.value, ViewGroupLayoutParams.MATCH_PARENT.value));
|
|
166
|
+
(0, style_1.applyStyle)(panel, "overlay", this.options.theme);
|
|
167
|
+
try {
|
|
168
|
+
panel.setClipToOutline(true);
|
|
169
|
+
}
|
|
170
|
+
catch (e) { }
|
|
171
|
+
try {
|
|
172
|
+
panel.setClipChildren(false);
|
|
173
|
+
}
|
|
174
|
+
catch (e) { }
|
|
175
|
+
try {
|
|
176
|
+
panel.setClipToPadding(false);
|
|
177
|
+
}
|
|
178
|
+
catch (e) { }
|
|
179
|
+
this.menuPanelView = panel;
|
|
180
|
+
const overlay = FrameLayout.$new(this.context);
|
|
181
|
+
overlay.setLayoutParams(FrameLayoutParams.$new(ViewGroupLayoutParams.MATCH_PARENT.value, ViewGroupLayoutParams.MATCH_PARENT.value));
|
|
182
|
+
try {
|
|
183
|
+
overlay.setClipChildren(false);
|
|
184
|
+
}
|
|
185
|
+
catch (e) { }
|
|
186
|
+
try {
|
|
187
|
+
overlay.setClipToPadding(false);
|
|
188
|
+
}
|
|
189
|
+
catch (e) { }
|
|
190
|
+
try {
|
|
191
|
+
overlay.setElevation(100000);
|
|
192
|
+
}
|
|
193
|
+
catch (e) { }
|
|
194
|
+
try {
|
|
195
|
+
overlay.setTranslationZ(100000);
|
|
196
|
+
}
|
|
197
|
+
catch (e) { }
|
|
198
|
+
this.logPanelView = overlay;
|
|
199
|
+
this.menuContainerWin.addView(panel);
|
|
200
|
+
this.menuContainerWin.addView(overlay);
|
|
201
|
+
this.menuWindowParams = LayoutParams.$new(this.options.width, this.options.height, 0, 0, 2038, LayoutParams.FLAG_NOT_FOCUSABLE.value |
|
|
202
|
+
LayoutParams.FLAG_NOT_TOUCH_MODAL.value, 1);
|
|
203
|
+
this.createHeaderView(this.context);
|
|
204
|
+
this.tabsView.createTabView(this.menuPanelView);
|
|
205
|
+
this.windowManager.addView(this.menuContainerWin, this.menuWindowParams);
|
|
206
|
+
this.menuContainerWin.setVisibility(View.GONE.value);
|
|
207
|
+
}
|
|
208
|
+
updatePosition(window, winParams, newPos) {
|
|
209
|
+
const { x: wx, y: wy } = (0, utils_1.logicalToWindow)(newPos.x, newPos.y, this.screenWidth, this.screenHeight, this.isIconMode ? this.options.iconWidth : this.options.width, this.isIconMode ? this.options.iconHeight : this.options.height);
|
|
210
|
+
winParams.x.value = wx | 0;
|
|
211
|
+
winParams.y.value = wy | 0;
|
|
212
|
+
Java.scheduleOnMainThread(() => {
|
|
213
|
+
this.windowManager.updateViewLayout(window, winParams);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
createIconWindow() {
|
|
217
|
+
const ImageView = api_1.API.ImageView;
|
|
218
|
+
const ImageView$ScaleType = api_1.API.ImageViewScaleType;
|
|
219
|
+
const FrameLayoutParams = api_1.API.FrameLayoutParams;
|
|
220
|
+
const Gravity = api_1.API.Gravity;
|
|
221
|
+
const LayoutParams = api_1.API.LayoutParams;
|
|
222
|
+
const BitmapFactory = api_1.API.BitmapFactory;
|
|
223
|
+
const Base64 = api_1.API.Base64;
|
|
224
|
+
const FrameLayout = api_1.API.FrameLayout;
|
|
225
|
+
this.iconView = ImageView.$new(this.context);
|
|
226
|
+
if (this.options.iconBase64) {
|
|
227
|
+
const decoded = Base64.decode(this.options.iconBase64, Base64.DEFAULT.value);
|
|
228
|
+
const bitmap = BitmapFactory.decodeByteArray(decoded, 0, decoded.length);
|
|
229
|
+
this.iconView.setImageBitmap(bitmap);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
this.iconView.setBackgroundColor(0xff4285f4 | 0);
|
|
233
|
+
try {
|
|
234
|
+
this.iconView.setClipToOutline(true);
|
|
235
|
+
}
|
|
236
|
+
catch { }
|
|
237
|
+
}
|
|
238
|
+
this.iconView.setScaleType(ImageView$ScaleType.FIT_CENTER.value);
|
|
239
|
+
const { x, y } = (0, utils_1.logicalToWindow)(this.options.x, this.options.y, this.screenWidth, this.screenHeight, this.options.iconWidth, this.options.iconHeight);
|
|
240
|
+
this.iconWindowParams = LayoutParams.$new(this.options.iconWidth, this.options.iconHeight, x, y, 2038, LayoutParams.FLAG_NOT_FOCUSABLE.value |
|
|
241
|
+
LayoutParams.FLAG_NOT_TOUCH_MODAL.value, 1);
|
|
242
|
+
this.iconContainerWin = FrameLayout.$new(this.context);
|
|
243
|
+
this.iconContainerWin.setLayoutParams(FrameLayoutParams.$new(this.options.iconWidth, this.options.iconHeight, Gravity.CENTER.value));
|
|
244
|
+
this.iconContainerWin.addView(this.iconView);
|
|
245
|
+
this.windowManager.addView(this.iconContainerWin, this.iconWindowParams);
|
|
246
|
+
this.addDragListener(this.iconContainerWin, this.iconContainerWin, this.iconWindowParams);
|
|
247
|
+
}
|
|
248
|
+
toggleView() {
|
|
249
|
+
Java.scheduleOnMainThread(() => {
|
|
250
|
+
const View = api_1.API.View;
|
|
251
|
+
if (this.isIconMode) {
|
|
252
|
+
this.menuContainerWin.setVisibility(View.GONE.value);
|
|
253
|
+
this.iconContainerWin.setVisibility(View.VISIBLE.value);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
this.menuContainerWin.setVisibility(View.VISIBLE.value);
|
|
257
|
+
this.iconContainerWin.setVisibility(View.GONE.value);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
show() {
|
|
262
|
+
Java.scheduleOnMainThread(() => {
|
|
263
|
+
const Settings = Java.use("android.provider.Settings");
|
|
264
|
+
if (!Settings.canDrawOverlays(this.context)) {
|
|
265
|
+
this.toast("进程没有悬浮窗权限!");
|
|
266
|
+
console.error("Draw overlays permission not granted");
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
this.createIconWindow();
|
|
271
|
+
this.createMenuContainerWindow();
|
|
272
|
+
this.processPendingComponents(this.context);
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
console.error("Failed to show floating window: ", error);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
processPendingComponents(context) {
|
|
280
|
+
if (this.pendingComponents.length === 0)
|
|
281
|
+
return;
|
|
282
|
+
this.logger.debug(`Processing ${this.pendingComponents.length} pending components`);
|
|
283
|
+
for (const { id, component, tabId } of this.pendingComponents) {
|
|
284
|
+
try {
|
|
285
|
+
const tabInfo = this.tabsView.tabs.get(tabId);
|
|
286
|
+
if (!tabInfo) {
|
|
287
|
+
this.logger.error(`Cannot add pending component ${id} - tab ${tabId} not found`);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
component.init(context);
|
|
291
|
+
const view = component.getView();
|
|
292
|
+
if (tabInfo.container) {
|
|
293
|
+
tabInfo.container.addView(view);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
this.tabsView.currentContentContainer.addView(view);
|
|
297
|
+
}
|
|
298
|
+
tabInfo.components.add(id);
|
|
299
|
+
component.on("valueChanged", (value) => {
|
|
300
|
+
this.eventEmitter.emit("component:" + id + ":valueChanged", value);
|
|
301
|
+
});
|
|
302
|
+
component.on("action", (data) => {
|
|
303
|
+
this.eventEmitter.emit("component:" + id + ":action", data);
|
|
304
|
+
});
|
|
305
|
+
component.on("click", (data) => {
|
|
306
|
+
this.eventEmitter.emit("component:" + id + ":click", data);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
console.error(`Failed to add pending component ${id}: ` + error);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
this.pendingComponents = [];
|
|
314
|
+
}
|
|
315
|
+
hide() {
|
|
316
|
+
Java.scheduleOnMainThread(() => {
|
|
317
|
+
try {
|
|
318
|
+
this.iconContainerWin.setAlpha(0);
|
|
319
|
+
this.windowManager.updateViewLayout(this.iconContainerWin, this.iconWindowParams);
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
console.error("Failed to hide floating window: " + error);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
toast(msg, duration = 0) {
|
|
327
|
+
Java.scheduleOnMainThread(() => {
|
|
328
|
+
var toast = Java.use("android.widget.Toast");
|
|
329
|
+
toast
|
|
330
|
+
.makeText(this.context, Java.use("java.lang.String").$new(msg), duration)
|
|
331
|
+
.show();
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
addComponent(component, tabId) {
|
|
335
|
+
const id = component.getId();
|
|
336
|
+
const targetTabId = tabId || this.tabsView.activeTabId;
|
|
337
|
+
const tabInfo = this.tabsView.tabs.get(targetTabId);
|
|
338
|
+
if (!tabInfo) {
|
|
339
|
+
console.error(`Cannot add component ${id} - tab ${targetTabId} not found`);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
this.uiComponents.set(id, component);
|
|
343
|
+
component.setMenu(this);
|
|
344
|
+
tabInfo.components.add(id);
|
|
345
|
+
if (!this.menuPanelView) {
|
|
346
|
+
this.pendingComponents.push({ id, component, tabId: targetTabId });
|
|
347
|
+
console.debug(`Component ${id} queued for tab ${targetTabId} (window not shown)`);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
Java.scheduleOnMainThread(() => {
|
|
351
|
+
const context = this.menuPanelView.getContext();
|
|
352
|
+
component.init(context);
|
|
353
|
+
const view = component.getView();
|
|
354
|
+
if (tabInfo.container) {
|
|
355
|
+
tabInfo.container.addView(view);
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
console.warn(`Tab container for ${targetTabId} not found, using contentContainer`);
|
|
359
|
+
this.tabsView.currentContentContainer.addView(view);
|
|
360
|
+
}
|
|
361
|
+
component.on("valueChanged", (value) => {
|
|
362
|
+
this.eventEmitter.emit("component:" + id + ":valueChanged", value);
|
|
363
|
+
});
|
|
364
|
+
component.on("action", (data) => {
|
|
365
|
+
this.eventEmitter.emit("component:" + id + ":action", data);
|
|
366
|
+
});
|
|
367
|
+
component.on("click", (data) => {
|
|
368
|
+
this.eventEmitter.emit("component:" + id + ":click", data);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
removeComponent(id) {
|
|
373
|
+
const component = this.uiComponents.get(id);
|
|
374
|
+
if (!component)
|
|
375
|
+
return;
|
|
376
|
+
let targetTabId = null;
|
|
377
|
+
for (const [tabId, tabInfo] of this.tabsView.tabs) {
|
|
378
|
+
if (tabInfo.components.has(id)) {
|
|
379
|
+
targetTabId = tabId;
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
this.pendingComponents = this.pendingComponents.filter((p) => p.id !== id);
|
|
384
|
+
Java.scheduleOnMainThread(() => {
|
|
385
|
+
const view = component.getView();
|
|
386
|
+
if (targetTabId) {
|
|
387
|
+
const tabInfo = this.tabsView.tabs.get(targetTabId);
|
|
388
|
+
if (tabInfo && tabInfo.container) {
|
|
389
|
+
try {
|
|
390
|
+
tabInfo.container.removeView(view);
|
|
391
|
+
}
|
|
392
|
+
catch (e) {
|
|
393
|
+
if (this.tabsView.currentContentContainer) {
|
|
394
|
+
try {
|
|
395
|
+
this.tabsView.currentContentContainer.removeView(view);
|
|
396
|
+
}
|
|
397
|
+
catch (e2) {
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
else if (this.tabsView.currentContentContainer) {
|
|
403
|
+
try {
|
|
404
|
+
this.tabsView.currentContentContainer.removeView(view);
|
|
405
|
+
}
|
|
406
|
+
catch (e) {
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
if (this.tabsView.currentContentContainer) {
|
|
412
|
+
try {
|
|
413
|
+
this.tabsView.currentContentContainer.removeView(view);
|
|
414
|
+
}
|
|
415
|
+
catch (e) {
|
|
416
|
+
this.menuContainerWin.removeView(view);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
else if (this.menuContainerWin) {
|
|
420
|
+
this.menuContainerWin.removeView(view);
|
|
421
|
+
}
|
|
422
|
+
else
|
|
423
|
+
console.error("error");
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
if (targetTabId) {
|
|
427
|
+
const tabInfo = this.tabsView.tabs.get(targetTabId);
|
|
428
|
+
if (tabInfo) {
|
|
429
|
+
tabInfo.components.delete(id);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
this.uiComponents.delete(id);
|
|
433
|
+
console.debug(`Component ${id} removed${targetTabId ? ` from tab ${targetTabId}` : ""}`);
|
|
434
|
+
}
|
|
435
|
+
getComponent(id) {
|
|
436
|
+
return this.uiComponents.get(id);
|
|
437
|
+
}
|
|
438
|
+
setComponentValue(id, value) {
|
|
439
|
+
const component = this.uiComponents.get(id);
|
|
440
|
+
if (component) {
|
|
441
|
+
component.setValue(value);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
on(event, callback) {
|
|
445
|
+
this.eventEmitter.on(event, callback);
|
|
446
|
+
}
|
|
447
|
+
off(event, callback) {
|
|
448
|
+
this.eventEmitter.off(event, callback);
|
|
449
|
+
}
|
|
450
|
+
createHeaderView(context) {
|
|
451
|
+
try {
|
|
452
|
+
const LinearLayout = api_1.API.LinearLayout;
|
|
453
|
+
const LinearLayoutParams = api_1.API.LinearLayoutParams;
|
|
454
|
+
const TextView = api_1.API.TextView;
|
|
455
|
+
const JString = api_1.API.JString;
|
|
456
|
+
const GradientDrawable = api_1.API.GradientDrawable;
|
|
457
|
+
const Gravity = api_1.API.Gravity;
|
|
458
|
+
const self = this;
|
|
459
|
+
const PAD_H = (0, style_1.dp)(context, 10);
|
|
460
|
+
const PAD_V = (0, style_1.dp)(context, 8);
|
|
461
|
+
const BTN_SIZE = (0, style_1.dp)(context, 34);
|
|
462
|
+
const BTN_RADIUS = (0, style_1.dp)(context, 10);
|
|
463
|
+
const createIconCharBtn = (ch, isDanger = false) => {
|
|
464
|
+
const btn = TextView.$new(context);
|
|
465
|
+
btn.setText(JString.$new(ch));
|
|
466
|
+
btn.setGravity(Gravity.CENTER.value);
|
|
467
|
+
btn.setSingleLine(true);
|
|
468
|
+
btn.setTextSize(2, this.options.theme.textSp.title);
|
|
469
|
+
btn.setTextColor(isDanger
|
|
470
|
+
? this.options.theme.colors.accent
|
|
471
|
+
: this.options.theme.colors.text);
|
|
472
|
+
const lp = LinearLayoutParams.$new(BTN_SIZE, BTN_SIZE);
|
|
473
|
+
btn.setLayoutParams(lp);
|
|
474
|
+
const d = GradientDrawable.$new();
|
|
475
|
+
d.setCornerRadius(BTN_RADIUS);
|
|
476
|
+
d.setColor(0x00000000);
|
|
477
|
+
d.setStroke((0, style_1.dp)(context, 1), this.options.theme.colors.divider);
|
|
478
|
+
btn.setBackgroundDrawable(d);
|
|
479
|
+
btn.setPadding((0, style_1.dp)(context, 6), (0, style_1.dp)(context, 6), (0, style_1.dp)(context, 6), (0, style_1.dp)(context, 6));
|
|
480
|
+
return btn;
|
|
481
|
+
};
|
|
482
|
+
this.headerView = LinearLayout.$new(context);
|
|
483
|
+
this.headerView.setOrientation(0);
|
|
484
|
+
this.headerView.setGravity(Gravity.CENTER_VERTICAL.value);
|
|
485
|
+
const headerLp = LinearLayoutParams.$new(LinearLayoutParams.MATCH_PARENT.value, LinearLayoutParams.WRAP_CONTENT.value);
|
|
486
|
+
this.headerView.setLayoutParams(headerLp);
|
|
487
|
+
this.headerView.setPadding(PAD_H, PAD_V, PAD_H, PAD_V);
|
|
488
|
+
const bg = GradientDrawable.$new();
|
|
489
|
+
bg.setCornerRadius((0, style_1.dp)(context, 14));
|
|
490
|
+
bg.setColor(this.options.theme.colors.cardBg);
|
|
491
|
+
bg.setStroke((0, style_1.dp)(context, 1), this.options.theme.colors.divider);
|
|
492
|
+
this.headerView.setBackgroundDrawable(bg);
|
|
493
|
+
const titleView = TextView.$new(context);
|
|
494
|
+
titleView.setText(JString.$new(this.options.title));
|
|
495
|
+
titleView.setSingleLine(true);
|
|
496
|
+
titleView.setGravity(Gravity.CENTER_VERTICAL.value);
|
|
497
|
+
titleView.setTypeface(null, 1);
|
|
498
|
+
titleView.setTextColor(this.options.theme.colors.text);
|
|
499
|
+
titleView.setTextSize(2, this.options.theme.textSp.title);
|
|
500
|
+
const titleLp = LinearLayoutParams.$new(0, LinearLayoutParams.WRAP_CONTENT.value, 1.0);
|
|
501
|
+
titleView.setLayoutParams(titleLp);
|
|
502
|
+
titleView.setPadding(0, (0, style_1.dp)(context, 2), (0, style_1.dp)(context, 10), (0, style_1.dp)(context, 2));
|
|
503
|
+
const rightBox = LinearLayout.$new(context);
|
|
504
|
+
rightBox.setOrientation(0);
|
|
505
|
+
rightBox.setGravity(Gravity.CENTER_VERTICAL.value);
|
|
506
|
+
const rightLp = LinearLayoutParams.$new(LinearLayoutParams.WRAP_CONTENT.value, LinearLayoutParams.WRAP_CONTENT.value);
|
|
507
|
+
rightBox.setLayoutParams(rightLp);
|
|
508
|
+
const logView = new log_view_1.LogView(context, this.options.height - 240, this.options.theme, this.options.logMaxLines);
|
|
509
|
+
const logButton = createIconCharBtn("L", false);
|
|
510
|
+
logButton.setOnClickListener(Java.registerClass({
|
|
511
|
+
name: "LogButtonClickListener" + Date.now(),
|
|
512
|
+
implements: [api_1.API.OnClickListener],
|
|
513
|
+
methods: {
|
|
514
|
+
onClick: function () {
|
|
515
|
+
logView.createViewOnce(self.logPanelView);
|
|
516
|
+
if (logView.isLogDrawerOpen) {
|
|
517
|
+
logView.closeLogDrawer();
|
|
518
|
+
logButton.setText(api_1.API.JString.$new("L"));
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
logView.openLogDrawer();
|
|
522
|
+
logButton.setText(api_1.API.JString.$new("←"));
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
}).$new());
|
|
527
|
+
const minButton = createIconCharBtn("—", false);
|
|
528
|
+
minButton.setOnClickListener(Java.registerClass({
|
|
529
|
+
name: "MinButtonClickListener" + Date.now(),
|
|
530
|
+
implements: [api_1.API.OnClickListener],
|
|
531
|
+
methods: {
|
|
532
|
+
onClick: function () {
|
|
533
|
+
self.isIconMode = true;
|
|
534
|
+
self.toggleView();
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
}).$new());
|
|
538
|
+
const hideButton = createIconCharBtn("X", true);
|
|
539
|
+
hideButton.setOnClickListener(Java.registerClass({
|
|
540
|
+
name: "HideButtonClickListener" + Date.now(),
|
|
541
|
+
implements: [api_1.API.OnClickListener],
|
|
542
|
+
methods: {
|
|
543
|
+
onClick: function () {
|
|
544
|
+
self.isIconMode = true;
|
|
545
|
+
self.toggleView();
|
|
546
|
+
self.hide();
|
|
547
|
+
self.toast("菜单已隐藏,单击原来位置显示");
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
}).$new());
|
|
551
|
+
const lpBtn = LinearLayoutParams.$new(BTN_SIZE, BTN_SIZE);
|
|
552
|
+
lpBtn.setMargins(0, 0, (0, style_1.dp)(context, 8), 0);
|
|
553
|
+
logButton.setLayoutParams(lpBtn);
|
|
554
|
+
minButton.setLayoutParams(lpBtn);
|
|
555
|
+
rightBox.addView(logButton);
|
|
556
|
+
rightBox.addView(minButton);
|
|
557
|
+
rightBox.addView(hideButton);
|
|
558
|
+
this.headerView.addView(titleView);
|
|
559
|
+
this.headerView.addView(rightBox);
|
|
560
|
+
this.menuPanelView.addView(this.headerView);
|
|
561
|
+
this.addDragListener(this.headerView, this.menuContainerWin, this.menuWindowParams);
|
|
562
|
+
}
|
|
563
|
+
catch (error) {
|
|
564
|
+
console.error("Failed to create header view: " + error);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
exports.FloatMenu = FloatMenu;
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { EventEmitter } from "./event-emitter";
|
|
2
|
+
export { Logger, LogLevel } from "./logger";
|
|
3
|
+
export { Button } from "./component/button";
|
|
4
|
+
export { Category } from "./component/category";
|
|
5
|
+
export { Collapsible } from "./component/collapsible";
|
|
6
|
+
export { NumberInput, TextInput } from "./component/input";
|
|
7
|
+
export { Selector } from "./component/selector";
|
|
8
|
+
export { Slider } from "./component/slider";
|
|
9
|
+
export { Switch } from "./component/switch";
|
|
10
|
+
export { TextView as Text } from "./component/text";
|
|
11
|
+
export { ImageView } from "./component/image";
|
|
12
|
+
export { FloatMenu, FloatMenuOptions, TabDefinition } from "./float-menu";
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FloatMenu = exports.ImageView = exports.Text = exports.Switch = exports.Slider = exports.Selector = exports.TextInput = exports.NumberInput = exports.Collapsible = exports.Category = exports.Button = exports.Logger = exports.EventEmitter = void 0;
|
|
4
|
+
var event_emitter_1 = require("./event-emitter");
|
|
5
|
+
Object.defineProperty(exports, "EventEmitter", { enumerable: true, get: function () { return event_emitter_1.EventEmitter; } });
|
|
6
|
+
var logger_1 = require("./logger");
|
|
7
|
+
Object.defineProperty(exports, "Logger", { enumerable: true, get: function () { return logger_1.Logger; } });
|
|
8
|
+
var button_1 = require("./component/button");
|
|
9
|
+
Object.defineProperty(exports, "Button", { enumerable: true, get: function () { return button_1.Button; } });
|
|
10
|
+
var category_1 = require("./component/category");
|
|
11
|
+
Object.defineProperty(exports, "Category", { enumerable: true, get: function () { return category_1.Category; } });
|
|
12
|
+
var collapsible_1 = require("./component/collapsible");
|
|
13
|
+
Object.defineProperty(exports, "Collapsible", { enumerable: true, get: function () { return collapsible_1.Collapsible; } });
|
|
14
|
+
var input_1 = require("./component/input");
|
|
15
|
+
Object.defineProperty(exports, "NumberInput", { enumerable: true, get: function () { return input_1.NumberInput; } });
|
|
16
|
+
Object.defineProperty(exports, "TextInput", { enumerable: true, get: function () { return input_1.TextInput; } });
|
|
17
|
+
var selector_1 = require("./component/selector");
|
|
18
|
+
Object.defineProperty(exports, "Selector", { enumerable: true, get: function () { return selector_1.Selector; } });
|
|
19
|
+
var slider_1 = require("./component/slider");
|
|
20
|
+
Object.defineProperty(exports, "Slider", { enumerable: true, get: function () { return slider_1.Slider; } });
|
|
21
|
+
var switch_1 = require("./component/switch");
|
|
22
|
+
Object.defineProperty(exports, "Switch", { enumerable: true, get: function () { return switch_1.Switch; } });
|
|
23
|
+
var text_1 = require("./component/text");
|
|
24
|
+
Object.defineProperty(exports, "Text", { enumerable: true, get: function () { return text_1.TextView; } });
|
|
25
|
+
var image_1 = require("./component/image");
|
|
26
|
+
Object.defineProperty(exports, "ImageView", { enumerable: true, get: function () { return image_1.ImageView; } });
|
|
27
|
+
var float_menu_1 = require("./float-menu");
|
|
28
|
+
Object.defineProperty(exports, "FloatMenu", { enumerable: true, get: function () { return float_menu_1.FloatMenu; } });
|
package/lib/logger.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export declare function log(message: string): void;
|
|
2
|
+
export type LogLevel = "debug" | "info" | "warn" | "error" | "none";
|
|
3
|
+
type LogItem = {
|
|
4
|
+
ts: number;
|
|
5
|
+
level: LogLevel;
|
|
6
|
+
message: string;
|
|
7
|
+
};
|
|
8
|
+
type LogListener = (items: LogItem[]) => void;
|
|
9
|
+
export declare class Logger {
|
|
10
|
+
on(arg0: string, arg1: (level: LogLevel, message: string) => void): void;
|
|
11
|
+
private static _instance;
|
|
12
|
+
static get instance(): Logger;
|
|
13
|
+
private levelPriority;
|
|
14
|
+
private currentLevel;
|
|
15
|
+
private maxBuffer;
|
|
16
|
+
private buffer;
|
|
17
|
+
private head;
|
|
18
|
+
private size;
|
|
19
|
+
private flushIntervalMs;
|
|
20
|
+
private pending;
|
|
21
|
+
private flushTimer;
|
|
22
|
+
private listeners;
|
|
23
|
+
constructor(level?: LogLevel, options?: {
|
|
24
|
+
maxBuffer?: number;
|
|
25
|
+
flushIntervalMs?: number;
|
|
26
|
+
});
|
|
27
|
+
setLevel(level: LogLevel): void;
|
|
28
|
+
setMaxBuffer(max: number): void;
|
|
29
|
+
debug(...args: any[]): void;
|
|
30
|
+
info(...args: any[]): void;
|
|
31
|
+
warn(...args: any[]): void;
|
|
32
|
+
error(...args: any[]): void;
|
|
33
|
+
onLog(listener: LogListener, replay?: boolean): () => void;
|
|
34
|
+
getRecent(limit?: number): LogItem[];
|
|
35
|
+
clear(): void;
|
|
36
|
+
private safeStringify;
|
|
37
|
+
private formatArgs;
|
|
38
|
+
private _log;
|
|
39
|
+
private pushToBuffer;
|
|
40
|
+
private scheduleFlush;
|
|
41
|
+
private emitBatch;
|
|
42
|
+
}
|
|
43
|
+
export {};
|