diff --git a/frontend/package-lock.json b/frontend/package-lock.json index aade951..6820593 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index 93e0c7e..8b4d859 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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" diff --git a/frontend/src/app/api/LibraryManagementApi.ts b/frontend/src/app/api/LibraryManagementApi.ts index 2b3d0ec..bb4b8b1 100644 --- a/frontend/src/app/api/LibraryManagementApi.ts +++ b/frontend/src/app/api/LibraryManagementApi.ts @@ -6,7 +6,7 @@ import {UnmappedFileDto} from "../models/dtos/UnmappedFileDto"; export interface LibraryManagementApi { mapGame(pathToSlugDto: PathToSlugDto): Observable; getUnmappedFiles(): Observable; - confirmGameMapping(slug: string): Observable; + confirmGameMapping(slug: string, confirm: boolean): Observable; deleteGame(slug: string): Observable; deleteUnmappedFile(id: number): Observable; } diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index d6f3d00..f5062b5 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -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', diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 3012076..9d929f7 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -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, 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 b5e0b7a..f02d49a 100644 --- a/frontend/src/app/components/error-dialog/error-dialog.component.ts +++ b/frontend/src/app/components/error-dialog/error-dialog.component.ts @@ -10,11 +10,7 @@ import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; `, - styles: [` - mat-dialog-content { - min-width: 250px; - } - `] + styles: [] }) export class ErrorDialogComponent implements OnInit { 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 29bc7c5..56de44a 100644 --- a/frontend/src/app/components/library-management/library-management.component.html +++ b/frontend/src/app/components/library-management/library-management.component.html @@ -1,70 +1,10 @@
- - - - - - - - - - - - - - - - - - -
Path {{element.path}} Game {{element.title}} ({{getFullYearFromTimestamp(element.releaseDate)}} - ) - - - - - - -
+
- - - - - - - - - - - - - - - -
Path {{element.path}} - - - - -
+
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 7df5396..dd017e9 100644 --- a/frontend/src/app/components/library-management/library-management.component.scss +++ b/frontend/src/app/components/library-management/library-management.component.scss @@ -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); - } diff --git a/frontend/src/app/components/library-management/library-management.component.ts b/frontend/src/app/components/library-management/library-management.component.ts index 5ee7bd4..dd76f2b 100644 --- a/frontend/src/app/components/library-management/library-management.component.ts +++ b/frontend/src/app/components/library-management/library-management.component.ts @@ -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); - } - } diff --git a/frontend/src/app/components/map-game-dialog/map-game-dialog.component.html b/frontend/src/app/components/map-game-dialog/map-game-dialog.component.html index 25412bd..eb9b888 100644 --- a/frontend/src/app/components/map-game-dialog/map-game-dialog.component.html +++ b/frontend/src/app/components/map-game-dialog/map-game-dialog.component.html @@ -10,6 +10,6 @@ - - + + diff --git a/frontend/src/app/components/map-game-dialog/map-game-dialog.component.ts b/frontend/src/app/components/map-game-dialog/map-game-dialog.component.ts index 30df899..49d446b 100644 --- a/frontend/src/app/components/map-game-dialog/map-game-dialog.component.ts +++ b/frontend/src/app/components/map-game-dialog/map-game-dialog.component.ts @@ -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) + } + ) } } diff --git a/frontend/src/app/components/mapped-games-table/mapped-games-table.component.html b/frontend/src/app/components/mapped-games-table/mapped-games-table.component.html new file mode 100644 index 0000000..b8953fa --- /dev/null +++ b/frontend/src/app/components/mapped-games-table/mapped-games-table.component.html @@ -0,0 +1,50 @@ +
+ + + + + + + + + + + + + + + + + + + + + + +
Path {{element.path}} Game {{element.title}} ({{getFullYearFromTimestamp(element.releaseDate)}}) + + + + + + +
+ + + +
diff --git a/frontend/src/app/components/mapped-games-table/mapped-games-table.component.scss b/frontend/src/app/components/mapped-games-table/mapped-games-table.component.scss new file mode 100644 index 0000000..1c98660 --- /dev/null +++ b/frontend/src/app/components/mapped-games-table/mapped-games-table.component.scss @@ -0,0 +1,10 @@ +table { + min-width: 50vw; +} + +.mat-column-path { + width: 50%; +} +.mat-column-game { + width: 35%; +} diff --git a/frontend/src/app/components/mapped-games-table/mapped-games-table.component.spec.ts b/frontend/src/app/components/mapped-games-table/mapped-games-table.component.spec.ts new file mode 100644 index 0000000..d19ac4f --- /dev/null +++ b/frontend/src/app/components/mapped-games-table/mapped-games-table.component.spec.ts @@ -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; + + 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(); + }); +}); diff --git a/frontend/src/app/components/mapped-games-table/mapped-games-table.component.ts b/frontend/src/app/components/mapped-games-table/mapped-games-table.component.ts new file mode 100644 index 0000000..889e770 --- /dev/null +++ b/frontend/src/app/components/mapped-games-table/mapped-games-table.component.ts @@ -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; + @Input() mappedGames!: DetectedGameDto[]; + + dataSource: MatTableDataSource = 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); + } +} diff --git a/frontend/src/app/components/unmapped-files-table/unmapped-files-table.component.html b/frontend/src/app/components/unmapped-files-table/unmapped-files-table.component.html new file mode 100644 index 0000000..9336cc7 --- /dev/null +++ b/frontend/src/app/components/unmapped-files-table/unmapped-files-table.component.html @@ -0,0 +1,36 @@ +
+ + + + + + + + + + + + + + + +
Path {{element.path}} + + + + +
+ + + +
diff --git a/frontend/src/app/components/unmapped-files-table/unmapped-files-table.component.scss b/frontend/src/app/components/unmapped-files-table/unmapped-files-table.component.scss new file mode 100644 index 0000000..a21bb6b --- /dev/null +++ b/frontend/src/app/components/unmapped-files-table/unmapped-files-table.component.scss @@ -0,0 +1,8 @@ +table { + min-width: 50vw; +} + +.mat-column-path { + width: 85%; +} + diff --git a/frontend/src/app/components/unmapped-files-table/unmapped-files-table.component.spec.ts b/frontend/src/app/components/unmapped-files-table/unmapped-files-table.component.spec.ts new file mode 100644 index 0000000..cac285f --- /dev/null +++ b/frontend/src/app/components/unmapped-files-table/unmapped-files-table.component.spec.ts @@ -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; + + 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(); + }); +}); diff --git a/frontend/src/app/components/unmapped-files-table/unmapped-files-table.component.ts b/frontend/src/app/components/unmapped-files-table/unmapped-files-table.component.ts new file mode 100644 index 0000000..41cf571 --- /dev/null +++ b/frontend/src/app/components/unmapped-files-table/unmapped-files-table.component.ts @@ -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; + @Input() unmappedFiles!: UnmappedFileDto[]; + + dataSource: MatTableDataSource = 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); + } +} diff --git a/frontend/src/app/models/dtos/DetectedGameDto.ts b/frontend/src/app/models/dtos/DetectedGameDto.ts index deb2caf..543c091 100644 --- a/frontend/src/app/models/dtos/DetectedGameDto.ts +++ b/frontend/src/app/models/dtos/DetectedGameDto.ts @@ -29,5 +29,5 @@ export class DetectedGameDto { path!: string; diskSize!: number; - confirmedMatch!: boolean; + confirmedMatch!: boolean | undefined; } diff --git a/frontend/src/app/services/dialog.service.ts b/frontend/src/app/services/dialog.service.ts index 0428514..5e5eec8 100644 --- a/frontend/src/app/services/dialog.service.ts +++ b/frontend/src/app/services/dialog.service.ts @@ -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 { 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 { 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(); } } diff --git a/frontend/src/app/services/games.service.ts b/frontend/src/app/services/games.service.ts index d0388c5..855012f 100644 --- a/frontend/src/app/services/games.service.ts +++ b/frontend/src/app/services/games.service.ts @@ -54,6 +54,10 @@ export class GamesService implements GamesApi { return this.http.get>(`${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)); diff --git a/frontend/src/app/services/library-management.service.ts b/frontend/src/app/services/library-management.service.ts index 23e992b..d1dfb37 100644 --- a/frontend/src/app/services/library-management.service.ts +++ b/frontend/src/app/services/library-management.service.ts @@ -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 { @@ -24,11 +26,15 @@ export class LibraryManagementService implements LibraryManagementApi { return this.http.get(`${this.apiPath}/unmapped-files`); } - confirmGameMapping(slug: string): Observable { - return this.http.get(`${this.apiPath}/confirm-game/${slug}`); + confirmGameMapping(slug: string, confirm: boolean): Observable { + let queryParams = new HttpParams(); + queryParams = queryParams.append("confirm", confirm); + + return this.http.get(`${this.apiPath}/confirm-game/${slug}`, {params:queryParams}); } deleteGame(slug: string): Observable { + this.gamesService.removeGameFromCache(slug); return this.http.delete(`${this.apiPath}/delete-game/${slug}`); }