@limrun/ui 0.5.0 → 0.5.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@limrun/ui",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -7,6 +7,8 @@ import { ANDROID_KEYS, AMOTION_EVENT, codeMap } from '../core/constants';
7
7
  import iphoneFrameImage from '../assets/iphone16pro_black_bg.webp';
8
8
  import pixelFrameImage from '../assets/pixel9_black.webp';
9
9
  import pixelFrameImageLandscape from '../assets/pixel9_black_landscape.webp';
10
+ import pixelTabletFrameImage from '../assets/pixel_tablet_portrait.webp';
11
+ import pixelTabletFrameImageLandscape from '../assets/pixel_tablet_landscape.webp';
10
12
  import iphoneFrameImageLandscape from '../assets/iphone16pro_black_landscape_bg.webp';
11
13
  import appleLogoSvg from '../assets/Apple_logo_white.svg';
12
14
  import androidBootImage from '../assets/android_boot.webp';
@@ -68,6 +70,7 @@ export interface RemoteControlHandle {
68
70
  openUrl: (url: string) => void;
69
71
  sendKeyEvent: (event: ImperativeKeyboardEvent) => void;
70
72
  screenshot: () => Promise<ScreenshotData>;
73
+ terminateApp: (bundleId: string) => Promise<void>;
71
74
  }
72
75
 
73
76
  const debugLog = (...args: any[]) => {
@@ -114,6 +117,13 @@ type DeviceConfig = {
114
117
  }
115
118
  }
116
119
 
120
+ const ANDROID_TABLET_VIDEO_WIDTH = 1920;
121
+ const ANDROID_TABLET_VIDEO_HEIGHT = 1200;
122
+
123
+ const isAndroidTabletVideo = (width: number, height: number): boolean =>
124
+ (width === ANDROID_TABLET_VIDEO_WIDTH && height === ANDROID_TABLET_VIDEO_HEIGHT) ||
125
+ (width === ANDROID_TABLET_VIDEO_HEIGHT && height === ANDROID_TABLET_VIDEO_WIDTH);
126
+
117
127
  // Device-specific configuration for frame sizing and video positioning
118
128
  // Video position percentages are relative to the frame image dimensions
119
129
  const deviceConfig: Record<DevicePlatform, DeviceConfig> = {
@@ -184,6 +194,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
184
194
  const frameRef = useRef<HTMLImageElement>(null);
185
195
  const [videoLoaded, setVideoLoaded] = useState(false);
186
196
  const [isLandscape, setIsLandscape] = useState(false);
197
+ const [useAndroidTabletFrame, setUseAndroidTabletFrame] = useState(false);
187
198
  const [videoStyle, setVideoStyle] = useState<React.CSSProperties>({});
188
199
  const wsRef = useRef<WebSocket | null>(null);
189
200
  const peerConnectionRef = useRef<RTCPeerConnection | null>(null);
@@ -193,6 +204,8 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
193
204
  Map<string, (value: ScreenshotData | PromiseLike<ScreenshotData>) => void>
194
205
  >(new Map());
195
206
  const pendingScreenshotRejectersRef = useRef<Map<string, (reason?: any) => void>>(new Map());
207
+ const pendingTerminateAppResolversRef = useRef<Map<string, () => void>>(new Map());
208
+ const pendingTerminateAppRejectersRef = useRef<Map<string, (reason?: any) => void>>(new Map());
196
209
 
197
210
  // Map to track active pointers for real touch/mouse single-finger events.
198
211
  // Key: pointerId (-1 for mouse, touch.identifier for touch), Value: { x: number, y: number }
@@ -1273,6 +1286,33 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
1273
1286
  pendingScreenshotResolversRef.current.delete(message.id);
1274
1287
  pendingScreenshotRejectersRef.current.delete(message.id);
1275
1288
  break;
1289
+ case 'terminateAppResult':
1290
+ if (typeof message.id !== 'string') {
1291
+ debugWarn('Received invalid terminateApp result message:', message);
1292
+ break;
1293
+ }
1294
+ if (typeof message.error === 'string') {
1295
+ const terminateRejecter = pendingTerminateAppRejectersRef.current.get(message.id);
1296
+ if (!terminateRejecter) {
1297
+ debugWarn(`Received terminateApp error for unknown or handled id: ${message.id}`);
1298
+ break;
1299
+ }
1300
+ debugWarn(`Received terminateApp error for id ${message.id}: ${message.error}`);
1301
+ terminateRejecter(new Error(message.error));
1302
+ pendingTerminateAppResolversRef.current.delete(message.id);
1303
+ pendingTerminateAppRejectersRef.current.delete(message.id);
1304
+ break;
1305
+ }
1306
+ const terminateResolver = pendingTerminateAppResolversRef.current.get(message.id);
1307
+ if (!terminateResolver) {
1308
+ debugWarn(`Received terminateApp result for unknown or handled id: ${message.id}`);
1309
+ break;
1310
+ }
1311
+ debugLog(`Received terminateApp success for id ${message.id}`);
1312
+ terminateResolver();
1313
+ pendingTerminateAppResolversRef.current.delete(message.id);
1314
+ pendingTerminateAppRejectersRef.current.delete(message.id);
1315
+ break;
1276
1316
  default:
1277
1317
  debugWarn(`Received unhandled message type: ${message.type}`, message);
1278
1318
  break;
@@ -1367,6 +1407,9 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
1367
1407
  // Determine landscape based on video's intrinsic dimensions
1368
1408
  const landscape = video.videoWidth > video.videoHeight;
1369
1409
  setIsLandscape(landscape);
1410
+ setUseAndroidTabletFrame(
1411
+ platform === 'android' && isAndroidTabletVideo(video.videoWidth, video.videoHeight),
1412
+ );
1370
1413
 
1371
1414
  const pos = landscape ? config.videoPosition.landscape : config.videoPosition.portrait;
1372
1415
  let newStyle: React.CSSProperties = {};
@@ -1518,10 +1561,51 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
1518
1561
  }, 30000); // 30-second timeout
