android-midscene-automation 0.1.26 → 0.1.27

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.
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed, shallowRef } from 'vue';
2
+ import { computed, shallowRef, watch } from 'vue';
3
3
  import { Cpu, Refresh } from '@element-plus/icons-vue';
4
4
  import type { AndroidDevice, DeviceAction } from '../../types';
5
5
 
@@ -11,6 +11,13 @@ type DeviceOverlayBounds = {
11
11
  bottom: number;
12
12
  };
13
13
 
14
+ type DeviceRegionSelection = {
15
+ x: number;
16
+ y: number;
17
+ width: number;
18
+ height: number;
19
+ };
20
+
14
21
  const props = defineProps<{
15
22
  available: boolean;
16
23
  devices: AndroidDevice[];
@@ -21,6 +28,9 @@ const props = defineProps<{
21
28
  actions?: readonly DeviceAction[];
22
29
  overlayBounds?: readonly DeviceOverlayBounds[];
23
30
  selectedBounds?: DeviceOverlayBounds;
31
+ selectedRegion?: DeviceOverlayBounds;
32
+ regionSelection?: boolean;
33
+ regionDrawMode?: boolean;
24
34
  deviceWidth?: number;
25
35
  deviceHeight?: number;
26
36
  }>();
@@ -32,6 +42,7 @@ const emit = defineEmits<{
32
42
  previewError: [];
33
43
  refreshPreview: [];
34
44
  tap: [point: { x: number; y: number }];
45
+ regionSelect: [region: DeviceRegionSelection];
35
46
  swipe: [gesture: {
36
47
  startX: number;
37
48
  startY: number;
@@ -43,11 +54,14 @@ const emit = defineEmits<{
43
54
 
44
55
  const imageSize = shallowRef({ width: 0, height: 0 });
45
56
  let pointerSession: {
57
+ mode: 'gesture' | 'region-draw' | 'region-move';
46
58
  pointerId: number;
47
59
  start: { x: number; y: number };
48
60
  startClientX: number;
49
61
  startClientY: number;
62
+ origin?: DeviceOverlayBounds;
50
63
  } | null = null;
64
+ const draftRegion = shallowRef<DeviceOverlayBounds | null>(null);
51
65
  const hasPreview = computed(() => Boolean(props.frameUrl || props.imageUrl));
52
66
  const imageBoxStyle = computed(() => {
53
67
  if (props.frameUrl) return {};
@@ -61,6 +75,12 @@ const overlayViewBox = computed(() => {
61
75
  return width && height ? `0 0 ${width} ${height}` : '';
62
76
  });
63
77
 
78
+ watch(() => props.regionSelection, (enabled) => {
79
+ if (enabled) return;
80
+ pointerSession = null;
81
+ draftRegion.value = null;
82
+ });
83
+
64
84
  function updateImageSize(event: Event) {
65
85
  const image = event.target as HTMLImageElement;
66
86
  imageSize.value = { width: image.naturalWidth, height: image.naturalHeight };
@@ -97,18 +117,109 @@ function getDevicePoint(target: HTMLElement, clientX: number, clientY: number) {
97
117
  };
98
118
  }
99
119
 
120
+ function boundsFromPoints(
121
+ id: string,
122
+ start: { x: number; y: number },
123
+ end: { x: number; y: number },
124
+ ) {
125
+ return {
126
+ id,
127
+ left: Math.min(start.x, end.x),
128
+ top: Math.min(start.y, end.y),
129
+ right: Math.max(start.x, end.x),
130
+ bottom: Math.max(start.y, end.y),
131
+ };
132
+ }
133
+
134
+ function pointInBounds(point: { x: number; y: number }, bounds?: DeviceOverlayBounds) {
135
+ if (!bounds) return false;
136
+ return point.x >= bounds.left
137
+ && point.x <= bounds.right
138
+ && point.y >= bounds.top
139
+ && point.y <= bounds.bottom;
140
+ }
141
+
142
+ function clampRegion(bounds: DeviceOverlayBounds) {
143
+ const width = props.deviceWidth || imageSize.value.width;
144
+ const height = props.deviceHeight || imageSize.value.height;
145
+ const regionWidth = Math.max(1, bounds.right - bounds.left);
146
+ const regionHeight = Math.max(1, bounds.bottom - bounds.top);
147
+ const left = Math.max(0, Math.min(bounds.left, Math.max(0, width - regionWidth)));
148
+ const top = Math.max(0, Math.min(bounds.top, Math.max(0, height - regionHeight)));
149
+ return {
150
+ id: bounds.id,
151
+ left,
152
+ top,
153
+ right: left + regionWidth,
154
+ bottom: top + regionHeight,
155
+ };
156
+ }
157
+
158
+ function moveBounds(
159
+ origin: DeviceOverlayBounds,
160
+ start: { x: number; y: number },
161
+ end: { x: number; y: number },
162
+ ) {
163
+ const dx = end.x - start.x;
164
+ const dy = end.y - start.y;
165
+ return clampRegion({
166
+ id: origin.id,
167
+ left: origin.left + dx,
168
+ top: origin.top + dy,
169
+ right: origin.right + dx,
170
+ bottom: origin.bottom + dy,
171
+ });
172
+ }
173
+
174
+ function emitRegion(bounds: DeviceOverlayBounds) {
175
+ const width = bounds.right - bounds.left;
176
+ const height = bounds.bottom - bounds.top;
177
+ if (width < 2 || height < 2) return;
178
+ emit('regionSelect', {
179
+ x: bounds.left,
180
+ y: bounds.top,
181
+ width,
182
+ height,
183
+ });
184
+ }
185
+
100
186
  function handlePointerDown(event: PointerEvent) {
101
187
  if (event.pointerType === 'mouse' && event.button !== 0) return;
102
188
  const target = event.currentTarget as HTMLElement;
103
189
  const point = getDevicePoint(target, event.clientX, event.clientY);
104
190
  if (!point) return;
191
+ if (props.regionSelection) {
192
+ event.preventDefault();
193
+ event.stopPropagation();
194
+ }
105
195
  target.setPointerCapture(event.pointerId);
196
+ const isMovingRegion = props.regionSelection
197
+ && !props.regionDrawMode
198
+ && pointInBounds(point, props.selectedRegion);
106
199
  pointerSession = {
200
+ mode: props.regionSelection ? (isMovingRegion ? 'region-move' : 'region-draw') : 'gesture',
107
201
  pointerId: event.pointerId,
108
202
  start: point,
109
203
  startClientX: event.clientX,
110
204
  startClientY: event.clientY,
205
+ origin: isMovingRegion && props.selectedRegion ? { ...props.selectedRegion } : undefined,
111
206
  };
207
+ if (props.regionSelection) {
208
+ draftRegion.value = pointerSession.origin || boundsFromPoints('draft-region', point, point);
209
+ }
210
+ }
211
+
212
+ function handlePointerMove(event: PointerEvent) {
213
+ if (!pointerSession || pointerSession.pointerId !== event.pointerId || pointerSession.mode === 'gesture') return;
214
+ const target = event.currentTarget as HTMLElement;
215
+ const point = getDevicePoint(target, event.clientX, event.clientY);
216
+ if (!point) return;
217
+ event.preventDefault();
218
+ if (pointerSession.mode === 'region-move' && pointerSession.origin) {
219
+ draftRegion.value = moveBounds(pointerSession.origin, pointerSession.start, point);
220
+ return;
221
+ }
222
+ draftRegion.value = boundsFromPoints('draft-region', pointerSession.start, point);
112
223
  }
113
224
 
114
225
  function handlePointerUp(event: PointerEvent) {
@@ -118,7 +229,17 @@ function handlePointerUp(event: PointerEvent) {
118
229
  const session = pointerSession;
119
230
  pointerSession = null;
120
231
  if (target.hasPointerCapture(event.pointerId)) target.releasePointerCapture(event.pointerId);
121
- if (!point) return;
232
+ if (!point) {
233
+ if (session.mode !== 'gesture') draftRegion.value = null;
234
+ return;
235
+ }
236
+
237
+ if (session.mode !== 'gesture') {
238
+ const bounds = draftRegion.value || boundsFromPoints('selected-region', session.start, point);
239
+ draftRegion.value = null;
240
+ emitRegion(bounds);
241
+ return;
242
+ }
122
243
 
123
244
  const moved = Math.hypot(
124
245
  event.clientX - session.startClientX,
@@ -139,7 +260,10 @@ function handlePointerUp(event: PointerEvent) {
139
260
  }
140
261
 
141
262
  function handlePointerCancel(event: PointerEvent) {
142
- if (pointerSession?.pointerId === event.pointerId) pointerSession = null;
263
+ if (pointerSession?.pointerId === event.pointerId) {
264
+ pointerSession = null;
265
+ draftRegion.value = null;
266
+ }
143
267
  }
144
268
  </script>
145
269
 
@@ -208,7 +332,9 @@ function handlePointerCancel(event: PointerEvent) {
208
332
  >
209
333
  <div
210
334
  class="device-preview__surface"
335
+ :class="{ 'device-preview__surface--selecting': regionSelection }"
211
336
  @pointerdown="handlePointerDown"
337
+ @pointermove="handlePointerMove"
212
338
  @pointerup="handlePointerUp"
213
339
  @pointercancel="handlePointerCancel"
214
340
  />
@@ -229,7 +355,7 @@ function handlePointerCancel(event: PointerEvent) {
229
355
  @error="emit('previewError')"
230
356
  />
231
357
  <svg
232
- v-if="overlayViewBox && (overlayBounds?.length || selectedBounds)"
358
+ v-if="overlayViewBox && (overlayBounds?.length || selectedBounds || selectedRegion || draftRegion)"
233
359
  class="device-preview__overlay"
234
360
  :viewBox="overlayViewBox"
235
361
  preserveAspectRatio="xMidYMid meet"
@@ -254,6 +380,24 @@ function handlePointerCancel(event: PointerEvent) {
254
380
  :height="Math.max(0, selectedBounds.bottom - selectedBounds.top)"
255
381
  vector-effect="non-scaling-stroke"
256
382
  />
383
+ <rect
384
+ v-if="selectedRegion && !draftRegion"
385
+ class="device-preview__selected-region"
386
+ :x="selectedRegion.left"
387
+ :y="selectedRegion.top"
388
+ :width="Math.max(0, selectedRegion.right - selectedRegion.left)"
389
+ :height="Math.max(0, selectedRegion.bottom - selectedRegion.top)"
390
+ vector-effect="non-scaling-stroke"
391
+ />
392
+ <rect
393
+ v-if="draftRegion"
394
+ class="device-preview__draft-region"
395
+ :x="draftRegion.left"
396
+ :y="draftRegion.top"
397
+ :width="Math.max(0, draftRegion.right - draftRegion.left)"
398
+ :height="Math.max(0, draftRegion.bottom - draftRegion.top)"
399
+ vector-effect="non-scaling-stroke"
400
+ />
257
401
  </svg>
258
402
  </div>
259
403
  <div v-else class="device-preview__empty">
package/src/style.css CHANGED
@@ -1251,6 +1251,10 @@ select {
1251
1251
  grid-template-columns: repeat(3, minmax(0, 1fr));
1252
1252
  }
1253
1253
 
1254
+ .appium-flow-editor__grid--visual {
1255
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1256
+ }
1257
+
1254
1258
  .appium-swipe-message-box {
1255
1259
  width: min(560px, calc(100vw - 32px));
1256
1260
  }
@@ -1261,6 +1265,26 @@ select {
1261
1265
  gap: 10px 12px;
1262
1266
  }
1263
1267
 
1268
+ .appium-visual-change-dialog {
1269
+ margin-left: auto;
1270
+ margin-right: 32px;
1271
+ }
1272
+
1273
+ .appium-visual-change-dialog .el-dialog__body {
1274
+ padding-top: 8px;
1275
+ }
1276
+
1277
+ .appium-visual-change-form {
1278
+ display: grid;
1279
+ gap: 10px;
1280
+ }
1281
+
1282
+ .appium-visual-change-form__grid {
1283
+ display: grid;
1284
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1285
+ gap: 10px 12px;
1286
+ }
1287
+
1264
1288
  .appium-flow-insert {
1265
1289
  position: relative;
1266
1290
  z-index: 1;
@@ -2332,6 +2356,10 @@ select {
2332
2356
  cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='28' viewBox='0 0 28 28'%3E%3Ccircle cx='14' cy='14' r='7' fill='none' stroke='%231f2937' stroke-width='2'/%3E%3Ccircle cx='14' cy='14' r='2' fill='%231f2937'/%3E%3C/svg%3E") 14 14, crosshair;
2333
2357
  }
2334
2358
 
2359
+ .device-preview__surface--selecting {
2360
+ cursor: crosshair;
2361
+ }
2362
+
2335
2363
  .device-preview__frame {
2336
2364
  width: 100%;
2337
2365
  height: 100%;
@@ -2366,6 +2394,17 @@ select {
2366
2394
  stroke-width: 2;
2367
2395
  }
2368
2396
 
2397
+ .device-preview__selected-region,
2398
+ .device-preview__draft-region {
2399
+ fill: rgba(22, 119, 255, 0.16);
2400
+ stroke: #1677ff;
2401
+ stroke-width: 2;
2402
+ }
2403
+
2404
+ .device-preview__draft-region {
2405
+ stroke-dasharray: 8 5;
2406
+ }
2407
+
2369
2408
  @media (max-width: 1200px) {
2370
2409
  .device-actions {
2371
2410
  flex-wrap: wrap;