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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cuoral
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Cuoral Ionic Library
|
|
2
|
+
|
|
3
|
+
Proactive customer success platform integration for Ionic/Capacitor applications. Cuoral provides support ticketing, customer intelligence, screen recording, and comprehensive customer engagement tools.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
✅ **Customer Support** - Integrated support ticketing system
|
|
8
|
+
✅ **Customer Intelligence** - Track and analyze customer behavior
|
|
9
|
+
✅ **Screen Recording** - Native iOS screen recording for issue reproduction
|
|
10
|
+
✅ **Modal Display** - Full-screen modal with floating chat button
|
|
11
|
+
✅ **TypeScript Support** - Full type definitions included
|
|
12
|
+
✅ **Simple API** - Just pass your public key and optional user info
|
|
13
|
+
✅ **Zero Configuration** - Everything handled automatically
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install cuoral-ionic
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
After installing, sync your Capacitor project:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx cap sync
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### iOS Setup
|
|
28
|
+
|
|
29
|
+
Add to your `Info.plist`:
|
|
30
|
+
|
|
31
|
+
```xml
|
|
32
|
+
<key>NSMicrophoneUsageDescription</key>
|
|
33
|
+
<string>We need access to your microphone to record audio with screen recordings</string>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
### Option 1: Modal with Floating Button (Recommended)
|
|
39
|
+
|
|
40
|
+
The easiest way to integrate Cuoral - displays a floating chat button that opens the widget in a full-screen modal.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { Component, OnInit, OnDestroy } from '@angular/core';
|
|
44
|
+
import { Cuoral } from 'cuoral-ionic';
|
|
45
|
+
|
|
46
|
+
@Component({
|
|
47
|
+
selector: 'app-home',
|
|
48
|
+
template: `
|
|
49
|
+
<ion-content>
|
|
50
|
+
<!-- Your app content -->
|
|
51
|
+
<h1>Welcome to Support</h1>
|
|
52
|
+
|
|
53
|
+
<!-- Optional: Custom button to open widget -->
|
|
54
|
+
<ion-button (click)="openSupport()"> Contact Support </ion-button>
|
|
55
|
+
</ion-content>
|
|
56
|
+
`,
|
|
57
|
+
})
|
|
58
|
+
export class HomePage implements OnInit, OnDestroy {
|
|
59
|
+
private cuoral: Cuoral;
|
|
60
|
+
|
|
61
|
+
constructor() {
|
|
62
|
+
this.cuoral = new Cuoral({
|
|
63
|
+
publicKey: 'your-public-key-here',
|
|
64
|
+
email: 'user@example.com', // Optional
|
|
65
|
+
firstName: 'John', // Optional
|
|
66
|
+
lastName: 'Doe', // Optional
|
|
67
|
+
showFloatingButton: true, // Show floating chat button
|
|
68
|
+
useModal: true, // Use modal display mode
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
ngOnInit() {
|
|
73
|
+
this.cuoral.initialize();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
ngOnDestroy() {
|
|
77
|
+
this.cuoral.destroy();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Open modal programmatically
|
|
81
|
+
openSupport() {
|
|
82
|
+
this.cuoral.openModal();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Features:**
|
|
88
|
+
|
|
89
|
+
- 🎈 Floating blue chat button in bottom-right corner
|
|
90
|
+
- 📱 Opens widget in full-screen modal with small margins
|
|
91
|
+
- ❌ Close button in top-right
|
|
92
|
+
- 🎨 Smooth animations and transitions
|
|
93
|
+
- 🖱️ Click backdrop to close
|
|
94
|
+
|
|
95
|
+
### Option 2: Embedded Iframe
|
|
96
|
+
|
|
97
|
+
For more control over widget placement:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { Component, OnInit, OnDestroy } from '@angular/core';
|
|
101
|
+
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
|
102
|
+
import { Cuoral } from 'cuoral-ionic';
|
|
103
|
+
|
|
104
|
+
@Component({
|
|
105
|
+
selector: 'app-support',
|
|
106
|
+
template: `
|
|
107
|
+
<ion-content>
|
|
108
|
+
<iframe [src]="widgetUrl" style="width: 100%; height: 600px; border: none;"> </iframe>
|
|
109
|
+
</ion-content>
|
|
110
|
+
`,
|
|
111
|
+
})
|
|
112
|
+
export class SupportPage implements OnInit, OnDestroy {
|
|
113
|
+
widgetUrl: SafeResourceUrl;
|
|
114
|
+
private cuoral: Cuoral;
|
|
115
|
+
|
|
116
|
+
constructor(private sanitizer: DomSanitizer) {
|
|
117
|
+
this.cuoral = new Cuoral({
|
|
118
|
+
publicKey: 'your-public-key-here',
|
|
119
|
+
useModal: false, // Disable modal mode
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const url = this.cuoral.getWidgetUrl();
|
|
123
|
+
this.widgetUrl = this.sanitizer.bypassSecurityTrustResourceUrl(url);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
ngOnInit() {
|
|
127
|
+
this.cuoral.initialize();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
ngOnDestroy() {
|
|
131
|
+
this.cuoral.destroy();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Configuration
|
|
137
|
+
|
|
138
|
+
### CuoralOptions
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
interface CuoralOptions {
|
|
142
|
+
publicKey: string; // Required: Your Cuoral public key
|
|
143
|
+
email?: string; // Optional: User email
|
|
144
|
+
firstName?: string; // Optional: User first name
|
|
145
|
+
lastName?: string; // Optional: User last name
|
|
146
|
+
debug?: boolean; // Optional: Enable debug logging (default: false)
|
|
147
|
+
widgetBaseUrl?: string; // Optional: Custom widget URL (default: CDN)
|
|
148
|
+
showFloatingButton?: boolean; // Optional: Show floating chat button (default: true)
|
|
149
|
+
useModal?: boolean; // Optional: Use modal display mode (default: true)
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Widget URL
|
|
154
|
+
|
|
155
|
+
By default, the widget loads from `https://js.cuoral.com/mobile.html` (production CDN).
|
|
156
|
+
|
|
157
|
+
**Production (Default):**
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
new Cuoral({ publicKey: 'your-key' });
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Custom URL (Optional):**
|
|
164
|
+
|
|
165
|
+
If you need to use a custom widget URL for testing or self-hosting:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
new Cuoral({
|
|
169
|
+
publicKey: 'your-key',
|
|
170
|
+
widgetBaseUrl: 'https://your-domain.com/mobile.html',
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## API Reference
|
|
175
|
+
|
|
176
|
+
### Cuoral Methods
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
// Initialize Cuoral
|
|
180
|
+
cuoral.initialize(): void
|
|
181
|
+
|
|
182
|
+
// Get widget URL for iframe embedding
|
|
183
|
+
cuoral.getWidgetUrl(): string
|
|
184
|
+
|
|
185
|
+
// Open modal programmatically
|
|
186
|
+
cuoral.openModal(): void
|
|
187
|
+
|
|
188
|
+
// Close modal programmatically
|
|
189
|
+
cuoral.closeModal(): void
|
|
190
|
+
|
|
191
|
+
// Check if modal is open
|
|
192
|
+
cuoral.isModalOpen(): boolean
|
|
193
|
+
|
|
194
|
+
// Clean up resources
|
|
195
|
+
cuoral.destroy(): void
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## What Gets Handled Automatically
|
|
199
|
+
|
|
200
|
+
- ✅ Support ticket creation and management
|
|
201
|
+
- ✅ Customer intelligence tracking
|
|
202
|
+
- ✅ Screen recording start/stop
|
|
203
|
+
- ✅ File path conversion for video playback
|
|
204
|
+
- ✅ Communication between widget and native code
|
|
205
|
+
- ✅ Video upload to Cuoral backend
|
|
206
|
+
- ✅ State management
|
|
207
|
+
- ✅ Permissions handling
|
|
208
|
+
|
|
209
|
+
## Troubleshooting
|
|
210
|
+
|
|
211
|
+
### Recording doesn't start
|
|
212
|
+
|
|
213
|
+
**Check iOS permissions:**
|
|
214
|
+
|
|
215
|
+
- Verify `NSMicrophoneUsageDescription` is in Info.plist
|
|
216
|
+
- System will prompt user automatically on first recording
|
|
217
|
+
|
|
218
|
+
**Enable debug mode:**
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
new Cuoral({
|
|
222
|
+
publicKey: 'your-key',
|
|
223
|
+
debug: true, // See console logs
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Widget not appearing
|
|
228
|
+
|
|
229
|
+
**Modal mode:**
|
|
230
|
+
|
|
231
|
+
- Make sure you called `cuoral.initialize()`
|
|
232
|
+
- Check that `useModal: true` is set (default)
|
|
233
|
+
- Look for the blue floating button in bottom-right
|
|
234
|
+
|
|
235
|
+
**Iframe mode:**
|
|
236
|
+
|
|
237
|
+
- Verify `widgetUrl` is properly sanitized with `DomSanitizer`
|
|
238
|
+
- Check browser console for CORS errors
|
|
239
|
+
- Ensure iframe has proper dimensions in CSS
|
|
240
|
+
|
|
241
|
+
### Build errors
|
|
242
|
+
|
|
243
|
+
**iOS Pod Issues:**
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
cd ios
|
|
247
|
+
pod deintegrate
|
|
248
|
+
pod install
|
|
249
|
+
cd ..
|
|
250
|
+
npx cap sync ios
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Clean build:**
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
rm -rf node_modules dist
|
|
257
|
+
npm install
|
|
258
|
+
npx cap sync
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Support
|
|
262
|
+
|
|
263
|
+
- 📧 Email: support@cuoral.com
|
|
264
|
+
- 📖 Docs: https://docs.cuoral.com
|
|
265
|
+
- 🐛 Issues: https://github.com/cuoral/cuoral-ionic/issues
|
|
266
|
+
|
|
267
|
+
## License
|
|
268
|
+
|
|
269
|
+
MIT License - see [LICENSE](LICENSE) file
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
Built with ❤️ by Cuoral
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
+
package="com.cuoral.ionic">
|
|
4
|
+
|
|
5
|
+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
6
|
+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
|
7
|
+
android:maxSdkVersion="28" />
|
|
8
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
9
|
+
|
|
10
|
+
</manifest>
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
package com.cuoral.ionic;
|
|
2
|
+
|
|
3
|
+
import android.Manifest;
|
|
4
|
+
import android.app.Activity;
|
|
5
|
+
import android.content.Context;
|
|
6
|
+
import android.content.Intent;
|
|
7
|
+
import android.content.pm.PackageManager;
|
|
8
|
+
import android.graphics.Bitmap;
|
|
9
|
+
import android.media.MediaRecorder;
|
|
10
|
+
import android.media.projection.MediaProjection;
|
|
11
|
+
import android.media.projection.MediaProjectionManager;
|
|
12
|
+
import android.os.Build;
|
|
13
|
+
import android.util.Base64;
|
|
14
|
+
import android.util.DisplayMetrics;
|
|
15
|
+
import android.view.View;
|
|
16
|
+
|
|
17
|
+
import androidx.core.app.ActivityCompat;
|
|
18
|
+
import androidx.core.content.ContextCompat;
|
|
19
|
+
|
|
20
|
+
import com.getcapacitor.JSObject;
|
|
21
|
+
import com.getcapacitor.Plugin;
|
|
22
|
+
import com.getcapacitor.PluginCall;
|
|
23
|
+
import com.getcapacitor.PluginMethod;
|
|
24
|
+
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
25
|
+
import com.getcapacitor.annotation.Permission;
|
|
26
|
+
import com.getcapacitor.annotation.ActivityCallback;
|
|
27
|
+
|
|
28
|
+
import java.io.ByteArrayOutputStream;
|
|
29
|
+
import java.io.File;
|
|
30
|
+
import java.io.IOException;
|
|
31
|
+
|
|
32
|
+
@CapacitorPlugin(name = "CuoralPlugin", permissions = {
|
|
33
|
+
@Permission(strings = { Manifest.permission.RECORD_AUDIO }, alias = "audio"),
|
|
34
|
+
@Permission(strings = { Manifest.permission.WRITE_EXTERNAL_STORAGE }, alias = "storage")
|
|
35
|
+
})
|
|
36
|
+
public class CuoralPlugin extends Plugin {
|
|
37
|
+
|
|
38
|
+
private static final int SCREEN_CAPTURE_REQUEST_CODE = 1001;
|
|
39
|
+
private static final int PERMISSION_REQUEST_CODE = 1002;
|
|
40
|
+
|
|
41
|
+
private MediaProjectionManager projectionManager;
|
|
42
|
+
private MediaProjection mediaProjection;
|
|
43
|
+
private MediaRecorder mediaRecorder;
|
|
44
|
+
private boolean isRecording = false;
|
|
45
|
+
private long recordingStartTime = 0;
|
|
46
|
+
private String videoFilePath;
|
|
47
|
+
private PluginCall currentCall;
|
|
48
|
+
|
|
49
|
+
@Override
|
|
50
|
+
public void load() {
|
|
51
|
+
super.load();
|
|
52
|
+
projectionManager = (MediaProjectionManager) getContext()
|
|
53
|
+
.getSystemService(Context.MEDIA_PROJECTION_SERVICE);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@PluginMethod
|
|
57
|
+
public void startRecording(PluginCall call) {
|
|
58
|
+
if (isRecording) {
|
|
59
|
+
call.resolve(createErrorResult("Already recording"));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Save call for later use
|
|
64
|
+
currentCall = call;
|
|
65
|
+
|
|
66
|
+
// Check if we need audio permission
|
|
67
|
+
boolean includeAudio = call.getBoolean("includeAudio", false);
|
|
68
|
+
if (includeAudio && !hasAudioPermission()) {
|
|
69
|
+
requestPermissionForAlias("audio", call, "audioPermissionCallback");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Request screen capture
|
|
74
|
+
Intent captureIntent = projectionManager.createScreenCaptureIntent();
|
|
75
|
+
startActivityForResult(call, captureIntent, "screenCaptureCallback");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@ActivityCallback
|
|
79
|
+
private void screenCaptureCallback(PluginCall call, com.getcapacitor.PluginResult result) {
|
|
80
|
+
if (result.getResultCode() != Activity.RESULT_OK) {
|
|
81
|
+
call.reject("Screen capture permission denied");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
Intent data = result.getData();
|
|
86
|
+
mediaProjection = projectionManager.getMediaProjection(result.getResultCode(), data);
|
|
87
|
+
|
|
88
|
+
// Setup media recorder
|
|
89
|
+
boolean includeAudio = call.getBoolean("includeAudio", false);
|
|
90
|
+
float quality = call.getFloat("quality", 1.0f);
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
setupMediaRecorder(includeAudio, quality);
|
|
94
|
+
mediaRecorder.start();
|
|
95
|
+
isRecording = true;
|
|
96
|
+
recordingStartTime = System.currentTimeMillis();
|
|
97
|
+
|
|
98
|
+
JSObject ret = new JSObject();
|
|
99
|
+
ret.put("success", true);
|
|
100
|
+
call.resolve(ret);
|
|
101
|
+
|
|
102
|
+
// Notify listeners
|
|
103
|
+
JSObject eventData = new JSObject();
|
|
104
|
+
eventData.put("timestamp", recordingStartTime);
|
|
105
|
+
notifyListeners("recordingStarted", eventData);
|
|
106
|
+
|
|
107
|
+
} catch (Exception e) {
|
|
108
|
+
call.reject("Failed to start recording: " + e.getMessage());
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@ActivityCallback
|
|
113
|
+
private void audioPermissionCallback(PluginCall call) {
|
|
114
|
+
if (!hasAudioPermission()) {
|
|
115
|
+
call.reject("Audio permission required");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// Retry starting recording
|
|
119
|
+
startRecording(call);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@PluginMethod
|
|
123
|
+
public void stopRecording(PluginCall call) {
|
|
124
|
+
if (!isRecording) {
|
|
125
|
+
call.resolve(createErrorResult("Not recording"));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
mediaRecorder.stop();
|
|
131
|
+
mediaRecorder.reset();
|
|
132
|
+
mediaRecorder.release();
|
|
133
|
+
mediaRecorder = null;
|
|
134
|
+
|
|
135
|
+
if (mediaProjection != null) {
|
|
136
|
+
mediaProjection.stop();
|
|
137
|
+
mediaProjection = null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
isRecording = false;
|
|
141
|
+
long duration = (System.currentTimeMillis() - recordingStartTime) / 1000;
|
|
142
|
+
|
|
143
|
+
JSObject ret = new JSObject();
|
|
144
|
+
ret.put("success", true);
|
|
145
|
+
ret.put("filePath", videoFilePath);
|
|
146
|
+
ret.put("duration", duration);
|
|
147
|
+
call.resolve(ret);
|
|
148
|
+
|
|
149
|
+
// Notify listeners
|
|
150
|
+
JSObject eventData = new JSObject();
|
|
151
|
+
eventData.put("filePath", videoFilePath);
|
|
152
|
+
eventData.put("duration", duration);
|
|
153
|
+
notifyListeners("recordingStopped", eventData);
|
|
154
|
+
|
|
155
|
+
} catch (Exception e) {
|
|
156
|
+
call.reject("Failed to stop recording: " + e.getMessage());
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@PluginMethod
|
|
161
|
+
public void getRecordingState(PluginCall call) {
|
|
162
|
+
long duration = isRecording ? (System.currentTimeMillis() - recordingStartTime) / 1000 : 0;
|
|
163
|
+
|
|
164
|
+
JSObject ret = new JSObject();
|
|
165
|
+
ret.put("isRecording", isRecording);
|
|
166
|
+
ret.put("duration", duration);
|
|
167
|
+
ret.put("filePath", videoFilePath != null ? videoFilePath : "");
|
|
168
|
+
call.resolve(ret);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@PluginMethod
|
|
172
|
+
public void takeScreenshot(PluginCall call) {
|
|
173
|
+
try {
|
|
174
|
+
Activity activity = getActivity();
|
|
175
|
+
View rootView = activity.getWindow().getDecorView().getRootView();
|
|
176
|
+
rootView.setDrawingCacheEnabled(true);
|
|
177
|
+
|
|
178
|
+
Bitmap bitmap = Bitmap.createBitmap(rootView.getDrawingCache());
|
|
179
|
+
rootView.setDrawingCacheEnabled(false);
|
|
180
|
+
|
|
181
|
+
// Get options
|
|
182
|
+
float quality = call.getFloat("quality", 0.92f);
|
|
183
|
+
String format = call.getString("format", "png");
|
|
184
|
+
|
|
185
|
+
// Convert to base64
|
|
186
|
+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
|
187
|
+
if (format.equals("jpeg")) {
|
|
188
|
+
bitmap.compress(Bitmap.CompressFormat.JPEG, (int) (quality * 100), outputStream);
|
|
189
|
+
} else {
|
|
190
|
+
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
byte[] bytes = outputStream.toByteArray();
|
|
194
|
+
String base64 = Base64.encodeToString(bytes, Base64.NO_WRAP);
|
|
195
|
+
|
|
196
|
+
JSObject ret = new JSObject();
|
|
197
|
+
ret.put("base64", base64);
|
|
198
|
+
ret.put("mimeType", "image/" + format);
|
|
199
|
+
ret.put("width", bitmap.getWidth());
|
|
200
|
+
ret.put("height", bitmap.getHeight());
|
|
201
|
+
call.resolve(ret);
|
|
202
|
+
|
|
203
|
+
// Notify listeners
|
|
204
|
+
JSObject eventData = new JSObject();
|
|
205
|
+
eventData.put("success", true);
|
|
206
|
+
notifyListeners("screenshotTaken", eventData);
|
|
207
|
+
|
|
208
|
+
bitmap.recycle();
|
|
209
|
+
|
|
210
|
+
} catch (Exception e) {
|
|
211
|
+
call.reject("Failed to take screenshot: " + e.getMessage());
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@PluginMethod
|
|
216
|
+
public void isRecordingSupported(PluginCall call) {
|
|
217
|
+
JSObject ret = new JSObject();
|
|
218
|
+
ret.put("supported", Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP);
|
|
219
|
+
call.resolve(ret);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
@PluginMethod
|
|
223
|
+
public void requestPermissions(PluginCall call) {
|
|
224
|
+
if (hasRequiredPermissions()) {
|
|
225
|
+
JSObject ret = new JSObject();
|
|
226
|
+
ret.put("granted", true);
|
|
227
|
+
call.resolve(ret);
|
|
228
|
+
} else {
|
|
229
|
+
requestAllPermissions(call, "permissionsCallback");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
@ActivityCallback
|
|
234
|
+
private void permissionsCallback(PluginCall call) {
|
|
235
|
+
JSObject ret = new JSObject();
|
|
236
|
+
ret.put("granted", hasRequiredPermissions());
|
|
237
|
+
call.resolve(ret);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Helper methods
|
|
241
|
+
|
|
242
|
+
private void setupMediaRecorder(boolean includeAudio, float quality) throws IOException {
|
|
243
|
+
DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
|
|
244
|
+
|
|
245
|
+
File outputDir = getContext().getFilesDir();
|
|
246
|
+
videoFilePath = new File(outputDir, "cuoral_recording_" + System.currentTimeMillis() + ".mp4")
|
|
247
|
+
.getAbsolutePath();
|
|
248
|
+
|
|
249
|
+
mediaRecorder = new MediaRecorder();
|
|
250
|
+
|
|
251
|
+
if (includeAudio) {
|
|
252
|
+
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
|
|
253
|
+
}
|
|
254
|
+
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
|
|
255
|
+
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
|
|
256
|
+
mediaRecorder.setOutputFile(videoFilePath);
|
|
257
|
+
|
|
258
|
+
int bitRate = (int) (6000000 * quality); // Base bitrate 6Mbps
|
|
259
|
+
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
|
|
260
|
+
mediaRecorder.setVideoSize(metrics.widthPixels, metrics.heightPixels);
|
|
261
|
+
mediaRecorder.setVideoFrameRate(30);
|
|
262
|
+
mediaRecorder.setVideoEncodingBitRate(bitRate);
|
|
263
|
+
|
|
264
|
+
if (includeAudio) {
|
|
265
|
+
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
|
|
266
|
+
mediaRecorder.setAudioEncodingBitRate(128000);
|
|
267
|
+
mediaRecorder.setAudioSamplingRate(44100);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
mediaRecorder.prepare();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private boolean hasAudioPermission() {
|
|
274
|
+
return ContextCompat.checkSelfPermission(getContext(),
|
|
275
|
+
Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private boolean hasRequiredPermissions() {
|
|
279
|
+
return hasAudioPermission() &&
|
|
280
|
+
(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q ||
|
|
281
|
+
ContextCompat.checkSelfPermission(getContext(),
|
|
282
|
+
Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private JSObject createErrorResult(String message) {
|
|
286
|
+
JSObject ret = new JSObject();
|
|
287
|
+
ret.put("success", false);
|
|
288
|
+
ret.put("error", message);
|
|
289
|
+
return ret;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
Binary file
|
package/dist/bridge.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { CuoralMessage, CuoralMessageType, CuoralConfig } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Bridge for bidirectional communication between WebView and Native code
|
|
4
|
+
*/
|
|
5
|
+
export declare class CuoralBridge {
|
|
6
|
+
private config;
|
|
7
|
+
private messageHandlers;
|
|
8
|
+
private isInitialized;
|
|
9
|
+
private widgetIframe;
|
|
10
|
+
constructor(config: CuoralConfig);
|
|
11
|
+
/**
|
|
12
|
+
* Initialize the bridge
|
|
13
|
+
*/
|
|
14
|
+
initialize(): void;
|
|
15
|
+
/**
|
|
16
|
+
* Send message to native code
|
|
17
|
+
*/
|
|
18
|
+
sendToNative(message: CuoralMessage): void;
|
|
19
|
+
/**
|
|
20
|
+
* Send message to widget iframe
|
|
21
|
+
*/
|
|
22
|
+
sendToWidget(message: CuoralMessage): void;
|
|
23
|
+
/**
|
|
24
|
+
* Register message handler
|
|
25
|
+
*/
|
|
26
|
+
on(type: CuoralMessageType, handler: (payload: any) => void): () => void;
|
|
27
|
+
/**
|
|
28
|
+
* Setup window message listener
|
|
29
|
+
*/
|
|
30
|
+
private setupMessageListener;
|
|
31
|
+
/**
|
|
32
|
+
* Debug logging
|
|
33
|
+
*/
|
|
34
|
+
private log;
|
|
35
|
+
/**
|
|
36
|
+
* Destroy the bridge
|
|
37
|
+
*/
|
|
38
|
+
destroy(): void;
|
|
39
|
+
}
|
|
40
|
+
declare global {
|
|
41
|
+
interface Window {
|
|
42
|
+
webkit?: {
|
|
43
|
+
messageHandlers?: {
|
|
44
|
+
cuoral?: {
|
|
45
|
+
postMessage: (message: any) => void;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEzE;;GAEG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,eAAe,CAAoE;IAC3F,OAAO,CAAC,aAAa,CAAkB;IACvC,OAAO,CAAC,YAAY,CAAkC;gBAE1C,MAAM,EAAE,YAAY;IAKhC;;OAEG;IACI,UAAU,IAAI,IAAI;IAgBzB;;OAEG;IACI,YAAY,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI;IAoBjD;;OAEG;IACI,YAAY,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI;IAyCjD;;OAEG;IACI,EAAE,CAAC,IAAI,EAAE,iBAAiB,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,GAAG,MAAM,IAAI;IAmB/E;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAqC5B;;OAEG;IACH,OAAO,CAAC,GAAG;IAMX;;OAEG;IACI,OAAO,IAAI,IAAI;CAKvB;AAGD,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,MAAM,CAAC,EAAE;YACP,eAAe,CAAC,EAAE;gBAChB,MAAM,CAAC,EAAE;oBACP,WAAW,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,CAAC;iBACrC,CAAC;aACH,CAAC;SACH,CAAC;KACH;CACF"}
|