io.appium.settings 8.0.9 → 8.1.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +126 -0
  3. package/apks/settings_apk-debug.apk +0 -0
  4. package/build/lib/client.d.ts +4 -0
  5. package/build/lib/client.d.ts.map +1 -1
  6. package/build/lib/client.js +4 -0
  7. package/build/lib/client.js.map +1 -1
  8. package/build/lib/commands/clipboard.d.ts.map +1 -1
  9. package/build/lib/commands/clipboard.js +1 -1
  10. package/build/lib/commands/clipboard.js.map +1 -1
  11. package/build/lib/commands/jpeg-stream.d.ts +51 -0
  12. package/build/lib/commands/jpeg-stream.d.ts.map +1 -0
  13. package/build/lib/commands/jpeg-stream.js +152 -0
  14. package/build/lib/commands/jpeg-stream.js.map +1 -0
  15. package/build/lib/commands/media-projection.d.ts.map +1 -1
  16. package/build/lib/commands/media-projection.js.map +1 -1
  17. package/build/lib/commands/media.js +1 -1
  18. package/build/lib/commands/media.js.map +1 -1
  19. package/build/lib/commands/network.js +1 -1
  20. package/build/lib/commands/network.js.map +1 -1
  21. package/build/lib/commands/types.d.ts +65 -0
  22. package/build/lib/commands/types.d.ts.map +1 -1
  23. package/build/lib/commands/video-stream.d.ts +50 -0
  24. package/build/lib/commands/video-stream.d.ts.map +1 -0
  25. package/build/lib/commands/video-stream.js +160 -0
  26. package/build/lib/commands/video-stream.js.map +1 -0
  27. package/build/lib/constants.d.ts +7 -0
  28. package/build/lib/constants.d.ts.map +1 -1
  29. package/build/lib/constants.js +7 -0
  30. package/build/lib/constants.js.map +1 -1
  31. package/build/lib/index.js +1 -1
  32. package/build/lib/index.js.map +1 -1
  33. package/build/lib/{utils.d.ts → utils/apk-path.d.ts} +1 -1
  34. package/build/lib/utils/apk-path.d.ts.map +1 -0
  35. package/build/lib/{utils.js → utils/apk-path.js} +1 -1
  36. package/build/lib/utils/apk-path.js.map +1 -0
  37. package/build/lib/utils/index.d.ts +5 -0
  38. package/build/lib/utils/index.d.ts.map +1 -0
  39. package/build/lib/utils/index.js +4 -0
  40. package/build/lib/utils/index.js.map +1 -0
  41. package/build/lib/utils/streaming-protocol.d.ts +47 -0
  42. package/build/lib/utils/streaming-protocol.d.ts.map +1 -0
  43. package/build/lib/utils/streaming-protocol.js +67 -0
  44. package/build/lib/utils/streaming-protocol.js.map +1 -0
  45. package/build/lib/utils/streaming-transport.d.ts +142 -0
  46. package/build/lib/utils/streaming-transport.d.ts.map +1 -0
  47. package/build/lib/utils/streaming-transport.js +416 -0
  48. package/build/lib/utils/streaming-transport.js.map +1 -0
  49. package/lib/client.ts +5 -0
  50. package/lib/commands/clipboard.ts +1 -0
  51. package/lib/commands/jpeg-stream.ts +171 -0
  52. package/lib/commands/media-projection.ts +1 -5
  53. package/lib/commands/media.ts +1 -1
  54. package/lib/commands/network.ts +1 -1
  55. package/lib/commands/types.ts +70 -0
  56. package/lib/commands/video-stream.ts +179 -0
  57. package/lib/constants.ts +10 -0
  58. package/lib/index.ts +1 -1
  59. package/lib/utils/index.ts +4 -0
  60. package/lib/utils/streaming-protocol.ts +93 -0
  61. package/lib/utils/streaming-transport.ts +450 -0
  62. package/package.json +1 -1
  63. package/build/lib/utils.d.ts.map +0 -1
  64. package/build/lib/utils.js.map +0 -1
  65. /package/lib/{utils.ts → utils/apk-path.ts} +0 -0
