diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 9d6b2f1..5b8f69f 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,4 +1,5 @@ import { Component } from '@angular/core'; +import {ActivatedRoute, NavigationEnd, Router} from "@angular/router"; @Component({ selector: 'app-root', @@ -7,4 +8,15 @@ import { Component } from '@angular/core'; }) export class AppComponent { title = 'frontend'; + mySubscription; + + constructor(private router: Router, private activatedRoute: ActivatedRoute){ + this.router.routeReuseStrategy.shouldReuseRoute = () => false; + this.mySubscription = this.router.events.subscribe((event) => { + if (event instanceof NavigationEnd) { + // Trick the Router into believing it's last link wasn't previously loaded + this.router.navigated = false; + } + }); + } } diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 5e9fac7..3012076 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -29,7 +29,7 @@ import {MatPaginatorModule} from "@angular/material/paginator"; import {MatSortModule} from "@angular/material/sort"; import {GameCoverComponent} from './components/game-cover/game-cover.component'; import {GameDetailViewComponent} from './components/game-detail-view/game-detail-view.component'; -import {MatSnackBarModule} from '@angular/material/snack-bar'; +import {MAT_SNACK_BAR_DEFAULT_OPTIONS, MatSnackBarModule} from '@angular/material/snack-bar'; import {MatGridListModule} from "@angular/material/grid-list"; import {GameScreenshotComponent} from './components/game-screenshot/game-screenshot.component'; import {YouTubePlayerModule} from "@angular/youtube-player"; @@ -94,6 +94,10 @@ import {MapGameDialogComponent} from "./components/map-game-dialog/map-game-dial provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true + }, + { + provide: MAT_SNACK_BAR_DEFAULT_OPTIONS, + useValue: { panelClass: ['snackbar-dark'] }, } ], bootstrap: [AppComponent] diff --git a/frontend/src/app/components/error-dialog/error-dialog.component.ts b/frontend/src/app/components/error-dialog/error-dialog.component.ts index 998d314..b5e0b7a 100644 --- a/frontend/src/app/components/error-dialog/error-dialog.component.ts +++ b/frontend/src/app/components/error-dialog/error-dialog.component.ts @@ -5,9 +5,7 @@ import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; selector: 'app-error-dialog', template: `

Error

- - {{message}} - + diff --git a/frontend/src/app/components/game-detail-view/game-detail-view.component.html b/frontend/src/app/components/game-detail-view/game-detail-view.component.html index 2eb59d3..5843682 100644 --- a/frontend/src/app/components/game-detail-view/game-detail-view.component.html +++ b/frontend/src/app/components/game-detail-view/game-detail-view.component.html @@ -8,6 +8,7 @@

{{game.title}}

+

Release: {{game.releaseDate | date: 'longDate'}}

{{game.summary}}

diff --git a/frontend/src/app/components/header/header.component.html b/frontend/src/app/components/header/header.component.html index 419a2c1..a8c333f 100644 --- a/frontend/src/app/components/header/header.component.html +++ b/frontend/src/app/components/header/header.component.html @@ -1,11 +1,15 @@ - - + + diff --git a/frontend/src/app/components/header/header.component.ts b/frontend/src/app/components/header/header.component.ts index 4853af8..1616fcb 100644 --- a/frontend/src/app/components/header/header.component.ts +++ b/frontend/src/app/components/header/header.component.ts @@ -3,6 +3,9 @@ import {LibraryService} from "../../services/library.service"; import {MatSnackBar} from '@angular/material/snack-bar'; import {timeInterval} from "rxjs"; import {ActivatedRoute, Router} from "@angular/router"; +import {GamesService} from "../../services/games.service"; +import {LibraryManagementComponent} from "../library-management/library-management.component"; +import {LibraryOverviewComponent} from "../library-overview/library-overview.component"; @Component({ selector: 'app-header', @@ -12,16 +15,21 @@ import {ActivatedRoute, Router} from "@angular/router"; export class HeaderComponent { constructor(private libraryService: LibraryService, + private gameService: GamesService, private snackBar: MatSnackBar, private router: Router) { } - reloadLibrary(): void { + scanLibrary(): void { this.libraryService.scanLibrary().pipe(timeInterval()).subscribe({ next: value => this.snackBar.open(`Library scan completed in ${Math.trunc(value.interval / 1000)} seconds.`, undefined, {duration: 2000}), error: error => this.snackBar.open(`Error while scanning library: ${error.error.message}`, undefined, {duration: 5000}) }) - this.snackBar.open('Library scan started in the background. This could take some time.', undefined, {duration: 5000}) + this.snackBar.open('Library scan started in the background. This could take some time.\nYou will get another notification once it\'s done', undefined, {duration: 5000}) + } + + reloadLibrary(): void { + this.gameService.getAllGames(true).subscribe(() => this.router.navigate(['/library'])); } goToLibraryScreen(): void { @@ -32,8 +40,8 @@ export class HeaderComponent { this.router.navigate(['/library-management']); } - notOnLibraryScreen(): boolean { - return !(this.router.url === "/library"); + onLibraryScreen(): boolean { + return this.router.url === "/library"; } onLibraryManagementScreen(): boolean { diff --git a/frontend/src/app/components/library-management/library-management.component.html b/frontend/src/app/components/library-management/library-management.component.html index 40932b1..29bc7c5 100644 --- a/frontend/src/app/components/library-management/library-management.component.html +++ b/frontend/src/app/components/library-management/library-management.component.html @@ -1,77 +1,91 @@ -
-
- - - - - - - +
+ + +
Path {{element.path}}
+ + + + - - - - + + + + - - - - + + + + - - -
Path {{element.path}} Game {{element.title}} ({{getFullYearFromTimestamp(element.releaseDate)}}) Game {{element.title}} ({{getFullYearFromTimestamp(element.releaseDate)}} + ) + - - - - - - + + + + + +
-
+ + + + - - - - - - + +
Path {{element.path}}
+ + + + - - - - + + + + - - -
Path {{element.path}} - - - - - + + + + +
-
-
-
+ + + + +
- lock + + lock +

Please log in to manage your game library

+ +
+
+ north_east +

Use the library management to scan your file system for games

+
+ +
+ videogame_asset_off +

Your game library is empty!

+
+
diff --git a/frontend/src/app/components/library-management/library-management.component.scss b/frontend/src/app/components/library-management/library-management.component.scss index b2493c2..7df5396 100644 --- a/frontend/src/app/components/library-management/library-management.component.scss +++ b/frontend/src/app/components/library-management/library-management.component.scss @@ -1,6 +1,7 @@ @use 'sass:map'; @use '@angular/material' as mat; -@import '../../theme/default-theme'; +@import 'src/app/theme/default-theme'; +@import 'src/app/components/library-overview/library-overview.component'; td, th { padding: 16px !important; diff --git a/frontend/src/app/components/library-overview/library-overview.component.ts b/frontend/src/app/components/library-overview/library-overview.component.ts index 69af14b..43f54c1 100644 --- a/frontend/src/app/components/library-overview/library-overview.component.ts +++ b/frontend/src/app/components/library-overview/library-overview.component.ts @@ -1,4 +1,4 @@ -import {AfterViewInit, Component} from '@angular/core'; +import {AfterContentInit, AfterViewInit, Component} from '@angular/core'; import {GamesService} from "../../services/games.service"; import {DetectedGameDto} from "../../models/dtos/DetectedGameDto"; @@ -7,7 +7,7 @@ import {DetectedGameDto} from "../../models/dtos/DetectedGameDto"; templateUrl: './library-overview.component.html', styleUrls: ['./library-overview.component.scss'] }) -export class LibraryOverviewComponent implements AfterViewInit { +export class LibraryOverviewComponent implements AfterContentInit { detectedGames: DetectedGameDto[] = []; loading: boolean = true; @@ -15,7 +15,7 @@ export class LibraryOverviewComponent implements AfterViewInit { constructor(private gameServerService: GamesService) { } - ngAfterViewInit(): void { + ngAfterContentInit(): void { this.gameServerService.getAllGames().subscribe( (detectedGames: DetectedGameDto[]) => { this.detectedGames = detectedGames; diff --git a/frontend/src/app/interceptor/error.interceptor.ts b/frontend/src/app/interceptor/error.interceptor.ts index 528cfe0..11036b5 100644 --- a/frontend/src/app/interceptor/error.interceptor.ts +++ b/frontend/src/app/interceptor/error.interceptor.ts @@ -20,17 +20,15 @@ export class ErrorInterceptor implements HttpInterceptor { this.dialogService.showErrorDialog(err.error.message); } break; - case 401: - this.dialogService.showErrorDialog(err.error.message); - break; case 409: case 500: + case 401: + this.dialogService.showErrorDialog(err.error.message); this.dialogService.showErrorDialog(err.error.message); break; case 503: case 504: - this.dialogService.showErrorDialog('Can\'t reach the backend at the moment.\n' + - 'Please ensure that the backend is running and try again'); + this.dialogService.showErrorDialog(`Can't reach the backend at the moment.
Please ensure that the backend is running and reload this page`); break; } return throwError(err); diff --git a/frontend/src/app/services/games.service.ts b/frontend/src/app/services/games.service.ts index 9a82f53..d0388c5 100644 --- a/frontend/src/app/services/games.service.ts +++ b/frontend/src/app/services/games.service.ts @@ -12,14 +12,33 @@ export class GamesService implements GamesApi { private readonly apiPath = '/games'; + private cache: Map = new Map(); + constructor(private http: HttpClient) { } - getAllGames(): Observable { - return this.http.get(this.apiPath).pipe(map(games => games.sort((g1, g2) => g1.title.localeCompare(g2.title)))); + getAllGames(forceReloadFromServer: boolean = false): Observable { + + if (this.cache.size === 0 || forceReloadFromServer) { + let gamesObservable: Observable = this.http.get(this.apiPath).pipe(map(games => games.sort((g1, g2) => g1.title.localeCompare(g2.title)))); + gamesObservable.subscribe(g => this.cacheGames(g)); + return gamesObservable; + } + + return new Observable(subscriber => { + subscriber.next(Array.from(this.cache.values())); + subscriber.complete(); + }); } - getGame(slug: String): Observable { + getGame(slug: string): Observable { + if (this.cache.has(slug)) { + return new Observable(subscriber => { + subscriber.next(this.cache.get(slug)); + subscriber.complete(); + }); + } + return this.http.get(`${this.apiPath}/game/${slug}`); } @@ -34,4 +53,9 @@ export class GamesService implements GamesApi { getAllGameMappings(): Observable> { return this.http.get>(`${this.apiPath}/game-mappings`); } + + private cacheGames(gameList: DetectedGameDto[]): void { + this.cache.clear(); + gameList.forEach(game => this.cache.set(game.slug, game)); + } } diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index c5842ad..da5e8b5 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -1,5 +1,6 @@ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming +@use 'sass:map'; @use '@angular/material' as mat; // Plus imports for other components in your app. @@ -18,3 +19,13 @@ html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } + +.snackbar-dark { + color: white; + $config: mat.get-color-config($custom-theme); + $background: map.get($config, background); + background: mat.get-color-from-palette($background, app-bar); + + // add support for formatting (newlines) + white-space: pre-wrap; +}