@angular-helpers/browser-web-apis 1.41.2
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 +826 -0
- package/README.md +918 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,918 @@
|
|
|
1
|
+
# @angular-helpers/browser-web-apis
|
|
2
|
+
|
|
3
|
+
Angular services package for a structured and secure access layer over browser Web APIs.
|
|
4
|
+
|
|
5
|
+
π **Documentation & Demo**: https://gaspar1992.github.io/angular-helpers/
|
|
6
|
+
|
|
7
|
+
[Leer en espaΓ±ol](./README.es.md)
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Unified browser API access through strongly typed services
|
|
12
|
+
- **True tree-shaking** β individual `provideX()` functions import only their own service
|
|
13
|
+
- All-in-one `provideBrowserWebApis()` for quick setup when bundle size is not a concern
|
|
14
|
+
- Reactive APIs using signals and observables
|
|
15
|
+
- Lifecycle-safe integration with `DestroyRef` (automatic cleanup)
|
|
16
|
+
- Centralized logging and error handling via `BrowserApiBaseService`
|
|
17
|
+
- Secure context validation and browser support detection built in
|
|
18
|
+
|
|
19
|
+
## Available Services
|
|
20
|
+
|
|
21
|
+
### Media and Device APIs
|
|
22
|
+
|
|
23
|
+
- `CameraService` - Camera access with permission handling
|
|
24
|
+
- `MediaDevicesService` - Media device listing and management
|
|
25
|
+
- `GeolocationService` - Geolocation API access
|
|
26
|
+
- `NotificationService` - Browser notifications API
|
|
27
|
+
- `MediaRecorderService` - Record audio/video from MediaStream
|
|
28
|
+
|
|
29
|
+
### Observer APIs
|
|
30
|
+
|
|
31
|
+
- `IntersectionObserverService` - Detect when elements enter/exit viewport
|
|
32
|
+
- `ResizeObserverService` - Watch for element size changes
|
|
33
|
+
- `MutationObserverService` - Watch for DOM mutations
|
|
34
|
+
- `PerformanceObserverService` - Monitor performance entries (LCP, CLS, etc.)
|
|
35
|
+
|
|
36
|
+
### System APIs
|
|
37
|
+
|
|
38
|
+
- `BatteryService` - Monitor battery status and charging state
|
|
39
|
+
- `PageVisibilityService` - Track document visibility state changes
|
|
40
|
+
- `ScreenWakeLockService` - Prevent screen from dimming or locking
|
|
41
|
+
- `ScreenOrientationService` - Read and lock screen orientation
|
|
42
|
+
- `FullscreenService` - Toggle fullscreen mode for elements
|
|
43
|
+
- `VibrationService` - Trigger haptic feedback patterns
|
|
44
|
+
- `SpeechSynthesisService` - Text-to-speech with voice selection
|
|
45
|
+
- `IdleDetectorService` - Detect user idle state and screen lock
|
|
46
|
+
- `GamepadService` - Game controller input polling
|
|
47
|
+
- `WebAudioService` - Audio context, oscillators, and analysers
|
|
48
|
+
- `WebLocksService` - Cross-tab resource locking coordination
|
|
49
|
+
- `StorageManagerService` - Storage quotas and persistence
|
|
50
|
+
- `CompressionService` - Gzip/deflate compression streams
|
|
51
|
+
|
|
52
|
+
### Network APIs
|
|
53
|
+
|
|
54
|
+
- `WebSocketService` - WebSocket connection handling
|
|
55
|
+
- `ServerSentEventsService` - Server-Sent Events client
|
|
56
|
+
- `BroadcastChannelService` - Inter-tab communication
|
|
57
|
+
- `NetworkInformationService` - Connection info and online status
|
|
58
|
+
|
|
59
|
+
### Storage & I/O APIs
|
|
60
|
+
|
|
61
|
+
- `WebStorageService` - LocalStorage and SessionStorage helpers
|
|
62
|
+
- `WebShareService` - Native Web Share API support
|
|
63
|
+
- `ClipboardService` - System clipboard access
|
|
64
|
+
- `FileSystemAccessService` - Open/save files via native picker
|
|
65
|
+
|
|
66
|
+
### Web APIs
|
|
67
|
+
|
|
68
|
+
- `WebWorkerService` - Web Worker management
|
|
69
|
+
|
|
70
|
+
### Detection APIs
|
|
71
|
+
|
|
72
|
+
- `EyeDropperService` - Screen color picker
|
|
73
|
+
|
|
74
|
+
### Security & Capabilities
|
|
75
|
+
|
|
76
|
+
- `PermissionsService` - Centralized browser permissions handling
|
|
77
|
+
- `BrowserCapabilityService` - Feature-detection for browser API availability
|
|
78
|
+
|
|
79
|
+
### Utilities
|
|
80
|
+
|
|
81
|
+
- `BrowserApiBaseService` - Shared base class for browser API services
|
|
82
|
+
|
|
83
|
+
## Installation
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pnpm add @angular-helpers/browser-web-apis
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Quick Setup
|
|
90
|
+
|
|
91
|
+
### All-in-one (zero bundle budget concern)
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { provideBrowserWebApis } from '@angular-helpers/browser-web-apis';
|
|
95
|
+
import {
|
|
96
|
+
CameraService,
|
|
97
|
+
GeolocationService,
|
|
98
|
+
NotificationService,
|
|
99
|
+
} from '@angular-helpers/browser-web-apis';
|
|
100
|
+
|
|
101
|
+
bootstrapApplication(AppComponent, {
|
|
102
|
+
providers: [
|
|
103
|
+
provideBrowserWebApis({
|
|
104
|
+
services: [
|
|
105
|
+
CameraService,
|
|
106
|
+
GeolocationService,
|
|
107
|
+
NotificationService,
|
|
108
|
+
// Add more services as needed
|
|
109
|
+
],
|
|
110
|
+
}),
|
|
111
|
+
],
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Granular setup (recommended for production)
|
|
116
|
+
|
|
117
|
+
Each `provideX()` lives in its own module and imports only the service it needs. Bundlers (webpack, Rollup, Vite) will tree-shake anything you don't include.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import {
|
|
121
|
+
provideCamera,
|
|
122
|
+
provideGeolocation,
|
|
123
|
+
provideWebStorage,
|
|
124
|
+
} from '@angular-helpers/browser-web-apis';
|
|
125
|
+
|
|
126
|
+
bootstrapApplication(AppComponent, {
|
|
127
|
+
providers: [
|
|
128
|
+
provideCamera(), // β only CameraService + PermissionsService
|
|
129
|
+
provideGeolocation(), // β only GeolocationService + PermissionsService
|
|
130
|
+
provideWebStorage(), // β only WebStorageService
|
|
131
|
+
],
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Every service has a matching `provideX()` function:
|
|
136
|
+
|
|
137
|
+
| Function | Services included |
|
|
138
|
+
| ------------------------------- | ------------------------------------------- |
|
|
139
|
+
| `provideCamera()` | `PermissionsService`, `CameraService` |
|
|
140
|
+
| `provideGeolocation()` | `PermissionsService`, `GeolocationService` |
|
|
141
|
+
| `provideNotifications()` | `PermissionsService`, `NotificationService` |
|
|
142
|
+
| `provideClipboard()` | `PermissionsService`, `ClipboardService` |
|
|
143
|
+
| `provideMediaDevices()` | `PermissionsService`, `MediaDevicesService` |
|
|
144
|
+
| `provideWebStorage()` | `WebStorageService` |
|
|
145
|
+
| `provideWebSocket()` | `WebSocketService` |
|
|
146
|
+
| `provideWebWorker()` | `WebWorkerService` |
|
|
147
|
+
| `provideBattery()` | `BatteryService` |
|
|
148
|
+
| `provideWebShare()` | `WebShareService` |
|
|
149
|
+
| `provideIntersectionObserver()` | `IntersectionObserverService` |
|
|
150
|
+
| `provideResizeObserver()` | `ResizeObserverService` |
|
|
151
|
+
| `provideMutationObserver()` | `MutationObserverService` |
|
|
152
|
+
| `providePerformanceObserver()` | `PerformanceObserverService` |
|
|
153
|
+
| `providePageVisibility()` | `PageVisibilityService` |
|
|
154
|
+
| `provideNetworkInformation()` | `NetworkInformationService` |
|
|
155
|
+
| `provideScreenWakeLock()` | `ScreenWakeLockService` |
|
|
156
|
+
| `provideScreenOrientation()` | `ScreenOrientationService` |
|
|
157
|
+
| `provideFullscreen()` | `FullscreenService` |
|
|
158
|
+
| `provideFileSystemAccess()` | `FileSystemAccessService` |
|
|
159
|
+
| `provideMediaRecorder()` | `MediaRecorderService` |
|
|
160
|
+
| `provideBroadcastChannel()` | `BroadcastChannelService` |
|
|
161
|
+
| `provideServerSentEvents()` | `ServerSentEventsService` |
|
|
162
|
+
| `provideVibration()` | `VibrationService` |
|
|
163
|
+
| `provideSpeechSynthesis()` | `SpeechSynthesisService` |
|
|
164
|
+
| `provideWebAudio()` | `WebAudioService` |
|
|
165
|
+
| `provideGamepad()` | `GamepadService` |
|
|
166
|
+
| `provideWebLocks()` | `WebLocksService` |
|
|
167
|
+
| `provideStorageManager()` | `StorageManagerService` |
|
|
168
|
+
| `provideCompression()` | `CompressionService` |
|
|
169
|
+
| `provideEyeDropper()` | `EyeDropperService` |
|
|
170
|
+
| `provideIdleDetector()` | `IdleDetectorService` |
|
|
171
|
+
| `providePermissions()` | `PermissionsService` |
|
|
172
|
+
|
|
173
|
+
### Combo providers
|
|
174
|
+
|
|
175
|
+
Convenience functions that bundle related services:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import {
|
|
179
|
+
provideMediaApis,
|
|
180
|
+
provideLocationApis,
|
|
181
|
+
provideStorageApis,
|
|
182
|
+
provideCommunicationApis,
|
|
183
|
+
} from '@angular-helpers/browser-web-apis';
|
|
184
|
+
|
|
185
|
+
bootstrapApplication(AppComponent, {
|
|
186
|
+
providers: [
|
|
187
|
+
provideMediaApis(), // Camera + MediaDevices + Permissions
|
|
188
|
+
provideLocationApis(), // Geolocation + Permissions
|
|
189
|
+
provideStorageApis(), // Clipboard + WebStorage + Permissions
|
|
190
|
+
provideCommunicationApis(), // Notification + WebShare + WebSocket + Permissions
|
|
191
|
+
],
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Usage by Service
|
|
196
|
+
|
|
197
|
+
### CameraService
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import { CameraService } from '@angular-helpers/browser-web-apis';
|
|
201
|
+
|
|
202
|
+
export class PhotoComponent {
|
|
203
|
+
private cameraService = inject(CameraService);
|
|
204
|
+
|
|
205
|
+
async takePhoto() {
|
|
206
|
+
try {
|
|
207
|
+
const stream = await this.cameraService.startCamera({
|
|
208
|
+
video: true,
|
|
209
|
+
audio: false,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Use stream for video/photo capture
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.error('Error accessing camera:', error);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async stopCamera() {
|
|
219
|
+
await this.cameraService.stopCamera();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### BrowserCapabilityService
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
import { BrowserCapabilityService } from '@angular-helpers/browser-web-apis';
|
|
228
|
+
|
|
229
|
+
export class MyComponent {
|
|
230
|
+
private capability = inject(BrowserCapabilityService);
|
|
231
|
+
|
|
232
|
+
ngOnInit() {
|
|
233
|
+
if (this.capability.isSupported('geolocation')) {
|
|
234
|
+
console.log('Geolocation is available');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### GeolocationService
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
import { GeolocationService } from '@angular-helpers/browser-web-apis';
|
|
244
|
+
|
|
245
|
+
export class LocationComponent {
|
|
246
|
+
private geolocation = inject(GeolocationService);
|
|
247
|
+
|
|
248
|
+
async getCurrentLocation() {
|
|
249
|
+
try {
|
|
250
|
+
const position = await this.geolocation.getCurrentPosition({
|
|
251
|
+
enableHighAccuracy: true,
|
|
252
|
+
timeout: 10000,
|
|
253
|
+
maximumAge: 60000,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
console.log('Position:', position.coords);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.error('Error getting location:', error);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### IntersectionObserverService
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
import { IntersectionObserverService } from '@angular-helpers/browser-web-apis';
|
|
268
|
+
|
|
269
|
+
export class LazyImageComponent {
|
|
270
|
+
private intersectionService = inject(IntersectionObserverService);
|
|
271
|
+
private elementRef = inject(ElementRef);
|
|
272
|
+
|
|
273
|
+
isVisible = signal(false);
|
|
274
|
+
|
|
275
|
+
ngAfterViewInit() {
|
|
276
|
+
this.intersectionService
|
|
277
|
+
.observeVisibility(this.elementRef.nativeElement, { threshold: 0.5 })
|
|
278
|
+
.subscribe((visible) => this.isVisible.set(visible));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### ResizeObserverService
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
import { ResizeObserverService } from '@angular-helpers/browser-web-apis';
|
|
287
|
+
|
|
288
|
+
export class ResponsiveComponent {
|
|
289
|
+
private resizeService = inject(ResizeObserverService);
|
|
290
|
+
private elementRef = inject(ElementRef);
|
|
291
|
+
|
|
292
|
+
elementSize = signal<ElementSize | null>(null);
|
|
293
|
+
|
|
294
|
+
ngAfterViewInit() {
|
|
295
|
+
this.resizeService
|
|
296
|
+
.observeSize(this.elementRef.nativeElement)
|
|
297
|
+
.subscribe((size) => this.elementSize.set(size));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### PageVisibilityService
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
import { PageVisibilityService } from '@angular-helpers/browser-web-apis';
|
|
306
|
+
|
|
307
|
+
export class AnalyticsComponent {
|
|
308
|
+
private visibilityService = inject(PageVisibilityService);
|
|
309
|
+
|
|
310
|
+
ngOnInit() {
|
|
311
|
+
this.visibilityService.watch().subscribe((state) => {
|
|
312
|
+
console.log('Page is now:', state);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### FullscreenService
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
import { FullscreenService } from '@angular-helpers/browser-web-apis';
|
|
322
|
+
|
|
323
|
+
export class VideoPlayerComponent {
|
|
324
|
+
private fullscreenService = inject(FullscreenService);
|
|
325
|
+
|
|
326
|
+
async toggleFullscreen() {
|
|
327
|
+
await this.fullscreenService.toggle();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### ScreenWakeLockService
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
import { ScreenWakeLockService } from '@angular-helpers/browser-web-apis';
|
|
336
|
+
|
|
337
|
+
export class PresentationComponent {
|
|
338
|
+
private wakeLockService = inject(ScreenWakeLockService);
|
|
339
|
+
|
|
340
|
+
async keepScreenOn() {
|
|
341
|
+
await this.wakeLockService.request();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async releaseScreen() {
|
|
345
|
+
await this.wakeLockService.release();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### BroadcastChannelService
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
import { BroadcastChannelService } from '@angular-helpers/browser-web-apis';
|
|
354
|
+
|
|
355
|
+
export class SyncComponent {
|
|
356
|
+
private broadcastService = inject(BroadcastChannelService);
|
|
357
|
+
|
|
358
|
+
ngOnInit() {
|
|
359
|
+
// Listen for messages from other tabs
|
|
360
|
+
this.broadcastService.open<string>('app-sync').subscribe((msg) => {
|
|
361
|
+
console.log('Received:', msg);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
sendMessage(data: string) {
|
|
366
|
+
this.broadcastService.post('app-sync', data);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### ServerSentEventsService
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
import { ServerSentEventsService } from '@angular-helpers/browser-web-apis';
|
|
375
|
+
|
|
376
|
+
export class LiveFeedComponent {
|
|
377
|
+
private sseService = inject(ServerSentEventsService);
|
|
378
|
+
|
|
379
|
+
connectToEvents() {
|
|
380
|
+
this.sseService.connect('https://api.example.com/events').subscribe({
|
|
381
|
+
next: (message) => console.log('Event:', message),
|
|
382
|
+
error: (err) => console.error('SSE error:', err),
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### VibrationService
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
import { VibrationService } from '@angular-helpers/browser-web-apis';
|
|
392
|
+
|
|
393
|
+
export class FeedbackComponent {
|
|
394
|
+
private vibrationService = inject(VibrationService);
|
|
395
|
+
|
|
396
|
+
onSuccess() {
|
|
397
|
+
this.vibrationService.success();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
onError() {
|
|
401
|
+
this.vibrationService.error();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### SpeechSynthesisService
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
import { SpeechSynthesisService } from '@angular-helpers/browser-web-apis';
|
|
410
|
+
|
|
411
|
+
export class VoiceComponent {
|
|
412
|
+
private speechService = inject(SpeechSynthesisService);
|
|
413
|
+
|
|
414
|
+
speakText(text: string) {
|
|
415
|
+
this.speechService.speak(text).subscribe((state) => {
|
|
416
|
+
console.log('Speech state:', state);
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### BatteryService
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
import { BatteryService } from '@angular-helpers/browser-web-apis';
|
|
426
|
+
|
|
427
|
+
export class PowerComponent {
|
|
428
|
+
private batteryService = inject(BatteryService);
|
|
429
|
+
|
|
430
|
+
async ngOnInit() {
|
|
431
|
+
try {
|
|
432
|
+
// Initialize and get initial battery info
|
|
433
|
+
const batteryInfo = await this.batteryService.initialize();
|
|
434
|
+
console.log('Battery level:', batteryInfo.level);
|
|
435
|
+
console.log('Is charging:', batteryInfo.charging);
|
|
436
|
+
|
|
437
|
+
// Watch for battery changes
|
|
438
|
+
this.batteryService.watchBatteryInfo().subscribe((info) => {
|
|
439
|
+
console.log('Battery updated:', info);
|
|
440
|
+
});
|
|
441
|
+
} catch (error) {
|
|
442
|
+
console.error('Battery API not supported:', error);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### ClipboardService
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
import { ClipboardService } from '@angular-helpers/browser-web-apis';
|
|
452
|
+
|
|
453
|
+
export class CopyComponent {
|
|
454
|
+
private clipboardService = inject(ClipboardService);
|
|
455
|
+
|
|
456
|
+
async copyToClipboard(text: string) {
|
|
457
|
+
try {
|
|
458
|
+
await this.clipboardService.writeText(text);
|
|
459
|
+
console.log('Copied successfully');
|
|
460
|
+
} catch (error) {
|
|
461
|
+
console.error('Failed to copy:', error);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async pasteFromClipboard(): Promise<string> {
|
|
466
|
+
try {
|
|
467
|
+
const text = await this.clipboardService.readText();
|
|
468
|
+
return text;
|
|
469
|
+
} catch (error) {
|
|
470
|
+
console.error('Failed to read clipboard:', error);
|
|
471
|
+
return '';
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### FileSystemAccessService
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
import { FileSystemAccessService } from '@angular-helpers/browser-web-apis';
|
|
481
|
+
|
|
482
|
+
export class FileManagerComponent {
|
|
483
|
+
private fileService = inject(FileSystemAccessService);
|
|
484
|
+
|
|
485
|
+
async openFiles() {
|
|
486
|
+
try {
|
|
487
|
+
const files = await this.fileService.openFile({
|
|
488
|
+
multiple: true,
|
|
489
|
+
types: [
|
|
490
|
+
{
|
|
491
|
+
description: 'Text files',
|
|
492
|
+
accept: { 'text/plain': ['.txt'] },
|
|
493
|
+
},
|
|
494
|
+
],
|
|
495
|
+
});
|
|
496
|
+
console.log('Selected files:', files);
|
|
497
|
+
} catch (error) {
|
|
498
|
+
console.error('Failed to open files:', error);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async saveContent(content: string) {
|
|
503
|
+
try {
|
|
504
|
+
await this.fileService.saveFile(content, {
|
|
505
|
+
suggestedName: 'document.txt',
|
|
506
|
+
});
|
|
507
|
+
} catch (error) {
|
|
508
|
+
console.error('Failed to save file:', error);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### MediaRecorderService
|
|
515
|
+
|
|
516
|
+
```typescript
|
|
517
|
+
import { MediaRecorderService } from '@angular-helpers/browser-web-apis';
|
|
518
|
+
|
|
519
|
+
export class RecorderComponent {
|
|
520
|
+
private recorderService = inject(MediaRecorderService);
|
|
521
|
+
private stream: MediaStream | null = null;
|
|
522
|
+
|
|
523
|
+
async startRecording() {
|
|
524
|
+
try {
|
|
525
|
+
this.stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
|
|
526
|
+
await this.recorderService.start(this.stream, { mimeType: 'video/webm' });
|
|
527
|
+
} catch (error) {
|
|
528
|
+
console.error('Failed to start recording:', error);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
stopRecording() {
|
|
533
|
+
const result = this.recorderService.stop();
|
|
534
|
+
if (result) {
|
|
535
|
+
console.log('Recording saved, blob URL:', result.url);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
### WebSocketService
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
import { WebSocketService } from '@angular-helpers/browser-web-apis';
|
|
545
|
+
|
|
546
|
+
export class LiveComponent {
|
|
547
|
+
private wsService = inject(WebSocketService);
|
|
548
|
+
|
|
549
|
+
connect() {
|
|
550
|
+
this.wsService
|
|
551
|
+
.connect({
|
|
552
|
+
url: 'wss://example.com/socket',
|
|
553
|
+
reconnectInterval: 3000,
|
|
554
|
+
maxReconnectAttempts: 5,
|
|
555
|
+
})
|
|
556
|
+
.subscribe((status) => {
|
|
557
|
+
console.log('Connection status:', status);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
this.wsService.getMessages().subscribe((message) => {
|
|
561
|
+
console.log('Received:', message);
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
sendMessage(data: unknown) {
|
|
566
|
+
this.wsService.send({ type: 'message', data });
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### WebStorageService
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
import { WebStorageService } from '@angular-helpers/browser-web-apis';
|
|
575
|
+
|
|
576
|
+
export class SettingsComponent {
|
|
577
|
+
private storageService = inject(WebStorageService);
|
|
578
|
+
|
|
579
|
+
saveSetting(key: string, value: unknown) {
|
|
580
|
+
this.storageService.setLocalStorage(key, value);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
getSetting<T>(key: string): T | null {
|
|
584
|
+
return this.storageService.getLocalStorage<T>(key);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
watchSetting<T>(key: string) {
|
|
588
|
+
return this.storageService.watchLocalStorage<T>(key).subscribe((value) => {
|
|
589
|
+
console.log('Setting changed:', value);
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### WebWorkerService
|
|
596
|
+
|
|
597
|
+
```typescript
|
|
598
|
+
import { WebWorkerService } from '@angular-helpers/browser-web-apis';
|
|
599
|
+
|
|
600
|
+
export class WorkerComponent {
|
|
601
|
+
private workerService = inject(WebWorkerService);
|
|
602
|
+
|
|
603
|
+
async createWorker() {
|
|
604
|
+
this.workerService
|
|
605
|
+
.createWorker('calc-worker', '/assets/workers/calc.worker.js')
|
|
606
|
+
.subscribe((status) => {
|
|
607
|
+
console.log('Worker status:', status);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
this.workerService.getMessages('calc-worker').subscribe((message) => {
|
|
611
|
+
console.log('Worker response:', message);
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
sendTask(data: unknown) {
|
|
616
|
+
this.workerService.postMessage('calc-worker', {
|
|
617
|
+
id: 'task-1',
|
|
618
|
+
type: 'CALCULATE',
|
|
619
|
+
data,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
## Signal Fn Primitives
|
|
626
|
+
|
|
627
|
+
Zero-boilerplate reactive alternatives to the RxJS-based services. Each `inject*` function returns a ref object with read-only signals and handles cleanup automatically via `DestroyRef`.
|
|
628
|
+
|
|
629
|
+
### injectPageVisibility
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
import { injectPageVisibility } from '@angular-helpers/browser-web-apis';
|
|
633
|
+
|
|
634
|
+
@Component({...})
|
|
635
|
+
export class MyComponent {
|
|
636
|
+
readonly visibility = injectPageVisibility();
|
|
637
|
+
|
|
638
|
+
// visibility.state() β 'visible' | 'hidden'
|
|
639
|
+
// visibility.isVisible() β boolean
|
|
640
|
+
// visibility.isHidden() β boolean
|
|
641
|
+
}
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### injectResizeObserver
|
|
645
|
+
|
|
646
|
+
Accepts `Element`, `ElementRef`, or a **`Signal<ElementRef | undefined>`** (e.g. from `viewChild`). When a signal is passed, the observer automatically starts when the element becomes available.
|
|
647
|
+
|
|
648
|
+
```typescript
|
|
649
|
+
import { injectResizeObserver } from '@angular-helpers/browser-web-apis';
|
|
650
|
+
|
|
651
|
+
@Component({...})
|
|
652
|
+
export class MyComponent {
|
|
653
|
+
readonly boxRef = viewChild<ElementRef>('box');
|
|
654
|
+
readonly resize = injectResizeObserver(this.boxRef);
|
|
655
|
+
|
|
656
|
+
// resize.width() β number
|
|
657
|
+
// resize.height() β number
|
|
658
|
+
// resize.inlineSize() β number (logical)
|
|
659
|
+
// resize.blockSize() β number (logical)
|
|
660
|
+
// resize.size() β ElementSize | null
|
|
661
|
+
}
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
### injectIntersectionObserver
|
|
665
|
+
|
|
666
|
+
Same `ElementInput` flexibility β works with `viewChild` signals out of the box.
|
|
667
|
+
|
|
668
|
+
```typescript
|
|
669
|
+
import { injectIntersectionObserver } from '@angular-helpers/browser-web-apis';
|
|
670
|
+
|
|
671
|
+
@Component({...})
|
|
672
|
+
export class MyComponent {
|
|
673
|
+
readonly targetRef = viewChild<ElementRef>('target');
|
|
674
|
+
readonly inView = injectIntersectionObserver(this.targetRef, { threshold: 0.25 });
|
|
675
|
+
|
|
676
|
+
// inView.isIntersecting() β boolean
|
|
677
|
+
// inView.isVisible() β boolean
|
|
678
|
+
}
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
### injectNetworkInformation
|
|
682
|
+
|
|
683
|
+
```typescript
|
|
684
|
+
import { injectNetworkInformation } from '@angular-helpers/browser-web-apis';
|
|
685
|
+
|
|
686
|
+
@Component({...})
|
|
687
|
+
export class MyComponent {
|
|
688
|
+
readonly net = injectNetworkInformation();
|
|
689
|
+
|
|
690
|
+
// net.online() β boolean
|
|
691
|
+
// net.effectiveType() β '4g' | '3g' | '2g' | 'slow-2g' | undefined
|
|
692
|
+
// net.downlink() β number | undefined (Mbps)
|
|
693
|
+
// net.rtt() β number | undefined (ms)
|
|
694
|
+
// net.type() β ConnectionType | undefined
|
|
695
|
+
// net.saveData() β boolean | undefined
|
|
696
|
+
}
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### injectScreenOrientation
|
|
700
|
+
|
|
701
|
+
```typescript
|
|
702
|
+
import { injectScreenOrientation } from '@angular-helpers/browser-web-apis';
|
|
703
|
+
|
|
704
|
+
@Component({...})
|
|
705
|
+
export class MyComponent {
|
|
706
|
+
readonly orientation = injectScreenOrientation();
|
|
707
|
+
|
|
708
|
+
// orientation.type() β OrientationType
|
|
709
|
+
// orientation.angle() β number
|
|
710
|
+
// orientation.isPortrait() β boolean
|
|
711
|
+
// orientation.isLandscape() β boolean
|
|
712
|
+
// orientation.lock('landscape') β Promise<void>
|
|
713
|
+
// orientation.unlock()
|
|
714
|
+
}
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
### injectMutationObserver
|
|
718
|
+
|
|
719
|
+
Accepts the same `ElementInput` type β works with `viewChild` signals.
|
|
720
|
+
|
|
721
|
+
```typescript
|
|
722
|
+
import { injectMutationObserver } from '@angular-helpers/browser-web-apis';
|
|
723
|
+
|
|
724
|
+
@Component({...})
|
|
725
|
+
export class MyComponent {
|
|
726
|
+
readonly targetRef = viewChild<ElementRef>('target');
|
|
727
|
+
readonly mo = injectMutationObserver(this.targetRef, { childList: true });
|
|
728
|
+
|
|
729
|
+
// mo.mutations() β MutationRecord[]
|
|
730
|
+
// mo.mutationCount() β number
|
|
731
|
+
}
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
### injectPerformanceObserver
|
|
735
|
+
|
|
736
|
+
```typescript
|
|
737
|
+
import { injectPerformanceObserver } from '@angular-helpers/browser-web-apis';
|
|
738
|
+
|
|
739
|
+
@Component({...})
|
|
740
|
+
export class MyComponent {
|
|
741
|
+
readonly perf = injectPerformanceObserver({ type: 'navigation', buffered: true });
|
|
742
|
+
|
|
743
|
+
// perf.entries() β PerformanceEntryList
|
|
744
|
+
// perf.entryCount() β number
|
|
745
|
+
// perf.latestEntry() β PerformanceEntry | undefined
|
|
746
|
+
}
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
### injectIdleDetector
|
|
750
|
+
|
|
751
|
+
```typescript
|
|
752
|
+
import { injectIdleDetector } from '@angular-helpers/browser-web-apis';
|
|
753
|
+
|
|
754
|
+
@Component({...})
|
|
755
|
+
export class MyComponent {
|
|
756
|
+
readonly idle = injectIdleDetector({ threshold: 120_000 });
|
|
757
|
+
|
|
758
|
+
// idle.userState() β 'active' | 'idle'
|
|
759
|
+
// idle.screenState() β 'locked' | 'unlocked'
|
|
760
|
+
// idle.isUserIdle() β boolean
|
|
761
|
+
// idle.isScreenLocked() β boolean
|
|
762
|
+
}
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
### injectGamepad
|
|
766
|
+
|
|
767
|
+
```typescript
|
|
768
|
+
import { injectGamepad } from '@angular-helpers/browser-web-apis';
|
|
769
|
+
|
|
770
|
+
@Component({...})
|
|
771
|
+
export class MyComponent {
|
|
772
|
+
readonly gp = injectGamepad(0);
|
|
773
|
+
|
|
774
|
+
// gp.connected() β boolean
|
|
775
|
+
// gp.buttons() β ReadonlyArray<{ pressed: boolean; value: number }>
|
|
776
|
+
// gp.axes() β readonly number[]
|
|
777
|
+
// gp.state() β GamepadState | null
|
|
778
|
+
}
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
### injectClipboard
|
|
782
|
+
|
|
783
|
+
```typescript
|
|
784
|
+
import { injectClipboard } from '@angular-helpers/browser-web-apis';
|
|
785
|
+
|
|
786
|
+
@Component({...})
|
|
787
|
+
export class MyComponent {
|
|
788
|
+
readonly cb = injectClipboard();
|
|
789
|
+
|
|
790
|
+
// cb.text() β string | null (last copied text)
|
|
791
|
+
// cb.error() β string | null
|
|
792
|
+
// cb.busy() β boolean
|
|
793
|
+
// cb.isSupported() β boolean
|
|
794
|
+
|
|
795
|
+
async copy(text: string) {
|
|
796
|
+
await this.cb.writeText(text);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
### injectGeolocation
|
|
802
|
+
|
|
803
|
+
```typescript
|
|
804
|
+
import { injectGeolocation } from '@angular-helpers/browser-web-apis';
|
|
805
|
+
|
|
806
|
+
@Component({...})
|
|
807
|
+
export class MyComponent {
|
|
808
|
+
readonly geo = injectGeolocation({ watch: true });
|
|
809
|
+
|
|
810
|
+
// geo.position() β GeolocationPosition | null
|
|
811
|
+
// geo.error() β GeolocationPositionError | null
|
|
812
|
+
// geo.watching() β boolean
|
|
813
|
+
// geo.isSupported() β boolean
|
|
814
|
+
|
|
815
|
+
stopWatching() {
|
|
816
|
+
this.geo.stop();
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
### injectBattery
|
|
822
|
+
|
|
823
|
+
```typescript
|
|
824
|
+
import { injectBattery } from '@angular-helpers/browser-web-apis';
|
|
825
|
+
|
|
826
|
+
@Component({...})
|
|
827
|
+
export class MyComponent {
|
|
828
|
+
readonly battery = injectBattery();
|
|
829
|
+
|
|
830
|
+
// battery.info() β BatteryInfo | null (level, charging, times)
|
|
831
|
+
// battery.error() β string | null
|
|
832
|
+
// battery.isSupported() β boolean
|
|
833
|
+
|
|
834
|
+
async refresh() {
|
|
835
|
+
await this.battery.refresh();
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
### injectWakeLock
|
|
841
|
+
|
|
842
|
+
```typescript
|
|
843
|
+
import { injectWakeLock } from '@angular-helpers/browser-web-apis';
|
|
844
|
+
|
|
845
|
+
@Component({...})
|
|
846
|
+
export class MyComponent {
|
|
847
|
+
readonly wakeLock = injectWakeLock();
|
|
848
|
+
|
|
849
|
+
// wakeLock.active() β boolean
|
|
850
|
+
// wakeLock.error() β string | null
|
|
851
|
+
// wakeLock.isSupported() β boolean
|
|
852
|
+
|
|
853
|
+
async toggle() {
|
|
854
|
+
if (this.wakeLock.active()) {
|
|
855
|
+
await this.wakeLock.release();
|
|
856
|
+
} else {
|
|
857
|
+
await this.wakeLock.request();
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
### injectEyeDropper
|
|
864
|
+
|
|
865
|
+
```typescript
|
|
866
|
+
import { injectEyeDropper } from '@angular-helpers/browser-web-apis';
|
|
867
|
+
|
|
868
|
+
@Component({...})
|
|
869
|
+
export class MyComponent {
|
|
870
|
+
readonly dropper = injectEyeDropper();
|
|
871
|
+
|
|
872
|
+
// dropper.color() β Signal<string | null> (sRGBHex)
|
|
873
|
+
// dropper.isOpening() β Signal<boolean>
|
|
874
|
+
// dropper.error() β Signal<Error | null>
|
|
875
|
+
// dropper.isSupported() β Signal<boolean>
|
|
876
|
+
|
|
877
|
+
async pickColor() {
|
|
878
|
+
const result = await this.dropper.open();
|
|
879
|
+
if (result) {
|
|
880
|
+
console.log('Selected color:', result.sRGBHex);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
### ElementInput type
|
|
887
|
+
|
|
888
|
+
Both `injectResizeObserver` and `injectIntersectionObserver` accept the `ElementInput` type:
|
|
889
|
+
|
|
890
|
+
```typescript
|
|
891
|
+
type ElementInput =
|
|
892
|
+
| Element
|
|
893
|
+
| ElementRef<Element>
|
|
894
|
+
| Signal<Element | ElementRef<Element> | undefined>;
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
This means you can pass a `viewChild` signal directly β the function will internally use an `effect` to start observing once the element is rendered, with automatic cleanup.
|
|
898
|
+
|
|
899
|
+
## Browser Support
|
|
900
|
+
|
|
901
|
+
The services automatically validate browser support and unsupported-path handling:
|
|
902
|
+
|
|
903
|
+
- Chrome: full support
|
|
904
|
+
- Firefox: full support
|
|
905
|
+
- Safari: partial support
|
|
906
|
+
- Edge: full support
|
|
907
|
+
- Mobile browsers: depends on platform and API
|
|
908
|
+
|
|
909
|
+
## Notes
|
|
910
|
+
|
|
911
|
+
- Secure context (HTTPS) is required for several APIs
|
|
912
|
+
- Some APIs require explicit user interaction
|
|
913
|
+
- Permission behavior varies by browser
|
|
914
|
+
- Always implement error handling and fallback logic
|
|
915
|
+
|
|
916
|
+
## License
|
|
917
|
+
|
|
918
|
+
MIT
|