@@ -0,0 +1,450 @@
1
+ import net from 'node:net';
2
+
3
+ import type {ADB} from 'appium-adb';
4
+ import {retryInterval} from 'asyncbox';
5
+
6
+ import type {LocalPortOpts} from '../commands/types.js';
7
+ import {createParserState, parseFrames, StreamTrack, type StreamFrame} from './streaming-protocol.js';
8
+
9
+ const DEFAULT_QUEUE_CAPACITY = 60;
10
+ const CONNECT_TIMEOUT_MS = 5000;
11
+ const CONNECT_RETRY_INTERVAL_MS = 200;
12
+ // adb forward's host-side TCP listener accepts a connection immediately, independent of
13
+ // whether the on-device local socket is bound yet; if it isn't, adb tears the connection
14
+ // down right away with no data. Wait this long past 'connect' before trusting the socket.
15
+ const CONNECT_VERIFY_GRACE_MS = 150;
16
+
17
+ /**
18
+ * Describes a decode-dependency chain within the queued items (e.g. H.264/HEVC video
19
+ * frames, where a delta frame is undecodable without every frame back to its last
20
+ * keyframe). Items outside the chain (e.g. an interleaved audio track) are unaffected.
21
+ */
22
+ export interface DependencyChain<T> {
23
+ /** True for items that participate in the chain at all (e.g. non-CONFIG video frames). */
24
+ isChainMember: (item: T) => boolean;
25
+ /** True for a chain member that doesn't depend on earlier ones (e.g. a keyframe). */
26
+ isSyncPoint: (item: T) => boolean;
27
+ }
28
+
29
+ /**
30
+ * A bounded async-iterable queue that drops the oldest buffered item once its
31
+ * capacity is exceeded, so a slow consumer never causes unbounded memory growth.
32
+ * An optional `isProtected` predicate can shield items (e.g. video CONFIG frames)
33
+ * from eviction as long as any non-protected item remains to drop instead. An
34
+ * optional `dependencyChain` additionally makes eviction dependency-aware: dropping
35
+ * any chain member cascades to every following chain member up to (not including)
36
+ * the next sync point, and further chain-member pushes are discarded until a fresh
37
+ * sync point arrives - since anything in between is undecodable, buffering it only
38
+ * to have the consumer choke on it later isn't useful.
39
+ */
40
+ export class BoundedFrameQueue<T> {
41
+ private readonly items: T[] = [];
42
+ private readonly waiters: Array<(result: IteratorResult<T>) => void> = [];
43
+ private readonly errorWaiters: Array<(err: Error) => void> = [];
44
+ private ended = false;
45
+ private endError: Error | undefined;
46
+ private awaitingSyncPoint = false;
47
+
48
+ constructor(
49
+ private readonly capacity: number = DEFAULT_QUEUE_CAPACITY,
50
+ private readonly isProtected?: (item: T) => boolean,
51
+ private readonly dependencyChain?: DependencyChain<T>,
52
+ ) {}
53
+
54
+ /**
55
+ * Enqueues an item. If a consumer is already awaiting the next item, it is
56
+ * delivered directly; otherwise it is buffered, dropping the oldest droppable
57
+ * (non-protected) buffered item first if `capacity` would be exceeded. If a
58
+ * `dependencyChain` was given and a prior eviction broke it without a later sync
59
+ * point already buffered, chain-member items are silently discarded here until a
60
+ * fresh sync point item is pushed.
61
+ *
62
+ * @param item - The item to enqueue
63
+ */
64
+ push(item: T): void {
65
+ if (this.ended) {
66
+ return;
67
+ }
68
+ const chain = this.dependencyChain;
69
+ if (this.awaitingSyncPoint && chain?.isChainMember(item)) {
70
+ if (!chain.isSyncPoint(item)) {
71
+ return;
72
+ }
73
+ this.awaitingSyncPoint = false;
74
+ }
75
+
76
+ const waiter = this.waiters.shift();
77
+ if (waiter) {
78
+ this.errorWaiters.shift();
79
+ waiter({value: item, done: false});
80
+ return;
81
+ }
82
+ this.items.push(item);
83
+ while (this.items.length > this.capacity) {
84
+ this.evictOnce();
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Marks the dependency chain (if configured) as broken by something outside this
90
+ * queue's own eviction - e.g. a producer detecting a sequence gap in items it never
91
+ * even offered here. The next chain-member item pushed is discarded unless it is
92
+ * itself a sync point, exactly as if a local eviction had cascaded off the end of
93
+ * the buffer. A no-op if no `dependencyChain` was configured.
94
+ */
95
+ notifyGap(): void {
96
+ if (this.dependencyChain) {
97
+ this.awaitingSyncPoint = true;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Evicts one item to bring the queue back under capacity: the oldest non-protected
103
+ * item, or (if every buffered item is protected, which should not happen in
104
+ * practice) the true oldest one regardless. If the evicted item was a
105
+ * `dependencyChain` member, cascades to drop every following chain member up to
106
+ * the next sync point, setting {@link awaitingSyncPoint} if none is found.
107
+ */
108
+ private evictOnce(): void {
109
+ const isProtected = this.isProtected;
110
+ const index = isProtected ? this.items.findIndex((item) => !isProtected(item)) : 0;
111
+ if (index === -1) {
112
+ this.items.shift();
113
+ return;
114
+ }
115
+ const [evicted] = this.items.splice(index, 1);
116
+ const chain = this.dependencyChain;
117
+ if (chain?.isChainMember(evicted) && !this.dropDependentsFrom(index, chain)) {
118
+ this.awaitingSyncPoint = true;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Starting at `index`, removes chain-member items up to (not including) the next
124
+ * sync point, skipping over non-chain-member items (e.g. an interleaved audio
125
+ * track) in place without disturbing them.
126
+ *
127
+ * @returns True if a sync point was found (and kept), false if the scan reached
128
+ * the end of the buffer first
129
+ */
130
+ private dropDependentsFrom(index: number, chain: DependencyChain<T>): boolean {
131
+ while (index < this.items.length) {
132
+ const item = this.items[index];
133
+ if (!chain.isChainMember(item)) {
134
+ index++;
135
+ continue;
136
+ }
137
+ if (chain.isSyncPoint(item)) {
138
+ return true;
139
+ }
140
+ this.items.splice(index, 1);
141
+ }
142
+ return false;
143
+ }
144
+
145
+ /**
146
+ * Ends the queue. Any already-buffered items are still delivered first; once
147
+ * drained, the async iterator completes, rejecting with `error` if one is given.
148
+ * A no-op if the queue has already ended.
149
+ *
150
+ * @param error - Optional error to surface to the consumer once buffered items are drained
151
+ */
152
+ end(error?: Error): void {
153
+ if (this.ended) {
154
+ return;
155
+ }
156
+ this.ended = true;
157
+ this.endError = error;
158
+ for (;;) {
159
+ const waiter = this.waiters.shift();
160
+ const errorWaiter = this.errorWaiters.shift();
161
+ if (!waiter || !errorWaiter) {
162
+ return;
163
+ }
164
+ if (error) {
165
+ errorWaiter(error);
166
+ } else {
167
+ waiter({value: undefined, done: true});
168
+ }
169
+ }
170
+ }
171
+
172
+ private next(): Promise<IteratorResult<T>> {
173
+ if (this.items.length > 0) {
174
+ return Promise.resolve({value: this.items.shift() as T, done: false});
175
+ }
176
+ if (this.ended) {
177
+ return this.endError ? Promise.reject(this.endError) : Promise.resolve({value: undefined, done: true});
178
+ }
179
+ return new Promise((resolve, reject) => {
180
+ this.waiters.push(resolve);
181
+ this.errorWaiters.push(reject);
182
+ });
183
+ }
184
+
185
+ /**
186
+ * Async-iterates the queue, yielding items as they are pushed until {@link end} is called.
187
+ */
188
+ async *[Symbol.asyncIterator](): AsyncGenerator<T> {
189
+ for (;;) {
190
+ const result = await this.next();
191
+ if (result.done) {
192
+ return;
193
+ }
194
+ yield result.value;
195
+ }
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Validates local-port selection options for a live stream session.
201
+ *
202
+ * @param opts - Options containing an optional localPort and/or localPortRange
203
+ * @throws {TypeError} If both localPort and localPortRange are provided, or either is malformed
204
+ */
205
+ export function validateLocalPortOpts(opts: LocalPortOpts): void {
206
+ const {localPort, localPortRange} = opts;
207
+ if (localPort !== undefined && localPortRange !== undefined) {
208
+ throw new TypeError('Only one of localPort or localPortRange may be provided');
209
+ }
210
+ if (localPort !== undefined && (!Number.isInteger(localPort) || localPort < 1 || localPort > 65535)) {
211
+ throw new TypeError(`localPort must be an integer between 1 and 65535, got ${localPort}`);
212
+ }
213
+ if (localPortRange !== undefined) {
214
+ const [min, max] = localPortRange;
215
+ if (!Number.isInteger(min) || !Number.isInteger(max) || min < 1 || max > 65535 || min > max) {
216
+ throw new TypeError(
217
+ `localPortRange must be a [min, max] tuple with 1 <= min <= max <= 65535, got [${min}, ${max}]`,
218
+ );
219
+ }
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Tries to bind a local TCP server to `port` (0 lets the OS pick an ephemeral one),
225
+ * immediately closes it, and returns the bound port - or `null` if `port` is already
226
+ * in use (EADDRINUSE/EACCES), so the caller can try another candidate.
227
+ */
228
+ async function tryBindPort(port: number): Promise<number | null> {
229
+ return new Promise((resolve, reject) => {
230
+ const server = net.createServer();
231
+ server.unref();
232
+ server.once('error', (err: NodeJS.ErrnoException) => {
233
+ if (err.code === 'EADDRINUSE' || err.code === 'EACCES') {
234
+ resolve(null);
235
+ } else {
236
+ reject(err);
237
+ }
238
+ });
239
+ server.listen(port, '127.0.0.1', () => {
240
+ const address = server.address();
241
+ const boundPort = address && typeof address === 'object' ? address.port : null;
242
+ server.close(() => {
243
+ if (boundPort) {
244
+ resolve(boundPort);
245
+ } else {
246
+ reject(new Error('Unable to determine a free local port'));
247
+ }
248
+ });
249
+ });
250
+ });
251
+ }
252
+
253
+ /**
254
+ * Resolves the local TCP port to use for the `adb forward` bridge: the exact
255
+ * `localPort` if given, the first free port in `localPortRange` if given, or an
256
+ * OS-assigned ephemeral port otherwise.
257
+ *
258
+ * @param opts - Port selection options; see {@link validateLocalPortOpts}
259
+ * @returns The bound, free local TCP port
260
+ * @throws {Error} If the requested port is busy, or no free port exists in the requested range
261
+ */
262
+ export async function resolveLocalPort(opts: LocalPortOpts): Promise<number> {
263
+ const {localPort, localPortRange} = opts;
264
+
265
+ if (localPort !== undefined) {
266
+ const bound = await tryBindPort(localPort);
267
+ if (bound === null) {
268
+ throw new Error(`Local port ${localPort} is already in use`);
269
+ }
270
+ return bound;
271
+ }
272
+
273
+ if (localPortRange) {
274
+ const [min, max] = localPortRange;
275
+ for (let port = min; port <= max; port++) {
276
+ const bound = await tryBindPort(port);
277
+ if (bound !== null) {
278
+ return bound;
279
+ }
280
+ }
281
+ throw new Error(`No free local port found in range ${min}-${max}`);
282
+ }
283
+
284
+ // No preference given - let the OS assign an ephemeral port.
285
+ const bound = await tryBindPort(0);
286
+ if (bound === null) {
287
+ throw new Error('Unable to determine a free local port');
288
+ }
289
+ return bound;
290
+ }
291
+
292
+ async function connectOnce(port: number): Promise<net.Socket> {
293
+ return new Promise<net.Socket>((resolve, reject) => {
294
+ const socket = net.connect(port, '127.0.0.1');
295
+ let settled = false;
296
+ let verifyTimer: NodeJS.Timeout | undefined;
297
+
298
+ const cleanup = () => {
299
+ socket.removeListener('error', onError);
300
+ socket.removeListener('close', onClose);
301
+ clearTimeout(verifyTimer);
302
+ };
303
+ const onError = (err: Error) => {
304
+ if (settled) {
305
+ return;
306
+ }
307
+ settled = true;
308
+ cleanup();
309
+ socket.destroy();
310
+ reject(err);
311
+ };
312
+ const onClose = () => {
313
+ if (settled) {
314
+ return;
315
+ }
316
+ settled = true;
317
+ cleanup();
318
+ reject(new Error('Connection was closed before the on-device stream became ready'));
319
+ };
320
+ socket.once('error', onError);
321
+ socket.once('close', onClose);
322
+ socket.once('connect', () => {
323
+ verifyTimer = setTimeout(() => {
324
+ if (settled) {
325
+ return;
326
+ }
327
+ settled = true;
328
+ cleanup();
329
+ resolve(socket);
330
+ }, CONNECT_VERIFY_GRACE_MS);
331
+ });
332
+ });
333
+ }
334
+
335
+ async function connectWithRetry(port: number, timeoutMs: number, intervalMs: number): Promise<net.Socket> {
336
+ const times = Math.max(1, Math.ceil(timeoutMs / intervalMs));
337
+ let socket: net.Socket | null;
338
+ try {
339
+ socket = await retryInterval(times, intervalMs, connectOnce, port);
340
+ } catch (e) {
341
+ throw new Error(
342
+ `Could not connect to the local streaming socket on port ${port} after ${times} attempts. ` +
343
+ `Last error: ${e instanceof Error ? e.message : e}`,
344
+ {cause: e},
345
+ );
346
+ }
347
+ if (!socket) {
348
+ throw new Error(`Could not connect to the local streaming socket on port ${port}`);
349
+ }
350
+ return socket;
351
+ }
352
+
353
+ /**
354
+ * Bridges the on-device local abstract socket to the host over `adb forward`,
355
+ * parses the streaming wire protocol, and exposes the result as a bounded
356
+ * async-iterable frame queue.
357
+ */
358
+ export class StreamTransport {
359
+ // Config frames (e.g. video SPS/PPS) carry decoder state a consumer needs to make sense
360
+ // of everything that follows, so protect them from the bounded queue's drop-oldest policy.
361
+ // Non-config video frames form a decode-dependency chain (a delta frame is undecodable
362
+ // without every frame back to its last keyframe), so eviction there must cascade up to
363
+ // the next keyframe rather than leaving orphaned, undecodable frames behind - audio
364
+ // frames have no such dependency and are left alone by this either way.
365
+ private readonly queue = new BoundedFrameQueue<StreamFrame>(DEFAULT_QUEUE_CAPACITY, (frame) => frame.isConfig, {
366
+ isChainMember: (frame) => frame.track === StreamTrack.Video && !frame.isConfig,
367
+ isSyncPoint: (frame) => frame.isKeyFrame,
368
+ });
369
+ private readonly parserState = createParserState();
370
+ private closed = false;
371
+ // Tracks the video track's shared sequence counter (CONFIG and data frames both draw
372
+ // from it) so a gap - meaning the on-device FrameQueue dropped a frame under
373
+ // backpressure before we ever saw it - can be detected on receipt, not just when our
374
+ // own queue evicts something locally.
375
+ private lastVideoSequence: bigint | undefined;
376
+
377
+ private constructor(
378
+ private readonly adb: ADB,
379
+ private readonly localPort: number,
380
+ private readonly socket: net.Socket,
381
+ ) {}
382
+
383
+ /**
384
+ * Resolves a local port, forwards it to the on-device local abstract socket via
385
+ * `adb forward`, and connects to it (retrying until the on-device session accepts).
386
+ * On failure, any port forward that was already set up is best-effort removed.
387
+ *
388
+ * @param adb - ADB instance for device communication
389
+ * @param socketName - Name of the on-device local abstract socket to forward
390
+ * @param portOpts - Optional local port selection options
391
+ * @returns A connected StreamTransport instance
392
+ * @throws {Error} If the local port cannot be resolved, or the connection fails
393
+ */
394
+ static async connect(adb: ADB, socketName: string, portOpts: LocalPortOpts = {}): Promise<StreamTransport> {
395
+ const localPort = await resolveLocalPort(portOpts);
396
+ await adb.forwardAbstractPort(localPort, socketName);
397
+ let socket: net.Socket;
398
+ try {
399
+ socket = await connectWithRetry(localPort, CONNECT_TIMEOUT_MS, CONNECT_RETRY_INTERVAL_MS);
400
+ } catch (e) {
401
+ await adb.removePortForward(localPort).catch(() => {});
402
+ throw e;
403
+ }
404
+ const transport = new StreamTransport(adb, localPort, socket);
405
+ transport.wire();
406
+ return transport;
407
+ }
408
+
409
+ private wire(): void {
410
+ this.socket.on('data', (chunk: Buffer) => {
411
+ try {
412
+ for (const frame of parseFrames(this.parserState, chunk)) {
413
+ if (frame.track === StreamTrack.Video) {
414
+ if (this.lastVideoSequence !== undefined && frame.sequence !== this.lastVideoSequence + 1n) {
415
+ this.queue.notifyGap();
416
+ }
417
+ this.lastVideoSequence = frame.sequence;
418
+ }
419
+ this.queue.push(frame);
420
+ }
421
+ } catch (e) {
422
+ this.queue.end(e instanceof Error ? e : new Error(String(e)));
423
+ this.socket.destroy();
424
+ }
425
+ });
426
+ this.socket.on('close', () => this.queue.end());
427
+ this.socket.on('error', (err) => this.queue.end(err));
428
+ }
429
+
430
+ /**
431
+ * Returns an async generator yielding parsed frames as they arrive on the socket,
432
+ * until the connection closes or errors.
433
+ */
434
+ frames(): AsyncGenerator<StreamFrame> {
435
+ return this.queue[Symbol.asyncIterator]();
436
+ }
437
+
438
+ /**
439
+ * Destroys the client socket and best-effort removes the `adb forward` port
440
+ * mapping. Safe to call more than once.
441
+ */
442
+ async close(): Promise<void> {
443
+ if (this.closed) {
444
+ return;
445
+ }
446
+ this.closed = true;
447
+ this.socket.destroy();
448
+ await this.adb.removePortForward(this.localPort).catch(() => {});
449
+ }
450
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "io.appium.settings",
3
- "version": "8.0.9",
3
+ "version": "8.1.0",
4
4
  "description": "App for dealing with Android settings",
5
5
  "keywords": [
6
6
  "android",
@@ -1 +0,0 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../lib/utils.ts"],"names":[],"mappings":"AAMA;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../lib/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAC,aAAa,EAAC,MAAM,UAAU,CAAC;AAEvC,MAAM,WAAW,GAAG,oBAAoB,CAAC;AAEzC;;;;GAIG;AACH,MAAM,UAAU,kBAAkB;IAChC,OAAO,IAAI,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,MAAM,EAAE,wBAAwB,CAAC,CAAC;AAC7E,CAAC;AAED;;;;;GAKG;AACH,SAAS,iBAAiB;IACxB,IAAI,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9D,IAAI,UAAU,GAAG,KAAK,CAAC;IACvB,OAAO,CAAC,UAAU,EAAE,CAAC;QACnB,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;QAC3D,IAAI,CAAC;YACH,IAAI,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;gBAC1G,OAAO,UAAU,CAAC;YACpB,CAAC;QACH,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;QACV,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACtC,UAAU,GAAG,UAAU,CAAC,MAAM,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC;IACpE,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,+BAA+B,WAAW,iBAAiB,CAAC,CAAC;AAC/E,CAAC"}
File without changes