Differenzansicht 16g-pwa
im Vergleich zu 16-guards

← Zurück zur Ãœbersicht | Demo | Quelltext auf GitHub
angular.json CHANGED
@@ -55,7 +55,8 @@
55
  "maximumError": "8kB"
56
  }
57
  ],
58
- "outputHashing": "all"
 
59
  },
60
  "development": {
61
  "optimization": false,
55
  "maximumError": "8kB"
56
  }
57
  ],
58
+ "outputHashing": "all",
59
+ "serviceWorker": "ngsw-config.json"
60
  },
61
  "development": {
62
  "optimization": false,
ngsw-config.json ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "./node_modules/@angular/service-worker/config/schema.json",
3
+ "index": "/index.html",
4
+ "appData": {
5
+ "version": "1.1.0",
6
+ "changelog": "Updated Version"
7
+ },
8
+ "assetGroups": [
9
+ {
10
+ "name": "app",
11
+ "installMode": "prefetch",
12
+ "resources": {
13
+ "files": [
14
+ "/favicon.ico",
15
+ "/index.html",
16
+ "/manifest.webmanifest",
17
+ "/*.css",
18
+ "/*.js"
19
+ ]
20
+ }
21
+ },
22
+ {
23
+ "name": "assets",
24
+ "installMode": "lazy",
25
+ "updateMode": "prefetch",
26
+ "resources": {
27
+ "files": [
28
+ "/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
29
+ ]
30
+ }
31
+ }
32
+ ],
33
+ "dataGroups": [
34
+ {
35
+ "name": "Books",
36
+ "urls": [
37
+ "/books",
38
+ "/books/search/**",
39
+ "/books/**"
40
+ ],
41
+ "cacheConfig": {
42
+ "strategy": "freshness",
43
+ "maxSize": 50,
44
+ "maxAge": "1d2h",
45
+ "timeout": "3s"
46
+ }
47
+ }
48
+ ]
49
+ }
package.json CHANGED
@@ -20,6 +20,7 @@
20
  "@angular/platform-browser": "^19.1.0",
21
  "@angular/platform-browser-dynamic": "^19.1.0",
22
  "@angular/router": "^19.1.0",
 
23
  "angular-date-value-accessor": "^3.0.0",
24
  "book-monkey5-styles": "^1.0.4",
25
  "rxjs": "~7.8.0",
@@ -42,4 +43,4 @@
42
  "typescript": "~5.7.2",
43
  "typescript-eslint": "8.18.0"
44
  }
45
- }
20
  "@angular/platform-browser": "^19.1.0",
21
  "@angular/platform-browser-dynamic": "^19.1.0",
22
  "@angular/router": "^19.1.0",
23
+ "@angular/service-worker": "^19.1.0",
24
  "angular-date-value-accessor": "^3.0.0",
25
  "book-monkey5-styles": "^1.0.4",
26
  "rxjs": "~7.8.0",
43
  "typescript": "~5.7.2",
44
  "typescript-eslint": "8.18.0"
45
  }
46
+ }
public/manifest.webmanifest ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "book-monkey",
3
+ "short_name": "book-monkey",
4
+ "theme_color": "#1976d2",
5
+ "background_color": "#fafafa",
6
+ "display": "standalone",
7
+ "scope": "./",
8
+ "start_url": "./",
9
+ "icons": [
10
+ {
11
+ "src": "icons/icon-72x72.png",
12
+ "sizes": "72x72",
13
+ "type": "image/png",
14
+ "purpose": "maskable any"
15
+ },
16
+ {
17
+ "src": "icons/icon-96x96.png",
18
+ "sizes": "96x96",
19
+ "type": "image/png",
20
+ "purpose": "maskable any"
21
+ },
22
+ {
23
+ "src": "icons/icon-128x128.png",
24
+ "sizes": "128x128",
25
+ "type": "image/png",
26
+ "purpose": "maskable any"
27
+ },
28
+ {
29
+ "src": "icons/icon-144x144.png",
30
+ "sizes": "144x144",
31
+ "type": "image/png",
32
+ "purpose": "maskable any"
33
+ },
34
+ {
35
+ "src": "icons/icon-152x152.png",
36
+ "sizes": "152x152",
37
+ "type": "image/png",
38
+ "purpose": "maskable any"
39
+ },
40
+ {
41
+ "src": "icons/icon-192x192.png",
42
+ "sizes": "192x192",
43
+ "type": "image/png",
44
+ "purpose": "maskable any"
45
+ },
46
+ {
47
+ "src": "icons/icon-384x384.png",
48
+ "sizes": "384x384",
49
+ "type": "image/png",
50
+ "purpose": "maskable any"
51
+ },
52
+ {
53
+ "src": "icons/icon-512x512.png",
54
+ "sizes": "512x512",
55
+ "type": "image/png",
56
+ "purpose": "maskable any"
57
+ }
58
+ ]
59
+ }
src/app/app.component.html CHANGED
@@ -9,6 +9,12 @@
9
  <button class="red"
