@limrun/ui 0.3.1 â 0.4.0-rc.1
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/dist/demo.d.ts +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.css +1 -1
- package/dist/index.js +457 -421
- package/index.html +179 -0
- package/package.json +1 -1
- package/src/assets/Apple_logo_white.svg +4 -0
- package/src/assets/iphone16pro_black.webp +0 -0
- package/src/assets/pixel9_black.webp +0 -0
- package/src/components/remote-control.css +22 -42
- package/src/components/remote-control.tsx +77 -15
- package/src/demo.tsx +192 -0
package/index.html
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>RemoteControl Demo - @limrun/ui</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
16
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
17
|
+
min-height: 100vh;
|
|
18
|
+
padding: 20px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#root {
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
gap: 20px;
|
|
25
|
+
max-width: 1400px;
|
|
26
|
+
margin: 0 auto;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.header {
|
|
30
|
+
text-align: center;
|
|
31
|
+
color: white;
|
|
32
|
+
padding: 20px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.header h1 {
|
|
36
|
+
font-size: 2.5rem;
|
|
37
|
+
margin-bottom: 10px;
|
|
38
|
+
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.header p {
|
|
42
|
+
font-size: 1.1rem;
|
|
43
|
+
opacity: 0.9;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.demo-container {
|
|
47
|
+
background: white;
|
|
48
|
+
border-radius: 20px;
|
|
49
|
+
padding: 30px;
|
|
50
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.controls {
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
gap: 15px;
|
|
57
|
+
margin-bottom: 30px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.control-group {
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
gap: 8px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.control-group label {
|
|
67
|
+
font-weight: 600;
|
|
68
|
+
font-size: 0.9rem;
|
|
69
|
+
color: #374151;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.control-group input,
|
|
73
|
+
.control-group select {
|
|
74
|
+
padding: 10px 15px;
|
|
75
|
+
border: 2px solid #e5e7eb;
|
|
76
|
+
border-radius: 8px;
|
|
77
|
+
font-size: 0.95rem;
|
|
78
|
+
transition: border-color 0.2s;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.control-group input:focus,
|
|
82
|
+
.control-group select:focus {
|
|
83
|
+
outline: none;
|
|
84
|
+
border-color: #667eea;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.button-group {
|
|
88
|
+
display: flex;
|
|
89
|
+
gap: 10px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
button {
|
|
93
|
+
padding: 12px 24px;
|
|
94
|
+
border: none;
|
|
95
|
+
border-radius: 8px;
|
|
96
|
+
font-size: 1rem;
|
|
97
|
+
font-weight: 600;
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
transition: all 0.2s;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
button.primary {
|
|
103
|
+
background: #667eea;
|
|
104
|
+
color: white;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
button.primary:hover {
|
|
108
|
+
background: #5568d3;
|
|
109
|
+
transform: translateY(-1px);
|
|
110
|
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
button.secondary {
|
|
114
|
+
background: #e5e7eb;
|
|
115
|
+
color: #374151;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
button.secondary:hover {
|
|
119
|
+
background: #d1d5db;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
button:disabled {
|
|
123
|
+
opacity: 0.5;
|
|
124
|
+
cursor: not-allowed;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.device-preview {
|
|
128
|
+
display: flex;
|
|
129
|
+
gap: 30px;
|
|
130
|
+
justify-content: center;
|
|
131
|
+
flex-wrap: wrap;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.preview-item {
|
|
135
|
+
flex: 1;
|
|
136
|
+
min-width: 300px;
|
|
137
|
+
max-width: 500px;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.preview-item h3 {
|
|
141
|
+
text-align: center;
|
|
142
|
+
margin-bottom: 15px;
|
|
143
|
+
font-size: 1.2rem;
|
|
144
|
+
color: #374151;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.device-wrapper {
|
|
148
|
+
height: 700px;
|
|
149
|
+
background: #f9fafb;
|
|
150
|
+
border-radius: 12px;
|
|
151
|
+
overflow: hidden;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.info-box {
|
|
155
|
+
background: #fef3c7;
|
|
156
|
+
border: 2px solid #fbbf24;
|
|
157
|
+
border-radius: 8px;
|
|
158
|
+
padding: 15px;
|
|
159
|
+
margin-bottom: 20px;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.info-box h4 {
|
|
163
|
+
color: #92400e;
|
|
164
|
+
margin-bottom: 8px;
|
|
165
|
+
font-size: 0.95rem;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.info-box p {
|
|
169
|
+
color: #78350f;
|
|
170
|
+
font-size: 0.9rem;
|
|
171
|
+
line-height: 1.5;
|
|
172
|
+
}
|
|
173
|
+
</style>
|
|
174
|
+
</head>
|
|
175
|
+
<body>
|
|
176
|
+
<div id="root"></div>
|
|
177
|
+
<script type="module" src="/src/demo.tsx"></script>
|
|
178
|
+
</body>
|
|
179
|
+
</html>
|
package/package.json
CHANGED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="842.32007" height="1000.0001">
|
|
3
|
+
<path fill="#fff" d="M824.66636 779.30363c-15.12299 34.93724-33.02368 67.09674-53.7638 96.66374-28.27076 40.3074-51.4182 68.2078-69.25717 83.7012-27.65347 25.4313-57.2822 38.4556-89.00964 39.1963-22.77708 0-50.24539-6.4813-82.21973-19.629-32.07926-13.0861-61.55985-19.5673-88.51583-19.5673-28.27075 0-58.59083 6.4812-91.02193 19.5673-32.48053 13.1477-58.64639 19.9994-78.65196 20.6784-30.42501 1.29623-60.75123-12.0985-91.02193-40.2457-19.32039-16.8514-43.48632-45.7394-72.43607-86.6641-31.060778-43.7024-56.597041-94.37983-76.602609-152.15586C10.740416 658.44309 0 598.01283 0 539.50845c0-67.01648 14.481044-124.8172 43.486336-173.25401C66.28194 327.34823 96.60818 296.6578 134.5638 274.1276c37.95566-22.53016 78.96676-34.01129 123.1321-34.74585 24.16591 0 55.85633 7.47508 95.23784 22.166 39.27042 14.74029 64.48571 22.21538 75.54091 22.21538 8.26518 0 36.27668-8.7405 83.7629-26.16587 44.90607-16.16001 82.80614-22.85118 113.85458-20.21546 84.13326 6.78992 147.34122 39.95559 189.37699 99.70686-75.24463 45.59122-112.46573 109.4473-111.72502 191.36456.67899 63.8067 23.82643 116.90384 69.31888 159.06309 20.61664 19.56727 43.64066 34.69027 69.2571 45.4307-5.55531 16.11062-11.41933 31.54225-17.65372 46.35662zM631.70926 20.0057c0 50.01141-18.27108 96.70693-54.6897 139.92782-43.94932 51.38118-97.10817 81.07162-154.75459 76.38659-.73454-5.99983-1.16045-12.31444-1.16045-18.95003 0-48.01091 20.9006-99.39207 58.01678-141.40314 18.53027-21.27094 42.09746-38.95744 70.67685-53.0663C578.3158 9.00229 605.2903 1.31621 630.65988 0c.74076 6.68575 1.04938 13.37191 1.04938 20.00505z"/>
|
|
4
|
+
</svg>
|
|
Binary file
|
|
Binary file
|
|
@@ -1,60 +1,40 @@
|
|
|
1
1
|
.rc-container {
|
|
2
2
|
position: relative;
|
|
3
|
-
display:
|
|
3
|
+
display: inline-block;
|
|
4
4
|
height: 100%;
|
|
5
|
-
|
|
6
|
-
justify-content: center;
|
|
5
|
+
box-sizing: border-box;
|
|
7
6
|
isolation: isolate;
|
|
8
|
-
contain: layout style;
|
|
9
7
|
|
|
10
|
-
background-color: rgba(0, 0, 0, 0.05);
|
|
11
8
|
touch-action: none;
|
|
12
|
-
|
|
13
|
-
--rc-spinner-color: #3b82f6;
|
|
14
|
-
--rc-text-muted: #6b7280;
|
|
15
9
|
}
|
|
16
10
|
|
|
17
|
-
.rc-
|
|
11
|
+
.rc-phone-frame {
|
|
12
|
+
position: relative;
|
|
13
|
+
z-index: 20;
|
|
14
|
+
display: block;
|
|
18
15
|
height: 100%;
|
|
19
|
-
width: 100%;
|
|
20
|
-
max-height: 100%;
|
|
21
|
-
max-width: 100%;
|
|
22
|
-
object-fit: contain;
|
|
23
|
-
outline: none;
|
|
24
16
|
pointer-events: none;
|
|
25
|
-
|
|
17
|
+
user-select: none;
|
|
18
|
+
border-radius: 16.8% / 8.7%;
|
|
26
19
|
}
|
|
27
20
|
|
|
28
|
-
.rc-
|
|
21
|
+
.rc-video {
|
|
29
22
|
position: absolute;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
text-align: center;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
.rc-placeholder-content {
|
|
39
|
-
color: var(--rc-text-muted);
|
|
40
|
-
font-size: 0.875rem;
|
|
41
|
-
line-height: 1.25rem;
|
|
42
|
-
margin: 0;
|
|
43
|
-
font-family: inherit;
|
|
23
|
+
width: auto;
|
|
24
|
+
outline: none;
|
|
25
|
+
pointer-events: none;
|
|
26
|
+
cursor: none;
|
|
27
|
+
background-color: black;
|
|
44
28
|
}
|
|
45
29
|
|
|
46
|
-
.rc-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
border-top-color: var(--rc-spinner-color);
|
|
51
|
-
border-radius: 50%;
|
|
52
|
-
margin: 0 auto 8px;
|
|
53
|
-
animation: rc-spin 1s linear infinite;
|
|
30
|
+
.rc-video-ios {
|
|
31
|
+
top: 1.6%;
|
|
32
|
+
left: 3.9%;
|
|
33
|
+
height: 96.76%;
|
|
54
34
|
}
|
|
55
35
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
36
|
+
.rc-video-android {
|
|
37
|
+
top: 2.3%;
|
|
38
|
+
left: 4.5%;
|
|
39
|
+
height: 95.9%;
|
|
60
40
|
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import React, { useEffect, useRef,
|
|
1
|
+
import React, { useEffect, useRef, useMemo, forwardRef, useImperativeHandle } from 'react';
|
|
2
2
|
import { clsx } from 'clsx';
|
|
3
3
|
import './remote-control.css';
|
|
4
4
|
|
|
5
5
|
import { ANDROID_KEYS, AMOTION_EVENT, codeMap } from '../core/constants';
|
|
6
|
+
|
|
7
|
+
import iphoneFrameImage from '../assets/iphone16pro_black.webp';
|
|
8
|
+
import pixelFrameImage from '../assets/pixel9_black.webp';
|
|
9
|
+
import appleLogoSvg from '../assets/Apple_logo_white.svg';
|
|
6
10
|
import {
|
|
7
11
|
createTouchControlMessage,
|
|
8
12
|
createInjectKeycodeMessage,
|
|
@@ -70,6 +74,32 @@ const debugWarn = (...args: any[]) => {
|
|
|
70
74
|
}
|
|
71
75
|
};
|
|
72
76
|
|
|
77
|
+
type DevicePlatform = 'ios' | 'android';
|
|
78
|
+
|
|
79
|
+
const detectPlatform = (url: string): DevicePlatform => {
|
|
80
|
+
if (url.includes('/android_')) {
|
|
81
|
+
return 'android';
|
|
82
|
+
}
|
|
83
|
+
// Default to iOS if no Android pattern is found
|
|
84
|
+
return 'ios';
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Device-specific configuration for frame sizing and video positioning
|
|
88
|
+
const deviceConfig = {
|
|
89
|
+
ios: {
|
|
90
|
+
frameImage: iphoneFrameImage,
|
|
91
|
+
frameWidthMultiplier: 1.0841,
|
|
92
|
+
videoBorderRadiusMultiplier: 0.157,
|
|
93
|
+
loadingLogo: appleLogoSvg,
|
|
94
|
+
},
|
|
95
|
+
android: {
|
|
96
|
+
frameImage: pixelFrameImage,
|
|
97
|
+
frameWidthMultiplier: 1.107,
|
|
98
|
+
videoBorderRadiusMultiplier: 0.137,
|
|
99
|
+
loadingLogo: null,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
73
103
|
function getAndroidKeycodeAndMeta(event: React.KeyboardEvent): { keycode: number; metaState: number } | null {
|
|
74
104
|
const code = event.code;
|
|
75
105
|
const keycode = codeMap[code];
|
|
@@ -103,10 +133,10 @@ function getAndroidKeycodeAndMeta(event: React.KeyboardEvent): { keycode: number
|
|
|
103
133
|
export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>(
|
|
104
134
|
({ className, url, token, sessionId: propSessionId, openUrl }: RemoteControlProps, ref) => {
|
|
105
135
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
136
|
+
const frameRef = useRef<HTMLImageElement>(null);
|
|
106
137
|
const wsRef = useRef<WebSocket | null>(null);
|
|
107
138
|
const peerConnectionRef = useRef<RTCPeerConnection | null>(null);
|
|
108
139
|
const dataChannelRef = useRef<RTCDataChannel | null>(null);
|
|
109
|
-
const [isConnected, setIsConnected] = useState<boolean>(false);
|
|
110
140
|
const keepAliveIntervalRef = useRef<number | undefined>(undefined);
|
|
111
141
|
const pendingScreenshotResolversRef = useRef<
|
|
112
142
|
Map<string, (value: ScreenshotData | PromiseLike<ScreenshotData>) => void>
|
|
@@ -124,6 +154,9 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
124
154
|
[propSessionId],
|
|
125
155
|
);
|
|
126
156
|
|
|
157
|
+
const platform = useMemo(() => detectPlatform(url), [url]);
|
|
158
|
+
const config = deviceConfig[platform];
|
|
159
|
+
|
|
127
160
|
const updateStatus = (message: string) => {
|
|
128
161
|
// Use the wrapper for conditional logging
|
|
129
162
|
debugLog(message);
|
|
@@ -452,13 +485,13 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
452
485
|
await new Promise((resolve, reject) => {
|
|
453
486
|
if (wsRef.current) {
|
|
454
487
|
wsRef.current.onopen = resolve;
|
|
455
|
-
setTimeout(() => reject(new Error('WebSocket connection timeout')),
|
|
488
|
+
setTimeout(() => reject(new Error('WebSocket connection timeout')), 30000);
|
|
456
489
|
}
|
|
457
490
|
});
|
|
458
491
|
|
|
459
492
|
// Request RTCConfiguration
|
|
460
493
|
const rtcConfigPromise = new Promise<RTCConfiguration>((resolve, reject) => {
|
|
461
|
-
const timeoutId = setTimeout(() => reject(new Error('RTCConfiguration timeout')),
|
|
494
|
+
const timeoutId = setTimeout(() => reject(new Error('RTCConfiguration timeout')), 30000);
|
|
462
495
|
|
|
463
496
|
const messageHandler = (event: MessageEvent) => {
|
|
464
497
|
try {
|
|
@@ -569,7 +602,6 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
569
602
|
// Set up connection state monitoring
|
|
570
603
|
peerConnectionRef.current.onconnectionstatechange = () => {
|
|
571
604
|
updateStatus('Connection state: ' + peerConnectionRef.current?.connectionState);
|
|
572
|
-
setIsConnected(peerConnectionRef.current?.connectionState === 'connected');
|
|
573
605
|
};
|
|
574
606
|
|
|
575
607
|
peerConnectionRef.current.oniceconnectionstatechange = () => {
|
|
@@ -716,7 +748,6 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
716
748
|
dataChannelRef.current.close();
|
|
717
749
|
dataChannelRef.current = null;
|
|
718
750
|
}
|
|
719
|
-
setIsConnected(false);
|
|
720
751
|
updateStatus('Stopped');
|
|
721
752
|
};
|
|
722
753
|
|
|
@@ -740,6 +771,27 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
740
771
|
};
|
|
741
772
|
}, [url, token, propSessionId]);
|
|
742
773
|
|
|
774
|
+
// Resize phone frame and video border-radius relative to video size
|
|
775
|
+
useEffect(() => {
|
|
776
|
+
const video = videoRef.current;
|
|
777
|
+
const frame = frameRef.current;
|
|
778
|
+
if (!video || !frame) return;
|
|
779
|
+
|
|
780
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
781
|
+
for (const entry of entries) {
|
|
782
|
+
const videoWidth = entry.contentRect.width;
|
|
783
|
+
frame.style.width = `${videoWidth * config.frameWidthMultiplier}px`;
|
|
784
|
+
video.style.borderRadius = `${videoWidth * config.videoBorderRadiusMultiplier}px`;
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
resizeObserver.observe(video);
|
|
789
|
+
|
|
790
|
+
return () => {
|
|
791
|
+
resizeObserver.disconnect();
|
|
792
|
+
};
|
|
793
|
+
}, [config]);
|
|
794
|
+
|
|
743
795
|
const handleVideoClick = () => {
|
|
744
796
|
if (videoRef.current) {
|
|
745
797
|
videoRef.current.focus();
|
|
@@ -866,14 +918,30 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
866
918
|
onTouchEnd={handleInteraction}
|
|
867
919
|
onTouchCancel={handleInteraction}
|
|
868
920
|
>
|
|
921
|
+
<img
|
|
922
|
+
ref={frameRef}
|
|
923
|
+
src={config.frameImage}
|
|
924
|
+
alt=""
|
|
925
|
+
className="rc-phone-frame"
|
|
926
|
+
draggable={false}
|
|
927
|
+
/>
|
|
869
928
|
<video
|
|
870
929
|
ref={videoRef}
|
|
871
|
-
className=
|
|
930
|
+
className={clsx('rc-video', platform === 'ios' ? 'rc-video-ios' : 'rc-video-android')}
|
|
931
|
+
style={
|
|
932
|
+
config.loadingLogo
|
|
933
|
+
? {
|
|
934
|
+
backgroundImage: `url(${config.loadingLogo})`,
|
|
935
|
+
backgroundRepeat: 'no-repeat',
|
|
936
|
+
backgroundPosition: 'center',
|
|
937
|
+
backgroundSize: '20%',
|
|
938
|
+
}
|
|
939
|
+
: undefined
|
|
940
|
+
}
|
|
872
941
|
autoPlay
|
|
873
942
|
playsInline
|
|
874
943
|
muted
|
|
875
|
-
tabIndex={0}
|
|
876
|
-
style={{ outline: 'none', pointerEvents: 'none' }}
|
|
944
|
+
tabIndex={0}
|
|
877
945
|
onKeyDown={handleKeyboard}
|
|
878
946
|
onKeyUp={handleKeyboard}
|
|
879
947
|
onClick={handleVideoClick}
|
|
@@ -888,12 +956,6 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
888
956
|
}
|
|
889
957
|
}}
|
|
890
958
|
/>
|
|
891
|
-
{!isConnected && (
|
|
892
|
-
<div className="rc-placeholder-wrapper">
|
|
893
|
-
<div className="rc-spinner"></div>
|
|
894
|
-
<p className="rc-placeholder-content">Connecting...</p>
|
|
895
|
-
</div>
|
|
896
|
-
)}
|
|
897
959
|
</div>
|
|
898
960
|
);
|
|
899
961
|
},
|
package/src/demo.tsx
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { useState, useRef } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { RemoteControl, RemoteControlHandle } from './components/remote-control';
|
|
4
|
+
|
|
5
|
+
function Demo() {
|
|
6
|
+
const [url, setUrl] = useState('wss://eu-hel1-5-staging.limrun.dev/v1/organizations/org_01k3v8bvxvecfvscdhvcp3rga1/ios.limrun.com/v1/instances/ios_eustg_01kd5hvbcmf7krej28t927cskb/endpointWebSocket');
|
|
7
|
+
const [token, setToken] = useState('lim_c8a8f9a45008ee01441fbd01f41d5f1b04eac8c4fc3f20de');
|
|
8
|
+
const [platform, setPlatform] = useState<'ios' | 'android'>('ios');
|
|
9
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
10
|
+
const [key, setKey] = useState(0);
|
|
11
|
+
const [showDebugInfo, setShowDebugInfo] = useState(false);
|
|
12
|
+
|
|
13
|
+
const remoteControlRef = useRef<RemoteControlHandle>(null);
|
|
14
|
+
|
|
15
|
+
const handleConnect = () => {
|
|
16
|
+
if (url) {
|
|
17
|
+
setIsConnected(true);
|
|
18
|
+
// Force remount by changing key
|
|
19
|
+
setKey(prev => prev + 1);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const handleDisconnect = () => {
|
|
24
|
+
setIsConnected(false);
|
|
25
|
+
setKey(prev => prev + 1);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const handleScreenshot = async () => {
|
|
29
|
+
if (remoteControlRef.current) {
|
|
30
|
+
try {
|
|
31
|
+
const screenshot = await remoteControlRef.current.screenshot();
|
|
32
|
+
// Open screenshot in new window
|
|
33
|
+
const win = window.open();
|
|
34
|
+
if (win) {
|
|
35
|
+
win.document.write(`<img src="${screenshot.dataUri}" style="max-width: 100%;" />`);
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Screenshot failed:', error);
|
|
39
|
+
alert('Screenshot failed: ' + (error as Error).message);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<>
|
|
46
|
+
<div className="header">
|
|
47
|
+
<h1>đą RemoteControl Component Demo</h1>
|
|
48
|
+
<p>Test the iOS device frame and remote control features</p>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div className="demo-container">
|
|
52
|
+
<div className="info-box">
|
|
53
|
+
<h4>âšī¸ How to Use:</h4>
|
|
54
|
+
<p>
|
|
55
|
+
Enter your WebSocket URL and authentication token below, select iOS or Android platform,
|
|
56
|
+
then click Connect to see the remote control in action. The iOS platform will display
|
|
57
|
+
a realistic iPhone frame around the stream.
|
|
58
|
+
</p>
|
|
59
|
+
<p style={{ marginTop: '10px', fontWeight: 600 }}>
|
|
60
|
+
⨠iOS Feature: Touches can start from the bottom bezel area (below the screen, near the
|
|
61
|
+
home indicator) to enable authentic iOS swipe-up gestures for going home or switching apps!
|
|
62
|
+
</p>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div className="controls">
|
|
66
|
+
<div className="control-group">
|
|
67
|
+
<label htmlFor="url">WebSocket URL</label>
|
|
68
|
+
<input
|
|
69
|
+
id="url"
|
|
70
|
+
type="text"
|
|
71
|
+
value={url}
|
|
72
|
+
onChange={(e) => setUrl(e.target.value)}
|
|
73
|
+
placeholder="wss://your-instance.limrun.com/control"
|
|
74
|
+
disabled={isConnected}
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div className="control-group">
|
|
79
|
+
<label htmlFor="token">Authentication Token</label>
|
|
80
|
+
<input
|
|
81
|
+
id="token"
|
|
82
|
+
type="password"
|
|
83
|
+
value={token}
|
|
84
|
+
onChange={(e) => setToken(e.target.value)}
|
|
85
|
+
placeholder="Enter your token"
|
|
86
|
+
disabled={isConnected}
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div className="control-group">
|
|
91
|
+
<label htmlFor="platform">Platform</label>
|
|
92
|
+
<select
|
|
93
|
+
id="platform"
|
|
94
|
+
value={platform}
|
|
95
|
+
onChange={(e) => setPlatform(e.target.value as 'ios' | 'android')}
|
|
96
|
+
disabled={isConnected}
|
|
97
|
+
>
|
|
98
|
+
<option value="ios">iOS (with frame)</option>
|
|
99
|
+
<option value="android">Android (no frame)</option>
|
|
100
|
+
</select>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div className="control-group">
|
|
104
|
+
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
|
105
|
+
<input
|
|
106
|
+
type="checkbox"
|
|
107
|
+
checked={showDebugInfo}
|
|
108
|
+
onChange={(e) => setShowDebugInfo(e.target.checked)}
|
|
109
|
+
style={{ cursor: 'pointer' }}
|
|
110
|
+
/>
|
|
111
|
+
Show iOS Extended Touch Area Info
|
|
112
|
+
</label>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div className="button-group">
|
|
116
|
+
{!isConnected ? (
|
|
117
|
+
<button
|
|
118
|
+
className="primary"
|
|
119
|
+
onClick={handleConnect}
|
|
120
|
+
disabled={!url}
|
|
121
|
+
>
|
|
122
|
+
Connect
|
|
123
|
+
</button>
|
|
124
|
+
) : (
|
|
125
|
+
<>
|
|
126
|
+
<button className="secondary" onClick={handleDisconnect}>
|
|
127
|
+
Disconnect
|
|
128
|
+
</button>
|
|
129
|
+
<button className="primary" onClick={handleScreenshot}>
|
|
130
|
+
Take Screenshot
|
|
131
|
+
</button>
|
|
132
|
+
</>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{showDebugInfo && platform === 'ios' && (
|
|
138
|
+
<div style={{
|
|
139
|
+
background: '#e0f2fe',
|
|
140
|
+
border: '2px solid #0284c7',
|
|
141
|
+
borderRadius: '8px',
|
|
142
|
+
padding: '15px',
|
|
143
|
+
marginBottom: '20px'
|
|
144
|
+
}}>
|
|
145
|
+
<h4 style={{ color: '#0c4a6e', marginBottom: '8px', fontSize: '0.95rem' }}>
|
|
146
|
+
đ§ iOS Extended Touch Area
|
|
147
|
+
</h4>
|
|
148
|
+
<p style={{ color: '#075985', fontSize: '0.9rem', lineHeight: '1.5', margin: 0 }}>
|
|
149
|
+
The iOS frame includes a <strong>60-pixel extended touch area</strong> below the visible screen.
|
|
150
|
+
This area (where the home indicator is located) can receive touch events and sends coordinates
|
|
151
|
+
beyond the screen bounds (y > screenHeight), allowing iOS to properly detect gestures that
|
|
152
|
+
start from outside the screen - just like on a real iPhone. Try starting a swipe gesture from
|
|
153
|
+
the home indicator area!
|
|
154
|
+
</p>
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
{isConnected ? (
|
|
159
|
+
<div className="device-preview">
|
|
160
|
+
<div className="preview-item">
|
|
161
|
+
<h3>{platform === 'ios' ? 'đą iOS with Frame' : 'đ¤ Android (No Frame)'}</h3>
|
|
162
|
+
<div className="device-wrapper">
|
|
163
|
+
<RemoteControl
|
|
164
|
+
key={key}
|
|
165
|
+
ref={remoteControlRef}
|
|
166
|
+
url={url}
|
|
167
|
+
token={token}
|
|
168
|
+
/>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
) : (
|
|
173
|
+
<div style={{
|
|
174
|
+
textAlign: 'center',
|
|
175
|
+
padding: '60px 20px',
|
|
176
|
+
color: '#9ca3af',
|
|
177
|
+
fontSize: '1.1rem'
|
|
178
|
+
}}>
|
|
179
|
+
Enter your connection details above and click Connect to start
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
</>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Mount the demo app
|
|
188
|
+
const container = document.getElementById('root');
|
|
189
|
+
if (container) {
|
|
190
|
+
const root = createRoot(container);
|
|
191
|
+
root.render(<Demo />);
|
|
192
|
+
}
|