frida-ui-runtime 2.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 +181 -0
- package/lib/android-listener.d.ts +20 -0
- package/lib/android-listener.js +92 -0
- package/lib/android-runtime.d.ts +25 -0
- package/lib/android-runtime.js +310 -0
- package/lib/api.d.ts +1 -0
- package/lib/api.js +156 -0
- package/lib/component/button.d.ts +11 -0
- package/lib/component/button.js +61 -0
- package/lib/component/category.d.ts +9 -0
- package/lib/component/category.js +57 -0
- package/lib/component/checkBox.d.ts +33 -0
- package/lib/component/checkBox.js +276 -0
- package/lib/component/collapsible.d.ts +19 -0
- package/lib/component/collapsible.js +197 -0
- package/lib/component/image.d.ts +18 -0
- package/lib/component/image.js +85 -0
- package/lib/component/infoCardBox.d.ts +13 -0
- package/lib/component/infoCardBox.js +68 -0
- package/lib/component/input.d.ts +41 -0
- package/lib/component/input.js +342 -0
- package/lib/component/selector.d.ts +31 -0
- package/lib/component/selector.js +246 -0
- package/lib/component/slider.d.ts +18 -0
- package/lib/component/slider.js +156 -0
- package/lib/component/style/style.d.ts +4 -0
- package/lib/component/style/style.js +325 -0
- package/lib/component/style/theme.d.ts +33 -0
- package/lib/component/style/theme.js +24 -0
- package/lib/component/switch.d.ts +12 -0
- package/lib/component/switch.js +117 -0
- package/lib/component/text.d.ts +9 -0
- package/lib/component/text.js +36 -0
- package/lib/component/ui-components.d.ts +34 -0
- package/lib/component/ui-components.js +108 -0
- package/lib/component/views/header-view.d.ts +20 -0
- package/lib/component/views/header-view.js +198 -0
- package/lib/component/views/log-view.d.ts +41 -0
- package/lib/component/views/log-view.js +511 -0
- package/lib/component/views/tabs-view.d.ts +41 -0
- package/lib/component/views/tabs-view.js +334 -0
- package/lib/constant-config.d.ts +5 -0
- package/lib/constant-config.js +3 -0
- package/lib/event-emitter.d.ts +10 -0
- package/lib/event-emitter.js +52 -0
- package/lib/float-menu.d.ts +103 -0
- package/lib/float-menu.js +648 -0
- package/lib/index.d.ts +17 -0
- package/lib/index.js +16 -0
- package/lib/java-runtime.d.ts +2 -0
- package/lib/java-runtime.js +2 -0
- package/lib/logger.d.ts +43 -0
- package/lib/logger.js +200 -0
- package/lib/runtime/component-registry.d.ts +24 -0
- package/lib/runtime/component-registry.js +48 -0
- package/lib/runtime/launch-sequence.d.ts +5 -0
- package/lib/runtime/launch-sequence.js +20 -0
- package/lib/runtime/lifecycle-controller.d.ts +9 -0
- package/lib/runtime/lifecycle-controller.js +31 -0
- package/lib/runtime/listener-registry.d.ts +7 -0
- package/lib/runtime/listener-registry.js +40 -0
- package/lib/runtime/overlay-options.d.ts +9 -0
- package/lib/runtime/overlay-options.js +42 -0
- package/lib/runtime/safe-runtime.d.ts +3 -0
- package/lib/runtime/safe-runtime.js +20 -0
- package/lib/utils.d.ts +8 -0
- package/lib/utils.js +13 -0
- package/package.json +43 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import Java from "../java-runtime.js";
|
|
2
|
+
import { API } from "../api.js";
|
|
3
|
+
import { Logger } from "../logger.js";
|
|
4
|
+
import { applyStyle, dp } from "./style/style.js";
|
|
5
|
+
import { UIComponent } from "./ui-components.js";
|
|
6
|
+
import { getOverlayWindowType } from "../android-runtime.js";
|
|
7
|
+
import { createJavaListener, disposeJavaListenerOwner, } from "../android-listener.js";
|
|
8
|
+
function setDialogOverlayType(dialog) {
|
|
9
|
+
try {
|
|
10
|
+
const window = dialog.getWindow();
|
|
11
|
+
if (!window)
|
|
12
|
+
return;
|
|
13
|
+
const LayoutParams = API.LayoutParams;
|
|
14
|
+
window.setType(getOverlayWindowType(LayoutParams));
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
Logger.instance.warn(`[Input] Failed to set dialog overlay type: ${e}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
class BaseInputButton extends UIComponent {
|
|
21
|
+
constructor(id, title, hint) {
|
|
22
|
+
super(id);
|
|
23
|
+
this.isShowDialog = false;
|
|
24
|
+
this.title = title;
|
|
25
|
+
this.hint = hint;
|
|
26
|
+
}
|
|
27
|
+
createBaseView(context) {
|
|
28
|
+
const LinearLayout = API.LinearLayout;
|
|
29
|
+
const LinearLayoutParams = API.LinearLayoutParams;
|
|
30
|
+
const ViewGroupLayoutParams = API.ViewGroupLayoutParams;
|
|
31
|
+
const TextView = API.TextView;
|
|
32
|
+
const Gravity = API.Gravity;
|
|
33
|
+
const String = API.JString;
|
|
34
|
+
const root = LinearLayout.$new(context);
|
|
35
|
+
root.setOrientation(LinearLayout.HORIZONTAL.value);
|
|
36
|
+
root.setGravity(Gravity.CENTER_VERTICAL.value);
|
|
37
|
+
applyStyle(root, "inputButton", this.menu.options.theme);
|
|
38
|
+
const labelView = TextView.$new(context);
|
|
39
|
+
labelView.setText.overload('java.lang.CharSequence').call(labelView, this.buildDisplayText());
|
|
40
|
+
applyStyle(labelView, "text", this.menu.options.theme);
|
|
41
|
+
labelView.setTextSize(2, 14);
|
|
42
|
+
try {
|
|
43
|
+
labelView.setTypeface(null, 1);
|
|
44
|
+
}
|
|
45
|
+
catch (_e) { }
|
|
46
|
+
labelView.setSingleLine(true);
|
|
47
|
+
labelView.setGravity(Gravity.LEFT.value | Gravity.CENTER_VERTICAL.value);
|
|
48
|
+
labelView.setLayoutParams(LinearLayoutParams.$new(0, ViewGroupLayoutParams.WRAP_CONTENT.value, 1.0));
|
|
49
|
+
const iconView = TextView.$new(context);
|
|
50
|
+
iconView.setText.overload('java.lang.CharSequence').call(iconView, "✎");
|
|
51
|
+
iconView.setTextColor(this.menu.options.theme.colors.accent);
|
|
52
|
+
iconView.setTextSize(2, 16);
|
|
53
|
+
iconView.setGravity(Gravity.CENTER.value);
|
|
54
|
+
const iconLp = LinearLayoutParams.$new(ViewGroupLayoutParams.WRAP_CONTENT.value, ViewGroupLayoutParams.WRAP_CONTENT.value);
|
|
55
|
+
iconLp.leftMargin = dp(context, 12);
|
|
56
|
+
iconView.setLayoutParams(iconLp);
|
|
57
|
+
root.addView(labelView);
|
|
58
|
+
root.addView(iconView);
|
|
59
|
+
this.view = root;
|
|
60
|
+
this.buttonView = root;
|
|
61
|
+
this.buttonLabelView = labelView;
|
|
62
|
+
this.buttonIconView = iconView;
|
|
63
|
+
}
|
|
64
|
+
refreshButtonText() {
|
|
65
|
+
if (!this.buttonLabelView)
|
|
66
|
+
return;
|
|
67
|
+
Java.scheduleOnMainThread(() => {
|
|
68
|
+
const String = API.JString;
|
|
69
|
+
this.buttonLabelView.setText.overload('java.lang.CharSequence').call(this.buttonLabelView, this.buildDisplayText());
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export class NumberInput extends BaseInputButton {
|
|
74
|
+
constructor(id, initialValue = 0, title = "请输入数值", hint = "请输入数值") {
|
|
75
|
+
super(id, title, hint);
|
|
76
|
+
this.min = null;
|
|
77
|
+
this.max = null;
|
|
78
|
+
this.value = initialValue;
|
|
79
|
+
}
|
|
80
|
+
buildDisplayText() {
|
|
81
|
+
const hasValue = this.value !== null &&
|
|
82
|
+
this.value !== undefined &&
|
|
83
|
+
!isNaN(this.value);
|
|
84
|
+
return hasValue
|
|
85
|
+
? `${this.title}: ${this.value}`
|
|
86
|
+
: `${this.title}: ${this.hint}`;
|
|
87
|
+
}
|
|
88
|
+
createView(context) {
|
|
89
|
+
this.createBaseView(context);
|
|
90
|
+
const self = this;
|
|
91
|
+
const clickListener = createJavaListener({
|
|
92
|
+
key: "view-click",
|
|
93
|
+
interfaceClass: API.OnClickListener,
|
|
94
|
+
callbacks: {
|
|
95
|
+
onClick: () => {
|
|
96
|
+
if (self.isShowDialog)
|
|
97
|
+
return;
|
|
98
|
+
self.isShowDialog = true;
|
|
99
|
+
self.showDialog(context);
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
this.own(() => clickListener.dispose());
|
|
104
|
+
this.buttonView.setClickable(true);
|
|
105
|
+
this.buttonView.setOnClickListener(clickListener.instance);
|
|
106
|
+
}
|
|
107
|
+
updateView() {
|
|
108
|
+
this.applyConstraints();
|
|
109
|
+
this.refreshButtonText();
|
|
110
|
+
}
|
|
111
|
+
showDialog(context) {
|
|
112
|
+
Java.scheduleOnMainThread(() => {
|
|
113
|
+
try {
|
|
114
|
+
const AlertDialogBuilder = API.AlertDialogBuilder;
|
|
115
|
+
const EditText = API.EditText;
|
|
116
|
+
const JString = API.JString;
|
|
117
|
+
const LinearLayout = API.LinearLayout;
|
|
118
|
+
const LinearLayoutParams = API.LinearLayoutParams;
|
|
119
|
+
const ViewGroupLayoutParams = API.ViewGroupLayoutParams;
|
|
120
|
+
const InputType = Java.use("android.text.InputType");
|
|
121
|
+
const TextViewBufferType = API.TextViewBufferType;
|
|
122
|
+
const builder = AlertDialogBuilder.$new(context);
|
|
123
|
+
builder.setTitle(JString.$new(this.title));
|
|
124
|
+
const input = EditText.$new(context);
|
|
125
|
+
input.setHint(JString.$new(this.hint));
|
|
126
|
+
const text = this.value === null ||
|
|
127
|
+
this.value === undefined ||
|
|
128
|
+
isNaN(this.value)
|
|
129
|
+
? ""
|
|
130
|
+
: String(this.value);
|
|
131
|
+
input.setText.overload('java.lang.CharSequence', 'android.widget.TextView$BufferType').call(input, JString.$new(text), TextViewBufferType.NORMAL.value);
|
|
132
|
+
try {
|
|
133
|
+
input.setInputType(InputType.TYPE_CLASS_NUMBER.value |
|
|
134
|
+
InputType.TYPE_NUMBER_FLAG_SIGNED.value |
|
|
135
|
+
InputType.TYPE_NUMBER_FLAG_DECIMAL.value);
|
|
136
|
+
}
|
|
137
|
+
catch (_e) { }
|
|
138
|
+
const lp = LinearLayoutParams.$new(ViewGroupLayoutParams.MATCH_PARENT.value, ViewGroupLayoutParams.WRAP_CONTENT.value);
|
|
139
|
+
input.setLayoutParams(lp);
|
|
140
|
+
const container = LinearLayout.$new(context);
|
|
141
|
+
container.setPadding(dp(context, 16), dp(context, 12), dp(context, 16), dp(context, 8));
|
|
142
|
+
container.addView(input);
|
|
143
|
+
builder.setView(container);
|
|
144
|
+
const self = this;
|
|
145
|
+
const dialogListenerOwner = `NumberInput:${this.id}:dialog:${Date.now()}:${Math.random()}`;
|
|
146
|
+
const releaseDialogListeners = this.own(() => disposeJavaListenerOwner(dialogListenerOwner));
|
|
147
|
+
builder.setPositiveButton(JString.$new("确认"), createJavaListener({
|
|
148
|
+
key: "dialog-click",
|
|
149
|
+
owner: dialogListenerOwner,
|
|
150
|
+
interfaceClass: API.DialogInterfaceOnClickListener,
|
|
151
|
+
callbacks: {
|
|
152
|
+
onClick: (_dialog, _which) => {
|
|
153
|
+
self.isShowDialog = false;
|
|
154
|
+
try {
|
|
155
|
+
const raw = Java.cast(input.getText(), Java.use("java.lang.CharSequence")).toString() + "";
|
|
156
|
+
const parsed = parseFloat(raw.trim());
|
|
157
|
+
if (!isNaN(parsed)) {
|
|
158
|
+
self.value = parsed;
|
|
159
|
+
self.applyConstraints();
|
|
160
|
+
self.refreshButtonText();
|
|
161
|
+
self.emit("valueChanged", self.value);
|
|
162
|
+
if (self.handler)
|
|
163
|
+
self.handler(self.value);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
Logger.instance.error(`[NumberInput:${self.id}] ${e}`);
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
}).instance);
|
|
172
|
+
builder.setNegativeButton(JString.$new("取消"), createJavaListener({
|
|
173
|
+
key: "dialog-click",
|
|
174
|
+
owner: dialogListenerOwner,
|
|
175
|
+
interfaceClass: API.DialogInterfaceOnClickListener,
|
|
176
|
+
callbacks: {
|
|
177
|
+
onClick: (_dialog, _which) => {
|
|
178
|
+
self.isShowDialog = false;
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
}).instance);
|
|
182
|
+
const dialog = builder.create();
|
|
183
|
+
const dismissListener = createJavaListener({
|
|
184
|
+
key: "dialog-dismiss",
|
|
185
|
+
owner: dialogListenerOwner,
|
|
186
|
+
interfaceClass: API.DialogInterfaceOnDismissListener,
|
|
187
|
+
callbacks: {
|
|
188
|
+
onDismiss: releaseDialogListeners,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
dialog.setOnDismissListener(dismissListener.instance);
|
|
192
|
+
setDialogOverlayType(dialog);
|
|
193
|
+
dialog.show();
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
this.isShowDialog = false;
|
|
197
|
+
Logger.instance.error(`[NumberInput:${this.id}] Failed to show dialog: ${e}`);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
onValueChange(handler) {
|
|
202
|
+
this.handler = handler;
|
|
203
|
+
}
|
|
204
|
+
applyConstraints() {
|
|
205
|
+
let constrained = this.value;
|
|
206
|
+
if (this.min !== null)
|
|
207
|
+
constrained = Math.max(this.min, constrained);
|
|
208
|
+
if (this.max !== null)
|
|
209
|
+
constrained = Math.min(this.max, constrained);
|
|
210
|
+
this.value = constrained;
|
|
211
|
+
}
|
|
212
|
+
setHint(hint) {
|
|
213
|
+
this.hint = hint;
|
|
214
|
+
}
|
|
215
|
+
setConstraints(min, max) {
|
|
216
|
+
this.min = min;
|
|
217
|
+
this.max = max;
|
|
218
|
+
this.applyConstraints();
|
|
219
|
+
this.refreshButtonText();
|
|
220
|
+
}
|
|
221
|
+
getNumber() {
|
|
222
|
+
return this.value;
|
|
223
|
+
}
|
|
224
|
+
setNumber(value) {
|
|
225
|
+
this.value = value;
|
|
226
|
+
this.applyConstraints();
|
|
227
|
+
this.refreshButtonText();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
export class TextInput extends BaseInputButton {
|
|
231
|
+
constructor(id, initialValue = "", title = "请输入文本", hint = "请输入文本") {
|
|
232
|
+
super(id, title, hint);
|
|
233
|
+
this.value = initialValue;
|
|
234
|
+
}
|
|
235
|
+
buildDisplayText() {
|
|
236
|
+
const text = ((this.value ?? "") + "").trim();
|
|
237
|
+
return text.length > 0
|
|
238
|
+
? `${this.title}: ${text}`
|
|
239
|
+
: `${this.title}: ${this.hint}`;
|
|
240
|
+
}
|
|
241
|
+
createView(context) {
|
|
242
|
+
this.createBaseView(context);
|
|
243
|
+
const self = this;
|
|
244
|
+
const clickListener = createJavaListener({
|
|
245
|
+
key: "view-click",
|
|
246
|
+
interfaceClass: API.OnClickListener,
|
|
247
|
+
callbacks: {
|
|
248
|
+
onClick: () => {
|
|
249
|
+
if (self.isShowDialog)
|
|
250
|
+
return;
|
|
251
|
+
self.isShowDialog = true;
|
|
252
|
+
self.showDialog(context);
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
this.own(() => clickListener.dispose());
|
|
257
|
+
this.buttonView.setClickable(true);
|
|
258
|
+
this.buttonView.setOnClickListener(clickListener.instance);
|
|
259
|
+
}
|
|
260
|
+
updateView() {
|
|
261
|
+
this.refreshButtonText();
|
|
262
|
+
}
|
|
263
|
+
emitValue(value) {
|
|
264
|
+
this.emit("valueChanged", value);
|
|
265
|
+
}
|
|
266
|
+
onValueChange(handler) {
|
|
267
|
+
this.handler = handler;
|
|
268
|
+
}
|
|
269
|
+
showDialog(context) {
|
|
270
|
+
Java.scheduleOnMainThread(() => {
|
|
271
|
+
try {
|
|
272
|
+
const AlertDialogBuilder = API.AlertDialogBuilder;
|
|
273
|
+
const EditText = API.EditText;
|
|
274
|
+
const String = API.JString;
|
|
275
|
+
const TextViewBufferType = API.TextViewBufferType;
|
|
276
|
+
const LinearLayoutParams = API.LinearLayoutParams;
|
|
277
|
+
const ViewGroupLayoutParams = API.ViewGroupLayoutParams;
|
|
278
|
+
const LinearLayout = API.LinearLayout;
|
|
279
|
+
const builder = AlertDialogBuilder.$new(context);
|
|
280
|
+
builder.setTitle(String.$new(this.title));
|
|
281
|
+
const input = EditText.$new(context);
|
|
282
|
+
input.setHint(String.$new(this.hint));
|
|
283
|
+
input.setText.overload('java.lang.CharSequence', 'android.widget.TextView$BufferType').call(input, String.$new(this.value ?? ""), TextViewBufferType.NORMAL.value);
|
|
284
|
+
const lp = LinearLayoutParams.$new(ViewGroupLayoutParams.MATCH_PARENT.value, ViewGroupLayoutParams.WRAP_CONTENT.value);
|
|
285
|
+
input.setLayoutParams(lp);
|
|
286
|
+
const container = LinearLayout.$new(context);
|
|
287
|
+
container.setPadding(dp(context, 16), dp(context, 12), dp(context, 16), dp(context, 8));
|
|
288
|
+
container.addView(input);
|
|
289
|
+
builder.setView(container);
|
|
290
|
+
const self = this;
|
|
291
|
+
const dialogListenerOwner = `TextInput:${this.id}:dialog:${Date.now()}:${Math.random()}`;
|
|
292
|
+
const releaseDialogListeners = this.own(() => disposeJavaListenerOwner(dialogListenerOwner));
|
|
293
|
+
builder.setPositiveButton(String.$new("确认"), createJavaListener({
|
|
294
|
+
key: "dialog-click",
|
|
295
|
+
owner: dialogListenerOwner,
|
|
296
|
+
interfaceClass: API.DialogInterfaceOnClickListener,
|
|
297
|
+
callbacks: {
|
|
298
|
+
onClick: (_dialog, _which) => {
|
|
299
|
+
self.isShowDialog = false;
|
|
300
|
+
const text = Java.cast(input.getText(), Java.use("java.lang.CharSequence")).toString() + "";
|
|
301
|
+
self.value = text.trim();
|
|
302
|
+
self.refreshButtonText();
|
|
303
|
+
self.emit("valueChanged", self.value);
|
|
304
|
+
if (self.handler)
|
|
305
|
+
self.handler(self.value);
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
}).instance);
|
|
309
|
+
builder.setNegativeButton(String.$new("取消"), createJavaListener({
|
|
310
|
+
key: "dialog-click",
|
|
311
|
+
owner: dialogListenerOwner,
|
|
312
|
+
interfaceClass: API.DialogInterfaceOnClickListener,
|
|
313
|
+
callbacks: {
|
|
314
|
+
onClick: (_dialog, _which) => {
|
|
315
|
+
self.isShowDialog = false;
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
}).instance);
|
|
319
|
+
const dialog = builder.create();
|
|
320
|
+
const dismissListener = createJavaListener({
|
|
321
|
+
key: "dialog-dismiss",
|
|
322
|
+
owner: dialogListenerOwner,
|
|
323
|
+
interfaceClass: API.DialogInterfaceOnDismissListener,
|
|
324
|
+
callbacks: {
|
|
325
|
+
onDismiss: releaseDialogListeners,
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
dialog.setOnDismissListener(dismissListener.instance);
|
|
329
|
+
setDialogOverlayType(dialog);
|
|
330
|
+
dialog.show();
|
|
331
|
+
}
|
|
332
|
+
catch (e) {
|
|
333
|
+
this.isShowDialog = false;
|
|
334
|
+
Logger.instance.error(`[TextInput:${this.id}] Failed to show dialog: ${e}`);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
setText(text) {
|
|
339
|
+
this.value = text ?? "";
|
|
340
|
+
this.refreshButtonText();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { UIComponent } from "./ui-components.js";
|
|
2
|
+
export declare class Selector extends UIComponent {
|
|
3
|
+
private title;
|
|
4
|
+
private items;
|
|
5
|
+
private selectedIndex;
|
|
6
|
+
private handler?;
|
|
7
|
+
private context;
|
|
8
|
+
private titleView;
|
|
9
|
+
private valueView;
|
|
10
|
+
constructor(id: string, title: string, items: {
|
|
11
|
+
label: string;
|
|
12
|
+
[key: string]: any;
|
|
13
|
+
}[], selectedIndex?: number, handler?: (value: any) => void);
|
|
14
|
+
getValue(): {
|
|
15
|
+
label: string;
|
|
16
|
+
[key: string]: any;
|
|
17
|
+
};
|
|
18
|
+
onValueChange(handler: (value: any) => void): void;
|
|
19
|
+
protected createView(context: any): void;
|
|
20
|
+
private getSelectedLabel;
|
|
21
|
+
private getDisplayText;
|
|
22
|
+
private showSelectDialog;
|
|
23
|
+
private refreshUi;
|
|
24
|
+
protected updateView(): void;
|
|
25
|
+
setItems(items: {
|
|
26
|
+
label: string;
|
|
27
|
+
[key: string]: any;
|
|
28
|
+
}[]): void;
|
|
29
|
+
getSelectedIndex(): number;
|
|
30
|
+
setTitle(title: string): void;
|
|
31
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import Java from "../java-runtime.js";
|
|
2
|
+
import { API } from "../api.js";
|
|
3
|
+
import { Logger } from "../logger.js";
|
|
4
|
+
import { applyStyle, dp } from "./style/style.js";
|
|
5
|
+
import { UIComponent } from "./ui-components.js";
|
|
6
|
+
import { deferSafe } from "../runtime/safe-runtime.js";
|
|
7
|
+
import { getOverlayWindowType } from "../android-runtime.js";
|
|
8
|
+
import { createJavaListener } from "../android-listener.js";
|
|
9
|
+
export class Selector extends UIComponent {
|
|
10
|
+
constructor(id, title, items, selectedIndex = 0, handler) {
|
|
11
|
+
super(id);
|
|
12
|
+
this.title = title;
|
|
13
|
+
this.items = items ?? [];
|
|
14
|
+
this.selectedIndex = Math.max(0, Math.min(selectedIndex, Math.max(0, this.items.length - 1)));
|
|
15
|
+
this.value = this.items.length > 0 ? this.items[this.selectedIndex] : null;
|
|
16
|
+
this.handler = handler;
|
|
17
|
+
}
|
|
18
|
+
getValue() {
|
|
19
|
+
return this.value;
|
|
20
|
+
}
|
|
21
|
+
onValueChange(handler) {
|
|
22
|
+
this.handler = handler;
|
|
23
|
+
}
|
|
24
|
+
createView(context) {
|
|
25
|
+
this.context = context;
|
|
26
|
+
const LinearLayout = API.LinearLayout;
|
|
27
|
+
const LinearLayoutParams = API.LinearLayoutParams;
|
|
28
|
+
const ViewGroupLayoutParams = API.ViewGroupLayoutParams;
|
|
29
|
+
const TextView = API.TextView;
|
|
30
|
+
const Gravity = API.Gravity;
|
|
31
|
+
const String = API.JString;
|
|
32
|
+
const OnClickListener = API.OnClickListener;
|
|
33
|
+
const theme = this.menu.options.theme;
|
|
34
|
+
const root = LinearLayout.$new(context);
|
|
35
|
+
root.setOrientation(LinearLayout.VERTICAL.value);
|
|
36
|
+
root.setLayoutParams(LinearLayoutParams.$new(ViewGroupLayoutParams.MATCH_PARENT.value, ViewGroupLayoutParams.WRAP_CONTENT.value));
|
|
37
|
+
const titleView = TextView.$new(context);
|
|
38
|
+
titleView.setText.overload('java.lang.CharSequence').call(titleView, this.title);
|
|
39
|
+
applyStyle(titleView, "caption", theme);
|
|
40
|
+
try {
|
|
41
|
+
titleView.setTypeface(null, 1);
|
|
42
|
+
}
|
|
43
|
+
catch (_e) { }
|
|
44
|
+
try {
|
|
45
|
+
titleView.setAllCaps(true);
|
|
46
|
+
}
|
|
47
|
+
catch (_e) { }
|
|
48
|
+
try {
|
|
49
|
+
titleView.setLetterSpacing(0.04);
|
|
50
|
+
}
|
|
51
|
+
catch (_e) { }
|
|
52
|
+
const titleLp = LinearLayoutParams.$new(ViewGroupLayoutParams.MATCH_PARENT.value, ViewGroupLayoutParams.WRAP_CONTENT.value);
|
|
53
|
+
titleLp.setMargins(0, 0, 0, dp(context, 8));
|
|
54
|
+
titleView.setLayoutParams(titleLp);
|
|
55
|
+
const triggerRow = LinearLayout.$new(context);
|
|
56
|
+
triggerRow.setOrientation(LinearLayout.HORIZONTAL.value);
|
|
57
|
+
triggerRow.setGravity(Gravity.CENTER_VERTICAL.value);
|
|
58
|
+
applyStyle(triggerRow, "row", theme);
|
|
59
|
+
triggerRow.setPadding(dp(context, 14), dp(context, 14), dp(context, 14), dp(context, 14));
|
|
60
|
+
const valueView = TextView.$new(context);
|
|
61
|
+
valueView.setText.overload('java.lang.CharSequence').call(valueView, this.getDisplayText());
|
|
62
|
+
applyStyle(valueView, "text", theme);
|
|
63
|
+
valueView.setTextSize(2, 14);
|
|
64
|
+
try {
|
|
65
|
+
valueView.setTypeface(null, 1);
|
|
66
|
+
}
|
|
67
|
+
catch (_e) { }
|
|
68
|
+
valueView.setSingleLine(true);
|
|
69
|
+
valueView.setGravity(Gravity.LEFT.value | Gravity.CENTER_VERTICAL.value);
|
|
70
|
+
valueView.setLayoutParams(LinearLayoutParams.$new(0, ViewGroupLayoutParams.WRAP_CONTENT.value, 1.0));
|
|
71
|
+
const arrowView = TextView.$new(context);
|
|
72
|
+
arrowView.setText.overload('java.lang.CharSequence').call(arrowView, "⌄");
|
|
73
|
+
arrowView.setTextColor(theme.colors.accent);
|
|
74
|
+
arrowView.setTextSize(2, 18);
|
|
75
|
+
try {
|
|
76
|
+
arrowView.setTypeface(null, 1);
|
|
77
|
+
}
|
|
78
|
+
catch (_e) { }
|
|
79
|
+
arrowView.setGravity(Gravity.CENTER.value);
|
|
80
|
+
const arrowLp = LinearLayoutParams.$new(ViewGroupLayoutParams.WRAP_CONTENT.value, ViewGroupLayoutParams.WRAP_CONTENT.value);
|
|
81
|
+
arrowLp.leftMargin = dp(context, 12);
|
|
82
|
+
arrowView.setLayoutParams(arrowLp);
|
|
83
|
+
triggerRow.addView(valueView);
|
|
84
|
+
triggerRow.addView(arrowView);
|
|
85
|
+
const self = this;
|
|
86
|
+
const clickListener = createJavaListener({
|
|
87
|
+
key: "view-click",
|
|
88
|
+
interfaceClass: OnClickListener,
|
|
89
|
+
callbacks: {
|
|
90
|
+
onClick: (_v) => {
|
|
91
|
+
self.showSelectDialog();
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
this.own(() => clickListener.dispose());
|
|
96
|
+
triggerRow.setClickable(true);
|
|
97
|
+
triggerRow.setOnClickListener(clickListener.instance);
|
|
98
|
+
root.addView(titleView);
|
|
99
|
+
root.addView(triggerRow);
|
|
100
|
+
this.titleView = titleView;
|
|
101
|
+
this.valueView = valueView;
|
|
102
|
+
this.view = root;
|
|
103
|
+
this.refreshUi();
|
|
104
|
+
}
|
|
105
|
+
getSelectedLabel() {
|
|
106
|
+
if (this.selectedIndex >= 0 &&
|
|
107
|
+
this.selectedIndex < this.items.length &&
|
|
108
|
+
this.items[this.selectedIndex]) {
|
|
109
|
+
return this.items[this.selectedIndex].label ?? "";
|
|
110
|
+
}
|
|
111
|
+
return "";
|
|
112
|
+
}
|
|
113
|
+
getDisplayText() {
|
|
114
|
+
const item = this.getSelectedLabel();
|
|
115
|
+
if (!this.title && !item)
|
|
116
|
+
return "";
|
|
117
|
+
if (!this.title)
|
|
118
|
+
return item;
|
|
119
|
+
return `${item}`;
|
|
120
|
+
}
|
|
121
|
+
showSelectDialog() {
|
|
122
|
+
if (!this.context || this.items.length === 0)
|
|
123
|
+
return;
|
|
124
|
+
Java.scheduleOnMainThread(() => {
|
|
125
|
+
try {
|
|
126
|
+
const AlertDialogBuilder = API.AlertDialogBuilder ?? Java.use("android.app.AlertDialog$Builder");
|
|
127
|
+
const String = API.JString;
|
|
128
|
+
const DialogInterfaceOnClickListener = API.DialogInterfaceOnClickListener ??
|
|
129
|
+
Java.use("android.content.DialogInterface$OnClickListener");
|
|
130
|
+
const WindowManagerLayoutParams = Java.use("android.view.WindowManager$LayoutParams");
|
|
131
|
+
const labels = this.items.map((item) => String.$new(item.label));
|
|
132
|
+
const javaItems = Java.array("java.lang.CharSequence", labels);
|
|
133
|
+
const self = this;
|
|
134
|
+
const itemClickListener = createJavaListener({
|
|
135
|
+
key: "dialog-click",
|
|
136
|
+
interfaceClass: DialogInterfaceOnClickListener,
|
|
137
|
+
callbacks: {
|
|
138
|
+
onClick: (dialog, which) => {
|
|
139
|
+
self.selectedIndex = which;
|
|
140
|
+
self.value = self.items[which];
|
|
141
|
+
self.refreshUi();
|
|
142
|
+
self.emit("valueChanged", self.value);
|
|
143
|
+
if (self.handler) {
|
|
144
|
+
deferSafe(`Selector:${self.id}:valueChanged`, () => self.handler(self.value));
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
dialog.dismiss();
|
|
148
|
+
}
|
|
149
|
+
catch (_e) { }
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
const builder = AlertDialogBuilder.$new(this.context);
|
|
154
|
+
builder.setTitle(String.$new(this.title));
|
|
155
|
+
builder.setItems(javaItems, itemClickListener.instance);
|
|
156
|
+
const dialog = builder.create();
|
|
157
|
+
const dismissListener = createJavaListener({
|
|
158
|
+
key: "dialog-dismiss",
|
|
159
|
+
interfaceClass: API.DialogInterfaceOnDismissListener,
|
|
160
|
+
callbacks: { onDismiss: () => releaseDialogListeners() },
|
|
161
|
+
});
|
|
162
|
+
const releaseDialogListeners = this.own(() => {
|
|
163
|
+
itemClickListener.dispose();
|
|
164
|
+
dismissListener.dispose();
|
|
165
|
+
});
|
|
166
|
+
dialog.setOnDismissListener(dismissListener.instance);
|
|
167
|
+
try {
|
|
168
|
+
const window = dialog.getWindow();
|
|
169
|
+
if (window) {
|
|
170
|
+
window.setType(getOverlayWindowType(WindowManagerLayoutParams));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
Logger.instance.warn(`[Selector:${this.id}] Failed to set dialog overlay type: ${e}`);
|
|
175
|
+
}
|
|
176
|
+
dialog.show();
|
|
177
|
+
try {
|
|
178
|
+
const window = dialog.getWindow();
|
|
179
|
+
if (window) {
|
|
180
|
+
const dialogWidth = dp(this.context, 380);
|
|
181
|
+
window.setLayout(dialogWidth, WindowManagerLayoutParams.WRAP_CONTENT.value);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
Logger.instance.warn(`[Selector:${this.id}] Failed to resize dialog: ${e}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
Logger.instance.error(`[Selector:${this.id}] Failed to show selector dialog: ${error}`);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
refreshUi() {
|
|
194
|
+
if (!this.view)
|
|
195
|
+
return;
|
|
196
|
+
Java.scheduleOnMainThread(() => {
|
|
197
|
+
try {
|
|
198
|
+
const String = API.JString;
|
|
199
|
+
if (this.valueView) {
|
|
200
|
+
this.valueView.setText.overload('java.lang.CharSequence').call(this.valueView, this.getDisplayText());
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
Logger.instance.error(`[Selector:${this.id}] Failed to refresh UI: ${error}`);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
updateView() {
|
|
209
|
+
if (!this.view) {
|
|
210
|
+
Logger.instance.warn(`[Selector:${this.id}] Cannot update view - view not initialized`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const index = this.items.findIndex((value) => value && this.value && value.label == this.value.label);
|
|
214
|
+
if (index !== -1) {
|
|
215
|
+
this.selectedIndex = index;
|
|
216
|
+
}
|
|
217
|
+
this.refreshUi();
|
|
218
|
+
}
|
|
219
|
+
setItems(items) {
|
|
220
|
+
this.items = items ?? [];
|
|
221
|
+
if (this.items.length === 0) {
|
|
222
|
+
this.selectedIndex = -1;
|
|
223
|
+
this.value = null;
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
if (this.selectedIndex < 0 || this.selectedIndex >= this.items.length) {
|
|
227
|
+
this.selectedIndex = 0;
|
|
228
|
+
}
|
|
229
|
+
this.value = this.items[this.selectedIndex];
|
|
230
|
+
}
|
|
231
|
+
this.refreshUi();
|
|
232
|
+
}
|
|
233
|
+
getSelectedIndex() {
|
|
234
|
+
return this.selectedIndex;
|
|
235
|
+
}
|
|
236
|
+
setTitle(title) {
|
|
237
|
+
this.title = title;
|
|
238
|
+
if (!this.titleView)
|
|
239
|
+
return;
|
|
240
|
+
Java.scheduleOnMainThread(() => {
|
|
241
|
+
const String = API.JString;
|
|
242
|
+
this.titleView.setText.overload('java.lang.CharSequence').call(this.titleView, title);
|
|
243
|
+
this.refreshUi();
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { UIComponent } from "./ui-components.js";
|
|
2
|
+
export declare class Slider extends UIComponent {
|
|
3
|
+
private min;
|
|
4
|
+
private max;
|
|
5
|
+
private step;
|
|
6
|
+
private label;
|
|
7
|
+
private handler?;
|
|
8
|
+
constructor(id: string, label: string, min: number, max: number, initialValue?: number, step?: number, handler?: (value: number) => void);
|
|
9
|
+
protected createView(context: any): void;
|
|
10
|
+
onValueChange(handler: (value: number) => void): void;
|
|
11
|
+
protected updateView(): void;
|
|
12
|
+
setLabel(label: string): void;
|
|
13
|
+
setRange(min: number, max: number, step?: number): void;
|
|
14
|
+
private calculateSeekBarMax;
|
|
15
|
+
private valueToProgress;
|
|
16
|
+
private progressToValue;
|
|
17
|
+
private clampToStep;
|
|
18
|
+
}
|