diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts
index 9d929f7..c35ba06 100644
--- a/frontend/src/app/app.module.ts
+++ b/frontend/src/app/app.module.ts
@@ -44,6 +44,9 @@ 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';
+import {MatDividerModule} from "@angular/material/divider";
+import {MatListModule} from "@angular/material/list";
+import {MatAutocompleteModule} from "@angular/material/autocomplete";
@NgModule({
declarations: [
@@ -62,40 +65,43 @@ import { UnmappedFilesTableComponent } from './components/unmapped-files-table/u
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
- ],
+ 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,
+ MatDividerModule,
+ MatListModule,
+ MatAutocompleteModule
+ ],
providers: [
{
provide: HTTP_INTERCEPTORS,
diff --git a/frontend/src/app/components/game-detail-view/game-detail-view.component.html b/frontend/src/app/components/game-detail-view/game-detail-view.component.html
index 5843682..d7de92e 100644
--- a/frontend/src/app/components/game-detail-view/game-detail-view.component.html
+++ b/frontend/src/app/components/game-detail-view/game-detail-view.component.html
@@ -8,6 +8,7 @@
{{game.title}}
+
Rating: {{game.totalRating}}/100
Release: {{game.releaseDate | date: 'longDate'}}
{{game.summary}}
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 111ca44..274c041 100644
--- a/frontend/src/app/components/library-overview/library-overview.component.html
+++ b/frontend/src/app/components/library-overview/library-overview.component.html
@@ -4,26 +4,80 @@
Loading library...
-
+
north_east
Use the library management to scan your file system for games
- videogame_asset_off
+ videogame_asset_off
+
Your game library is empty!
-
0">
+
-
+
+
+
+
+ search
+
+
+
+
+ {{game.title}}
+
+
+
+
+
+
+
+
Gamemodes
+ error
+
+
+ Offline Co-op
+
+ Online Co-op
+
+ LAN Support
+
+
+
+
+
0">
+
Genres
+
+ {{genre.name}}
+
+
+
+
0">
+
Themes
+
+ {{theme.name}}
+
+
+
+
+
diff --git a/frontend/src/app/components/library-overview/library-overview.component.scss b/frontend/src/app/components/library-overview/library-overview.component.scss
index 2782144..92d78a1 100644
--- a/frontend/src/app/components/library-overview/library-overview.component.scss
+++ b/frontend/src/app/components/library-overview/library-overview.component.scss
@@ -39,3 +39,29 @@
.content {
padding: 16px;
}
+
+.filter-category-title {
+ margin-bottom: 0;
+}
+
+.filter-category-content {
+ margin-left: 6px;
+}
+
+::ng-deep .mat-checkbox-frame {
+ $config: mat.get-color-config($custom-theme);
+ $primary-palette: map.get($config, 'primary');
+ border-color: mat.get-color-from-palette($primary-palette, 500);
+}
+
+::ng-deep .mat-form-field-underline {
+ $config: mat.get-color-config($custom-theme);
+ $primary-palette: map.get($config, 'primary');
+ background-color: mat.get-color-from-palette($primary-palette, 500) !important;
+}
+
+.sticky {
+ position: sticky;
+ align-self: flex-start;
+ top: 80px; //64px height of app-header + 16px padding of content
+}
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 43f54c1..62615ce 100644
--- a/frontend/src/app/components/library-overview/library-overview.component.ts
+++ b/frontend/src/app/components/library-overview/library-overview.component.ts
@@ -1,6 +1,9 @@
-import {AfterContentInit, AfterViewInit, Component} from '@angular/core';
+import {AfterContentInit, AfterViewInit, Component, Input} from '@angular/core';
import {GamesService} from "../../services/games.service";
import {DetectedGameDto} from "../../models/dtos/DetectedGameDto";
+import {GenreDto} from "../../models/dtos/GenreDto";
+import {ThemeDto} from "../../models/dtos/ThemeDto";
+import {forkJoin, Observable} from "rxjs";
@Component({
selector: 'app-gameserver-list',
@@ -9,19 +12,98 @@ import {DetectedGameDto} from "../../models/dtos/DetectedGameDto";
})
export class LibraryOverviewComponent implements AfterContentInit {
- detectedGames: DetectedGameDto[] = [];
+ searchTerm: string = "";
+ offlineCoopFilterEnabled: boolean = false;
+ onlineCoopFilterEnabled: boolean = false;
+ lanSupportFilterEnabled: boolean = false;
+ activeThemeFilters: string[] = [];
+ activeGenreFilters: string[] = [];
+
+ games: DetectedGameDto[] = [];
+ availableGenres: GenreDto[] = [];
+ availableThemes: ThemeDto[] = [];
+
loading: boolean = true;
+ gameLibraryIsEmpty: boolean = false;
constructor(private gameServerService: GamesService) {
}
ngAfterContentInit(): void {
this.gameServerService.getAllGames().subscribe(
- (detectedGames: DetectedGameDto[]) => {
- this.detectedGames = detectedGames;
- this.loading = false;
+ detectedGames => {
+ if(detectedGames.length === 0) {
+ this.gameLibraryIsEmpty = true;
+ return;
+ }
+
+ this.games = detectedGames;
+
+ let genreObservable: Observable
= this.gameServerService.getAvailableGenres();
+ let themeObservable: Observable = this.gameServerService.getAvailableThemes();
+
+ forkJoin([themeObservable, genreObservable]).subscribe(result => {
+ this.availableThemes = result[0];
+ this.availableGenres = result[1];
+ this.filterGames();
+ this.loading = false;
+ });
}
);
}
+ filterGames(): void {
+ this.gameServerService.getAllGames().subscribe(games => {
+ let filteredGames: DetectedGameDto[] = games;
+
+ if(this.searchTerm.trim().toLowerCase().length > 0) {
+ filteredGames = filteredGames.filter(game => game.title.trim().toLowerCase().includes(this.searchTerm.trim().toLowerCase()));
+ }
+
+ if(this.offlineCoopFilterEnabled || this.onlineCoopFilterEnabled || this.lanSupportFilterEnabled) {
+ filteredGames = filteredGames.filter(game => (game.offlineCoop === this.offlineCoopFilterEnabled || game.onlineCoop === this.onlineCoopFilterEnabled || game.lanSupport === this.lanSupportFilterEnabled));
+ }
+
+ if(this.activeGenreFilters.length > 0) {
+ filteredGames = filteredGames.filter(game => this.activeGenreFilters.every(activeGenreFilter => game.genres?.map(g => g.slug).includes(activeGenreFilter)));
+ }
+
+ if(this.activeThemeFilters.length > 0) {
+ filteredGames = filteredGames.filter(game => this.activeThemeFilters.every(activeThemeFilter => game.themes?.map(g => g.slug).includes(activeThemeFilter)));
+ }
+
+ this.games = filteredGames;
+ })
+ }
+
+ toggleGenreFilter(slug: string): void {
+ if(this.activeGenreFilters.includes(slug)) {
+
+ const index = this.activeGenreFilters.indexOf(slug, 0);
+ if (index > -1) {
+ this.activeGenreFilters.splice(index, 1);
+ }
+
+ } else {
+ this.activeGenreFilters.push(slug);
+ }
+
+ this.filterGames();
+ }
+
+ toggleThemeFilter(slug: string) {
+ if(this.activeThemeFilters.includes(slug)) {
+
+ const index = this.activeThemeFilters.indexOf(slug, 0);
+ if (index > -1) {
+ this.activeThemeFilters.splice(index, 1);
+ }
+
+ } else {
+ this.activeThemeFilters.push(slug);
+ }
+
+ this.filterGames();
+ }
+
}
diff --git a/frontend/src/app/models/dtos/GenreDto.ts b/frontend/src/app/models/dtos/GenreDto.ts
index 06b3530..1a53bd5 100644
--- a/frontend/src/app/models/dtos/GenreDto.ts
+++ b/frontend/src/app/models/dtos/GenreDto.ts
@@ -1,4 +1,4 @@
export class GenreDto {
slug!: string;
- name?: string;
+ name!: string;
}
diff --git a/frontend/src/app/models/dtos/ThemeDto.ts b/frontend/src/app/models/dtos/ThemeDto.ts
index c49574f..f7ecd3c 100644
--- a/frontend/src/app/models/dtos/ThemeDto.ts
+++ b/frontend/src/app/models/dtos/ThemeDto.ts
@@ -1,4 +1,4 @@
export class ThemeDto {
slug!: string;
- name?: string;
+ name!: string;
}
diff --git a/frontend/src/app/services/games.service.ts b/frontend/src/app/services/games.service.ts
index 855012f..c91f940 100644
--- a/frontend/src/app/services/games.service.ts
+++ b/frontend/src/app/services/games.service.ts
@@ -1,9 +1,11 @@
import {Injectable} from '@angular/core';
import {GamesApi} from "../api/GamesApi";
import {HttpClient} from "@angular/common/http";
-import {map, Observable} from "rxjs";
+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 {ThemeDto} from "../models/dtos/ThemeDto";
@Injectable({
providedIn: 'root'
@@ -42,6 +44,34 @@ export class GamesService implements GamesApi {
return this.http.get(`${this.apiPath}/game/${slug}`);
}
+ // 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
+ getAvailableGenres(): Observable {
+ return this.getAllGames().pipe(
+ map(
+ games => {
+ let availableGenresMap: Map = new Map;
+ games.map(game => game.genres === undefined ? [] : game.genres).flat().forEach(genre => availableGenresMap.set(genre.slug, genre));
+ return Array.from(availableGenresMap.values()).sort((g1, g2) => g1.name.localeCompare(g2.name));
+ }
+ )
+ );
+ }
+
+ // 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
+ getAvailableThemes(): Observable {
+ return this.getAllGames().pipe(
+ map(
+ games => {
+ let availableThemesMap: Map = new Map;
+ games.map(game => game.themes === undefined ? [] : game.themes).flat().forEach(theme => availableThemesMap.set(theme.slug, theme));
+ return Array.from(availableThemesMap.values()).sort((t1, t2) => t1.name.localeCompare(t2.name));
+ }
+ )
+ );
+ }
+
downloadGame(slug: String): void {
window.open(`v1${this.apiPath}/game/${slug}/download`, '_top');
}
diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss
index da5e8b5..1b418e3 100644
--- a/frontend/src/styles.scss
+++ b/frontend/src/styles.scss
@@ -16,10 +16,13 @@
@include mat.all-component-themes($custom-theme);
/* You can add global styles to this file, and also import other style files */
-
html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
+html {
+ overflow-y: scroll;
+}
+
.snackbar-dark {
color: white;
$config: mat.get-color-config($custom-theme);