Various improvements to game management interface

This commit is contained in:
grimsi
2022-08-05 18:34:24 +02:00
parent 22c8e99f38
commit 98197fc4a6
23 changed files with 446 additions and 170 deletions
+35
View File
@@ -20,6 +20,7 @@
"@angular/platform-browser-dynamic": "^14.0.0",
"@angular/router": "^14.0.0",
"@angular/youtube-player": "^14.1.0",
"mat-table-filter": "^10.2.0",
"rxjs": "~7.5.0",
"tslib": "^2.3.0",
"zone.js": "~0.11.4"
@@ -7591,6 +7592,11 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -7889,6 +7895,21 @@
"node": ">= 8"
}
},
"node_modules/mat-table-filter": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/mat-table-filter/-/mat-table-filter-10.2.0.tgz",
"integrity": "sha512-IOuqsn+hKJRP7xRbhd71AzOoPH5FuzM9D8ySxk8OH2OqvdFeWOfwwgl1dEEwdV9L59We5OnlA9/5hd95w7/FvQ==",
"dependencies": {
"lodash-es": "^4.17.20",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@angular/cdk": ">=10.2.7",
"@angular/common": ">=10.2.4",
"@angular/core": ">=10.2.4",
"@angular/material": ">=10.2.7"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -17668,6 +17689,11 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -17903,6 +17929,15 @@
}
}
},
"mat-table-filter": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/mat-table-filter/-/mat-table-filter-10.2.0.tgz",
"integrity": "sha512-IOuqsn+hKJRP7xRbhd71AzOoPH5FuzM9D8ySxk8OH2OqvdFeWOfwwgl1dEEwdV9L59We5OnlA9/5hd95w7/FvQ==",
"requires": {
"lodash-es": "^4.17.20",
"tslib": "^2.0.0"
}
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+1
View File
@@ -22,6 +22,7 @@
"@angular/platform-browser-dynamic": "^14.0.0",
"@angular/router": "^14.0.0",
"@angular/youtube-player": "^14.1.0",
"mat-table-filter": "^10.2.0",
"rxjs": "~7.5.0",
"tslib": "^2.3.0",
"zone.js": "~0.11.4"
+1 -1
View File
@@ -6,7 +6,7 @@ import {UnmappedFileDto} from "../models/dtos/UnmappedFileDto";
export interface LibraryManagementApi {
mapGame(pathToSlugDto: PathToSlugDto): Observable<DetectedGameDto>;
getUnmappedFiles(): Observable<UnmappedFileDto[]>;
confirmGameMapping(slug: string): Observable<DetectedGameDto>;
confirmGameMapping(slug: string, confirm: boolean): Observable<DetectedGameDto>;
deleteGame(slug: string): Observable<Response>;
deleteUnmappedFile(id: number): Observable<Response>;
}
+5
View File
@@ -5,6 +5,7 @@ import {NavbarLayoutComponent} from "./layouts/navbar-layout/navbar-layout.compo
import {LibraryOverviewComponent} from "./components/library-overview/library-overview.component";
import {GameDetailViewComponent} from "./components/game-detail-view/game-detail-view.component";
import {LibraryManagementComponent} from "./components/library-management/library-management.component";
import {MappedGamesTableComponent} from "./components/mapped-games-table/mapped-games-table.component";
const appRoutes: Routes = [
{
@@ -23,6 +24,10 @@ const appRoutes: Routes = [
path: 'library-management',
component: LibraryManagementComponent
},
{
path: 'test',
component: MappedGamesTableComponent
},
{
path: '',
redirectTo: '/library',
+43 -31
View File
@@ -38,6 +38,12 @@ import {MatChipsModule} from "@angular/material/chips";
import { LibraryManagementComponent } from './components/library-management/library-management.component';
import {MatTooltipModule} from "@angular/material/tooltip";
import {MapGameDialogComponent} from "./components/map-game-dialog/map-game-dialog.component";
import {MatSlideToggleModule} from "@angular/material/slide-toggle";
import {MatCheckboxModule} from "@angular/material/checkbox";
import {A11yModule} from "@angular/cdk/a11y";
import { MappedGamesTableComponent } from './components/mapped-games-table/mapped-games-table.component';
import {MatTableFilterModule} from "mat-table-filter";
import { UnmappedFilesTableComponent } from './components/unmapped-files-table/unmapped-files-table.component';
@NgModule({
declarations: [
@@ -52,38 +58,44 @@ import {MapGameDialogComponent} from "./components/map-game-dialog/map-game-dial
GameScreenshotComponent,
GameVideoComponent,
LibraryManagementComponent,
MapGameDialogComponent
],
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
FormsModule,
MatFormFieldModule,
MatCardModule,
MatTabsModule,
MatToolbarModule,
MatMenuModule,
MatIconModule,
HttpClientModule,
FormsModule,
ReactiveFormsModule,
MatDialogModule,
MatButtonModule,
MatInputModule,
FlexModule,
MatProgressSpinnerModule,
MatTableModule,
MatPaginatorModule,
MatSortModule,
MatSnackBarModule,
MatGridListModule,
FlexLayoutModule,
GridModule,
YouTubePlayerModule,
MatChipsModule,
MatTooltipModule
MapGameDialogComponent,
MappedGamesTableComponent,
UnmappedFilesTableComponent
],
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
FormsModule,
MatFormFieldModule,
MatCardModule,
MatTabsModule,
MatToolbarModule,
MatMenuModule,
MatIconModule,
HttpClientModule,
FormsModule,
ReactiveFormsModule,
MatDialogModule,
MatButtonModule,
MatInputModule,
FlexModule,
MatProgressSpinnerModule,
MatTableModule,
MatPaginatorModule,
MatSortModule,
MatSnackBarModule,
MatGridListModule,
FlexLayoutModule,
GridModule,
YouTubePlayerModule,
MatChipsModule,
MatTooltipModule,
MatSlideToggleModule,
MatCheckboxModule,
A11yModule,
MatTableFilterModule
],
providers: [
{
provide: HTTP_INTERCEPTORS,
@@ -10,11 +10,7 @@ import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
<button mat-raised-button color="primary" (click)="onClick()">OK</button>
</mat-dialog-actions>
`,
styles: [`
mat-dialog-content {
min-width: 250px;
}
`]
styles: []
})
export class ErrorDialogComponent implements OnInit {
@@ -1,70 +1,10 @@
<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="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>
<mapped-games-table [mappedGames]="mappedGames"></mapped-games-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>
<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>
<unmapped-files-table [unmappedFiles]="unmappedFiles"></unmapped-files-table>
</mat-tab>
</mat-tab-group>
</div>
@@ -3,17 +3,8 @@
@import 'src/app/theme/default-theme';
@import 'src/app/components/library-overview/library-overview.component';
td, th {
padding: 16px !important;
}
table {
min-width: 50vw;
}
mat-tab-group {
$config: mat.get-color-config($custom-theme);
$background: map.get($config, background);
background: mat.get-color-from-palette($background, app-bar);
}
@@ -1,10 +1,8 @@
import {Component, OnInit} from '@angular/core';
import {DetectedGameDto} from "../../models/dtos/DetectedGameDto";
import {GamesService} from "../../services/games.service";
import {LibraryManagementService} from "../../services/library-management.service";
import {DetectedGameDto} from "../../models/dtos/DetectedGameDto";
import {UnmappedFileDto} from "../../models/dtos/UnmappedFileDto";
import {LibraryService} from "../../services/library.service";
import {DialogService} from "../../services/dialog.service";
@Component({
selector: 'app-library-management',
@@ -12,58 +10,21 @@ import {DialogService} from "../../services/dialog.service";
styleUrls: ['./library-management.component.scss']
})
export class LibraryManagementComponent implements OnInit {
gameMappingTableColumns: string[] = ["path", "game", "actions"];
unmappedGameTableColumns: string[] = ["path", "actions"];
loggedIn: boolean = false;
mappedGames!: DetectedGameDto[];
unmappedFiles!: UnmappedFileDto[];
loggedIn: boolean = false;
constructor(private gameService: GamesService,
private libraryManagementService: LibraryManagementService,
private dialogService: DialogService) {
constructor(private gamesService: GamesService,
private libraryManagementService: LibraryManagementService) {
}
ngOnInit(): void {
this.refreshMappedGamesList();
this.refreshUnmappedFilesList();
}
refreshMappedGamesList(): void {
this.gameService.getAllGames().subscribe(games => this.mappedGames = games);
}
getFullYearFromTimestamp(timestamp: number): number {
return new Date(timestamp).getFullYear();
}
confirmGameMapping(mappedGame: DetectedGameDto): void {
this.libraryManagementService.confirmGameMapping(mappedGame.slug).subscribe(() => mappedGame.confirmedMatch = true);
}
deleteGameMapping(mappedGame: DetectedGameDto): void {
this.libraryManagementService.deleteGame(mappedGame.slug).subscribe(() => this.mappedGames = this.mappedGames.filter(game => game !== mappedGame));
}
openCorrectMappingDialog(mappedGame: DetectedGameDto): void {
this.dialogService.correctGameMappingDialog(mappedGame);
}
refreshUnmappedFilesList(): void {
this.libraryManagementService.getUnmappedFiles().subscribe(unmappedFiles => {
this.unmappedFiles = unmappedFiles;
this.gamesService.getAllGames().subscribe(games => this.mappedGames = games);
this.libraryManagementService.getUnmappedFiles().subscribe(uf => {
this.unmappedFiles = uf;
this.loggedIn = true;
});
}
deleteUnmappedFile(unmappedFile: UnmappedFileDto): void {
this.libraryManagementService.deleteUnmappedFile(unmappedFile.id).subscribe(() => this.unmappedFiles = this.unmappedFiles.filter(uf => uf !== unmappedFile));
}
openMapUnmappedFileDialog(unmappedFile: UnmappedFileDto): void {
this.dialogService.mapUnmappedGameDialog(unmappedFile);
}
}
@@ -10,6 +10,6 @@
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-raised-button mat-dialog-close color="accent">Cancel</button>
<button mat-raised-button (click)="submit()">OK</button>
<button mat-raised-button [mat-dialog-close]="false" color="accent">Cancel</button>
<button mat-raised-button (click)="submit()" [disabled]="newSlugInput?.value?.length < 1" color="primary">OK</button>
</mat-dialog-actions>
@@ -27,11 +27,11 @@ export class MapGameDialogComponent implements OnInit {
ngOnInit() {
}
close() {
this.dialogRef.close();
}
submit(): void {
this.libraryManagementService.mapGame(new PathToSlugDto(this.newSlugInput.value, this.path)).subscribe(() => this.close())
this.libraryManagementService.mapGame(new PathToSlugDto(this.newSlugInput.value, this.path)).subscribe({
next: () => this.dialogRef.close(true),
error: () => this.dialogRef.close(false)
}
)
}
}
@@ -0,0 +1,50 @@
<div class="mat-elevation-z8">
<table mat-table matSort matTableFilter [dataSource]="dataSource" [exampleEntity]="filter" [debounceTime]="0">
<!-- Path column -->
<ng-container matColumnDef="path">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Path</th>
<td mat-cell *matCellDef="let element"> {{element.path}} </td>
</ng-container>
<!-- Game column -->
<ng-container matColumnDef="game">
<th mat-header-cell *matHeaderCellDef mat-sort-header>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)="toggleShowOnlyUnconfirmedMatches()">
<mat-icon *ngIf="showOnlyUnconfirmedMatches" matTooltip="Show all game mappings" matTooltipPosition="below" color="warn">playlist_add_check_circle</mat-icon>
<mat-icon *ngIf="!showOnlyUnconfirmedMatches" matTooltip="Show only unconfirmed game mappings" matTooltipPosition="below" fontSet="material-icons-outlined">playlist_add_check_circle</mat-icon>
</button>
<button mat-icon-button (click)="refreshMappedGamesList()">
<mat-icon matTooltip="Refresh game list" matTooltipPosition="below">refresh</mat-icon>
</button>
</th>
<!-- Action column -->
<td mat-cell *matCellDef="let element">
<button mat-icon-button (click)="toggleConfirmGameMapping(element)" [color]="element.confirmedMatch ? 'primary' : 'warn'">
<mat-icon [matTooltip]="element.confirmedMatch ? 'Unconfirm match':'Confirm match'" matTooltipPosition="below">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="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator
[length]="dataSource?.data?.length"
[pageIndex]="0"
[pageSize]="15"
[pageSizeOptions]="[10, 15, 25, 50]">
</mat-paginator>
</div>
@@ -0,0 +1,10 @@
table {
min-width: 50vw;
}
.mat-column-path {
width: 50%;
}
.mat-column-game {
width: 35%;
}
@@ -0,0 +1,34 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MappedGamesTableComponent } from './mapped-games-table.component';
describe('MappedGamesTableComponent', () => {
let component: MappedGamesTableComponent;
let fixture: ComponentFixture<MappedGamesTableComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ MappedGamesTableComponent ],
imports: [
NoopAnimationsModule,
MatPaginatorModule,
MatSortModule,
MatTableModule,
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MappedGamesTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should compile', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,87 @@
import {AfterViewInit, Component, Input, OnChanges, SimpleChanges, ViewChild} from '@angular/core';
import {MatPaginator} from '@angular/material/paginator';
import {MatSort} from '@angular/material/sort';
import {MatTable, MatTableDataSource} from '@angular/material/table';
import {DetectedGameDto} from "../../models/dtos/DetectedGameDto";
import {GamesService} from "../../services/games.service";
import {LibraryManagementService} from "../../services/library-management.service";
import {DialogService} from "../../services/dialog.service";
@Component({
selector: 'mapped-games-table',
templateUrl: './mapped-games-table.component.html',
styleUrls: ['./mapped-games-table.component.scss']
})
export class MappedGamesTableComponent implements AfterViewInit, OnChanges {
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
@ViewChild(MatTable) table!: MatTable<DetectedGameDto>;
@Input() mappedGames!: DetectedGameDto[];
dataSource: MatTableDataSource<DetectedGameDto> = new MatTableDataSource();
displayedColumns: string[] = ["path", "game", "actions"];
showOnlyUnconfirmedMatches: boolean = false;
filter: DetectedGameDto = new DetectedGameDto();
constructor(private gamesService: GamesService,
private libraryManagementService: LibraryManagementService,
private dialogService: DialogService) {
}
ngAfterViewInit(): void {
this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = (item: DetectedGameDto, property: string) => {
if (property === 'game') {
return item.title;
}
return (item as any)[property];
};
this.dataSource.paginator = this.paginator;
}
ngOnChanges(changes: SimpleChanges): void {
this.refreshData(changes['mappedGames'].currentValue);
}
refreshMappedGamesList(): void {
this.gamesService.getAllGames(true).subscribe(games => this.refreshData(games));
}
toggleShowOnlyUnconfirmedMatches() {
this.showOnlyUnconfirmedMatches = !this.showOnlyUnconfirmedMatches;
this.filter.confirmedMatch = this.showOnlyUnconfirmedMatches ? false : undefined;
}
getFullYearFromTimestamp(timestamp: number): number {
return new Date(timestamp).getFullYear();
}
toggleConfirmGameMapping(mappedGame: DetectedGameDto): void {
this.libraryManagementService.confirmGameMapping(mappedGame.slug, !mappedGame.confirmedMatch).subscribe(() => {
mappedGame.confirmedMatch = !mappedGame.confirmedMatch;
this.refreshData(this.dataSource.data);
});
}
deleteGameMapping(mappedGame: DetectedGameDto): void {
this.libraryManagementService.deleteGame(mappedGame.slug).subscribe(
() => this.refreshData(this.dataSource.data.filter(game => game !== mappedGame))
);
}
openCorrectMappingDialog(mappedGame: DetectedGameDto): void {
this.dialogService.correctGameMappingDialog(mappedGame);
}
private refreshData(newData: DetectedGameDto[]): void {
this.dataSource.data = newData;
// Dirty hack to force a re-render
// Did not find a better solution
this.paginator?._changePageSize(this.paginator?.pageSize);
}
}
@@ -0,0 +1,36 @@
<div class="mat-elevation-z8">
<table mat-table matSort [dataSource]="dataSource">
<!-- Path column -->
<ng-container matColumnDef="path">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Path</th>
<td mat-cell *matCellDef="let element"> {{element.path}} </td>
</ng-container>
<!-- Actions column -->
<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="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator
[length]="dataSource?.data?.length"
[pageIndex]="0"
[pageSize]="15"
[pageSizeOptions]="[10, 15, 25, 50]">
</mat-paginator>
</div>
@@ -0,0 +1,8 @@
table {
min-width: 50vw;
}
.mat-column-path {
width: 85%;
}
@@ -0,0 +1,34 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { UnmappedFilesTableComponent } from './unmapped-files-table.component';
describe('UnmappedFilesTableComponent', () => {
let component: UnmappedFilesTableComponent;
let fixture: ComponentFixture<UnmappedFilesTableComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ UnmappedFilesTableComponent ],
imports: [
NoopAnimationsModule,
MatPaginatorModule,
MatSortModule,
MatTableModule,
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(UnmappedFilesTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should compile', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,62 @@
import {AfterViewInit, Component, Input, OnChanges, SimpleChanges, ViewChild} from '@angular/core';
import {MatPaginator} from '@angular/material/paginator';
import {MatSort} from '@angular/material/sort';
import {MatTable, MatTableDataSource} from '@angular/material/table';
import {UnmappedFileDto} from "../../models/dtos/UnmappedFileDto";
import {GamesService} from "../../services/games.service";
import {LibraryManagementService} from "../../services/library-management.service";
import {DialogService} from "../../services/dialog.service";
@Component({
selector: 'unmapped-files-table',
templateUrl: './unmapped-files-table.component.html',
styleUrls: ['./unmapped-files-table.component.scss']
})
export class UnmappedFilesTableComponent implements AfterViewInit, OnChanges {
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
@ViewChild(MatTable) table!: MatTable<UnmappedFileDto>;
@Input() unmappedFiles!: UnmappedFileDto[];
dataSource: MatTableDataSource<UnmappedFileDto> = new MatTableDataSource();
displayedColumns: string[] = ["path", "actions"];
constructor(private gameService: GamesService,
private libraryManagementService: LibraryManagementService,
private dialogService: DialogService) {
}
ngAfterViewInit(): void {
this.dataSource.sort = this.sort;
this.dataSource.paginator = this.paginator;
}
ngOnChanges(changes: SimpleChanges): void {
this.refreshData(changes['unmappedFiles'].currentValue);
}
refreshUnmappedFilesList(): void {
this.libraryManagementService.getUnmappedFiles().subscribe(unmappedFiles => this.refreshData(unmappedFiles));
}
deleteUnmappedFile(unmappedFile: UnmappedFileDto): void {
this.libraryManagementService.deleteUnmappedFile(unmappedFile.id).subscribe(
() => this.refreshData(this.dataSource.data.filter(uf => uf !== unmappedFile))
);
}
openMapUnmappedFileDialog(unmappedFile: UnmappedFileDto): void {
this.dialogService.mapUnmappedGameDialog(unmappedFile).subscribe(gameSuccessfullyMapped => {
if (gameSuccessfullyMapped) this.refreshData(this.dataSource.data.filter(uf => uf !== unmappedFile));
})
}
private refreshData(newData: UnmappedFileDto[]): void {
this.dataSource.data = newData;
// Dirty hack to force a re-render
// Did not find a better solution
this.paginator?._changePageSize(this.paginator?.pageSize);
}
}
@@ -29,5 +29,5 @@ export class DetectedGameDto {
path!: string;
diskSize!: number;
confirmedMatch!: boolean;
confirmedMatch!: boolean | undefined;
}
+8 -4
View File
@@ -4,6 +4,7 @@ import {ErrorDialogComponent} from '../components/error-dialog/error-dialog.comp
import {DetectedGameDto} from "../models/dtos/DetectedGameDto";
import {MapGameDialogComponent} from "../components/map-game-dialog/map-game-dialog.component";
import {UnmappedFileDto} from "../models/dtos/UnmappedFileDto";
import {Observable} from "rxjs";
@Injectable({
providedIn: 'root'
@@ -19,6 +20,7 @@ export class DialogService {
dialogConfig.disableClose = true;
dialogConfig.autoFocus = true;
dialogConfig.closeOnNavigation = true;
dialogConfig.minWidth = '25vw';
dialogConfig.data = {
message
@@ -27,33 +29,35 @@ export class DialogService {
this.dialog.open(ErrorDialogComponent, dialogConfig);
}
public correctGameMappingDialog(game: DetectedGameDto): void {
public correctGameMappingDialog(game: DetectedGameDto): Observable<any> {
const dialogConfig = new MatDialogConfig();
dialogConfig.disableClose = true;
dialogConfig.autoFocus = true;
dialogConfig.closeOnNavigation = true;
dialogConfig.minWidth = '25vw';
dialogConfig.data = {
path: game.path,
slug: game.slug
};
this.dialog.open(MapGameDialogComponent, dialogConfig);
return this.dialog.open(MapGameDialogComponent, dialogConfig).afterClosed();
}
public mapUnmappedGameDialog(unmappedFile: UnmappedFileDto): void {
public mapUnmappedGameDialog(unmappedFile: UnmappedFileDto): Observable<any> {
const dialogConfig = new MatDialogConfig();
dialogConfig.disableClose = true;
dialogConfig.autoFocus = true;
dialogConfig.closeOnNavigation = true;
dialogConfig.minWidth = '25vw';
dialogConfig.data = {
path: unmappedFile.path
};
this.dialog.open(MapGameDialogComponent, dialogConfig);
return this.dialog.open(MapGameDialogComponent, dialogConfig).afterClosed();
}
}
@@ -54,6 +54,10 @@ export class GamesService implements GamesApi {
return this.http.get<Map<string, string>>(`${this.apiPath}/game-mappings`);
}
removeGameFromCache(slug: string): void {
this.cache.delete(slug);
}
private cacheGames(gameList: DetectedGameDto[]): void {
this.cache.clear();
gameList.forEach(game => this.cache.set(game.slug, game));
@@ -1,10 +1,11 @@
import {Injectable} from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {HttpClient, HttpParams} from "@angular/common/http";
import {Observable} from "rxjs";
import {DetectedGameDto} from "../models/dtos/DetectedGameDto";
import {PathToSlugDto} from "../models/dtos/PathToSlugDto";
import {UnmappedFileDto} from "../models/dtos/UnmappedFileDto";
import {LibraryManagementApi} from "../api/LibraryManagementApi";
import {GamesService} from "./games.service";
@Injectable({
providedIn: 'root'
@@ -13,7 +14,8 @@ export class LibraryManagementService implements LibraryManagementApi {
private readonly apiPath = '/library-management';
constructor(private http: HttpClient) {
constructor(private http: HttpClient,
private gamesService: GamesService) {
}
mapGame(pathToSlugDto: PathToSlugDto): Observable<DetectedGameDto> {
@@ -24,11 +26,15 @@ export class LibraryManagementService implements LibraryManagementApi {
return this.http.get<UnmappedFileDto[]>(`${this.apiPath}/unmapped-files`);
}
confirmGameMapping(slug: string): Observable<DetectedGameDto> {
return this.http.get<DetectedGameDto>(`${this.apiPath}/confirm-game/${slug}`);
confirmGameMapping(slug: string, confirm: boolean): Observable<DetectedGameDto> {
let queryParams = new HttpParams();
queryParams = queryParams.append("confirm", confirm);
return this.http.get<DetectedGameDto>(`${this.apiPath}/confirm-game/${slug}`, {params:queryParams});
}
deleteGame(slug: string): Observable<Response> {
this.gamesService.removeGameFromCache(slug);
return this.http.delete<Response>(`${this.apiPath}/delete-game/${slug}`);
}