Various styling and small QoL improvements

This commit is contained in:
grimsi
2022-08-05 10:26:01 +02:00
parent 7f2e77c8bc
commit 8ee217bbe8
12 changed files with 160 additions and 85 deletions
+12
View File
@@ -1,4 +1,5 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import {ActivatedRoute, NavigationEnd, Router} from "@angular/router";
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@@ -7,4 +8,15 @@ import { Component } from '@angular/core';
}) })
export class AppComponent { export class AppComponent {
title = 'frontend'; 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;
}
});
}
} }
+5 -1
View File
@@ -29,7 +29,7 @@ import {MatPaginatorModule} from "@angular/material/paginator";
import {MatSortModule} from "@angular/material/sort"; import {MatSortModule} from "@angular/material/sort";
import {GameCoverComponent} from './components/game-cover/game-cover.component'; import {GameCoverComponent} from './components/game-cover/game-cover.component';
import {GameDetailViewComponent} from './components/game-detail-view/game-detail-view.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 {MatGridListModule} from "@angular/material/grid-list";
import {GameScreenshotComponent} from './components/game-screenshot/game-screenshot.component'; import {GameScreenshotComponent} from './components/game-screenshot/game-screenshot.component';
import {YouTubePlayerModule} from "@angular/youtube-player"; import {YouTubePlayerModule} from "@angular/youtube-player";
@@ -94,6 +94,10 @@ import {MapGameDialogComponent} from "./components/map-game-dialog/map-game-dial
provide: HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,
useClass: ErrorInterceptor, useClass: ErrorInterceptor,
multi: true multi: true
},
{
provide: MAT_SNACK_BAR_DEFAULT_OPTIONS,
useValue: { panelClass: ['snackbar-dark'] },
} }
], ],
bootstrap: [AppComponent] bootstrap: [AppComponent]
@@ -5,9 +5,7 @@ import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
selector: 'app-error-dialog', selector: 'app-error-dialog',
template: ` template: `
<h1 mat-dialog-title>Error</h1> <h1 mat-dialog-title>Error</h1>
<mat-dialog-content> <mat-dialog-content [innerHTML]="message"></mat-dialog-content>
{{message}}
</mat-dialog-content>
<mat-dialog-actions style="justify-content: end"> <mat-dialog-actions style="justify-content: end">
<button mat-raised-button color="primary" (click)="onClick()">OK</button> <button mat-raised-button color="primary" (click)="onClick()">OK</button>
</mat-dialog-actions> </mat-dialog-actions>
@@ -8,6 +8,7 @@
<div fxFlex="40" fxLayout="column" id="game-details"> <div fxFlex="40" fxLayout="column" id="game-details">
<h1>{{game.title}}</h1> <h1>{{game.title}}</h1>
<h3>Release: {{game.releaseDate | date: 'longDate'}}</h3>
<p id="game-summary">{{game.summary}}</p> <p id="game-summary">{{game.summary}}</p>
</div> </div>
@@ -1,11 +1,15 @@
<mat-toolbar style="position: sticky; top: 0; z-index: 99999"> <mat-toolbar style="position: sticky; top: 0; z-index: 99999">
<button mat-icon-button (click)="goToLibraryScreen()" *ngIf="notOnLibraryScreen()"> <button mat-icon-button (click)="goToLibraryScreen()" *ngIf="!onLibraryScreen()">
<mat-icon>home</mat-icon> <mat-icon>home</mat-icon>
</button> </button>
<span class="spacer"></span> <span class="spacer"></span>
<button mat-icon-button (click)="reloadLibrary()" *ngIf="onLibraryManagementScreen()"> <button mat-icon-button (click)="reloadLibrary()" *ngIf="onLibraryScreen()">
<mat-icon>refresh</mat-icon>
</button>
<button mat-icon-button (click)="scanLibrary()" *ngIf="onLibraryManagementScreen()">
<mat-icon>youtube_searched_for</mat-icon> <mat-icon>youtube_searched_for</mat-icon>
</button> </button>
@@ -3,6 +3,9 @@ import {LibraryService} from "../../services/library.service";
import {MatSnackBar} from '@angular/material/snack-bar'; import {MatSnackBar} from '@angular/material/snack-bar';
import {timeInterval} from "rxjs"; import {timeInterval} from "rxjs";
import {ActivatedRoute, Router} from "@angular/router"; 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({ @Component({
selector: 'app-header', selector: 'app-header',
@@ -12,16 +15,21 @@ import {ActivatedRoute, Router} from "@angular/router";
export class HeaderComponent { export class HeaderComponent {
constructor(private libraryService: LibraryService, constructor(private libraryService: LibraryService,
private gameService: GamesService,
private snackBar: MatSnackBar, private snackBar: MatSnackBar,
private router: Router) { private router: Router) {
} }
reloadLibrary(): void { scanLibrary(): void {
this.libraryService.scanLibrary().pipe(timeInterval()).subscribe({ this.libraryService.scanLibrary().pipe(timeInterval()).subscribe({
next: value => this.snackBar.open(`Library scan completed in ${Math.trunc(value.interval / 1000)} seconds.`, undefined, {duration: 2000}), 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}) 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 { goToLibraryScreen(): void {
@@ -32,8 +40,8 @@ export class HeaderComponent {
this.router.navigate(['/library-management']); this.router.navigate(['/library-management']);
} }
notOnLibraryScreen(): boolean { onLibraryScreen(): boolean {
return !(this.router.url === "/library"); return this.router.url === "/library";
} }
onLibraryManagementScreen(): boolean { onLibraryManagementScreen(): boolean {
@@ -1,16 +1,17 @@
<div *ngIf="loggedIn" fxFlexFill fxLayoutAlign="center center"> <div *ngIf="loggedIn && (this.unmappedFiles.length > 0 || this.mappedGames.length > 0)" fxFlexFill fxLayoutAlign="center start">
<div>
<mat-tab-group> <mat-tab-group>
<mat-tab label="Game mappings"> <mat-tab label="Game mappings">
<table mat-table [dataSource]="mappedGames" class="mat-elevation-z8"> <table mat-table [dataSource]="mappedGames" class="mat-elevation-z8">
<ng-container matColumnDef="path"> <ng-container matColumnDef="path">
<th mat-header-cell *matHeaderCellDef> Path </th> <th mat-header-cell *matHeaderCellDef> Path</th>
<td mat-cell *matCellDef="let element"> {{element.path}} </td> <td mat-cell *matCellDef="let element"> {{element.path}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="game"> <ng-container matColumnDef="game">
<th mat-header-cell *matHeaderCellDef> Game </th> <th mat-header-cell *matHeaderCellDef> Game</th>
<td mat-cell *matCellDef="let element"> {{element.title}} ({{getFullYearFromTimestamp(element.releaseDate)}})</td> <td mat-cell *matCellDef="let element"> {{element.title}} ({{getFullYearFromTimestamp(element.releaseDate)}}
)
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
@@ -41,7 +42,7 @@
<mat-tab label="Unmapped files"> <mat-tab label="Unmapped files">
<table mat-table [dataSource]="unmappedFiles" class="mat-elevation-z8"> <table mat-table [dataSource]="unmappedFiles" class="mat-elevation-z8">
<ng-container matColumnDef="path"> <ng-container matColumnDef="path">
<th mat-header-cell *matHeaderCellDef> Path </th> <th mat-header-cell *matHeaderCellDef> Path</th>
<td mat-cell *matCellDef="let element"> {{element.path}} </td> <td mat-cell *matCellDef="let element"> {{element.path}} </td>
</ng-container> </ng-container>
@@ -66,12 +67,25 @@
</table> </table>
</mat-tab> </mat-tab>
</mat-tab-group> </mat-tab-group>
</div>
</div> </div>
<div *ngIf="!loggedIn" fxFlex fxLayout="column" fxLayoutAlign="center center" style="height: calc(100vh - 64px)"> <div *ngIf="!loggedIn" fxFlex fxLayout="column" fxLayoutAlign="center center" style="height: calc(100vh - 64px)">
<div fxLayout="column" fxLayoutAlign="center center"> <div fxLayout="column" fxLayoutAlign="center center">
<mat-icon fontSet="material-icons-outlined" color="primary" style="font-size: 128px; height: 128px; width: 128px;">lock</mat-icon> <mat-icon fontSet="material-icons-outlined" color="primary" style="font-size: 128px; height: 128px; width: 128px;">
lock
</mat-icon>
<h1>Please log in to manage your game library</h1> <h1>Please log in to manage your game library</h1>
</div> </div>
</div> </div>
<div *ngIf="loggedIn && this.unmappedFiles.length === 0 && this.mappedGames.length === 0" fxFlex fxLayout="column" fxLayoutAlign="center center" style="height: calc(100vh - 64px)">
<div class="library-management-hint" fxLayout="column" fxLayoutAlign="start end">
<mat-icon fontSet="material-icons-outlined">north_east</mat-icon>
<p>Use the library management to scan your file system for games</p>
</div>
<div fxLayout="column" fxLayoutAlign="center center">
<mat-icon fontSet="material-icons-outlined" color="primary" style="font-size: 128px; height: 128px; width: 128px;">videogame_asset_off</mat-icon>
<h1>Your game library is empty!</h1>
</div>
</div>
@@ -1,6 +1,7 @@
@use 'sass:map'; @use 'sass:map';
@use '@angular/material' as mat; @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 { td, th {
padding: 16px !important; padding: 16px !important;
@@ -1,4 +1,4 @@
import {AfterViewInit, Component} from '@angular/core'; import {AfterContentInit, AfterViewInit, Component} from '@angular/core';
import {GamesService} from "../../services/games.service"; import {GamesService} from "../../services/games.service";
import {DetectedGameDto} from "../../models/dtos/DetectedGameDto"; import {DetectedGameDto} from "../../models/dtos/DetectedGameDto";
@@ -7,7 +7,7 @@ import {DetectedGameDto} from "../../models/dtos/DetectedGameDto";
templateUrl: './library-overview.component.html', templateUrl: './library-overview.component.html',
styleUrls: ['./library-overview.component.scss'] styleUrls: ['./library-overview.component.scss']
}) })
export class LibraryOverviewComponent implements AfterViewInit { export class LibraryOverviewComponent implements AfterContentInit {
detectedGames: DetectedGameDto[] = []; detectedGames: DetectedGameDto[] = [];
loading: boolean = true; loading: boolean = true;
@@ -15,7 +15,7 @@ export class LibraryOverviewComponent implements AfterViewInit {
constructor(private gameServerService: GamesService) { constructor(private gameServerService: GamesService) {
} }
ngAfterViewInit(): void { ngAfterContentInit(): void {
this.gameServerService.getAllGames().subscribe( this.gameServerService.getAllGames().subscribe(
(detectedGames: DetectedGameDto[]) => { (detectedGames: DetectedGameDto[]) => {
this.detectedGames = detectedGames; this.detectedGames = detectedGames;
@@ -20,17 +20,15 @@ export class ErrorInterceptor implements HttpInterceptor {
this.dialogService.showErrorDialog(err.error.message); this.dialogService.showErrorDialog(err.error.message);
} }
break; break;
case 401:
this.dialogService.showErrorDialog(err.error.message);
break;
case 409: case 409:
case 500: case 500:
case 401:
this.dialogService.showErrorDialog(err.error.message);
this.dialogService.showErrorDialog(err.error.message); this.dialogService.showErrorDialog(err.error.message);
break; break;
case 503: case 503:
case 504: case 504:
this.dialogService.showErrorDialog('Can\'t reach the backend at the moment.\n' + this.dialogService.showErrorDialog(`Can't reach the backend at the moment.<br>Please ensure that the backend is running and reload this page`);
'Please ensure that the backend is running and try again');
break; break;
} }
return throwError(err); return throwError(err);
+27 -3
View File
@@ -12,14 +12,33 @@ export class GamesService implements GamesApi {
private readonly apiPath = '/games'; private readonly apiPath = '/games';
private cache: Map<string, DetectedGameDto> = new Map<string, DetectedGameDto>();
constructor(private http: HttpClient) { constructor(private http: HttpClient) {
} }
getAllGames(): Observable<DetectedGameDto[]> { getAllGames(forceReloadFromServer: boolean = false): Observable<DetectedGameDto[]> {
return this.http.get<DetectedGameDto[]>(this.apiPath).pipe(map(games => games.sort((g1, g2) => g1.title.localeCompare(g2.title))));
if (this.cache.size === 0 || forceReloadFromServer) {
let gamesObservable: Observable<DetectedGameDto[]> = this.http.get<DetectedGameDto[]>(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<DetectedGameDto[]>(subscriber => {
subscriber.next(Array.from(this.cache.values()));
subscriber.complete();
});
}
getGame(slug: string): Observable<DetectedGameDto> {
if (this.cache.has(slug)) {
return new Observable<DetectedGameDto>(subscriber => {
subscriber.next(this.cache.get(slug));
subscriber.complete();
});
} }
getGame(slug: String): Observable<DetectedGameDto> {
return this.http.get<DetectedGameDto>(`${this.apiPath}/game/${slug}`); return this.http.get<DetectedGameDto>(`${this.apiPath}/game/${slug}`);
} }
@@ -34,4 +53,9 @@ export class GamesService implements GamesApi {
getAllGameMappings(): Observable<Map<string, string>> { getAllGameMappings(): Observable<Map<string, string>> {
return this.http.get<Map<string, string>>(`${this.apiPath}/game-mappings`); return this.http.get<Map<string, string>>(`${this.apiPath}/game-mappings`);
} }
private cacheGames(gameList: DetectedGameDto[]): void {
this.cache.clear();
gameList.forEach(game => this.cache.set(game.slug, game));
}
} }
+11
View File
@@ -1,5 +1,6 @@
// Custom Theming for Angular Material // Custom Theming for Angular Material
// For more information: https://material.angular.io/guide/theming // For more information: https://material.angular.io/guide/theming
@use 'sass:map';
@use '@angular/material' as mat; @use '@angular/material' as mat;
// Plus imports for other components in your app. // Plus imports for other components in your app.
@@ -18,3 +19,13 @@
html, body { height: 100%; } html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 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;
}