1519
1562
  });
1520
1563
  },
1564
+ terminateApp: (bundleId: string): Promise<void> => {
1565
+ return new Promise<void>((resolve, reject) => {
1566
+ if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
1567
+ debugWarn('WebSocket not open, cannot send terminateApp command.');
1568
+ return reject(new Error('WebSocket is not connected or connection is not open.'));
1569
+ }
1570
+ const id = `ui-term-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
1571
+ const request = {
1572
+ type: 'terminateApp',
1573
+ id,
1574
+ bundleId,
1575
+ };
1576
+
1577
+ pendingTerminateAppResolversRef.current.set(id, resolve);
1578
+ pendingTerminateAppRejectersRef.current.set(id, reject);
1579
+
1580
+ debugLog('Sending terminateApp request:', request);
1581
+ try {
1582
+ wsRef.current.send(JSON.stringify(request));
1583
+ } catch (err) {
1584
+ debugWarn('Failed to send terminateApp request immediately:', err);
1585
+ pendingTerminateAppResolversRef.current.delete(id);
1586
+ pendingTerminateAppRejectersRef.current.delete(id);
1587
+ reject(err);
1588
+ return;
1589
+ }
1590
+
1591
+ setTimeout(() => {
1592
+ if (pendingTerminateAppResolversRef.current.has(id)) {
1593
+ debugWarn(`terminateApp request timed out for id ${id}`);
1594
+ pendingTerminateAppRejectersRef.current.get(id)?.(new Error('terminateApp request timed out'));
1595
+ pendingTerminateAppResolversRef.current.delete(id);
1596
+ pendingTerminateAppRejectersRef.current.delete(id);
1597
+ }
1598
+ }, 30000);
1599
+ });
1600
+ },
1521
1601
  }));
1522
1602
 
1523
1603
  // Show indicators when Alt is held and we have a valid hover point (null when outside)
1524
1604
  const showAltIndicators = isAltHeld && hoverPoint !== null;
1605
+ const frameImageSrc =
1606
+ platform === 'android' && useAndroidTabletFrame
1607
+ ? (isLandscape ? pixelTabletFrameImageLandscape : pixelTabletFrameImage)
1608
+ : (isLandscape ? config.frame.imageLandscape : config.frame.image);
1525
1609
 
1526
1610
  return (
1527
1611
  <div
@@ -1563,7 +1647,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
1563
1647
  {showFrame && (
1564
1648
  <img
1565
1649
  ref={frameRef}
1566
- src={isLandscape ? config.frame.imageLandscape : config.frame.image}
1650
+ src={frameImageSrc}
1567
1651
  alt=""
1568
1652
  className={platform === 'ios' ? clsx('rc-phone-frame', 'rc-phone-frame-ios') : 'rc-phone-frame'}
1569
1653
  draggable={false}
package/src/index.ts CHANGED
@@ -1 +1,2 @@
1
1
  export { RemoteControl } from './components/remote-control';
2
+ export type { RemoteControlHandle } from './components/remote-control';