diff --git a/frontend/src/app/components/game-detail-view/game-detail-view.component.ts b/frontend/src/app/components/game-detail-view/game-detail-view.component.ts
index 88f6db7..abaa756 100644
--- a/frontend/src/app/components/game-detail-view/game-detail-view.component.ts
+++ b/frontend/src/app/components/game-detail-view/game-detail-view.component.ts
@@ -3,6 +3,8 @@ import {ActivatedRoute, Params, Router} from "@angular/router";
import {DetectedGameDto} from "../../models/dtos/DetectedGameDto";
import {GamesService} from "../../services/games.service";
import {CompanyDto} from "../../models/dtos/CompanyDto";
+import {LibraryDto} from "../../models/dtos/LibraryDto";
+import {PlatformDto} from "../../models/dtos/PlatformDto";
@Component({
selector: 'app-game-detail-view',
@@ -110,4 +112,8 @@ export class GameDetailViewComponent {
return Math.floor(containerWidth / elementWidth);
}
+ hasPlatform(library: LibraryDto, platform: PlatformDto) {
+ return library.platforms.some((libPlatform) => libPlatform.slug == platform.slug)
+ }
+
}
diff --git a/frontend/src/app/components/header/header.component.ts b/frontend/src/app/components/header/header.component.ts
index eb3e802..46ccf6a 100644
--- a/frontend/src/app/components/header/header.component.ts
+++ b/frontend/src/app/components/header/header.component.ts
@@ -5,6 +5,7 @@ import {Router} from "@angular/router";
import {GamesService} from "../../services/games.service";
import {ThemingService} from "../../services/theming.service";
import {Location} from '@angular/common';
+import {LibraryScanRequestDto} from "../../models/dtos/LibraryScanRequestDto";
@Component({
selector: 'app-header',
@@ -25,7 +26,9 @@ export class HeaderComponent {
}
scanLibrary(): void {
- this.libraryService.scanLibrary().subscribe({
+ let request = new LibraryScanRequestDto();
+ request.downloadImages = true;
+ this.libraryService.scanLibrary(request).subscribe({
next: result => {
// Refresh the current page "angular style"
this.router.navigate([this.router.url]).then(() => {
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 6f20987..43bd999 100644
--- a/frontend/src/app/components/library-management/library-management.component.html
+++ b/frontend/src/app/components/library-management/library-management.component.html
@@ -1,6 +1,9 @@
-
0 || this.mappedGames.length > 0)" fxFlex fxLayoutAlign="center start">
+
0)" fxFlex fxLayoutAlign="center start">
+
+
+
@@ -19,7 +22,7 @@
-
+
north_east
Use the library management to scan your file system for games
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 0e9c25a..ffe987f 100644
--- a/frontend/src/app/components/library-management/library-management.component.ts
+++ b/frontend/src/app/components/library-management/library-management.component.ts
@@ -3,6 +3,7 @@ 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 {LibraryDto} from "../../models/dtos/LibraryDto";
@Component({
selector: 'app-library-management',
@@ -14,6 +15,7 @@ export class LibraryManagementComponent implements OnInit {
mappedGames: DetectedGameDto[] = [];
unmappedFiles: UnmappedFileDto[] = [];
+ mappedLibraries: LibraryDto[] = [];
constructor(private gamesService: GamesService,
private libraryManagementService: LibraryManagementService) {
@@ -25,6 +27,10 @@ export class LibraryManagementComponent implements OnInit {
this.unmappedFiles = uf;
this.loggedIn = true;
});
+ this.libraryManagementService.getLibraries().subscribe(libraries => {
+ this.mappedLibraries = libraries;
+ this.loggedIn = true;
+ });
}
}
diff --git a/frontend/src/app/components/library-overview/library-overview.component.html b/frontend/src/app/components/library-overview/library-overview.component.html
index e4bbf04..292ddf9 100644
--- a/frontend/src/app/components/library-overview/library-overview.component.html
+++ b/frontend/src/app/components/library-overview/library-overview.component.html
@@ -114,6 +114,18 @@
color="primary">{{playerPerspective.name}}
+
+
0" [expanded]="activePlatformFilters.length > 0">
+
+ Platforms
+
+
+
+ {{platform.name}}
+
+
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 c7a556a..4e16e5f 100644
--- a/frontend/src/app/components/library-overview/library-overview.component.ts
+++ b/frontend/src/app/components/library-overview/library-overview.component.ts
@@ -6,6 +6,7 @@ import {ThemeDto} from "../../models/dtos/ThemeDto";
import {firstValueFrom, forkJoin, Observable} from "rxjs";
import {SortDirection} from "@angular/material/sort";
import {PlayerPerspectiveDto} from "../../models/dtos/PlayerPerspectiveDto";
+import {PlatformDto} from "../../models/dtos/PlatformDto";
import {ActivatedRoute, ActivatedRouteSnapshot, Params, Router} from "@angular/router";
import {Location} from "@angular/common";
@@ -52,11 +53,13 @@ export class LibraryOverviewComponent implements AfterContentInit {
activeThemeFilters: string[] = [];
activeGenreFilters: string[] = [];
activePlayerPerspectiveFilters: string[] = [];
+ activePlatformFilters: string[] = [];
games: DetectedGameDto[] = [];
availableGenres: GenreDto[] = [];
availableThemes: ThemeDto[] = [];
availablePlayerPerspectives: PlayerPerspectiveDto[] = [];
+ availablePlatforms: PlatformDto[] = [];
loading: boolean = true;
gameLibraryIsEmpty: boolean = false;
@@ -82,11 +85,13 @@ export class LibraryOverviewComponent implements AfterContentInit {
let genreObservable: Observable
= this.gameServerService.getAvailableGenres();
let themeObservable: Observable = this.gameServerService.getAvailableThemes();
let playerPerspectiveObservable: Observable = this.gameServerService.getAvailablePlayerPerspectives();
+ let platformObservable: Observable = this.gameServerService.getAvailablePlatforms();
- forkJoin([genreObservable, themeObservable, playerPerspectiveObservable]).subscribe(result => {
+ forkJoin([genreObservable, themeObservable, playerPerspectiveObservable, platformObservable]).subscribe(result => {
this.availableGenres = result[0];
this.availableThemes = result[1];
this.availablePlayerPerspectives = result[2];
+ this.availablePlatforms = result[3];
this.previousStateParams = this.route.snapshot.queryParams;
if (this.previousStateParams['search'] !== undefined) this.searchTerm = this.previousStateParams['search'];
@@ -95,6 +100,7 @@ export class LibraryOverviewComponent implements AfterContentInit {
if (this.previousStateParams['genres'] !== undefined) this.activeGenreFilters = this.matchSelectedFilters(this.availableGenres, this.previousStateParams['genres']);
if (this.previousStateParams['themes'] !== undefined) this.activeThemeFilters = this.matchSelectedFilters(this.availableThemes, this.previousStateParams['themes']);
if (this.previousStateParams['playerPerspectives'] !== undefined) this.activePlayerPerspectiveFilters = this.matchSelectedFilters(this.availablePlayerPerspectives, this.previousStateParams['playerPerspectives']);
+ if (this.previousStateParams['platforms'] !== undefined) this.activePlatformFilters = this.matchSelectedFilters(this.availablePlatforms, this.previousStateParams['platforms']);
this.refreshLibraryView().then(() => this.loading = false);
});
@@ -134,6 +140,11 @@ export class LibraryOverviewComponent implements AfterContentInit {
games = games.filter(game => this.activePlayerPerspectiveFilters.every(activePlayerPerspectiveFilter => game.playerPerspectives?.map(g => g.slug).includes(activePlayerPerspectiveFilter)));
}
+ if (this.activePlatformFilters.length > 0) {
+ games = games.filter(game => this.activePlatformFilters.some(activePlatformFilter =>
+ game?.library?.platforms?.map(g => g.slug).includes(activePlatformFilter) && game?.platforms?.map(g => g.slug).includes(activePlatformFilter)));
+ }
+
return games;
}
@@ -197,6 +208,21 @@ export class LibraryOverviewComponent implements AfterContentInit {
this.refreshLibraryView();
}
+ togglePlatformFilter(slug: string): void {
+ if (this.activePlatformFilters.includes(slug)) {
+
+ const index = this.activePlatformFilters.indexOf(slug, 0);
+ if (index > -1) {
+ this.activePlatformFilters.splice(index, 1);
+ }
+
+ } else {
+ this.activePlatformFilters.push(slug);
+ }
+
+ this.refreshLibraryView();
+ }
+
private saveStateToRoute(): void {
let newStateParams: Params = {};
@@ -206,6 +232,7 @@ export class LibraryOverviewComponent implements AfterContentInit {
if (this.activeGenreFilters.length > 0) newStateParams['genres'] = this.activeGenreFilters.join(',');
if (this.activeThemeFilters.length > 0) newStateParams['themes'] = this.activeThemeFilters.join(',');
if (this.activePlayerPerspectiveFilters.length > 0) newStateParams['playerPerspectives'] = this.activePlayerPerspectiveFilters.join(',');
+ if (this.activePlatformFilters.length > 0) newStateParams['platforms'] = this.activePlatformFilters.join(',');
// only update the route if it changed
if (JSON.stringify(this.previousStateParams) !== JSON.stringify(newStateParams)) {
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 dac89ed..2826684 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
@@ -12,7 +12,7 @@
- {{suggestion.title}} ({{getFullYearFromTimestamp(suggestion.releaseDate)}})
+ {{suggestion.title}} ({{getFullYearFromTimestamp(suggestion.releaseDate)}}) - {{suggestion.platforms.join(', ')}}
diff --git a/frontend/src/app/components/map-library-dialog/map-library-dialog.component.html b/frontend/src/app/components/map-library-dialog/map-library-dialog.component.html
new file mode 100644
index 0000000..d593f4b
--- /dev/null
+++ b/frontend/src/app/components/map-library-dialog/map-library-dialog.component.html
@@ -0,0 +1,31 @@
+Map path to IGDB platform
+
+
+
+
+
+
+
diff --git a/frontend/src/app/components/map-library-dialog/map-library-dialog.component.scss b/frontend/src/app/components/map-library-dialog/map-library-dialog.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/app/components/map-library-dialog/map-library-dialog.component.spec.ts b/frontend/src/app/components/map-library-dialog/map-library-dialog.component.spec.ts
new file mode 100644
index 0000000..a55f4d0
--- /dev/null
+++ b/frontend/src/app/components/map-library-dialog/map-library-dialog.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { MapLibraryDialogComponent } from './map-library-dialog.component';
+
+describe('MapLibraryDialogComponent', () => {
+ let component: MapLibraryDialogComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ MapLibraryDialogComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(MapLibraryDialogComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/components/map-library-dialog/map-library-dialog.component.ts b/frontend/src/app/components/map-library-dialog/map-library-dialog.component.ts
new file mode 100644
index 0000000..01805b9
--- /dev/null
+++ b/frontend/src/app/components/map-library-dialog/map-library-dialog.component.ts
@@ -0,0 +1,90 @@
+import {Component, Inject, OnInit} from '@angular/core';
+import {LibraryManagementService} from "../../services/library-management.service";
+import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
+import {PathToSlugDto} from "../../models/dtos/PathToSlugDto";
+import {DialogService} from "../../services/dialog.service";
+import {ApiErrorResponse} from "../../models/dtos/ApiErrorResponse";
+import {PlatformDto} from "../../models/dtos/PlatformDto";
+
+@Component({
+ selector: 'app-map-library-dialog',
+ templateUrl: './map-library-dialog.component.html',
+ styleUrls: ['./map-library-dialog.component.scss']
+})
+export class MapLibraryDialogComponent implements OnInit {
+
+ path: string;
+ slugs: string;
+ previousSlugs: string;
+
+ autocompletePlatformSuggestions: PlatformDto[] = [];
+
+ submitLoading: boolean = false;
+ suggestionsLoading: boolean = false;
+
+ constructor(private libraryManagementService: LibraryManagementService,
+ private dialogService: DialogService,
+ public dialogRef: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA) data: any) {
+ this.path = data.path;
+ this.slugs = data.slugs ?? '';
+ this.previousSlugs = data.previousSlugs ?? '';
+ }
+
+ ngOnInit() {
+ this.loadInitialSuggestions();
+ }
+
+ submit(): void {
+ this.submitLoading = true;
+ this.libraryManagementService.mapLibrary(new PathToSlugDto(Array.isArray(this.slugs) ? this.slugs.join(',') : this.slugs, this.path)).subscribe({
+ next: () => this.dialogRef.close(true),
+ error: (error: ApiErrorResponse) => {
+ this.dialogRef.close(false);
+ this.dialogService.showErrorDialog(error.error.message);
+ }
+ }
+ )
+ }
+
+ loadInitialSuggestions(): void {
+ this.suggestionsLoading = true;
+
+ // Extract the last path element (folder name / file name)
+ let extractedPlatformFromPath: string = this.path.match(/([^\\/]*)[\\/]*$/)![1];
+ // Match it until the first special characters
+ extractedPlatformFromPath = extractedPlatformFromPath.match(/^[a-zA-Z0-9:\- ]+/)![0];
+
+ if(extractedPlatformFromPath == null) {
+ this.suggestionsLoading = false;
+ return;
+ }
+
+ this.libraryManagementService.getPlatforms(extractedPlatformFromPath, 10).subscribe({
+ next: suggestions => {
+ this.autocompletePlatformSuggestions = suggestions;
+ this.suggestionsLoading = false;
+ },
+ error: () => this.suggestionsLoading = false
+ })
+ }
+
+ loadSuggestions(): void {
+ this.suggestionsLoading = true;
+ let searchTerm = '';
+ if (this.slugs.length > 0) {
+ let slugArray = this.slugs.split(',');
+ // pop off the search term after the last comma
+ searchTerm = slugArray.pop() ?? '';
+ // if we already had slugs in our input field we need to add them back again
+ this.previousSlugs = (slugArray.length > 0 ? slugArray.join(',') + ',' : '');
+ }
+ this.libraryManagementService.getPlatforms(searchTerm, 50).subscribe({
+ next: suggestions => {
+ this.autocompletePlatformSuggestions = suggestions;
+ this.suggestionsLoading = false;
+ },
+ error: () => this.suggestionsLoading = false
+ })
+ }
+}
diff --git a/frontend/src/app/components/mapped-libraries-table/mapped-libraries-table.component.html b/frontend/src/app/components/mapped-libraries-table/mapped-libraries-table.component.html
new file mode 100644
index 0000000..c86c728
--- /dev/null
+++ b/frontend/src/app/components/mapped-libraries-table/mapped-libraries-table.component.html
@@ -0,0 +1,43 @@
+
+
+
+
+ | Path |
+ {{element.path}} |
+
+
+
+
+ Platforms |
+ {{item.name}}{{isLast ? '' : ', '}} |
+
+
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/app/components/mapped-libraries-table/mapped-libraries-table.component.scss b/frontend/src/app/components/mapped-libraries-table/mapped-libraries-table.component.scss
new file mode 100644
index 0000000..88a79ab
--- /dev/null
+++ b/frontend/src/app/components/mapped-libraries-table/mapped-libraries-table.component.scss
@@ -0,0 +1,8 @@
+table {
+ width: 50vw;
+ min-width: 750px;
+}
+
+.mat-column-actions {
+ width: 20%;
+}
diff --git a/frontend/src/app/components/mapped-libraries-table/mapped-libraries-table.component.spec.ts b/frontend/src/app/components/mapped-libraries-table/mapped-libraries-table.component.spec.ts
new file mode 100644
index 0000000..bd3413e
--- /dev/null
+++ b/frontend/src/app/components/mapped-libraries-table/mapped-libraries-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 { MappedLibrariesTableComponent } from './mapped-libraries-table.component';
+
+describe('MappedLibrariesTableComponent', () => {
+ let component: MappedLibrariesTableComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(waitForAsync(() => {
+ TestBed.configureTestingModule({
+ declarations: [ MappedLibrariesTableComponent ],
+ imports: [
+ NoopAnimationsModule,
+ MatPaginatorModule,
+ MatSortModule,
+ MatTableModule,
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MappedLibrariesTableComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should compile', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/components/mapped-libraries-table/mapped-libraries-table.component.ts b/frontend/src/app/components/mapped-libraries-table/mapped-libraries-table.component.ts
new file mode 100644
index 0000000..46ae844
--- /dev/null
+++ b/frontend/src/app/components/mapped-libraries-table/mapped-libraries-table.component.ts
@@ -0,0 +1,100 @@
+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 {LibraryDto} from "../../models/dtos/LibraryDto";
+import {LibraryScanRequestDto} from "../../models/dtos/LibraryScanRequestDto";
+import {GamesService} from "../../services/games.service";
+import {LibraryManagementService} from "../../services/library-management.service";
+import {DialogService} from "../../services/dialog.service";
+import {MatSnackBar} from '@angular/material/snack-bar';
+import {Router} from "@angular/router";
+import {LibraryService} from "../../services/library.service";
+
+@Component({
+ selector: 'mapped-libraries-table',
+ templateUrl: './mapped-libraries-table.component.html',
+ styleUrls: ['./mapped-libraries-table.component.scss']
+})
+export class MappedLibrariesTableComponent implements AfterViewInit, OnChanges {
+ @ViewChild(MatPaginator) paginator!: MatPaginator;
+ @ViewChild(MatSort) sort!: MatSort;
+ @ViewChild(MatTable) table!: MatTable;
+ @Input() mappedLibraries!: LibraryDto[];
+
+ dataSource: MatTableDataSource = new MatTableDataSource();
+
+ displayedColumns: string[] = ["path", "platforms", "actions"];
+
+ filter: LibraryDto = new LibraryDto();
+
+ constructor(private libraryManagementService: LibraryManagementService,
+ private dialogService: DialogService,
+ private libraryService: LibraryService,
+ private snackBar: MatSnackBar,
+ private router: Router) {
+ }
+
+ ngAfterViewInit(): void {
+ this.dataSource.sort = this.sort;
+ this.dataSource.sortingDataAccessor = (item: LibraryDto, property: string) => {
+ return (item as any)[property];
+ };
+
+ this.dataSource.paginator = this.paginator;
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ this.refreshData(changes['mappedLibraries'].currentValue);
+ }
+
+ refreshMappedLibrariesList(): void {
+ this.libraryManagementService.getLibraries().subscribe(libraries => this.refreshData(libraries));
+ }
+
+ openLibraryMappingDialog(mappedLibrary: LibraryDto): void {
+ this.dialogService.libraryMappingDialog(mappedLibrary).subscribe(librarySuccessfullyMapped => {
+ if (librarySuccessfullyMapped) this.refreshMappedLibrariesList();
+ })
+ }
+
+ scanLibrary(mappedLibrary: LibraryDto): void {
+ let request = new LibraryScanRequestDto();
+ request.path = mappedLibrary.path;
+ request.downloadImages = true;
+ this.libraryService.scanLibrary(request).subscribe({
+ next: result => {
+ // Refresh the current page "angular style"
+ this.router.navigate([this.router.url]).then(() => {
+ const snackBarDuration: number = 10000;
+
+ let snackbarContent: string = 'Library scan completed in ' + result.scanDuration + ' seconds:\n' +
+ '- ' + result.newGames + ' new games\n' +
+ '- ' + result.deletedGames + ' games removed\n' +
+ '- ' + result.newUnmappableFiles + ' files/folders could not be mapped\n' +
+ '- ' + result.totalGames + ' games currently in your library';
+
+ if (result.companyLogoDownloads !== undefined && result.coverDownloads !== undefined && result.screenshotDownloads !== undefined) {
+ snackbarContent = snackbarContent.concat('\n' +
+ '- ' + result.coverDownloads + ' covers downloaded\n' +
+ '- ' + result.screenshotDownloads + ' screenshots downloaded\n' +
+ '- ' + result.companyLogoDownloads + ' company logos downloaded');
+ }
+
+ this.snackBar.open(snackbarContent, undefined, {duration: snackBarDuration});
+ }
+ )
+ },
+ 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.\nYou will get another notification once it\'s done', undefined, {duration: 5000})
+ }
+
+ private refreshData(newData: LibraryDto[]): 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/AutocompleteSuggestionDto.ts b/frontend/src/app/models/dtos/AutocompleteSuggestionDto.ts
index f4386a2..9c7b024 100644
--- a/frontend/src/app/models/dtos/AutocompleteSuggestionDto.ts
+++ b/frontend/src/app/models/dtos/AutocompleteSuggestionDto.ts
@@ -2,4 +2,5 @@ export class AutocompleteSuggestionDto {
slug!: string;
title!: string;
releaseDate!: number;
+ platforms!: Array;
}
diff --git a/frontend/src/app/models/dtos/DetectedGameDto.ts b/frontend/src/app/models/dtos/DetectedGameDto.ts
index ccb7bff..61e2179 100644
--- a/frontend/src/app/models/dtos/DetectedGameDto.ts
+++ b/frontend/src/app/models/dtos/DetectedGameDto.ts
@@ -2,7 +2,9 @@ import {CompanyDto} from "./CompanyDto";
import {GenreDto} from "./GenreDto";
import {KeywordDto} from "./KeywordDto";
import {PlayerPerspectiveDto} from "./PlayerPerspectiveDto";
+import {PlatformDto} from "./PlatformDto";
import {ThemeDto} from "./ThemeDto";
+import {LibraryDto} from "./LibraryDto";
export class DetectedGameDto {
@@ -26,6 +28,8 @@ export class DetectedGameDto {
keywords?: KeywordDto[];
themes?: ThemeDto[];
playerPerspectives?: PlayerPerspectiveDto[];
+ platforms?: PlatformDto[];
+ library?: LibraryDto;
path!: string;
diskSize!: number;
diff --git a/frontend/src/app/models/dtos/LibraryDto.ts b/frontend/src/app/models/dtos/LibraryDto.ts
new file mode 100644
index 0000000..784dfbe
--- /dev/null
+++ b/frontend/src/app/models/dtos/LibraryDto.ts
@@ -0,0 +1,7 @@
+import {PlatformDto} from "./PlatformDto";
+
+export class LibraryDto {
+ path!: string;
+ platforms!: PlatformDto[];
+}
+
diff --git a/frontend/src/app/models/dtos/LibraryScanRequestDto.ts b/frontend/src/app/models/dtos/LibraryScanRequestDto.ts
new file mode 100644
index 0000000..3613d30
--- /dev/null
+++ b/frontend/src/app/models/dtos/LibraryScanRequestDto.ts
@@ -0,0 +1,7 @@
+import {PlatformDto} from "./PlatformDto";
+
+export class LibraryScanRequestDto {
+ path!: string;
+ downloadImages!: boolean;
+}
+
diff --git a/frontend/src/app/models/dtos/PlatformDto.ts b/frontend/src/app/models/dtos/PlatformDto.ts
new file mode 100644
index 0000000..c3d9619
--- /dev/null
+++ b/frontend/src/app/models/dtos/PlatformDto.ts
@@ -0,0 +1,5 @@
+export class PlatformDto {
+ slug!: string;
+ name!: string;
+ platformLogoId?: string;
+}
diff --git a/frontend/src/app/services/dialog.service.ts b/frontend/src/app/services/dialog.service.ts
index 5e5eec8..7513241 100644
--- a/frontend/src/app/services/dialog.service.ts
+++ b/frontend/src/app/services/dialog.service.ts
@@ -3,7 +3,9 @@ import {MatDialog, MatDialogConfig} from '@angular/material/dialog';
import {ErrorDialogComponent} from '../components/error-dialog/error-dialog.component';
import {DetectedGameDto} from "../models/dtos/DetectedGameDto";
import {MapGameDialogComponent} from "../components/map-game-dialog/map-game-dialog.component";
+import {MapLibraryDialogComponent} from "../components/map-library-dialog/map-library-dialog.component";
import {UnmappedFileDto} from "../models/dtos/UnmappedFileDto";
+import {LibraryDto} from "../models/dtos/LibraryDto";
import {Observable} from "rxjs";
@Injectable({
@@ -35,7 +37,7 @@ export class DialogService {
dialogConfig.disableClose = true;
dialogConfig.autoFocus = true;
dialogConfig.closeOnNavigation = true;
- dialogConfig.minWidth = '25vw';
+ dialogConfig.minWidth = '40vw';
dialogConfig.data = {
path: game.path,
@@ -51,7 +53,7 @@ export class DialogService {
dialogConfig.disableClose = true;
dialogConfig.autoFocus = true;
dialogConfig.closeOnNavigation = true;
- dialogConfig.minWidth = '25vw';
+ dialogConfig.minWidth = '40vw';
dialogConfig.data = {
path: unmappedFile.path
@@ -60,4 +62,20 @@ export class DialogService {
return this.dialog.open(MapGameDialogComponent, dialogConfig).afterClosed();
}
+ public libraryMappingDialog(library: LibraryDto): Observable {
+ const dialogConfig = new MatDialogConfig();
+
+ dialogConfig.disableClose = true;
+ dialogConfig.autoFocus = true;
+ dialogConfig.closeOnNavigation = true;
+ dialogConfig.minWidth = '40vw';
+
+ dialogConfig.data = {
+ path: library.path,
+ slugs: library.platforms.map((platform) => platform.slug)
+ };
+
+ return this.dialog.open(MapLibraryDialogComponent, dialogConfig).afterClosed();
+ }
+
}
diff --git a/frontend/src/app/services/games.service.ts b/frontend/src/app/services/games.service.ts
index ea7a5b8..87a9776 100644
--- a/frontend/src/app/services/games.service.ts
+++ b/frontend/src/app/services/games.service.ts
@@ -5,6 +5,7 @@ import {distinct, map, Observable} from "rxjs";
import {DetectedGameDto} from "../models/dtos/DetectedGameDto";
import {GameOverviewDto} from "../models/dtos/GameOverviewDto";
import {GenreDto} from "../models/dtos/GenreDto";
+import {PlatformDto} from "../models/dtos/PlatformDto";
import {ThemeDto} from "../models/dtos/ThemeDto";
import {CompanyDto} from "../models/dtos/CompanyDto";
import {PlayerPerspectiveDto} from "../models/dtos/PlayerPerspectiveDto";
@@ -88,6 +89,20 @@ export class GamesService implements GamesApi {
);
}
+ // TODO: This method of removing duplicates is most certainly an anti-pattern in RxJS
+ // TODO: However, I did not get the 'distinct()' pipe to work properly, so I have to take another look in the future
+ getAvailablePlatforms(): Observable {
+ return this.getAllGames().pipe(
+ map(
+ games => {
+ let availablePlatformsMap: Map = new Map;
+ games.map(game => game.library !== undefined && game.library.platforms.length > 0 ? game.library.platforms : []).flat().forEach(platform => availablePlatformsMap.set(platform.slug, platform));
+ return Array.from(availablePlatformsMap.values()).sort((p1, p2) => p1.name.localeCompare(p2.name));
+ }
+ )
+ );
+ }
+
downloadGame(slug: String): void {
window.open(`v1${this.apiPath}/game/${slug}/download`, '_top');
}
diff --git a/frontend/src/app/services/library-management.service.ts b/frontend/src/app/services/library-management.service.ts
index d36eb4c..e9ef5f3 100644
--- a/frontend/src/app/services/library-management.service.ts
+++ b/frontend/src/app/services/library-management.service.ts
@@ -7,6 +7,8 @@ import {UnmappedFileDto} from "../models/dtos/UnmappedFileDto";
import {LibraryManagementApi} from "../api/LibraryManagementApi";
import {GamesService} from "./games.service";
import {AutocompleteSuggestionDto} from "../models/dtos/AutocompleteSuggestionDto";
+import {LibraryDto} from "../models/dtos/LibraryDto";
+import {PlatformDto} from "../models/dtos/PlatformDto";
@Injectable({
providedIn: 'root'
@@ -50,4 +52,20 @@ export class LibraryManagementService implements LibraryManagementApi {
return this.http.get(`${this.apiPath}/autocomplete-suggestions`, {params:queryParams})
}
+
+ getPlatforms(searchTerm: string, limit: number): Observable {
+ let queryParams = new HttpParams();
+ queryParams = queryParams.append("searchTerm", searchTerm);
+ queryParams = queryParams.append("limit", limit);
+
+ return this.http.get(`${this.apiPath}/platforms`, {params:queryParams})
+ }
+
+ mapLibrary(pathToSlugDto: PathToSlugDto): Observable {
+ return this.http.post(`${this.apiPath}/map-library`, pathToSlugDto);
+ }
+
+ getLibraries(): Observable {
+ return this.http.get(`${this.apiPath}/libraries`);
+ }
}
diff --git a/frontend/src/app/services/library.service.ts b/frontend/src/app/services/library.service.ts
index 3fd7d82..3c07159 100644
--- a/frontend/src/app/services/library.service.ts
+++ b/frontend/src/app/services/library.service.ts
@@ -4,6 +4,8 @@ import {Observable} from "rxjs";
import {LibraryApi} from "../api/LibraryApi";
import {LibraryScanResultDto} from "../models/dtos/LibraryScanResultDto";
import {ImageDownloadResultDto} from "../models/dtos/ImageDownloadResultDto";
+import {LibraryDto} from "../models/dtos/LibraryDto";
+import {LibraryScanRequestDto} from "../models/dtos/LibraryScanRequestDto";
@Injectable({
providedIn: 'root'
@@ -15,8 +17,8 @@ export class LibraryService implements LibraryApi {
constructor(private http: HttpClient) {
}
- scanLibrary(): Observable {
- return this.http.get(`${this.apiPath}/scan`);
+ scanLibrary(library: LibraryScanRequestDto): Observable {
+ return this.http.post(`${this.apiPath}/scan`, library);
}
downloadImages(): Observable {
diff --git a/pom.xml b/pom.xml
index 1169c07..1dcd1ca 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
de.grimsi
gameyfin
- 1.2.6-SNAPSHOT
+ 1.3.0-SNAPSHOT
gameyfin
gameyfin