@limrun/ui 0.4.0-rc.9 â 0.4.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/dist/index.cjs +1 -1
- package/dist/index.css +1 -1
- package/dist/index.js +355 -331
- package/package.json +1 -1
- package/src/assets/iphone16pro_black_bg.webp +0 -0
- package/src/assets/iphone16pro_black_landscape_bg.webp +0 -0
- package/src/components/remote-control.css +29 -26
- package/src/components/remote-control.tsx +101 -33
- package/dist/demo.d.ts +0 -1
- package/index.html +0 -181
- package/src/assets/CHf0W2V8oUYM0sz1zN3AItWTl78EDajyAcUfQPYA5LRzxR69YBM334mnZAMo2PCKcIo=w480-h960-rw +0 -0
- package/src/demo.tsx +0 -192
- package/src/image.png +0 -0
- package/src/landscape.png +0 -0
- package/src/portrait.png +0 -0
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
.rc-container {
|
|
2
2
|
position: relative;
|
|
3
3
|
display: inline-block;
|
|
4
|
-
height: 100%;
|
|
5
4
|
box-sizing: border-box;
|
|
6
5
|
isolation: isolate;
|
|
7
6
|
|
|
8
7
|
touch-action: none;
|
|
9
8
|
}
|
|
10
9
|
|
|
10
|
+
.rc-container-portrait {
|
|
11
|
+
height: 100%;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.rc-container-landscape {
|
|
15
|
+
width: 100%;
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
.rc-phone-frame {
|
|
12
19
|
position: relative;
|
|
13
20
|
z-index: 20;
|
|
@@ -18,19 +25,38 @@
|
|
|
18
25
|
|
|
19
26
|
.rc-phone-frame-portrait {
|
|
20
27
|
height: 100%;
|
|
28
|
+
max-width: 100%;
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
.rc-phone-frame-landscape {
|
|
24
32
|
width: 100%;
|
|
33
|
+
max-height: 100%;
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
.rc-video {
|
|
28
37
|
position: absolute;
|
|
29
|
-
|
|
38
|
+
/* Size is controlled by inline style; defaults are for frameless mode */
|
|
39
|
+
width: 100%;
|
|
40
|
+
height: 100%;
|
|
30
41
|
outline: none;
|
|
31
42
|
pointer-events: none;
|
|
32
43
|
cursor: none;
|
|
33
44
|
background-color: black;
|
|
45
|
+
object-fit: contain;
|
|
46
|
+
object-position: center center;
|
|
47
|
+
inset: 0;
|
|
48
|
+
|
|
49
|
+
top: 50%;
|
|
50
|
+
left: 50%;
|
|
51
|
+
transform: translate(-50%, -50%);
|
|
52
|
+
right: auto;
|
|
53
|
+
bottom: auto;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.rc-video-ios-stretch {
|
|
57
|
+
/* iOS-only: intentionally stretch to fill the target screen box */
|
|
58
|
+
object-fit: fill;
|
|
59
|
+
z-index: 30;
|
|
34
60
|
}
|
|
35
61
|
|
|
36
62
|
.rc-video-frameless {
|
|
@@ -38,32 +64,9 @@
|
|
|
38
64
|
height: 100%;
|
|
39
65
|
top: auto;
|
|
40
66
|
left: auto;
|
|
67
|
+
width: 100%;
|
|
41
68
|
}
|
|
42
69
|
|
|
43
70
|
.rc-video-loading {
|
|
44
71
|
aspect-ratio: 9 / 19.5;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.rc-video-ios-portrait {
|
|
48
|
-
top: 1.61%;
|
|
49
|
-
left: 3.9%;
|
|
50
|
-
height: 96.78%;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
.rc-video-ios-landscape {
|
|
54
|
-
top: 3.9%;
|
|
55
|
-
left: 1.61%;
|
|
56
|
-
width: 96.78%;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
.rc-video-android-portrait {
|
|
60
|
-
top: 2.25%;
|
|
61
|
-
left: 4.5%;
|
|
62
|
-
height: 95.9%;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
.rc-video-android-landscape {
|
|
66
|
-
left: 2.25%;
|
|
67
|
-
top: 4.5%;
|
|
68
|
-
width: 95.9%;
|
|
69
72
|
}
|
|
@@ -4,10 +4,10 @@ import './remote-control.css';
|
|
|
4
4
|
|
|
5
5
|
import { ANDROID_KEYS, AMOTION_EVENT, codeMap } from '../core/constants';
|
|
6
6
|
|
|
7
|
-
import iphoneFrameImage from '../assets/
|
|
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 iphoneFrameImageLandscape from '../assets/
|
|
10
|
+
import iphoneFrameImageLandscape from '../assets/iphone16pro_black_landscape_bg.webp';
|
|
11
11
|
import appleLogoSvg from '../assets/Apple_logo_white.svg';
|
|
12
12
|
import androidBootImage from '../assets/android_boot.webp';
|
|
13
13
|
import {
|
|
@@ -91,23 +91,50 @@ const detectPlatform = (url: string): DevicePlatform => {
|
|
|
91
91
|
return 'ios';
|
|
92
92
|
};
|
|
93
93
|
|
|
94
|
+
type DeviceConfig = {
|
|
95
|
+
videoBorderRadiusMultiplier: number;
|
|
96
|
+
loadingLogo: string;
|
|
97
|
+
loadingLogoSize: string;
|
|
98
|
+
videoPosition: {
|
|
99
|
+
portrait: { heightMultiplier?: number; widthMultiplier?: number; };
|
|
100
|
+
landscape: { heightMultiplier?: number; widthMultiplier?: number; };
|
|
101
|
+
};
|
|
102
|
+
frame: {
|
|
103
|
+
image: string;
|
|
104
|
+
imageLandscape: string;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
94
108
|
// Device-specific configuration for frame sizing and video positioning
|
|
95
|
-
|
|
109
|
+
// Video position percentages are relative to the frame image dimensions
|
|
110
|
+
const deviceConfig: Record<DevicePlatform, DeviceConfig> = {
|
|
96
111
|
ios: {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
112
|
+
frame: {
|
|
113
|
+
image: iphoneFrameImage,
|
|
114
|
+
imageLandscape: iphoneFrameImageLandscape,
|
|
115
|
+
},
|
|
100
116
|
videoBorderRadiusMultiplier: 0.15,
|
|
101
117
|
loadingLogo: appleLogoSvg,
|
|
102
118
|
loadingLogoSize: '20%',
|
|
119
|
+
// Video position as percentage of frame dimensions
|
|
120
|
+
videoPosition: {
|
|
121
|
+
portrait: { heightMultiplier: 0.9678 },
|
|
122
|
+
landscape: { widthMultiplier: 0.9678 },
|
|
123
|
+
},
|
|
103
124
|
},
|
|
104
125
|
android: {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
126
|
+
frame: {
|
|
127
|
+
image: pixelFrameImage,
|
|
128
|
+
imageLandscape: pixelFrameImageLandscape,
|
|
129
|
+
},
|
|
108
130
|
videoBorderRadiusMultiplier: 0.13,
|
|
109
131
|
loadingLogo: androidBootImage,
|
|
110
132
|
loadingLogoSize: '40%',
|
|
133
|
+
// Video position as percentage of frame dimensions
|
|
134
|
+
videoPosition: {
|
|
135
|
+
portrait: { heightMultiplier: 0.967 },
|
|
136
|
+
landscape: { widthMultiplier: 0.962 },
|
|
137
|
+
},
|
|
111
138
|
},
|
|
112
139
|
};
|
|
113
140
|
|
|
@@ -147,6 +174,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
147
174
|
const frameRef = useRef<HTMLImageElement>(null);
|
|
148
175
|
const [videoLoaded, setVideoLoaded] = useState(false);
|
|
149
176
|
const [isLandscape, setIsLandscape] = useState(false);
|
|
177
|
+
const [videoStyle, setVideoStyle] = useState<React.CSSProperties>({});
|
|
150
178
|
const wsRef = useRef<WebSocket | null>(null);
|
|
151
179
|
const peerConnectionRef = useRef<RTCPeerConnection | null>(null);
|
|
152
180
|
const dataChannelRef = useRef<RTCDataChannel | null>(null);
|
|
@@ -787,33 +815,70 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
787
815
|
};
|
|
788
816
|
}, [url, token, propSessionId]);
|
|
789
817
|
|
|
790
|
-
//
|
|
818
|
+
// Calculate video position and border-radius based on frame dimensions
|
|
791
819
|
useEffect(() => {
|
|
792
820
|
const video = videoRef.current;
|
|
793
821
|
const frame = frameRef.current;
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
822
|
+
|
|
823
|
+
if (!video) return;
|
|
824
|
+
|
|
825
|
+
// If no frame, no positioning needed
|
|
826
|
+
if (!showFrame || !frame) {
|
|
827
|
+
setVideoStyle({});
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const updateVideoPosition = () => {
|
|
832
|
+
const frameWidth = frame.clientWidth;
|
|
833
|
+
const frameHeight = frame.clientHeight;
|
|
834
|
+
|
|
835
|
+
if (frameWidth === 0 || frameHeight === 0) return;
|
|
836
|
+
|
|
837
|
+
// Determine landscape based on video's intrinsic dimensions
|
|
838
|
+
const landscape = video.videoWidth > video.videoHeight;
|
|
839
|
+
setIsLandscape(landscape);
|
|
840
|
+
|
|
841
|
+
const pos = landscape ? config.videoPosition.landscape : config.videoPosition.portrait;
|
|
842
|
+
let newStyle: React.CSSProperties = {};
|
|
843
|
+
if (pos.heightMultiplier) {
|
|
844
|
+
newStyle.height = `${frameHeight * pos.heightMultiplier}px`;
|
|
845
|
+
// Let the other dimension follow the video stream's intrinsic aspect ratio.
|
|
846
|
+
newStyle.width = 'auto';
|
|
847
|
+
} else if (pos.widthMultiplier) {
|
|
848
|
+
newStyle.width = `${frameWidth * pos.widthMultiplier}px`;
|
|
849
|
+
// Let the other dimension follow the video stream's intrinsic aspect ratio.
|
|
850
|
+
newStyle.height = 'auto';
|
|
810
851
|
}
|
|
852
|
+
newStyle.borderRadius = `${landscape ? frameHeight * config.videoBorderRadiusMultiplier : frameWidth * config.videoBorderRadiusMultiplier}px`;
|
|
853
|
+
setVideoStyle(newStyle);
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
857
|
+
updateVideoPosition();
|
|
811
858
|
});
|
|
812
859
|
|
|
860
|
+
resizeObserver.observe(frame);
|
|
813
861
|
resizeObserver.observe(video);
|
|
862
|
+
|
|
863
|
+
// Also update when the frame image loads
|
|
864
|
+
frame.addEventListener('load', updateVideoPosition);
|
|
865
|
+
|
|
866
|
+
// Update when video metadata loads (to get correct intrinsic dimensions)
|
|
867
|
+
video.addEventListener('loadedmetadata', updateVideoPosition);
|
|
868
|
+
|
|
869
|
+
// IMPORTANT: When the WebRTC stream changes orientation, the intrinsic video size
|
|
870
|
+
// (videoWidth/videoHeight) can change without re-firing 'loadedmetadata'.
|
|
871
|
+
// The <video> element emits 'resize' in that case.
|
|
872
|
+
video.addEventListener('resize', updateVideoPosition);
|
|
873
|
+
|
|
874
|
+
// Initial calculation
|
|
875
|
+
updateVideoPosition();
|
|
814
876
|
|
|
815
877
|
return () => {
|
|
816
878
|
resizeObserver.disconnect();
|
|
879
|
+
video.removeEventListener('loadedmetadata', updateVideoPosition);
|
|
880
|
+
video.removeEventListener('resize', updateVideoPosition);
|
|
881
|
+
frame.removeEventListener('load', updateVideoPosition);
|
|
817
882
|
};
|
|
818
883
|
}, [config, showFrame]);
|
|
819
884
|
|
|
@@ -928,7 +993,8 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
928
993
|
return (
|
|
929
994
|
<div
|
|
930
995
|
className={clsx(
|
|
931
|
-
'rc-container',
|
|
996
|
+
'rc-container',
|
|
997
|
+
isLandscape ? 'rc-container-landscape' : 'rc-container-portrait', // Use custom CSS class instead of Tailwind
|
|
932
998
|
className,
|
|
933
999
|
)}
|
|
934
1000
|
style={{ touchAction: 'none' }} // Keep touchAction none for the container
|
|
@@ -946,7 +1012,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
946
1012
|
{showFrame && (
|
|
947
1013
|
<img
|
|
948
1014
|
ref={frameRef}
|
|
949
|
-
src={isLandscape ? config.
|
|
1015
|
+
src={isLandscape ? config.frame.imageLandscape : config.frame.image}
|
|
950
1016
|
alt=""
|
|
951
1017
|
className={clsx('rc-phone-frame', isLandscape ? 'rc-phone-frame-landscape' : 'rc-phone-frame-portrait')}
|
|
952
1018
|
draggable={false}
|
|
@@ -956,19 +1022,21 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
956
1022
|
ref={videoRef}
|
|
957
1023
|
className={clsx(
|
|
958
1024
|
'rc-video',
|
|
959
|
-
showFrame
|
|
1025
|
+
!showFrame && 'rc-video-frameless',
|
|
1026
|
+
showFrame && platform === 'ios' && 'rc-video-ios-stretch',
|
|
960
1027
|
!videoLoaded && 'rc-video-loading',
|
|
961
1028
|
)}
|
|
962
|
-
style={
|
|
963
|
-
|
|
1029
|
+
style={{
|
|
1030
|
+
...videoStyle,
|
|
1031
|
+
...(config.loadingLogo
|
|
964
1032
|
? {
|
|
965
1033
|
backgroundImage: `url("${config.loadingLogo}")`,
|
|
966
1034
|
backgroundRepeat: 'no-repeat',
|
|
967
1035
|
backgroundPosition: 'center',
|
|
968
1036
|
backgroundSize: config.loadingLogoSize,
|
|
969
1037
|
}
|
|
970
|
-
: {}
|
|
971
|
-
}
|
|
1038
|
+
: {}),
|
|
1039
|
+
}}
|
|
972
1040
|
autoPlay
|
|
973
1041
|
playsInline
|
|
974
1042
|
muted
|
package/dist/demo.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/index.html
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
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: 800px;
|
|
138
|
-
max-height: 800px;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
.preview-item h3 {
|
|
142
|
-
text-align: center;
|
|
143
|
-
margin-bottom: 15px;
|
|
144
|
-
font-size: 1.2rem;
|
|
145
|
-
color: #374151;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
.device-wrapper {
|
|
149
|
-
height: 100%;
|
|
150
|
-
width: 100%;
|
|
151
|
-
background: #f9fafb;
|
|
152
|
-
border-radius: 12px;
|
|
153
|
-
overflow: hidden;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
.info-box {
|
|
157
|
-
background: #fef3c7;
|
|
158
|
-
border: 2px solid #fbbf24;
|
|
159
|
-
border-radius: 8px;
|
|
160
|
-
padding: 15px;
|
|
161
|
-
margin-bottom: 20px;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
.info-box h4 {
|
|
165
|
-
color: #92400e;
|
|
166
|
-
margin-bottom: 8px;
|
|
167
|
-
font-size: 0.95rem;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
.info-box p {
|
|
171
|
-
color: #78350f;
|
|
172
|
-
font-size: 0.9rem;
|
|
173
|
-
line-height: 1.5;
|
|
174
|
-
}
|
|
175
|
-
</style>
|
|
176
|
-
</head>
|
|
177
|
-
<body>
|
|
178
|
-
<div id="root"></div>
|
|
179
|
-
<script type="module" src="/src/demo.tsx"></script>
|
|
180
|
-
</body>
|
|
181
|
-
</html>
|
package/src/assets/CHf0W2V8oUYM0sz1zN3AItWTl78EDajyAcUfQPYA5LRzxR69YBM334mnZAMo2PCKcIo=w480-h960-rw
DELETED
|
Binary file
|
package/src/demo.tsx
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
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/android_eustg_01kd887q1vedb9nt1xkp0s1nan/endpointWebSocket');
|
|
7
|
-
const [token, setToken] = useState('lim_6ffd2b5de18c6dcc59e047b7b8d923024fb45d59c802ca5d');
|
|
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
|
-
}
|