@angular-helpers/browser-web-apis 21.0.1 → 21.1.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/README.es.md +156 -0
- package/README.md +19 -26
- package/fesm2022/angular-helpers-browser-web-apis.mjs +1504 -0
- package/package.json +1 -6
- package/types/angular-helpers-browser-web-apis.d.ts +537 -0
|
@@ -0,0 +1,1504 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { Injectable, inject, DestroyRef, PLATFORM_ID, signal, makeEnvironmentProviders } from '@angular/core';
|
|
3
|
+
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
|
|
4
|
+
import { Observable, fromEvent, Subject } from 'rxjs';
|
|
5
|
+
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
|
|
6
|
+
import { filter, distinctUntilChanged, map } from 'rxjs/operators';
|
|
7
|
+
import { Router } from '@angular/router';
|
|
8
|
+
|
|
9
|
+
class PermissionsService {
|
|
10
|
+
async query(descriptor) {
|
|
11
|
+
if (!this.isSupported()) {
|
|
12
|
+
throw new Error('Permissions API not supported');
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
return await navigator.permissions.query(descriptor);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
console.error('Error querying permission:', error);
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
isSupported() {
|
|
23
|
+
return typeof navigator !== 'undefined' && 'permissions' in navigator;
|
|
24
|
+
}
|
|
25
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PermissionsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
26
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PermissionsService });
|
|
27
|
+
}
|
|
28
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PermissionsService, decorators: [{
|
|
29
|
+
type: Injectable
|
|
30
|
+
}] });
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Base class for all Browser Web API services
|
|
34
|
+
* Provides common functionality for:
|
|
35
|
+
* - Support checking
|
|
36
|
+
* - Permission management
|
|
37
|
+
* - Error handling
|
|
38
|
+
* - Lifecycle management with destroyRef
|
|
39
|
+
* - Logging
|
|
40
|
+
*/
|
|
41
|
+
class BrowserApiBaseService {
|
|
42
|
+
permissionsService = inject(PermissionsService);
|
|
43
|
+
destroyRef = inject(DestroyRef);
|
|
44
|
+
platformId = inject(PLATFORM_ID);
|
|
45
|
+
/**
|
|
46
|
+
* Check if running in browser environment using Angular's platform detection
|
|
47
|
+
*/
|
|
48
|
+
isBrowserEnvironment() {
|
|
49
|
+
return isPlatformBrowser(this.platformId);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Check if running in server environment using Angular's platform detection
|
|
53
|
+
*/
|
|
54
|
+
isServerEnvironment() {
|
|
55
|
+
return isPlatformServer(this.platformId);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Request a permission
|
|
59
|
+
*/
|
|
60
|
+
async requestPermission(permission) {
|
|
61
|
+
if (this.isServerEnvironment()) {
|
|
62
|
+
throw new Error(`${this.getApiName()} API not available in server environment`);
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const status = await this.permissionsService.query({ name: permission });
|
|
66
|
+
return status.state === 'granted';
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
console.error(`[${this.getApiName()}] Error requesting permission for ${permission}:`, error);
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Create an error with proper cause chaining
|
|
75
|
+
*/
|
|
76
|
+
createError(message, cause) {
|
|
77
|
+
const error = new Error(message);
|
|
78
|
+
if (cause !== undefined) {
|
|
79
|
+
error.cause = cause;
|
|
80
|
+
}
|
|
81
|
+
return error;
|
|
82
|
+
}
|
|
83
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BrowserApiBaseService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
84
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BrowserApiBaseService });
|
|
85
|
+
}
|
|
86
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BrowserApiBaseService, decorators: [{
|
|
87
|
+
type: Injectable
|
|
88
|
+
}] });
|
|
89
|
+
|
|
90
|
+
class CameraService extends BrowserApiBaseService {
|
|
91
|
+
currentStream = null;
|
|
92
|
+
getApiName() {
|
|
93
|
+
return 'camera';
|
|
94
|
+
}
|
|
95
|
+
ensureCameraSupport() {
|
|
96
|
+
if (!('mediaDevices' in navigator) || !('getUserMedia' in navigator.mediaDevices)) {
|
|
97
|
+
throw new Error('Camera API not supported in this browser');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async startCamera(constraints) {
|
|
101
|
+
this.ensureCameraSupport();
|
|
102
|
+
if (this.currentStream) {
|
|
103
|
+
this.stopCamera();
|
|
104
|
+
}
|
|
105
|
+
const permissionStatus = await this.permissionsService.query({ name: 'camera' });
|
|
106
|
+
if (permissionStatus.state !== 'granted') {
|
|
107
|
+
throw new Error('Camera permission required. Please grant camera access and try again.');
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const streamConstraints = constraints || {
|
|
111
|
+
video: {
|
|
112
|
+
width: { ideal: 1280 },
|
|
113
|
+
height: { ideal: 720 },
|
|
114
|
+
facingMode: 'user',
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
this.currentStream = await navigator.mediaDevices.getUserMedia(streamConstraints);
|
|
118
|
+
return this.currentStream;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
console.error('[CameraService] Error starting camera:', error);
|
|
122
|
+
if (error instanceof Error && error.name === 'NotAllowedError') {
|
|
123
|
+
throw this.createError('Camera permission denied by user. Please allow camera access in your browser settings and refresh the page.', error);
|
|
124
|
+
}
|
|
125
|
+
else if (error instanceof Error && error.name === 'NotFoundError') {
|
|
126
|
+
throw this.createError('No camera device found. Please connect a camera and try again.', error);
|
|
127
|
+
}
|
|
128
|
+
else if (error instanceof Error && error.name === 'NotReadableError') {
|
|
129
|
+
throw this.createError('Camera is already in use by another application. Please close other applications using the camera and try again.', error);
|
|
130
|
+
}
|
|
131
|
+
else if (error instanceof Error && error.name === 'OverconstrainedError') {
|
|
132
|
+
throw this.createError('Camera constraints cannot be satisfied. Try with different camera settings.', error);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
136
|
+
throw this.createError(`Camera error: ${errorMessage}`, error);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
stopCamera() {
|
|
141
|
+
if (this.currentStream) {
|
|
142
|
+
this.currentStream.getTracks().forEach((track) => {
|
|
143
|
+
track.stop();
|
|
144
|
+
});
|
|
145
|
+
this.currentStream = null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async switchCamera(deviceId, constraints = {
|
|
149
|
+
video: {
|
|
150
|
+
width: { ideal: 1280 },
|
|
151
|
+
height: { ideal: 720 },
|
|
152
|
+
},
|
|
153
|
+
}) {
|
|
154
|
+
this.stopCamera();
|
|
155
|
+
const finalConstraints = {
|
|
156
|
+
video: constraints.video && typeof constraints.video === 'object'
|
|
157
|
+
? { ...constraints.video, deviceId: { exact: deviceId } }
|
|
158
|
+
: { deviceId: { exact: deviceId } },
|
|
159
|
+
};
|
|
160
|
+
return this.startCamera(finalConstraints);
|
|
161
|
+
}
|
|
162
|
+
async getCameraCapabilities(deviceId) {
|
|
163
|
+
this.ensureCameraSupport();
|
|
164
|
+
try {
|
|
165
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
166
|
+
video: { deviceId: { exact: deviceId } },
|
|
167
|
+
});
|
|
168
|
+
const videoTrack = stream.getVideoTracks()[0];
|
|
169
|
+
const capabilities = videoTrack.getCapabilities();
|
|
170
|
+
// Clean up the stream
|
|
171
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
172
|
+
return capabilities || null;
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
console.error('[CameraService] Error getting camera capabilities:', error);
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
getCurrentStream() {
|
|
180
|
+
return this.currentStream;
|
|
181
|
+
}
|
|
182
|
+
isStreaming() {
|
|
183
|
+
return this.currentStream !== null;
|
|
184
|
+
}
|
|
185
|
+
async getVideoInputDevices() {
|
|
186
|
+
this.ensureCameraSupport();
|
|
187
|
+
try {
|
|
188
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
189
|
+
return devices.filter((device) => device.kind === 'videoinput');
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
console.error('[CameraService] Error enumerating video devices:', error);
|
|
193
|
+
throw this.createError('Failed to enumerate video devices', error);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Direct access to native camera API
|
|
197
|
+
getNativeMediaDevices() {
|
|
198
|
+
this.ensureCameraSupport();
|
|
199
|
+
return navigator.mediaDevices;
|
|
200
|
+
}
|
|
201
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: CameraService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
202
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: CameraService });
|
|
203
|
+
}
|
|
204
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: CameraService, decorators: [{
|
|
205
|
+
type: Injectable
|
|
206
|
+
}] });
|
|
207
|
+
|
|
208
|
+
class GeolocationService extends BrowserApiBaseService {
|
|
209
|
+
getApiName() {
|
|
210
|
+
return 'geolocation';
|
|
211
|
+
}
|
|
212
|
+
ensureGeolocationSupport() {
|
|
213
|
+
if (!('geolocation' in navigator)) {
|
|
214
|
+
throw new Error('Geolocation API not supported in this browser');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
getCurrentPosition(options) {
|
|
218
|
+
this.ensureGeolocationSupport();
|
|
219
|
+
return new Promise((resolve, reject) => {
|
|
220
|
+
navigator.geolocation.getCurrentPosition((position) => resolve(position), (error) => {
|
|
221
|
+
console.error('[GeolocationService] Error getting position:', error);
|
|
222
|
+
reject(error);
|
|
223
|
+
}, options);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
watchPosition(options) {
|
|
227
|
+
this.ensureGeolocationSupport();
|
|
228
|
+
return new Observable((observer) => {
|
|
229
|
+
const watchId = navigator.geolocation.watchPosition((position) => observer.next(position), (error) => {
|
|
230
|
+
console.error('[GeolocationService] Error watching position:', error);
|
|
231
|
+
observer.error(error);
|
|
232
|
+
}, options);
|
|
233
|
+
return () => {
|
|
234
|
+
navigator.geolocation.clearWatch(watchId);
|
|
235
|
+
};
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
clearWatch(watchId) {
|
|
239
|
+
navigator.geolocation.clearWatch(watchId);
|
|
240
|
+
}
|
|
241
|
+
// Direct access to native geolocation API
|
|
242
|
+
getNativeGeolocation() {
|
|
243
|
+
this.ensureGeolocationSupport();
|
|
244
|
+
return navigator.geolocation;
|
|
245
|
+
}
|
|
246
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: GeolocationService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
247
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: GeolocationService });
|
|
248
|
+
}
|
|
249
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: GeolocationService, decorators: [{
|
|
250
|
+
type: Injectable
|
|
251
|
+
}] });
|
|
252
|
+
|
|
253
|
+
class MediaDevicesService extends BrowserApiBaseService {
|
|
254
|
+
getApiName() {
|
|
255
|
+
return 'media-devices';
|
|
256
|
+
}
|
|
257
|
+
ensureMediaDevicesSupport() {
|
|
258
|
+
if (!('mediaDevices' in navigator)) {
|
|
259
|
+
throw new Error('MediaDevices API not supported in this browser');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async getDevices() {
|
|
263
|
+
this.ensureMediaDevicesSupport();
|
|
264
|
+
try {
|
|
265
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
266
|
+
return devices;
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
console.error('[MediaDevicesService] Error enumerating devices:', error);
|
|
270
|
+
throw this.createError('Failed to enumerate media devices', error);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
async getUserMedia(constraints) {
|
|
274
|
+
this.ensureMediaDevicesSupport();
|
|
275
|
+
try {
|
|
276
|
+
const defaultConstraints = {
|
|
277
|
+
video: true,
|
|
278
|
+
audio: true,
|
|
279
|
+
};
|
|
280
|
+
const finalConstraints = constraints || defaultConstraints;
|
|
281
|
+
return await navigator.mediaDevices.getUserMedia(finalConstraints);
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
console.error('[MediaDevicesService] Error getting user media:', error);
|
|
285
|
+
throw this.handleMediaError(error);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async getDisplayMedia(constraints) {
|
|
289
|
+
this.ensureMediaDevicesSupport();
|
|
290
|
+
if (!('getDisplayMedia' in navigator.mediaDevices)) {
|
|
291
|
+
throw new Error('Display media API not supported in this browser');
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
const defaultConstraints = {
|
|
295
|
+
video: true,
|
|
296
|
+
audio: false,
|
|
297
|
+
};
|
|
298
|
+
const finalConstraints = constraints || defaultConstraints;
|
|
299
|
+
return await navigator.mediaDevices.getDisplayMedia(finalConstraints);
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
console.error('[MediaDevicesService] Error getting display media:', error);
|
|
303
|
+
throw this.createError('Failed to get display media', error);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
watchDeviceChanges() {
|
|
307
|
+
this.ensureMediaDevicesSupport();
|
|
308
|
+
return new Observable((observer) => {
|
|
309
|
+
const handleDeviceChange = async () => {
|
|
310
|
+
try {
|
|
311
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
312
|
+
observer.next(devices);
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
console.error('[MediaDevicesService] Error handling device change:', error);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
// Listen for device changes
|
|
319
|
+
navigator.mediaDevices.addEventListener('devicechange', handleDeviceChange);
|
|
320
|
+
// Get initial devices
|
|
321
|
+
handleDeviceChange();
|
|
322
|
+
return () => {
|
|
323
|
+
navigator.mediaDevices.removeEventListener('devicechange', handleDeviceChange);
|
|
324
|
+
};
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
getVideoInputDevices() {
|
|
328
|
+
return this.getDevicesByKind('videoinput');
|
|
329
|
+
}
|
|
330
|
+
getAudioInputDevices() {
|
|
331
|
+
return this.getDevicesByKind('audioinput');
|
|
332
|
+
}
|
|
333
|
+
getAudioOutputDevices() {
|
|
334
|
+
return this.getDevicesByKind('audiooutput');
|
|
335
|
+
}
|
|
336
|
+
async getDevicesByKind(kind) {
|
|
337
|
+
const devices = await this.getDevices();
|
|
338
|
+
return devices.filter((device) => device.kind === kind);
|
|
339
|
+
}
|
|
340
|
+
handleMediaError(error) {
|
|
341
|
+
let message;
|
|
342
|
+
if (error instanceof Error) {
|
|
343
|
+
switch (error.name) {
|
|
344
|
+
case 'NotAllowedError':
|
|
345
|
+
message = 'Permission denied by user';
|
|
346
|
+
break;
|
|
347
|
+
case 'NotFoundError':
|
|
348
|
+
message = 'No media device found';
|
|
349
|
+
break;
|
|
350
|
+
case 'NotReadableError':
|
|
351
|
+
message = 'Media device is already in use';
|
|
352
|
+
break;
|
|
353
|
+
case 'OverconstrainedError':
|
|
354
|
+
message = 'Media constraints cannot be satisfied';
|
|
355
|
+
break;
|
|
356
|
+
case 'TypeError':
|
|
357
|
+
message = 'Invalid media constraints provided';
|
|
358
|
+
break;
|
|
359
|
+
default:
|
|
360
|
+
message = `Media error: ${error.message}`;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
message = 'Unknown media error occurred';
|
|
365
|
+
}
|
|
366
|
+
return this.createError(message, error);
|
|
367
|
+
}
|
|
368
|
+
// Direct access to native media devices API
|
|
369
|
+
getNativeMediaDevices() {
|
|
370
|
+
this.ensureMediaDevicesSupport();
|
|
371
|
+
return navigator.mediaDevices;
|
|
372
|
+
}
|
|
373
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: MediaDevicesService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
374
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: MediaDevicesService });
|
|
375
|
+
}
|
|
376
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: MediaDevicesService, decorators: [{
|
|
377
|
+
type: Injectable
|
|
378
|
+
}] });
|
|
379
|
+
|
|
380
|
+
class NotificationService extends BrowserApiBaseService {
|
|
381
|
+
getApiName() {
|
|
382
|
+
return 'notifications';
|
|
383
|
+
}
|
|
384
|
+
get permission() {
|
|
385
|
+
return Notification.permission;
|
|
386
|
+
}
|
|
387
|
+
isSupported() {
|
|
388
|
+
return 'Notification' in window;
|
|
389
|
+
}
|
|
390
|
+
async requestNotificationPermission() {
|
|
391
|
+
if (!this.isSupported()) {
|
|
392
|
+
throw new Error('Notification API not supported in this browser');
|
|
393
|
+
}
|
|
394
|
+
return Notification.requestPermission();
|
|
395
|
+
}
|
|
396
|
+
async showNotification(title, options) {
|
|
397
|
+
if (!this.isSupported()) {
|
|
398
|
+
throw new Error('Notification API not supported in this browser');
|
|
399
|
+
}
|
|
400
|
+
if (Notification.permission !== 'granted') {
|
|
401
|
+
throw new Error('Notification permission required. Please grant notification access and try again.');
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
return new Notification(title, options);
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
console.error('[NotificationService] Error showing notification:', error);
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: NotificationService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
412
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: NotificationService });
|
|
413
|
+
}
|
|
414
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: NotificationService, decorators: [{
|
|
415
|
+
type: Injectable
|
|
416
|
+
}] });
|
|
417
|
+
|
|
418
|
+
class ClipboardService extends BrowserApiBaseService {
|
|
419
|
+
getApiName() {
|
|
420
|
+
return 'clipboard';
|
|
421
|
+
}
|
|
422
|
+
async ensureClipboardPermission(action) {
|
|
423
|
+
if (!('clipboard' in navigator)) {
|
|
424
|
+
throw new Error('Clipboard API not supported in this browser');
|
|
425
|
+
}
|
|
426
|
+
const permissionStatus = await this.permissionsService.query({
|
|
427
|
+
name: `clipboard-${action}`,
|
|
428
|
+
});
|
|
429
|
+
if (permissionStatus.state !== 'granted') {
|
|
430
|
+
throw new Error(`Clipboard ${action} permission required. Please grant clipboard access and try again.`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
async writeText(text) {
|
|
434
|
+
await this.ensureClipboardPermission('write');
|
|
435
|
+
try {
|
|
436
|
+
await navigator.clipboard.writeText(text);
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
console.error('[ClipboardService] Error writing to clipboard:', error);
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
async readText() {
|
|
444
|
+
await this.ensureClipboardPermission('read');
|
|
445
|
+
try {
|
|
446
|
+
return await navigator.clipboard.readText();
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
console.error('[ClipboardService] Error reading from clipboard:', error);
|
|
450
|
+
throw error;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
async writeTextSecure(text) {
|
|
454
|
+
await this.ensureClipboardPermission('write');
|
|
455
|
+
try {
|
|
456
|
+
await navigator.clipboard.writeText(text);
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
console.error('[ClipboardService] Error writing to clipboard:', error);
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ClipboardService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
464
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ClipboardService });
|
|
465
|
+
}
|
|
466
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ClipboardService, decorators: [{
|
|
467
|
+
type: Injectable
|
|
468
|
+
}] });
|
|
469
|
+
|
|
470
|
+
const BROWSER_CAPABILITIES = [
|
|
471
|
+
{ id: 'permissions', label: 'Permissions API', requiresSecureContext: false },
|
|
472
|
+
{ id: 'geolocation', label: 'Geolocation API', requiresSecureContext: true },
|
|
473
|
+
{ id: 'clipboard', label: 'Clipboard API', requiresSecureContext: true },
|
|
474
|
+
{ id: 'notification', label: 'Notification API', requiresSecureContext: true },
|
|
475
|
+
{ id: 'mediaDevices', label: 'MediaDevices API', requiresSecureContext: true },
|
|
476
|
+
{ id: 'camera', label: 'Camera API', requiresSecureContext: true },
|
|
477
|
+
{ id: 'webWorker', label: 'Web Worker API', requiresSecureContext: false },
|
|
478
|
+
{ id: 'regexSecurity', label: 'Regex Security', requiresSecureContext: false },
|
|
479
|
+
{ id: 'webStorage', label: 'Web Storage', requiresSecureContext: false },
|
|
480
|
+
{ id: 'webShare', label: 'Web Share', requiresSecureContext: true },
|
|
481
|
+
{ id: 'battery', label: 'Battery API', requiresSecureContext: false },
|
|
482
|
+
{ id: 'webSocket', label: 'WebSocket API', requiresSecureContext: false },
|
|
483
|
+
];
|
|
484
|
+
class BrowserCapabilityService {
|
|
485
|
+
getCapabilities() {
|
|
486
|
+
return BROWSER_CAPABILITIES;
|
|
487
|
+
}
|
|
488
|
+
isSecureContext() {
|
|
489
|
+
return typeof window !== 'undefined' && window.isSecureContext;
|
|
490
|
+
}
|
|
491
|
+
isSupported(capability) {
|
|
492
|
+
switch (capability) {
|
|
493
|
+
case 'permissions':
|
|
494
|
+
return typeof navigator !== 'undefined' && 'permissions' in navigator;
|
|
495
|
+
case 'geolocation':
|
|
496
|
+
return typeof navigator !== 'undefined' && 'geolocation' in navigator;
|
|
497
|
+
case 'clipboard':
|
|
498
|
+
return typeof navigator !== 'undefined' && 'clipboard' in navigator;
|
|
499
|
+
case 'notification':
|
|
500
|
+
return typeof window !== 'undefined' && 'Notification' in window;
|
|
501
|
+
case 'mediaDevices':
|
|
502
|
+
case 'camera':
|
|
503
|
+
return typeof navigator !== 'undefined' && 'mediaDevices' in navigator;
|
|
504
|
+
case 'webWorker':
|
|
505
|
+
case 'regexSecurity':
|
|
506
|
+
return typeof Worker !== 'undefined';
|
|
507
|
+
case 'webStorage':
|
|
508
|
+
return typeof Storage !== 'undefined';
|
|
509
|
+
case 'webShare':
|
|
510
|
+
return typeof navigator !== 'undefined' && 'share' in navigator;
|
|
511
|
+
case 'battery':
|
|
512
|
+
return typeof navigator !== 'undefined' && 'getBattery' in navigator;
|
|
513
|
+
case 'webSocket':
|
|
514
|
+
return typeof WebSocket !== 'undefined';
|
|
515
|
+
default:
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
getAllStatuses() {
|
|
520
|
+
const secureContext = this.isSecureContext();
|
|
521
|
+
return this.getCapabilities().map((capability) => ({
|
|
522
|
+
id: capability.id,
|
|
523
|
+
label: capability.label,
|
|
524
|
+
supported: this.isSupported(capability.id),
|
|
525
|
+
secureContext,
|
|
526
|
+
requiresSecureContext: capability.requiresSecureContext,
|
|
527
|
+
}));
|
|
528
|
+
}
|
|
529
|
+
async getPermissionState(permission) {
|
|
530
|
+
if (typeof navigator === 'undefined' || !('permissions' in navigator)) {
|
|
531
|
+
return 'unknown';
|
|
532
|
+
}
|
|
533
|
+
try {
|
|
534
|
+
const status = await navigator.permissions.query({ name: permission });
|
|
535
|
+
return status.state;
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
return 'unknown';
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BrowserCapabilityService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
542
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BrowserCapabilityService, providedIn: 'root' });
|
|
543
|
+
}
|
|
544
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BrowserCapabilityService, decorators: [{
|
|
545
|
+
type: Injectable,
|
|
546
|
+
args: [{ providedIn: 'root' }]
|
|
547
|
+
}] });
|
|
548
|
+
|
|
549
|
+
class BatteryService extends BrowserApiBaseService {
|
|
550
|
+
batteryManager = null;
|
|
551
|
+
getApiName() {
|
|
552
|
+
return 'battery';
|
|
553
|
+
}
|
|
554
|
+
ensureBatterySupport() {
|
|
555
|
+
const nav = navigator;
|
|
556
|
+
if (!('getBattery' in nav)) {
|
|
557
|
+
throw new Error('Battery API not supported in this browser');
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
async initialize() {
|
|
561
|
+
this.ensureBatterySupport();
|
|
562
|
+
try {
|
|
563
|
+
const nav = navigator;
|
|
564
|
+
this.batteryManager = await nav.getBattery();
|
|
565
|
+
const batteryInfo = this.getBatteryInfo();
|
|
566
|
+
this.setupEventListeners();
|
|
567
|
+
return batteryInfo;
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
console.error('[BatteryService] Error initializing battery API:', error);
|
|
571
|
+
throw this.createError('Failed to initialize battery API', error);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
getBatteryInfo() {
|
|
575
|
+
if (!this.batteryManager) {
|
|
576
|
+
throw new Error('Battery service not initialized. Call initialize() first.');
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
charging: this.batteryManager.charging,
|
|
580
|
+
chargingTime: this.batteryManager.chargingTime,
|
|
581
|
+
dischargingTime: this.batteryManager.dischargingTime,
|
|
582
|
+
level: this.batteryManager.level,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
watchBatteryInfo() {
|
|
586
|
+
if (!this.batteryManager) {
|
|
587
|
+
throw new Error('Battery service not initialized. Call initialize() first.');
|
|
588
|
+
}
|
|
589
|
+
return new Observable((observer) => {
|
|
590
|
+
const updateBatteryInfo = () => {
|
|
591
|
+
observer.next(this.getBatteryInfo());
|
|
592
|
+
};
|
|
593
|
+
// Listen to all battery events
|
|
594
|
+
this.batteryManager.addEventListener('chargingchange', updateBatteryInfo);
|
|
595
|
+
this.batteryManager.addEventListener('levelchange', updateBatteryInfo);
|
|
596
|
+
this.batteryManager.addEventListener('chargingtimechange', updateBatteryInfo);
|
|
597
|
+
this.batteryManager.addEventListener('dischargingtimechange', updateBatteryInfo);
|
|
598
|
+
// Send initial value
|
|
599
|
+
updateBatteryInfo();
|
|
600
|
+
return () => {
|
|
601
|
+
// Cleanup event listeners
|
|
602
|
+
this.batteryManager.removeEventListener('chargingchange', updateBatteryInfo);
|
|
603
|
+
this.batteryManager.removeEventListener('levelchange', updateBatteryInfo);
|
|
604
|
+
this.batteryManager.removeEventListener('chargingtimechange', updateBatteryInfo);
|
|
605
|
+
this.batteryManager.removeEventListener('dischargingtimechange', updateBatteryInfo);
|
|
606
|
+
};
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
setupEventListeners() {
|
|
610
|
+
if (!this.batteryManager)
|
|
611
|
+
return;
|
|
612
|
+
this.batteryManager.addEventListener('chargingchange', () => {
|
|
613
|
+
console.log('[BatteryService] Charging status changed:', this.batteryManager.charging);
|
|
614
|
+
});
|
|
615
|
+
this.batteryManager.addEventListener('levelchange', () => {
|
|
616
|
+
console.log('[BatteryService] Battery level changed:', this.batteryManager.level);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
// Direct access to native battery API
|
|
620
|
+
getNativeBatteryManager() {
|
|
621
|
+
if (!this.batteryManager) {
|
|
622
|
+
throw new Error('Battery service not initialized. Call initialize() first.');
|
|
623
|
+
}
|
|
624
|
+
return this.batteryManager;
|
|
625
|
+
}
|
|
626
|
+
isCharging() {
|
|
627
|
+
return this.getBatteryInfo().charging;
|
|
628
|
+
}
|
|
629
|
+
getLevel() {
|
|
630
|
+
return this.getBatteryInfo().level;
|
|
631
|
+
}
|
|
632
|
+
getChargingTime() {
|
|
633
|
+
return this.getBatteryInfo().chargingTime;
|
|
634
|
+
}
|
|
635
|
+
getDischargingTime() {
|
|
636
|
+
return this.getBatteryInfo().dischargingTime;
|
|
637
|
+
}
|
|
638
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BatteryService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
639
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BatteryService });
|
|
640
|
+
}
|
|
641
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BatteryService, decorators: [{
|
|
642
|
+
type: Injectable
|
|
643
|
+
}] });
|
|
644
|
+
|
|
645
|
+
class WebShareService extends BrowserApiBaseService {
|
|
646
|
+
getApiName() {
|
|
647
|
+
return 'web-share';
|
|
648
|
+
}
|
|
649
|
+
ensureWebShareSupport() {
|
|
650
|
+
if (!('share' in navigator)) {
|
|
651
|
+
throw new Error('Web Share API not supported in this browser');
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
async share(data) {
|
|
655
|
+
this.ensureWebShareSupport();
|
|
656
|
+
try {
|
|
657
|
+
await navigator.share(data);
|
|
658
|
+
return { shared: true };
|
|
659
|
+
}
|
|
660
|
+
catch (error) {
|
|
661
|
+
console.error('[WebShareService] Error sharing:', error);
|
|
662
|
+
const errorMessage = error instanceof Error ? error.message : 'Share failed';
|
|
663
|
+
return { shared: false, error: errorMessage };
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
canShare() {
|
|
667
|
+
return 'share' in navigator;
|
|
668
|
+
}
|
|
669
|
+
canShareFiles() {
|
|
670
|
+
if (!('share' in navigator))
|
|
671
|
+
return false;
|
|
672
|
+
// Check if the browser supports file sharing
|
|
673
|
+
const testFiles = [new File([''], 'test.txt', { type: 'text/plain' })];
|
|
674
|
+
return navigator.canShare?.({ files: testFiles }) ?? false;
|
|
675
|
+
}
|
|
676
|
+
// Direct access to native share API
|
|
677
|
+
getNativeShare() {
|
|
678
|
+
this.ensureWebShareSupport();
|
|
679
|
+
return navigator.share;
|
|
680
|
+
}
|
|
681
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebShareService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
682
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebShareService });
|
|
683
|
+
}
|
|
684
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebShareService, decorators: [{
|
|
685
|
+
type: Injectable
|
|
686
|
+
}] });
|
|
687
|
+
|
|
688
|
+
class WebStorageService extends BrowserApiBaseService {
|
|
689
|
+
storageEvents = signal(null, ...(ngDevMode ? [{ debugName: "storageEvents" }] : /* istanbul ignore next */ []));
|
|
690
|
+
destroyRef = inject(DestroyRef);
|
|
691
|
+
constructor() {
|
|
692
|
+
super();
|
|
693
|
+
this.setupEventListeners();
|
|
694
|
+
}
|
|
695
|
+
getApiName() {
|
|
696
|
+
return 'storage';
|
|
697
|
+
}
|
|
698
|
+
ensureStorageSupport() {
|
|
699
|
+
if (!this.isBrowserEnvironment() || typeof Storage === 'undefined') {
|
|
700
|
+
throw new Error('Storage API not supported in this browser');
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
setupEventListeners() {
|
|
704
|
+
if (this.isBrowserEnvironment()) {
|
|
705
|
+
fromEvent(window, 'storage')
|
|
706
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
707
|
+
.subscribe((event) => {
|
|
708
|
+
const storageEvent = event;
|
|
709
|
+
if (storageEvent.key && storageEvent.newValue !== null) {
|
|
710
|
+
this.storageEvents.set({
|
|
711
|
+
key: storageEvent.key,
|
|
712
|
+
newValue: this.deserializeValue(storageEvent.newValue),
|
|
713
|
+
oldValue: storageEvent.oldValue ? this.deserializeValue(storageEvent.oldValue) : null,
|
|
714
|
+
storageArea: storageEvent.storageArea === localStorage ? 'localStorage' : 'sessionStorage',
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
serializeValue(value, options) {
|
|
721
|
+
if (options?.serialize) {
|
|
722
|
+
return options.serialize(value);
|
|
723
|
+
}
|
|
724
|
+
return JSON.stringify(value);
|
|
725
|
+
}
|
|
726
|
+
deserializeValue(value, options) {
|
|
727
|
+
if (value === null)
|
|
728
|
+
return null;
|
|
729
|
+
if (options?.deserialize) {
|
|
730
|
+
return options.deserialize(value);
|
|
731
|
+
}
|
|
732
|
+
try {
|
|
733
|
+
return JSON.parse(value);
|
|
734
|
+
}
|
|
735
|
+
catch {
|
|
736
|
+
return value;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
getKey(key, options) {
|
|
740
|
+
const prefix = options?.prefix || '';
|
|
741
|
+
return prefix ? `${prefix}:${key}` : key;
|
|
742
|
+
}
|
|
743
|
+
// Local Storage Methods
|
|
744
|
+
setLocalStorage(key, value, options = {}) {
|
|
745
|
+
this.ensureStorageSupport();
|
|
746
|
+
try {
|
|
747
|
+
const serializedValue = this.serializeValue(value, options);
|
|
748
|
+
const fullKey = this.getKey(key, options);
|
|
749
|
+
localStorage.setItem(fullKey, serializedValue);
|
|
750
|
+
return true;
|
|
751
|
+
}
|
|
752
|
+
catch (error) {
|
|
753
|
+
console.error('[WebStorageService] Error setting localStorage:', error);
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
getLocalStorage(key, defaultValue = null, options = {}) {
|
|
758
|
+
this.ensureStorageSupport();
|
|
759
|
+
try {
|
|
760
|
+
const fullKey = this.getKey(key, options);
|
|
761
|
+
const value = localStorage.getItem(fullKey);
|
|
762
|
+
return value !== null ? this.deserializeValue(value, options) : defaultValue;
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
console.error('[WebStorageService] Error getting localStorage:', error);
|
|
766
|
+
return defaultValue;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
removeLocalStorage(key, options = {}) {
|
|
770
|
+
this.ensureStorageSupport();
|
|
771
|
+
try {
|
|
772
|
+
const fullKey = this.getKey(key, options);
|
|
773
|
+
localStorage.removeItem(fullKey);
|
|
774
|
+
return true;
|
|
775
|
+
}
|
|
776
|
+
catch (error) {
|
|
777
|
+
console.error('[WebStorageService] Error removing localStorage:', error);
|
|
778
|
+
return false;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
clearLocalStorage(options = {}) {
|
|
782
|
+
this.ensureStorageSupport();
|
|
783
|
+
try {
|
|
784
|
+
const prefix = options?.prefix;
|
|
785
|
+
if (prefix) {
|
|
786
|
+
// Only remove keys with the specified prefix
|
|
787
|
+
const keysToRemove = [];
|
|
788
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
789
|
+
const key = localStorage.key(i);
|
|
790
|
+
if (key && key.startsWith(`${prefix}:`)) {
|
|
791
|
+
keysToRemove.push(key);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
keysToRemove.forEach((key) => localStorage.removeItem(key));
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
localStorage.clear();
|
|
798
|
+
}
|
|
799
|
+
return true;
|
|
800
|
+
}
|
|
801
|
+
catch (error) {
|
|
802
|
+
console.error('[WebStorageService] Error clearing localStorage:', error);
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
// Session Storage Methods
|
|
807
|
+
setSessionStorage(key, value, options = {}) {
|
|
808
|
+
this.ensureStorageSupport();
|
|
809
|
+
try {
|
|
810
|
+
const serializedValue = this.serializeValue(value, options);
|
|
811
|
+
const fullKey = this.getKey(key, options);
|
|
812
|
+
sessionStorage.setItem(fullKey, serializedValue);
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
815
|
+
catch (error) {
|
|
816
|
+
console.error('[WebStorageService] Error setting sessionStorage:', error);
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
getSessionStorage(key, defaultValue = null, options = {}) {
|
|
821
|
+
this.ensureStorageSupport();
|
|
822
|
+
try {
|
|
823
|
+
const fullKey = this.getKey(key, options);
|
|
824
|
+
const value = sessionStorage.getItem(fullKey);
|
|
825
|
+
return value !== null ? this.deserializeValue(value, options) : defaultValue;
|
|
826
|
+
}
|
|
827
|
+
catch (error) {
|
|
828
|
+
console.error('[WebStorageService] Error getting sessionStorage:', error);
|
|
829
|
+
return defaultValue;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
removeSessionStorage(key, options = {}) {
|
|
833
|
+
this.ensureStorageSupport();
|
|
834
|
+
try {
|
|
835
|
+
const fullKey = this.getKey(key, options);
|
|
836
|
+
sessionStorage.removeItem(fullKey);
|
|
837
|
+
return true;
|
|
838
|
+
}
|
|
839
|
+
catch (error) {
|
|
840
|
+
console.error('[WebStorageService] Error removing sessionStorage:', error);
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
clearSessionStorage(options = {}) {
|
|
845
|
+
this.ensureStorageSupport();
|
|
846
|
+
try {
|
|
847
|
+
const prefix = options?.prefix;
|
|
848
|
+
if (prefix) {
|
|
849
|
+
// Only remove keys with the specified prefix
|
|
850
|
+
const keysToRemove = [];
|
|
851
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
|
852
|
+
const key = sessionStorage.key(i);
|
|
853
|
+
if (key && key.startsWith(`${prefix}:`)) {
|
|
854
|
+
keysToRemove.push(key);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
keysToRemove.forEach((key) => sessionStorage.removeItem(key));
|
|
858
|
+
}
|
|
859
|
+
else {
|
|
860
|
+
sessionStorage.clear();
|
|
861
|
+
}
|
|
862
|
+
return true;
|
|
863
|
+
}
|
|
864
|
+
catch (error) {
|
|
865
|
+
console.error('[WebStorageService] Error clearing sessionStorage:', error);
|
|
866
|
+
return false;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
// Utility Methods
|
|
870
|
+
getLocalStorageSize(options = {}) {
|
|
871
|
+
this.ensureStorageSupport();
|
|
872
|
+
let totalSize = 0;
|
|
873
|
+
const prefix = options?.prefix;
|
|
874
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
875
|
+
const key = localStorage.key(i);
|
|
876
|
+
if (key && (!prefix || key.startsWith(`${prefix}:`))) {
|
|
877
|
+
totalSize += (localStorage.getItem(key)?.length || 0) + key.length;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return totalSize;
|
|
881
|
+
}
|
|
882
|
+
getSessionStorageSize(options = {}) {
|
|
883
|
+
this.ensureStorageSupport();
|
|
884
|
+
let totalSize = 0;
|
|
885
|
+
const prefix = options?.prefix;
|
|
886
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
|
887
|
+
const key = sessionStorage.key(i);
|
|
888
|
+
if (key && (!prefix || key.startsWith(`${prefix}:`))) {
|
|
889
|
+
totalSize += (sessionStorage.getItem(key)?.length || 0) + key.length;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return totalSize;
|
|
893
|
+
}
|
|
894
|
+
getStorageEvents() {
|
|
895
|
+
return toObservable(this.storageEvents).pipe(filter((event) => event !== null), distinctUntilChanged((prev, curr) => prev.key === curr.key &&
|
|
896
|
+
prev.newValue === curr.newValue &&
|
|
897
|
+
prev.oldValue === curr.oldValue));
|
|
898
|
+
}
|
|
899
|
+
watchLocalStorage(key, options = {}) {
|
|
900
|
+
return this.getStorageEvents().pipe(map((event) => {
|
|
901
|
+
const fullKey = this.getKey(key, options);
|
|
902
|
+
if (event.key === fullKey && event.storageArea === 'localStorage') {
|
|
903
|
+
return event.newValue;
|
|
904
|
+
}
|
|
905
|
+
return this.getLocalStorage(key, null, options);
|
|
906
|
+
}));
|
|
907
|
+
}
|
|
908
|
+
watchSessionStorage(key, options = {}) {
|
|
909
|
+
return this.getStorageEvents().pipe(map((event) => {
|
|
910
|
+
const fullKey = this.getKey(key, options);
|
|
911
|
+
if (event.key === fullKey && event.storageArea === 'sessionStorage') {
|
|
912
|
+
return event.newValue;
|
|
913
|
+
}
|
|
914
|
+
return this.getSessionStorage(key, null, options);
|
|
915
|
+
}));
|
|
916
|
+
}
|
|
917
|
+
// Direct access to native storage APIs
|
|
918
|
+
getNativeLocalStorage() {
|
|
919
|
+
this.ensureStorageSupport();
|
|
920
|
+
return localStorage;
|
|
921
|
+
}
|
|
922
|
+
getNativeSessionStorage() {
|
|
923
|
+
this.ensureStorageSupport();
|
|
924
|
+
return sessionStorage;
|
|
925
|
+
}
|
|
926
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebStorageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
927
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebStorageService });
|
|
928
|
+
}
|
|
929
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebStorageService, decorators: [{
|
|
930
|
+
type: Injectable
|
|
931
|
+
}], ctorParameters: () => [] });
|
|
932
|
+
|
|
933
|
+
class WebSocketService extends BrowserApiBaseService {
|
|
934
|
+
webSocket = null;
|
|
935
|
+
statusSubject = new Subject();
|
|
936
|
+
messageSubject = new Subject();
|
|
937
|
+
reconnectAttempts = 0;
|
|
938
|
+
reconnectTimer = null;
|
|
939
|
+
heartbeatTimer = null;
|
|
940
|
+
getApiName() {
|
|
941
|
+
return 'websocket';
|
|
942
|
+
}
|
|
943
|
+
ensureWebSocketSupport() {
|
|
944
|
+
if (typeof WebSocket === 'undefined') {
|
|
945
|
+
throw new Error('WebSocket API not supported in this browser');
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
connect(config) {
|
|
949
|
+
this.ensureWebSocketSupport();
|
|
950
|
+
return new Observable((observer) => {
|
|
951
|
+
this.disconnect(); // Disconnect existing connection if any
|
|
952
|
+
this.updateStatus({
|
|
953
|
+
connected: false,
|
|
954
|
+
connecting: true,
|
|
955
|
+
reconnecting: false,
|
|
956
|
+
reconnectAttempts: 0,
|
|
957
|
+
});
|
|
958
|
+
try {
|
|
959
|
+
this.webSocket = new WebSocket(config.url, config.protocols);
|
|
960
|
+
this.setupWebSocketHandlers(config);
|
|
961
|
+
observer.next(this.getCurrentStatus());
|
|
962
|
+
}
|
|
963
|
+
catch (error) {
|
|
964
|
+
console.error('[WebSocketService] Error creating WebSocket:', error);
|
|
965
|
+
this.updateStatus({
|
|
966
|
+
connected: false,
|
|
967
|
+
connecting: false,
|
|
968
|
+
reconnecting: false,
|
|
969
|
+
error: error instanceof Error ? error.message : 'Connection failed',
|
|
970
|
+
reconnectAttempts: 0,
|
|
971
|
+
});
|
|
972
|
+
observer.next(this.getCurrentStatus());
|
|
973
|
+
}
|
|
974
|
+
return () => {
|
|
975
|
+
this.disconnect();
|
|
976
|
+
};
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
disconnect() {
|
|
980
|
+
if (this.reconnectTimer) {
|
|
981
|
+
clearTimeout(this.reconnectTimer);
|
|
982
|
+
this.reconnectTimer = null;
|
|
983
|
+
}
|
|
984
|
+
if (this.heartbeatTimer) {
|
|
985
|
+
clearInterval(this.heartbeatTimer);
|
|
986
|
+
this.heartbeatTimer = null;
|
|
987
|
+
}
|
|
988
|
+
if (this.webSocket) {
|
|
989
|
+
this.webSocket.close();
|
|
990
|
+
this.webSocket = null;
|
|
991
|
+
}
|
|
992
|
+
this.updateStatus({
|
|
993
|
+
connected: false,
|
|
994
|
+
connecting: false,
|
|
995
|
+
reconnecting: false,
|
|
996
|
+
reconnectAttempts: 0,
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
send(message) {
|
|
1000
|
+
if (!this.webSocket || this.webSocket.readyState !== WebSocket.OPEN) {
|
|
1001
|
+
throw new Error('WebSocket is not connected');
|
|
1002
|
+
}
|
|
1003
|
+
const messageWithTimestamp = {
|
|
1004
|
+
...message,
|
|
1005
|
+
timestamp: Date.now(),
|
|
1006
|
+
};
|
|
1007
|
+
this.webSocket.send(JSON.stringify(messageWithTimestamp));
|
|
1008
|
+
}
|
|
1009
|
+
sendRaw(data) {
|
|
1010
|
+
if (!this.webSocket || this.webSocket.readyState !== WebSocket.OPEN) {
|
|
1011
|
+
throw new Error('WebSocket is not connected');
|
|
1012
|
+
}
|
|
1013
|
+
this.webSocket.send(data);
|
|
1014
|
+
}
|
|
1015
|
+
getStatus() {
|
|
1016
|
+
return this.statusSubject.asObservable();
|
|
1017
|
+
}
|
|
1018
|
+
getMessages() {
|
|
1019
|
+
return this.messageSubject
|
|
1020
|
+
.asObservable()
|
|
1021
|
+
.pipe(filter((msg) => true));
|
|
1022
|
+
}
|
|
1023
|
+
setupWebSocketHandlers(config) {
|
|
1024
|
+
if (!this.webSocket)
|
|
1025
|
+
return;
|
|
1026
|
+
this.webSocket.onopen = () => {
|
|
1027
|
+
console.log('[WebSocketService] Connected to:', config.url);
|
|
1028
|
+
this.reconnectAttempts = 0;
|
|
1029
|
+
this.updateStatus({
|
|
1030
|
+
connected: true,
|
|
1031
|
+
connecting: false,
|
|
1032
|
+
reconnecting: false,
|
|
1033
|
+
reconnectAttempts: 0,
|
|
1034
|
+
});
|
|
1035
|
+
// Start heartbeat if configured
|
|
1036
|
+
if (config.heartbeatInterval && config.heartbeatMessage) {
|
|
1037
|
+
this.startHeartbeat(config);
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
this.webSocket.onclose = (event) => {
|
|
1041
|
+
console.log('[WebSocketService] Connection closed:', event.code, event.reason);
|
|
1042
|
+
this.updateStatus({
|
|
1043
|
+
connected: false,
|
|
1044
|
+
connecting: false,
|
|
1045
|
+
reconnecting: false,
|
|
1046
|
+
reconnectAttempts: this.reconnectAttempts,
|
|
1047
|
+
});
|
|
1048
|
+
// Attempt reconnection if not a clean close and reconnect is enabled
|
|
1049
|
+
if (!event.wasClean && config.reconnectInterval && config.maxReconnectAttempts) {
|
|
1050
|
+
this.attemptReconnect(config);
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
this.webSocket.onerror = (error) => {
|
|
1054
|
+
console.error('[WebSocketService] WebSocket error:', error);
|
|
1055
|
+
this.updateStatus({
|
|
1056
|
+
connected: false,
|
|
1057
|
+
connecting: false,
|
|
1058
|
+
reconnecting: false,
|
|
1059
|
+
error: 'WebSocket connection error',
|
|
1060
|
+
reconnectAttempts: this.reconnectAttempts,
|
|
1061
|
+
});
|
|
1062
|
+
};
|
|
1063
|
+
this.webSocket.onmessage = (event) => {
|
|
1064
|
+
try {
|
|
1065
|
+
const message = JSON.parse(event.data);
|
|
1066
|
+
this.messageSubject.next(message);
|
|
1067
|
+
}
|
|
1068
|
+
catch (error) {
|
|
1069
|
+
console.error('[WebSocketService] Error parsing message:', error);
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
startHeartbeat(config) {
|
|
1074
|
+
this.heartbeatTimer = setInterval(() => {
|
|
1075
|
+
if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
|
|
1076
|
+
this.send({
|
|
1077
|
+
type: 'heartbeat',
|
|
1078
|
+
data: config.heartbeatMessage,
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
}, config.heartbeatInterval);
|
|
1082
|
+
}
|
|
1083
|
+
attemptReconnect(config) {
|
|
1084
|
+
if (this.reconnectAttempts >= (config.maxReconnectAttempts || 5)) {
|
|
1085
|
+
console.log('[WebSocketService] Max reconnect attempts reached');
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
this.reconnectAttempts++;
|
|
1089
|
+
this.updateStatus({
|
|
1090
|
+
connected: false,
|
|
1091
|
+
connecting: false,
|
|
1092
|
+
reconnecting: true,
|
|
1093
|
+
reconnectAttempts: this.reconnectAttempts,
|
|
1094
|
+
});
|
|
1095
|
+
this.reconnectTimer = setTimeout(() => {
|
|
1096
|
+
console.log(`[WebSocketService] Reconnect attempt ${this.reconnectAttempts}`);
|
|
1097
|
+
this.connect(config);
|
|
1098
|
+
}, config.reconnectInterval || 3000);
|
|
1099
|
+
}
|
|
1100
|
+
updateStatus(status) {
|
|
1101
|
+
const currentStatus = this.getCurrentStatus();
|
|
1102
|
+
const newStatus = { ...currentStatus, ...status };
|
|
1103
|
+
this.statusSubject.next(newStatus);
|
|
1104
|
+
}
|
|
1105
|
+
getCurrentStatus() {
|
|
1106
|
+
return {
|
|
1107
|
+
connected: this.webSocket?.readyState === WebSocket.OPEN,
|
|
1108
|
+
connecting: false,
|
|
1109
|
+
reconnecting: false,
|
|
1110
|
+
reconnectAttempts: this.reconnectAttempts,
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
// Direct access to native WebSocket
|
|
1114
|
+
getNativeWebSocket() {
|
|
1115
|
+
return this.webSocket;
|
|
1116
|
+
}
|
|
1117
|
+
isConnected() {
|
|
1118
|
+
return this.webSocket?.readyState === WebSocket.OPEN;
|
|
1119
|
+
}
|
|
1120
|
+
getReadyState() {
|
|
1121
|
+
return this.webSocket?.readyState ?? WebSocket.CLOSED;
|
|
1122
|
+
}
|
|
1123
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebSocketService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
1124
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebSocketService });
|
|
1125
|
+
}
|
|
1126
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebSocketService, decorators: [{
|
|
1127
|
+
type: Injectable
|
|
1128
|
+
}] });
|
|
1129
|
+
|
|
1130
|
+
class WebWorkerService extends BrowserApiBaseService {
|
|
1131
|
+
destroyRef = inject(DestroyRef);
|
|
1132
|
+
workers = new Map();
|
|
1133
|
+
workerStatuses = new Map();
|
|
1134
|
+
workerMessages = new Map();
|
|
1135
|
+
currentWorkerStatuses = new Map();
|
|
1136
|
+
getApiName() {
|
|
1137
|
+
return 'webworker';
|
|
1138
|
+
}
|
|
1139
|
+
ensureWorkerSupport() {
|
|
1140
|
+
if (typeof Worker === 'undefined') {
|
|
1141
|
+
throw new Error('Web Workers not supported in this browser');
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
createWorker(name, scriptUrl) {
|
|
1145
|
+
this.ensureWorkerSupport();
|
|
1146
|
+
return new Observable((observer) => {
|
|
1147
|
+
if (this.workers.has(name)) {
|
|
1148
|
+
observer.next(this.currentWorkerStatuses.get(name));
|
|
1149
|
+
return () => {
|
|
1150
|
+
// No-op: workers are managed explicitly via terminateWorker/terminateAllWorkers
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
try {
|
|
1154
|
+
const worker = new Worker(scriptUrl);
|
|
1155
|
+
this.workers.set(name, worker);
|
|
1156
|
+
this.setupWorker(name, worker);
|
|
1157
|
+
const status = {
|
|
1158
|
+
initialized: true,
|
|
1159
|
+
running: true,
|
|
1160
|
+
messageCount: 0,
|
|
1161
|
+
};
|
|
1162
|
+
this.currentWorkerStatuses.set(name, status);
|
|
1163
|
+
this.updateWorkerStatus(name, status);
|
|
1164
|
+
observer.next(status);
|
|
1165
|
+
}
|
|
1166
|
+
catch (error) {
|
|
1167
|
+
console.error(`[WebWorkerService] Failed to create worker ${name}:`, error);
|
|
1168
|
+
const status = {
|
|
1169
|
+
initialized: false,
|
|
1170
|
+
running: false,
|
|
1171
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
1172
|
+
messageCount: 0,
|
|
1173
|
+
};
|
|
1174
|
+
this.currentWorkerStatuses.set(name, status);
|
|
1175
|
+
this.updateWorkerStatus(name, status);
|
|
1176
|
+
observer.next(status);
|
|
1177
|
+
}
|
|
1178
|
+
return () => {
|
|
1179
|
+
// No-op: workers are managed explicitly via terminateWorker/terminateAllWorkers
|
|
1180
|
+
};
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
terminateWorker(name) {
|
|
1184
|
+
const worker = this.workers.get(name);
|
|
1185
|
+
if (worker) {
|
|
1186
|
+
worker.terminate();
|
|
1187
|
+
this.workers.delete(name);
|
|
1188
|
+
this.workerStatuses.delete(name);
|
|
1189
|
+
this.workerMessages.delete(name);
|
|
1190
|
+
this.currentWorkerStatuses.delete(name);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
terminateAllWorkers() {
|
|
1194
|
+
this.workers.forEach((_, name) => {
|
|
1195
|
+
this.terminateWorker(name);
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
postMessage(workerName, task) {
|
|
1199
|
+
const worker = this.workers.get(workerName);
|
|
1200
|
+
if (!worker) {
|
|
1201
|
+
console.error(`[WebWorkerService] Worker ${workerName} not found`);
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
try {
|
|
1205
|
+
const message = { ...task, timestamp: Date.now() };
|
|
1206
|
+
if (task.transferable) {
|
|
1207
|
+
worker.postMessage(message, task.transferable);
|
|
1208
|
+
}
|
|
1209
|
+
else {
|
|
1210
|
+
worker.postMessage(message);
|
|
1211
|
+
}
|
|
1212
|
+
const currentStatus = this.currentWorkerStatuses.get(workerName);
|
|
1213
|
+
if (currentStatus) {
|
|
1214
|
+
currentStatus.messageCount++;
|
|
1215
|
+
this.updateWorkerStatus(workerName, currentStatus);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
catch (error) {
|
|
1219
|
+
console.error(`[WebWorkerService] Failed to post message to worker ${workerName}:`, error);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
getMessages(workerName) {
|
|
1223
|
+
if (!this.workerMessages.has(workerName)) {
|
|
1224
|
+
this.workerMessages.set(workerName, new Subject());
|
|
1225
|
+
}
|
|
1226
|
+
return this.workerMessages.get(workerName).asObservable();
|
|
1227
|
+
}
|
|
1228
|
+
getStatus(workerName) {
|
|
1229
|
+
if (!this.workerStatuses.has(workerName)) {
|
|
1230
|
+
this.workerStatuses.set(workerName, new Subject());
|
|
1231
|
+
}
|
|
1232
|
+
return this.workerStatuses.get(workerName).asObservable();
|
|
1233
|
+
}
|
|
1234
|
+
getCurrentStatus(workerName) {
|
|
1235
|
+
return this.currentWorkerStatuses.get(workerName);
|
|
1236
|
+
}
|
|
1237
|
+
getAllStatuses() {
|
|
1238
|
+
return new Map(this.currentWorkerStatuses);
|
|
1239
|
+
}
|
|
1240
|
+
isWorkerRunning(workerName) {
|
|
1241
|
+
const status = this.currentWorkerStatuses.get(workerName);
|
|
1242
|
+
return status?.running ?? false;
|
|
1243
|
+
}
|
|
1244
|
+
setupWorker(name, worker) {
|
|
1245
|
+
worker.onmessage = (event) => {
|
|
1246
|
+
const message = {
|
|
1247
|
+
id: event.data.id || `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
1248
|
+
type: event.data.type || 'message',
|
|
1249
|
+
data: event.data.data,
|
|
1250
|
+
timestamp: event.data.timestamp || Date.now(),
|
|
1251
|
+
};
|
|
1252
|
+
if (!this.workerMessages.has(name)) {
|
|
1253
|
+
this.workerMessages.set(name, new Subject());
|
|
1254
|
+
}
|
|
1255
|
+
this.workerMessages.get(name).next(message);
|
|
1256
|
+
};
|
|
1257
|
+
worker.onerror = (error) => {
|
|
1258
|
+
console.error(`[WebWorkerService] Worker ${name} error:`, error);
|
|
1259
|
+
const status = {
|
|
1260
|
+
initialized: true,
|
|
1261
|
+
running: false,
|
|
1262
|
+
error: error instanceof Error ? error.message : 'Worker error',
|
|
1263
|
+
messageCount: this.currentWorkerStatuses.get(name)?.messageCount ?? 0,
|
|
1264
|
+
};
|
|
1265
|
+
this.currentWorkerStatuses.set(name, status);
|
|
1266
|
+
this.updateWorkerStatus(name, status);
|
|
1267
|
+
};
|
|
1268
|
+
// Auto-cleanup when service is destroyed
|
|
1269
|
+
this.destroyRef.onDestroy(() => {
|
|
1270
|
+
this.terminateWorker(name);
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
updateWorkerStatus(name, status) {
|
|
1274
|
+
if (!this.workerStatuses.has(name)) {
|
|
1275
|
+
this.workerStatuses.set(name, new Subject());
|
|
1276
|
+
}
|
|
1277
|
+
this.workerStatuses.get(name).next(status);
|
|
1278
|
+
}
|
|
1279
|
+
// Direct access to native Worker API
|
|
1280
|
+
getNativeWorker(name) {
|
|
1281
|
+
return this.workers.get(name);
|
|
1282
|
+
}
|
|
1283
|
+
getAllWorkers() {
|
|
1284
|
+
return new Map(this.workers);
|
|
1285
|
+
}
|
|
1286
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebWorkerService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
1287
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebWorkerService });
|
|
1288
|
+
}
|
|
1289
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebWorkerService, decorators: [{
|
|
1290
|
+
type: Injectable
|
|
1291
|
+
}] });
|
|
1292
|
+
|
|
1293
|
+
// Common types for browser APIs
|
|
1294
|
+
|
|
1295
|
+
class BrowserSupportUtil {
|
|
1296
|
+
static isSupported(feature) {
|
|
1297
|
+
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
|
|
1298
|
+
return false;
|
|
1299
|
+
}
|
|
1300
|
+
switch (feature) {
|
|
1301
|
+
case 'permissions':
|
|
1302
|
+
return 'permissions' in navigator;
|
|
1303
|
+
case 'camera':
|
|
1304
|
+
return 'mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices;
|
|
1305
|
+
case 'microphone':
|
|
1306
|
+
return 'mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices;
|
|
1307
|
+
case 'geolocation':
|
|
1308
|
+
return 'geolocation' in navigator;
|
|
1309
|
+
case 'notifications':
|
|
1310
|
+
return 'Notification' in window;
|
|
1311
|
+
case 'clipboard':
|
|
1312
|
+
return 'clipboard' in navigator;
|
|
1313
|
+
case 'clipboard-read':
|
|
1314
|
+
return 'clipboard' in navigator && 'readText' in navigator.clipboard;
|
|
1315
|
+
case 'clipboard-write':
|
|
1316
|
+
return 'clipboard' in navigator && 'writeText' in navigator.clipboard;
|
|
1317
|
+
case 'persistent-storage':
|
|
1318
|
+
return 'storage' in navigator && 'persist' in navigator.storage;
|
|
1319
|
+
default:
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
static getUnsupportedFeatures() {
|
|
1324
|
+
const features = [
|
|
1325
|
+
'permissions',
|
|
1326
|
+
'camera',
|
|
1327
|
+
'microphone',
|
|
1328
|
+
'geolocation',
|
|
1329
|
+
'notifications',
|
|
1330
|
+
'clipboard',
|
|
1331
|
+
'clipboard-read',
|
|
1332
|
+
'clipboard-write',
|
|
1333
|
+
'persistent-storage',
|
|
1334
|
+
];
|
|
1335
|
+
return features.filter((feature) => !this.isSupported(feature));
|
|
1336
|
+
}
|
|
1337
|
+
static isSecureContext() {
|
|
1338
|
+
return typeof window !== 'undefined' ? window.isSecureContext : false;
|
|
1339
|
+
}
|
|
1340
|
+
static getUserAgent() {
|
|
1341
|
+
return typeof navigator !== 'undefined' ? navigator.userAgent : '';
|
|
1342
|
+
}
|
|
1343
|
+
static isMobile() {
|
|
1344
|
+
const userAgent = this.getUserAgent().toLowerCase();
|
|
1345
|
+
return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent);
|
|
1346
|
+
}
|
|
1347
|
+
static isDesktop() {
|
|
1348
|
+
return !this.isMobile();
|
|
1349
|
+
}
|
|
1350
|
+
static getBrowserInfo() {
|
|
1351
|
+
const userAgent = this.getUserAgent();
|
|
1352
|
+
return {
|
|
1353
|
+
name: this.getBrowserName(userAgent),
|
|
1354
|
+
version: this.getBrowserVersion(userAgent),
|
|
1355
|
+
isChrome: /chrome/.test(userAgent) && !/edge/.test(userAgent),
|
|
1356
|
+
isFirefox: /firefox/.test(userAgent),
|
|
1357
|
+
isSafari: /safari/.test(userAgent) && !/chrome/.test(userAgent),
|
|
1358
|
+
isEdge: /edge/.test(userAgent) || /edg/.test(userAgent),
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
static getBrowserName(userAgent) {
|
|
1362
|
+
if (/chrome/.test(userAgent) && !/edge/.test(userAgent))
|
|
1363
|
+
return 'Chrome';
|
|
1364
|
+
if (/firefox/.test(userAgent))
|
|
1365
|
+
return 'Firefox';
|
|
1366
|
+
if (/safari/.test(userAgent) && !/chrome/.test(userAgent))
|
|
1367
|
+
return 'Safari';
|
|
1368
|
+
if (/edge/.test(userAgent) || /edg/.test(userAgent))
|
|
1369
|
+
return 'Edge';
|
|
1370
|
+
return 'Unknown';
|
|
1371
|
+
}
|
|
1372
|
+
static getBrowserVersion(userAgent) {
|
|
1373
|
+
const match = userAgent.match(/(chrome|firefox|safari|edge|edg)\/(\d+)/i);
|
|
1374
|
+
return match ? match[2] : 'Unknown';
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
/**
|
|
1379
|
+
* Functional guard that checks if the user has the required permission.
|
|
1380
|
+
* Usage in routes: { canActivate: [permissionGuard('camera')] }
|
|
1381
|
+
*/
|
|
1382
|
+
const permissionGuard = (permission) => {
|
|
1383
|
+
return async (_route) => {
|
|
1384
|
+
const permissionsService = inject(PermissionsService);
|
|
1385
|
+
const router = inject(Router);
|
|
1386
|
+
if (!permission) {
|
|
1387
|
+
return true;
|
|
1388
|
+
}
|
|
1389
|
+
try {
|
|
1390
|
+
const status = await permissionsService.query({ name: permission });
|
|
1391
|
+
if (status.state !== 'granted') {
|
|
1392
|
+
router.navigate(['/permission-denied'], {
|
|
1393
|
+
queryParams: { permission },
|
|
1394
|
+
});
|
|
1395
|
+
return false;
|
|
1396
|
+
}
|
|
1397
|
+
return true;
|
|
1398
|
+
}
|
|
1399
|
+
catch (error) {
|
|
1400
|
+
console.error('Permission guard error:', error);
|
|
1401
|
+
router.navigate(['/permission-denied'], {
|
|
1402
|
+
queryParams: { permission },
|
|
1403
|
+
});
|
|
1404
|
+
return false;
|
|
1405
|
+
}
|
|
1406
|
+
};
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1409
|
+
const defaultBrowserWebApisConfig = {
|
|
1410
|
+
enableCamera: true,
|
|
1411
|
+
enableGeolocation: true,
|
|
1412
|
+
enableNotifications: true,
|
|
1413
|
+
enableClipboard: true,
|
|
1414
|
+
enableBattery: false,
|
|
1415
|
+
enableMediaDevices: true,
|
|
1416
|
+
enableWebShare: false,
|
|
1417
|
+
enableWebStorage: false,
|
|
1418
|
+
enableWebSocket: false,
|
|
1419
|
+
enableWebWorker: false,
|
|
1420
|
+
};
|
|
1421
|
+
function provideBrowserWebApis(config = {}) {
|
|
1422
|
+
const mergedConfig = { ...defaultBrowserWebApisConfig, ...config };
|
|
1423
|
+
const providers = [PermissionsService];
|
|
1424
|
+
const conditionalProviders = [
|
|
1425
|
+
[mergedConfig.enableCamera, CameraService],
|
|
1426
|
+
[mergedConfig.enableGeolocation, GeolocationService],
|
|
1427
|
+
[mergedConfig.enableNotifications, NotificationService],
|
|
1428
|
+
[mergedConfig.enableClipboard, ClipboardService],
|
|
1429
|
+
[mergedConfig.enableMediaDevices, MediaDevicesService],
|
|
1430
|
+
[mergedConfig.enableBattery, BatteryService],
|
|
1431
|
+
[mergedConfig.enableWebShare, WebShareService],
|
|
1432
|
+
[mergedConfig.enableWebStorage, WebStorageService],
|
|
1433
|
+
[mergedConfig.enableWebSocket, WebSocketService],
|
|
1434
|
+
[mergedConfig.enableWebWorker, WebWorkerService],
|
|
1435
|
+
];
|
|
1436
|
+
for (const [enabled, provider] of conditionalProviders) {
|
|
1437
|
+
if (enabled) {
|
|
1438
|
+
providers.push(provider);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
return makeEnvironmentProviders(providers);
|
|
1442
|
+
}
|
|
1443
|
+
// Feature-specific providers for tree-shaking
|
|
1444
|
+
function provideCamera() {
|
|
1445
|
+
return makeEnvironmentProviders([PermissionsService, CameraService]);
|
|
1446
|
+
}
|
|
1447
|
+
function provideGeolocation() {
|
|
1448
|
+
return makeEnvironmentProviders([PermissionsService, GeolocationService]);
|
|
1449
|
+
}
|
|
1450
|
+
function provideNotifications() {
|
|
1451
|
+
return makeEnvironmentProviders([PermissionsService, NotificationService]);
|
|
1452
|
+
}
|
|
1453
|
+
function provideClipboard() {
|
|
1454
|
+
return makeEnvironmentProviders([PermissionsService, ClipboardService]);
|
|
1455
|
+
}
|
|
1456
|
+
function provideMediaDevices() {
|
|
1457
|
+
return makeEnvironmentProviders([PermissionsService, MediaDevicesService]);
|
|
1458
|
+
}
|
|
1459
|
+
function provideBattery() {
|
|
1460
|
+
return makeEnvironmentProviders([BatteryService]);
|
|
1461
|
+
}
|
|
1462
|
+
function provideWebShare() {
|
|
1463
|
+
return makeEnvironmentProviders([WebShareService]);
|
|
1464
|
+
}
|
|
1465
|
+
function provideWebStorage() {
|
|
1466
|
+
return makeEnvironmentProviders([WebStorageService]);
|
|
1467
|
+
}
|
|
1468
|
+
function provideWebSocket() {
|
|
1469
|
+
return makeEnvironmentProviders([WebSocketService]);
|
|
1470
|
+
}
|
|
1471
|
+
function provideWebWorker() {
|
|
1472
|
+
return makeEnvironmentProviders([WebWorkerService]);
|
|
1473
|
+
}
|
|
1474
|
+
function providePermissions() {
|
|
1475
|
+
return makeEnvironmentProviders([PermissionsService]);
|
|
1476
|
+
}
|
|
1477
|
+
// Combined providers for common use cases
|
|
1478
|
+
function provideMediaApis() {
|
|
1479
|
+
return makeEnvironmentProviders([PermissionsService, CameraService, MediaDevicesService]);
|
|
1480
|
+
}
|
|
1481
|
+
function provideLocationApis() {
|
|
1482
|
+
return makeEnvironmentProviders([PermissionsService, GeolocationService]);
|
|
1483
|
+
}
|
|
1484
|
+
function provideStorageApis() {
|
|
1485
|
+
return makeEnvironmentProviders([PermissionsService, ClipboardService, WebStorageService]);
|
|
1486
|
+
}
|
|
1487
|
+
function provideCommunicationApis() {
|
|
1488
|
+
return makeEnvironmentProviders([
|
|
1489
|
+
PermissionsService,
|
|
1490
|
+
NotificationService,
|
|
1491
|
+
WebShareService,
|
|
1492
|
+
WebSocketService,
|
|
1493
|
+
]);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// Browser Web APIs Services
|
|
1497
|
+
// Version
|
|
1498
|
+
const version = '0.1.0';
|
|
1499
|
+
|
|
1500
|
+
/**
|
|
1501
|
+
* Generated bundle index. Do not edit.
|
|
1502
|
+
*/
|
|
1503
|
+
|
|
1504
|
+
export { BatteryService, BrowserApiBaseService, BrowserCapabilityService, BrowserSupportUtil, CameraService, ClipboardService, GeolocationService, MediaDevicesService, NotificationService, PermissionsService, WebShareService, WebSocketService, WebStorageService, WebWorkerService, permissionGuard as createPermissionGuard, defaultBrowserWebApisConfig, permissionGuard, provideBattery, provideBrowserWebApis, provideCamera, provideClipboard, provideCommunicationApis, provideGeolocation, provideLocationApis, provideMediaApis, provideMediaDevices, provideNotifications, providePermissions, provideStorageApis, provideWebShare, provideWebSocket, provideWebStorage, provideWebWorker, version };
|