mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
Various styling and small QoL improvements
This commit is contained in:
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -5,9 +5,7 @@ import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
|
||||
selector: 'app-error-dialog',
|
||||
template: `
|
||||
<h1 mat-dialog-title>Error</h1>
|
||||
<mat-dialog-content>
|
||||
{{message}}
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-content [innerHTML]="message"></mat-dialog-content>
|
||||
<mat-dialog-actions style="justify-content: end">
|
||||
<button mat-raised-button color="primary" (click)="onClick()">OK</button>
|
||||
</mat-dialog-actions>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
<div fxFlex="40" fxLayout="column" id="game-details">
|
||||
<h1>{{game.title}}</h1>
|
||||
<h3>Release: {{game.releaseDate | date: 'longDate'}}</h3>
|
||||
<p id="game-summary">{{game.summary}}</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
<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>
|
||||
</button>
|
||||
|
||||
<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>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,77 +1,91 @@
|
||||
<div *ngIf="loggedIn" fxFlexFill fxLayoutAlign="center center">
|
||||
<div>
|
||||
<mat-tab-group>
|
||||
<mat-tab label="Game mappings">
|
||||
<table mat-table [dataSource]="mappedGames" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="path">
|
||||
<th mat-header-cell *matHeaderCellDef> Path </th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.path}} </td>
|
||||
</ng-container>
|
||||
<div *ngIf="loggedIn && (this.unmappedFiles.length > 0 || this.mappedGames.length > 0)" fxFlexFill fxLayoutAlign="center start">
|
||||
<mat-tab-group>
|
||||
<mat-tab label="Game mappings">
|
||||
<table mat-table [dataSource]="mappedGames" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="path">
|
||||
<th mat-header-cell *matHeaderCellDef> Path</th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.path}} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="game">
|
||||
<th mat-header-cell *matHeaderCellDef> Game </th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.title}} ({{getFullYearFromTimestamp(element.releaseDate)}})</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="game">
|
||||
<th mat-header-cell *matHeaderCellDef> Game</th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.title}} ({{getFullYearFromTimestamp(element.releaseDate)}}
|
||||
)
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
<button mat-icon-button (click)="refreshMappedGamesList()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<button mat-icon-button (click)="confirmGameMapping(element)" [disabled]="element.confirmedMatch">
|
||||
<mat-icon>check</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="openCorrectMappingDialog(element)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="deleteGameMapping(element)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
<button mat-icon-button (click)="refreshMappedGamesList()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<button mat-icon-button (click)="confirmGameMapping(element)" [disabled]="element.confirmedMatch">
|
||||
<mat-icon>check</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="openCorrectMappingDialog(element)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="deleteGameMapping(element)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="gameMappingTableColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: gameMappingTableColumns;"></tr>
|
||||
</table>
|
||||
</mat-tab>
|
||||
<tr mat-header-row *matHeaderRowDef="gameMappingTableColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: gameMappingTableColumns;"></tr>
|
||||
</table>
|
||||
</mat-tab>
|
||||
|
||||
|
||||
<mat-tab label="Unmapped files">
|
||||
<table mat-table [dataSource]="unmappedFiles" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="path">
|
||||
<th mat-header-cell *matHeaderCellDef> Path </th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.path}} </td>
|
||||
</ng-container>
|
||||
<mat-tab label="Unmapped files">
|
||||
<table mat-table [dataSource]="unmappedFiles" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="path">
|
||||
<th mat-header-cell *matHeaderCellDef> Path</th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.path}} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
<button mat-icon-button (click)="refreshUnmappedFilesList()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<button mat-icon-button (click)="openMapUnmappedFileDialog(element)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="deleteUnmappedFile(element)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
<button mat-icon-button (click)="refreshUnmappedFilesList()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<button mat-icon-button (click)="openMapUnmappedFileDialog(element)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="deleteUnmappedFile(element)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="unmappedGameTableColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: unmappedGameTableColumns;"></tr>
|
||||
</table>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
<tr mat-header-row *matHeaderRowDef="unmappedGameTableColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: unmappedGameTableColumns;"></tr>
|
||||
</table>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!loggedIn" fxFlex fxLayout="column" fxLayoutAlign="center center" style="height: calc(100vh - 64px)">
|
||||
<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>
|
||||
</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 '@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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.<br>Please ensure that the backend is running and reload this page`);
|
||||
break;
|
||||
}
|
||||
return throwError(err);
|
||||
|
||||
@@ -12,14 +12,33 @@ export class GamesService implements GamesApi {
|
||||
|
||||
private readonly apiPath = '/games';
|
||||
|
||||
private cache: Map<string, DetectedGameDto> = new Map<string, DetectedGameDto>();
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
}
|
||||
|
||||
getAllGames(): Observable<DetectedGameDto[]> {
|
||||
return this.http.get<DetectedGameDto[]>(this.apiPath).pipe(map(games => games.sort((g1, g2) => g1.title.localeCompare(g2.title))));
|
||||
getAllGames(forceReloadFromServer: boolean = false): Observable<DetectedGameDto[]> {
|
||||
|
||||
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> {
|
||||
getGame(slug: string): Observable<DetectedGameDto> {
|
||||
if (this.cache.has(slug)) {
|
||||
return new Observable<DetectedGameDto>(subscriber => {
|
||||
subscriber.next(this.cache.get(slug));
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
return this.http.get<DetectedGameDto>(`${this.apiPath}/game/${slug}`);
|
||||
}
|
||||
|
||||
@@ -34,4 +53,9 @@ export class GamesService implements GamesApi {
|
||||
getAllGameMappings(): Observable<Map<string, string>> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user