10
  (click)="auth.logout()"
11
  *ngIf="auth.isAuthenticated">Logout</button>
 
 
 
 
 
 
12
  </div>
13
  </nav>
14
 
9
  <button class="red"
10
  (click)="auth.logout()"
11
  *ngIf="auth.isAuthenticated">Logout</button>
12
+ <button
13
+ (click)="requestSubscription()"
14
+ [class.green]="permission === 'granted'"
15
+ [class.red]="permission === 'denied'"
16
+ [disabled]="!permission"
17
+ aria-label="Notifications">!</button>
18
  </div>
19
  </nav>
20
 
src/app/app.component.ts CHANGED
@@ -1,6 +1,13 @@
1
  import { Component } from '@angular/core';
 
2
 
3
  import { AuthService } from './shared/auth.service';
 
 
 
 
 
 
4
 
5
  @Component({
6
  selector: 'bm-root',
@@ -9,5 +16,62 @@ import { AuthService } from './shared/auth.service';
9
  styleUrl: './app.component.css'
10
  })
11
  export class AppComponent {
12
- constructor(public auth: AuthService) {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  }
1
  import { Component } from '@angular/core';
2
+ import { SwUpdate } from '@angular/service-worker';
3
 
4
  import { AuthService } from './shared/auth.service';
5
+ import { WebNotificationService } from './shared/web-notification.service';
6
+
7
+ interface AppData {
8
+ version: string;
9
+ changelog: string;
10
+ }
11
 
12
  @Component({
13
  selector: 'bm-root',
16
  styleUrl: './app.component.css'
17
  })
18
  export class AppComponent {
19
+ permission?: NotificationPermission;
20
+
21
+ constructor(
22
+ public auth: AuthService,
23
+ private swUpdate: SwUpdate,
24
+ private notificationService: WebNotificationService
25
+ ) {
26
+ this.swUpdate.versionUpdates.subscribe(e => {
27
+ switch (e.type) {
28
+ case 'VERSION_DETECTED': {
29
+ console.log(
30
+ 'Downloading new app version:',
31
+ e.version.appData
32
+ );
33
+ break;
34
+ }
35
+
36
+ case 'VERSION_READY': {
37
+ const current = e.currentVersion.appData as AppData;
38
+ const latest = e.latestVersion.appData as AppData;
39
+
40
+ const from = current.version;
41
+ const to = latest.version;
42
+ const changes = latest.changelog;
43
+
44
+ const confirmText = `Update from ${from} to ${to}. Changes: ${changes}. Install?`;
45
+
46
+ if (window.confirm(confirmText)) {
47
+ window.location.reload();
48
+ }
49
+ break;
50
+ }
51
+
52
+ case 'VERSION_INSTALLATION_FAILED': {
53
+ console.log(
54
+ `Failed to install ${e.version.appData}:`,
55
+ e.error
56
+ );
57
+ break;
58
+ }
59
+ }
60
+ });
61
+
62
+ if (this.notificationService.isEnabled) {
63
+ this.setPermission();
64
+ }
65
+ }
66
+
67
+ private setPermission() {
68
+ if ('Notification' in window) {
69
+ this.permission = Notification.permission;
70
+ }
71
+ }
72
+
73
+ requestSubscription() {
74
+ this.notificationService.requestSubscription()
75
+ .subscribe(() => this.setPermission());
76
+ }
77
  }
src/app/app.module.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { NgModule } from '@angular/core';
2
  import { provideHttpClient, withInterceptorsFromDi, HTTP_INTERCEPTORS } from '@angular/common/http';
3
  import { BrowserModule } from '@angular/platform-browser';
 
4
 
5
  import { AppRoutingModule } from './app-routing.module';
6
  import { AppComponent } from './app.component';
@@ -17,6 +18,12 @@ import { AuthInterceptor } from './shared/auth.interceptor';
17
  imports: [
18
  BrowserModule,
19
  AppRoutingModule,
 
 
 
 
 
 
20
  ],
21
  providers: [
22
  provideHttpClient(withInterceptorsFromDi()),
1
+ import { NgModule, isDevMode } from '@angular/core';
2
  import { provideHttpClient, withInterceptorsFromDi, HTTP_INTERCEPTORS } from '@angular/common/http';
3
  import { BrowserModule } from '@angular/platform-browser';
4
+ import { ServiceWorkerModule } from '@angular/service-worker';
5
 
6
  import { AppRoutingModule } from './app-routing.module';
7
  import { AppComponent } from './app.component';
18
  imports: [
19
  BrowserModule,
20
  AppRoutingModule,
21
+ ServiceWorkerModule.register('ngsw-worker.js', {
22
+ enabled: !isDevMode(),
23
+ // Register the ServiceWorker as soon as the application is stable
24
+ // or after 30 seconds (whichever comes first).
25
+ registrationStrategy: 'registerWhenStable:30000'
26
+ })
27
  ],
28
  providers: [
29
  provideHttpClient(withInterceptorsFromDi()),
src/app/shared/web-notification.service.ts ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@angular/core';
2
+ import { HttpClient } from '@angular/common/http';
3
+ import { SwPush } from '@angular/service-worker';
4
+ import { concatMap, from, Observable } from 'rxjs';
5
+ import { Router } from '@angular/router';
6
+
7
+ @Injectable({
8
+ providedIn: 'root',
9
+ })
10
+ export class WebNotificationService {
11
+ readonly VAPID_PUBLIC_KEY = 'BGk2Rx3DEjXdRv9qP8aKrypFoNjISAZ54l-3V05xpPOV-5ZQJvVH9OB9Rz5Ug7H_qH6CEr40f4Pi3DpjzYLbfCA';
12
+ private baseUrl = 'https://api5.angular-buch.com/notifications';
13
+
14
+ constructor(private http: HttpClient, private swPush: SwPush, private router: Router) {
15
+ // when user clicks a notification
16
+ this.swPush.notificationClicks.subscribe(e => {
17
+ // if ISBN is given, navigate to detail page
18
+ const data = e.notification.data;
19
+ if (data?.book?.isbn) {
20
+ this.router.navigate(['/books', data.book.isbn])
21
+ }
22
+ });
23
+ }
24
+
25
+ get isEnabled() {
26
+ return this.swPush.isEnabled;
27
+ }
28
+
29
+ // request a subscription from the browser and register it on the server
30
+ requestSubscription(): Observable<unknown> {
31
+ const request = this.swPush.requestSubscription({
32
+ serverPublicKey: this.VAPID_PUBLIC_KEY
33
+ });
34
+
35
+ return from(request).pipe(
36
+ concatMap(sub => this.registerOnServer(sub))
37
+ );
38
+ }
39
+
40
+ private registerOnServer(params: PushSubscription) {
41
+ return this.http.post(this.baseUrl, params);
42
+ }
43
+ }
src/index.html CHANGED
@@ -6,10 +6,18 @@
6
  <base href="/">
7
  <meta name="viewport" content="width=device-width, initial-scale=1">
8
  <link rel="icon" type="image/x-icon" href="favicon.ico">
 
 
 
 
 
 
 
9
  </head>
10
  <body>
11
  <bm-root>
12
  <div class="loader">Loading ...</div>
13
  </bm-root>
 
14
  </body>
15
  </html>
6
  <base href="/">
7
  <meta name="viewport" content="width=device-width, initial-scale=1">
8
  <link rel="icon" type="image/x-icon" href="favicon.ico">
9
+ <link rel="manifest" href="manifest.webmanifest">
10
+ <meta name="theme-color" content="#1976d2">
11
+ <meta name="apple-mobile-web-app-status-bar-style" content="default">
12
+ <meta name="mobile-web-app-capable" content="yes">
13
+ <link rel="apple-touch-startup-image" href="assets/icons/apple-splash-screen.png">
14
+ <link rel="apple-touch-icon" href="assets/icons/icon-512x512.png">
15
+ <link rel="apple-touch-icon" sizes="152x152" href="assets/icons/icon-152x152.png">
16
  </head>
17
  <body>
18
  <bm-root>
19
  <div class="loader">Loading ...</div>
20
  </bm-root>
21
+ <noscript>Please enable JavaScript to continue using this application.</noscript>
22
  </body>
23
  </html>