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!

-
+
-
+ +
+ +
+ search + + + + + {{game.title}} + + + +
+ +
+
+

Gamemodes

+ error +
+
+ Offline Co-op + + Online Co-op + + LAN Support + +
+
+ +
+

Genres

+
+ {{genre.name}} +
+
+ +
+

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);