cuoral-ionic 0.0.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/LICENSE +21 -0
- package/README.md +273 -0
- package/android/src/main/AndroidManifest.xml +10 -0
- package/android/src/main/java/com/cuoral/ionic/CuoralPlugin.java +291 -0
- package/assets/icon/favicon.png +0 -0
- package/dist/bridge.d.ts +51 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +159 -0
- package/dist/cuoral.d.ts +51 -0
- package/dist/cuoral.d.ts.map +1 -0
- package/dist/cuoral.js +173 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +819 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +825 -0
- package/dist/index.js.map +1 -0
- package/dist/modal.d.ts +46 -0
- package/dist/modal.d.ts.map +1 -0
- package/dist/modal.js +220 -0
- package/dist/plugin.d.ts +76 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +134 -0
- package/dist/types.d.ts +109 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +24 -0
- package/dist/web.d.ts +27 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +107 -0
- package/ios/Plugin/CuoralPlugin.m +11 -0
- package/ios/Plugin/CuoralPlugin.swift +246 -0
- package/package.json +59 -0
- package/src/bridge.ts +195 -0
- package/src/cuoral.ts +208 -0
- package/src/index.ts +5 -0
- package/src/modal.ts +257 -0
- package/src/plugin.ts +190 -0
- package/src/types.ts +129 -0
- package/src/web.ts +135 -0
package/src/cuoral.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { CuoralBridge } from './bridge';
|
|
2
|
+
import { CuoralRecorder } from './plugin';
|
|
3
|
+
import { CuoralMessageType } from './types';
|
|
4
|
+
import { CuoralModal } from './modal';
|
|
5
|
+
import { Capacitor } from '@capacitor/core';
|
|
6
|
+
|
|
7
|
+
export interface CuoralOptions {
|
|
8
|
+
publicKey: string;
|
|
9
|
+
email?: string;
|
|
10
|
+
firstName?: string;
|
|
11
|
+
lastName?: string;
|
|
12
|
+
debug?: boolean;
|
|
13
|
+
widgetBaseUrl?: string; // Allow custom widget URL
|
|
14
|
+
showFloatingButton?: boolean; // Show floating chat button (default: true)
|
|
15
|
+
useModal?: boolean; // Use modal display mode (default: true)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Main Cuoral class - simple API for users
|
|
20
|
+
*/
|
|
21
|
+
export class Cuoral {
|
|
22
|
+
private bridge: CuoralBridge;
|
|
23
|
+
private recorder: CuoralRecorder;
|
|
24
|
+
private modal?: CuoralModal;
|
|
25
|
+
private options: CuoralOptions;
|
|
26
|
+
private static readonly PRODUCTION_WIDGET_URL = 'https://js.cuoral.com/mobile.html';
|
|
27
|
+
private static readonly DEV_WIDGET_URL = 'assets/mobile.html';
|
|
28
|
+
|
|
29
|
+
constructor(options: CuoralOptions) {
|
|
30
|
+
this.options = {
|
|
31
|
+
showFloatingButton: true,
|
|
32
|
+
useModal: true,
|
|
33
|
+
...options
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Determine widget base URL
|
|
37
|
+
const baseUrl = options.widgetBaseUrl || Cuoral.PRODUCTION_WIDGET_URL;
|
|
38
|
+
const params = new URLSearchParams({
|
|
39
|
+
auto_start: 'true',
|
|
40
|
+
key: options.publicKey,
|
|
41
|
+
_t: Date.now().toString(),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (options.email) params.set('email', options.email);
|
|
45
|
+
if (options.firstName) params.set('first_name', options.firstName);
|
|
46
|
+
if (options.lastName) params.set('last_name', options.lastName);
|
|
47
|
+
|
|
48
|
+
const widgetUrl = `${baseUrl}?${params.toString()}`;
|
|
49
|
+
|
|
50
|
+
// Initialize modal if enabled
|
|
51
|
+
if (this.options.useModal) {
|
|
52
|
+
this.modal = new CuoralModal(widgetUrl, this.options.showFloatingButton);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Initialize bridge and recorder
|
|
56
|
+
this.bridge = new CuoralBridge({
|
|
57
|
+
widgetUrl,
|
|
58
|
+
debug: options.debug || false
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
this.recorder = new CuoralRecorder();
|
|
62
|
+
|
|
63
|
+
// Setup automatic message handlers
|
|
64
|
+
this.setupMessageHandlers();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Initialize Cuoral
|
|
69
|
+
*/
|
|
70
|
+
public initialize(): void {
|
|
71
|
+
this.bridge.initialize();
|
|
72
|
+
|
|
73
|
+
// Initialize modal if enabled
|
|
74
|
+
if (this.modal) {
|
|
75
|
+
this.modal.initialize();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Open the widget modal
|
|
81
|
+
*/
|
|
82
|
+
public openModal(): void {
|
|
83
|
+
if (this.modal) {
|
|
84
|
+
this.modal.open();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Close the widget modal
|
|
90
|
+
*/
|
|
91
|
+
public closeModal(): void {
|
|
92
|
+
if (this.modal) {
|
|
93
|
+
this.modal.close();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if modal is open
|
|
99
|
+
*/
|
|
100
|
+
public isModalOpen(): boolean {
|
|
101
|
+
return this.modal?.isModalOpen() || false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get the widget URL for iframe embedding
|
|
106
|
+
*/
|
|
107
|
+
public getWidgetUrl(): string {
|
|
108
|
+
const baseUrl = this.options.widgetBaseUrl || Cuoral.PRODUCTION_WIDGET_URL;
|
|
109
|
+
const params = new URLSearchParams({
|
|
110
|
+
auto_start: 'true',
|
|
111
|
+
key: this.options.publicKey,
|
|
112
|
+
_t: Date.now().toString(),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (this.options.email) params.set('email', this.options.email);
|
|
116
|
+
if (this.options.firstName) params.set('first_name', this.options.firstName);
|
|
117
|
+
if (this.options.lastName) params.set('last_name', this.options.lastName);
|
|
118
|
+
|
|
119
|
+
return `${baseUrl}?${params.toString()}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Clean up resources
|
|
124
|
+
*/
|
|
125
|
+
public destroy(): void {
|
|
126
|
+
this.bridge.destroy();
|
|
127
|
+
|
|
128
|
+
// Destroy modal if exists
|
|
129
|
+
if (this.modal) {
|
|
130
|
+
this.modal.destroy();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Setup automatic message handlers
|
|
136
|
+
*/
|
|
137
|
+
private setupMessageHandlers(): void {
|
|
138
|
+
// Handle start recording requests from widget
|
|
139
|
+
this.bridge.on(CuoralMessageType.START_RECORDING, async () => {
|
|
140
|
+
const success = await this.recorder.startRecording();
|
|
141
|
+
if (!success) {
|
|
142
|
+
// Error already handled by recorder
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Handle stop recording requests from widget
|
|
147
|
+
this.bridge.on(CuoralMessageType.STOP_RECORDING, async () => {
|
|
148
|
+
const result = await this.recorder.stopRecording();
|
|
149
|
+
|
|
150
|
+
if (result) {
|
|
151
|
+
// Convert file path to web-accessible URL
|
|
152
|
+
const capacitorUrl = result.filePath ? Capacitor.convertFileSrc(result.filePath) : '';
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
// Fetch the video blob from the capacitor URL
|
|
156
|
+
const response = await fetch(capacitorUrl);
|
|
157
|
+
const videoBlob = await response.blob();
|
|
158
|
+
|
|
159
|
+
// Convert blob to base64 data URL so it can be serialized and transferred cross-origin
|
|
160
|
+
const reader = new FileReader();
|
|
161
|
+
reader.onloadend = () => {
|
|
162
|
+
const base64data = reader.result as string;
|
|
163
|
+
|
|
164
|
+
// Send the base64 data via bridge to CDN widget
|
|
165
|
+
this.bridge.sendToWidget({
|
|
166
|
+
type: CuoralMessageType.RECORDING_STOPPED,
|
|
167
|
+
payload: {
|
|
168
|
+
videoData: base64data,
|
|
169
|
+
videoType: 'video/mp4',
|
|
170
|
+
originalPath: result.filePath,
|
|
171
|
+
duration: result.duration,
|
|
172
|
+
timestamp: Date.now()
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
reader.onerror = (error) => {
|
|
178
|
+
console.error('[Cuoral] Error reading blob:', error);
|
|
179
|
+
this.bridge.sendToWidget({
|
|
180
|
+
type: CuoralMessageType.RECORDING_STOPPED,
|
|
181
|
+
payload: {
|
|
182
|
+
error: 'Failed to process recording',
|
|
183
|
+
duration: result.duration,
|
|
184
|
+
timestamp: Date.now()
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Start reading the blob as data URL
|
|
190
|
+
reader.readAsDataURL(videoBlob);
|
|
191
|
+
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error('[Cuoral] Error processing video file:', error);
|
|
194
|
+
this.bridge.sendToWidget({
|
|
195
|
+
type: CuoralMessageType.RECORDING_STOPPED,
|
|
196
|
+
payload: {
|
|
197
|
+
error: 'Failed to process recording',
|
|
198
|
+
duration: result.duration,
|
|
199
|
+
timestamp: Date.now()
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
console.error('[Cuoral] No result from stopRecording');
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
package/src/index.ts
ADDED
package/src/modal.ts
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cuoral Modal Manager
|
|
3
|
+
* Handles full-screen modal display with floating button
|
|
4
|
+
*/
|
|
5
|
+
export class CuoralModal {
|
|
6
|
+
private modalElement?: HTMLDivElement;
|
|
7
|
+
private iframeElement?: HTMLIFrameElement;
|
|
8
|
+
private floatingButton?: HTMLDivElement;
|
|
9
|
+
private isOpen = false;
|
|
10
|
+
private widgetUrl: string;
|
|
11
|
+
private showFloatingButton: boolean;
|
|
12
|
+
|
|
13
|
+
constructor(widgetUrl: string, showFloatingButton = true) {
|
|
14
|
+
this.widgetUrl = widgetUrl;
|
|
15
|
+
this.showFloatingButton = showFloatingButton;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Initialize the modal and floating button
|
|
20
|
+
*/
|
|
21
|
+
public initialize(): void {
|
|
22
|
+
if (this.showFloatingButton) {
|
|
23
|
+
this.createFloatingButton();
|
|
24
|
+
}
|
|
25
|
+
this.createModal();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create floating chat button
|
|
30
|
+
*/
|
|
31
|
+
private createFloatingButton(): void {
|
|
32
|
+
this.floatingButton = document.createElement('div');
|
|
33
|
+
this.floatingButton.id = 'cuoral-floating-button';
|
|
34
|
+
this.floatingButton.innerHTML = `
|
|
35
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
36
|
+
<path d="M20 2H4C2.9 2 2 2.9 2 4V22L6 18H20C21.1 18 22 17.1 22 16V4C22 2.9 21.1 2 20 2ZM20 16H6L4 18V4H20V16Z" fill="white"/>
|
|
37
|
+
</svg>
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
// Styles
|
|
41
|
+
Object.assign(this.floatingButton.style, {
|
|
42
|
+
position: 'fixed',
|
|
43
|
+
bottom: '20px',
|
|
44
|
+
right: '20px',
|
|
45
|
+
width: '56px',
|
|
46
|
+
height: '56px',
|
|
47
|
+
borderRadius: '50%',
|
|
48
|
+
backgroundColor: '#007AFF',
|
|
49
|
+
display: 'flex',
|
|
50
|
+
alignItems: 'center',
|
|
51
|
+
justifyContent: 'center',
|
|
52
|
+
cursor: 'pointer',
|
|
53
|
+
boxShadow: '0 4px 12px rgba(0, 122, 255, 0.4)',
|
|
54
|
+
zIndex: '999999',
|
|
55
|
+
transition: 'transform 0.2s, box-shadow 0.2s'
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Hover effect
|
|
59
|
+
this.floatingButton.addEventListener('mouseenter', () => {
|
|
60
|
+
if (this.floatingButton) {
|
|
61
|
+
this.floatingButton.style.transform = 'scale(1.1)';
|
|
62
|
+
this.floatingButton.style.boxShadow = '0 6px 16px rgba(0, 122, 255, 0.5)';
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
this.floatingButton.addEventListener('mouseleave', () => {
|
|
67
|
+
if (this.floatingButton) {
|
|
68
|
+
this.floatingButton.style.transform = 'scale(1)';
|
|
69
|
+
this.floatingButton.style.boxShadow = '0 4px 12px rgba(0, 122, 255, 0.4)';
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Click to open modal
|
|
74
|
+
this.floatingButton.addEventListener('click', () => {
|
|
75
|
+
this.open();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
document.body.appendChild(this.floatingButton);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Create modal structure
|
|
83
|
+
*/
|
|
84
|
+
private createModal(): void {
|
|
85
|
+
// Modal container
|
|
86
|
+
this.modalElement = document.createElement('div');
|
|
87
|
+
this.modalElement.id = 'cuoral-modal';
|
|
88
|
+
|
|
89
|
+
Object.assign(this.modalElement.style, {
|
|
90
|
+
position: 'fixed',
|
|
91
|
+
top: '0',
|
|
92
|
+
left: '0',
|
|
93
|
+
width: '100%',
|
|
94
|
+
height: '100%',
|
|
95
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
96
|
+
display: 'none',
|
|
97
|
+
alignItems: 'center',
|
|
98
|
+
justifyContent: 'center',
|
|
99
|
+
zIndex: '1000000',
|
|
100
|
+
opacity: '0',
|
|
101
|
+
transition: 'opacity 0.3s ease',
|
|
102
|
+
padding: '60px 0'
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Modal content (with margin)
|
|
106
|
+
const modalContent = document.createElement('div');
|
|
107
|
+
Object.assign(modalContent.style, {
|
|
108
|
+
width: '100%',
|
|
109
|
+
height: '100%',
|
|
110
|
+
maxWidth: '100%',
|
|
111
|
+
maxHeight: '100%',
|
|
112
|
+
position: 'relative',
|
|
113
|
+
backgroundColor: 'transparent',
|
|
114
|
+
display: 'flex',
|
|
115
|
+
flexDirection: 'column',
|
|
116
|
+
borderRadius: '12px',
|
|
117
|
+
overflow: 'hidden'
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Close button
|
|
121
|
+
const closeButton = document.createElement('div');
|
|
122
|
+
closeButton.innerHTML = `
|
|
123
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
124
|
+
<path d="M18 6L6 18M6 6L18 18" stroke="white" stroke-width="2" stroke-linecap="round"/>
|
|
125
|
+
</svg>
|
|
126
|
+
`;
|
|
127
|
+
|
|
128
|
+
Object.assign(closeButton.style, {
|
|
129
|
+
position: 'absolute',
|
|
130
|
+
top: '20px',
|
|
131
|
+
right: '20px',
|
|
132
|
+
width: '40px',
|
|
133
|
+
height: '40px',
|
|
134
|
+
borderRadius: '50%',
|
|
135
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
136
|
+
display: 'flex',
|
|
137
|
+
alignItems: 'center',
|
|
138
|
+
justifyContent: 'center',
|
|
139
|
+
cursor: 'pointer',
|
|
140
|
+
zIndex: '1000001',
|
|
141
|
+
transition: 'background-color 0.2s'
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
closeButton.addEventListener('mouseenter', () => {
|
|
145
|
+
closeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
closeButton.addEventListener('mouseleave', () => {
|
|
149
|
+
closeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
closeButton.addEventListener('click', () => {
|
|
153
|
+
this.close();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Iframe
|
|
157
|
+
this.iframeElement = document.createElement('iframe');
|
|
158
|
+
this.iframeElement.id = 'cuoral-widget-iframe';
|
|
159
|
+
this.iframeElement.src = this.widgetUrl;
|
|
160
|
+
|
|
161
|
+
Object.assign(this.iframeElement.style, {
|
|
162
|
+
width: '100%',
|
|
163
|
+
height: '100%',
|
|
164
|
+
border: 'none',
|
|
165
|
+
backgroundColor: 'white'
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
modalContent.appendChild(closeButton);
|
|
169
|
+
modalContent.appendChild(this.iframeElement);
|
|
170
|
+
this.modalElement.appendChild(modalContent);
|
|
171
|
+
|
|
172
|
+
// Close on backdrop click
|
|
173
|
+
this.modalElement.addEventListener('click', (e) => {
|
|
174
|
+
if (e.target === this.modalElement) {
|
|
175
|
+
this.close();
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
document.body.appendChild(this.modalElement);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Open the modal
|
|
184
|
+
*/
|
|
185
|
+
public open(): void {
|
|
186
|
+
if (this.isOpen || !this.modalElement) return;
|
|
187
|
+
|
|
188
|
+
this.isOpen = true;
|
|
189
|
+
this.modalElement.style.display = 'flex';
|
|
190
|
+
|
|
191
|
+
// Trigger animation
|
|
192
|
+
setTimeout(() => {
|
|
193
|
+
if (this.modalElement) {
|
|
194
|
+
this.modalElement.style.opacity = '1';
|
|
195
|
+
}
|
|
196
|
+
}, 10);
|
|
197
|
+
|
|
198
|
+
// Hide floating button when modal is open
|
|
199
|
+
if (this.floatingButton) {
|
|
200
|
+
this.floatingButton.style.display = 'none';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Prevent body scroll
|
|
204
|
+
document.body.style.overflow = 'hidden';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Close the modal
|
|
209
|
+
*/
|
|
210
|
+
public close(): void {
|
|
211
|
+
if (!this.isOpen || !this.modalElement) return;
|
|
212
|
+
|
|
213
|
+
this.isOpen = false;
|
|
214
|
+
this.modalElement.style.opacity = '0';
|
|
215
|
+
|
|
216
|
+
setTimeout(() => {
|
|
217
|
+
if (this.modalElement) {
|
|
218
|
+
this.modalElement.style.display = 'none';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Show floating button again
|
|
222
|
+
if (this.floatingButton) {
|
|
223
|
+
this.floatingButton.style.display = 'flex';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Restore body scroll
|
|
227
|
+
document.body.style.overflow = '';
|
|
228
|
+
}, 300);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Check if modal is open
|
|
233
|
+
*/
|
|
234
|
+
public isModalOpen(): boolean {
|
|
235
|
+
return this.isOpen;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Destroy modal and floating button
|
|
240
|
+
*/
|
|
241
|
+
public destroy(): void {
|
|
242
|
+
if (this.modalElement) {
|
|
243
|
+
this.modalElement.remove();
|
|
244
|
+
}
|
|
245
|
+
if (this.floatingButton) {
|
|
246
|
+
this.floatingButton.remove();
|
|
247
|
+
}
|
|
248
|
+
document.body.style.overflow = '';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Get iframe element for communication
|
|
253
|
+
*/
|
|
254
|
+
public getIframe(): HTMLIFrameElement | undefined {
|
|
255
|
+
return this.iframeElement;
|
|
256
|
+
}
|
|
257
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { registerPlugin } from '@capacitor/core';
|
|
2
|
+
import {
|
|
3
|
+
CuoralMessageType,
|
|
4
|
+
RecordingOptions,
|
|
5
|
+
RecordingState,
|
|
6
|
+
ScreenshotOptions,
|
|
7
|
+
ScreenshotData,
|
|
8
|
+
} from './types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Capacitor Plugin Interface
|
|
12
|
+
*/
|
|
13
|
+
export interface CuoralPluginInterface {
|
|
14
|
+
/**
|
|
15
|
+
* Start screen recording
|
|
16
|
+
*/
|
|
17
|
+
startRecording(options?: RecordingOptions): Promise<{ success: boolean }>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Stop screen recording
|
|
21
|
+
*/
|
|
22
|
+
stopRecording(): Promise<{ success: boolean; filePath?: string; duration?: number }>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get recording state
|
|
26
|
+
*/
|
|
27
|
+
getRecordingState(): Promise<RecordingState>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Take screenshot
|
|
31
|
+
*/
|
|
32
|
+
takeScreenshot(options?: ScreenshotOptions): Promise<ScreenshotData>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if recording is supported
|
|
36
|
+
*/
|
|
37
|
+
isRecordingSupported(): Promise<{ supported: boolean }>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Request recording permissions
|
|
41
|
+
*/
|
|
42
|
+
requestPermissions(): Promise<{ granted: boolean }>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Register the Capacitor plugin
|
|
47
|
+
*/
|
|
48
|
+
const CuoralPlugin = registerPlugin<CuoralPluginInterface>('CuoralPlugin', {
|
|
49
|
+
web: () => import('./web').then(m => new m.CuoralPluginWeb()),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export { CuoralPlugin };
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* High-level API for easier integration
|
|
56
|
+
*/
|
|
57
|
+
export class CuoralRecorder {
|
|
58
|
+
private isRecording: boolean = false;
|
|
59
|
+
private recordingStartTime?: number;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Start recording with automatic permission handling
|
|
63
|
+
*/
|
|
64
|
+
async startRecording(options?: RecordingOptions): Promise<boolean> {
|
|
65
|
+
try {
|
|
66
|
+
// Check if already recording
|
|
67
|
+
if (this.isRecording) {
|
|
68
|
+
console.warn('[Cuoral] Already recording');
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check support
|
|
73
|
+
const { supported } = await CuoralPlugin.isRecordingSupported();
|
|
74
|
+
if (!supported) {
|
|
75
|
+
throw new Error('Screen recording is not supported on this device');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Request permissions
|
|
79
|
+
const { granted } = await CuoralPlugin.requestPermissions();
|
|
80
|
+
if (!granted) {
|
|
81
|
+
throw new Error('Recording permissions not granted');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Start recording
|
|
85
|
+
const result = await CuoralPlugin.startRecording(options);
|
|
86
|
+
if (result.success) {
|
|
87
|
+
this.isRecording = true;
|
|
88
|
+
this.recordingStartTime = Date.now();
|
|
89
|
+
|
|
90
|
+
// Post message to widget
|
|
91
|
+
this.postMessage({
|
|
92
|
+
type: CuoralMessageType.RECORDING_STARTED,
|
|
93
|
+
payload: { timestamp: this.recordingStartTime },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return result.success;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('[Cuoral] Failed to start recording:', error);
|
|
100
|
+
this.postMessage({
|
|
101
|
+
type: CuoralMessageType.RECORDING_ERROR,
|
|
102
|
+
payload: { error: (error as Error).message },
|
|
103
|
+
});
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Stop recording
|
|
110
|
+
*/
|
|
111
|
+
async stopRecording(): Promise<{ filePath?: string; duration?: number } | null> {
|
|
112
|
+
try {
|
|
113
|
+
if (!this.isRecording) {
|
|
114
|
+
console.warn('[Cuoral] Not recording');
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const result = await CuoralPlugin.stopRecording();
|
|
119
|
+
if (result.success) {
|
|
120
|
+
this.isRecording = false;
|
|
121
|
+
const duration = this.recordingStartTime
|
|
122
|
+
? Math.floor((Date.now() - this.recordingStartTime) / 1000)
|
|
123
|
+
: 0;
|
|
124
|
+
|
|
125
|
+
// Post message to widget
|
|
126
|
+
this.postMessage({
|
|
127
|
+
type: CuoralMessageType.RECORDING_STOPPED,
|
|
128
|
+
payload: {
|
|
129
|
+
filePath: result.filePath,
|
|
130
|
+
duration: result.duration || duration,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
filePath: result.filePath,
|
|
136
|
+
duration: result.duration || duration,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return null;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error('[Cuoral] Failed to stop recording:', error);
|
|
143
|
+
this.postMessage({
|
|
144
|
+
type: CuoralMessageType.RECORDING_ERROR,
|
|
145
|
+
payload: { error: (error as Error).message },
|
|
146
|
+
});
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Take screenshot
|
|
153
|
+
*/
|
|
154
|
+
async takeScreenshot(options?: ScreenshotOptions): Promise<ScreenshotData | null> {
|
|
155
|
+
try {
|
|
156
|
+
const screenshot = await CuoralPlugin.takeScreenshot(options);
|
|
157
|
+
|
|
158
|
+
// Post message to widget
|
|
159
|
+
this.postMessage({
|
|
160
|
+
type: CuoralMessageType.SCREENSHOT_TAKEN,
|
|
161
|
+
payload: screenshot,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return screenshot;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error('[Cuoral] Failed to take screenshot:', error);
|
|
167
|
+
this.postMessage({
|
|
168
|
+
type: CuoralMessageType.SCREENSHOT_ERROR,
|
|
169
|
+
payload: { error: (error as Error).message },
|
|
170
|
+
});
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get current recording state
|
|
177
|
+
*/
|
|
178
|
+
async getState(): Promise<RecordingState> {
|
|
179
|
+
return await CuoralPlugin.getRecordingState();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Post message to WebView
|
|
184
|
+
*/
|
|
185
|
+
private postMessage(message: any): void {
|
|
186
|
+
if (typeof window !== 'undefined' && window.postMessage) {
|
|
187
|
+
window.postMessage(message, '*');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|