mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 16:20:04 +00:00
Implemented filter
This commit is contained in:
@@ -44,6 +44,9 @@ import {A11yModule} from "@angular/cdk/a11y";
|
|||||||
import { MappedGamesTableComponent } from './components/mapped-games-table/mapped-games-table.component';
|
import { MappedGamesTableComponent } from './components/mapped-games-table/mapped-games-table.component';
|
||||||
import {MatTableFilterModule} from "mat-table-filter";
|
import {MatTableFilterModule} from "mat-table-filter";
|
||||||
import { UnmappedFilesTableComponent } from './components/unmapped-files-table/unmapped-files-table.component';
|
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({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@@ -62,40 +65,43 @@ import { UnmappedFilesTableComponent } from './components/unmapped-files-table/u
|
|||||||
MappedGamesTableComponent,
|
MappedGamesTableComponent,
|
||||||
UnmappedFilesTableComponent
|
UnmappedFilesTableComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
MatTabsModule,
|
MatTabsModule,
|
||||||
MatToolbarModule,
|
MatToolbarModule,
|
||||||
MatMenuModule,
|
MatMenuModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
FlexModule,
|
FlexModule,
|
||||||
MatProgressSpinnerModule,
|
MatProgressSpinnerModule,
|
||||||
MatTableModule,
|
MatTableModule,
|
||||||
MatPaginatorModule,
|
MatPaginatorModule,
|
||||||
MatSortModule,
|
MatSortModule,
|
||||||
MatSnackBarModule,
|
MatSnackBarModule,
|
||||||
MatGridListModule,
|
MatGridListModule,
|
||||||
FlexLayoutModule,
|
FlexLayoutModule,
|
||||||
GridModule,
|
GridModule,
|
||||||
YouTubePlayerModule,
|
YouTubePlayerModule,
|
||||||
MatChipsModule,
|
MatChipsModule,
|
||||||
MatTooltipModule,
|
MatTooltipModule,
|
||||||
MatSlideToggleModule,
|
MatSlideToggleModule,
|
||||||
MatCheckboxModule,
|
MatCheckboxModule,
|
||||||
A11yModule,
|
A11yModule,
|
||||||
MatTableFilterModule
|
MatTableFilterModule,
|
||||||
],
|
MatDividerModule,
|
||||||
|
MatListModule,
|
||||||
|
MatAutocompleteModule
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: HTTP_INTERCEPTORS,
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
<div fxFlex="40" fxLayout="column" id="game-details">
|
<div fxFlex="40" fxLayout="column" id="game-details">
|
||||||
<h1>{{game.title}}</h1>
|
<h1>{{game.title}}</h1>
|
||||||
|
<h3>Rating: {{game.totalRating}}/100</h3>
|
||||||
<h3>Release: {{game.releaseDate | date: 'longDate'}}</h3>
|
<h3>Release: {{game.releaseDate | date: 'longDate'}}</h3>
|
||||||
<p id="game-summary">{{game.summary}}</p>
|
<p id="game-summary">{{game.summary}}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,26 +4,80 @@
|
|||||||
<h2>Loading library...</h2>
|
<h2>Loading library...</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="!this.loading && this.detectedGames.length === 0" fxFlex fxLayout="column" fxLayoutAlign="center center" style="height: calc(100vh - 64px)">
|
<div *ngIf="!this.loading && this.gameLibraryIsEmpty" fxFlex fxLayout="column" fxLayoutAlign="center center"
|
||||||
|
style="height: calc(100vh - 64px)">
|
||||||
<div class="library-management-hint" fxLayout="column" fxLayoutAlign="start end">
|
<div class="library-management-hint" fxLayout="column" fxLayoutAlign="start end">
|
||||||
<mat-icon fontSet="material-icons-outlined">north_east</mat-icon>
|
<mat-icon fontSet="material-icons-outlined">north_east</mat-icon>
|
||||||
<p>Use the library management to scan your file system for games</p>
|
<p>Use the library management to scan your file system for games</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div fxLayout="column" fxLayoutAlign="center center">
|
<div fxLayout="column" fxLayoutAlign="center center">
|
||||||
<mat-icon fontSet="material-icons-outlined" color="primary" style="font-size: 128px; height: 128px; width: 128px;">videogame_asset_off</mat-icon>
|
<mat-icon fontSet="material-icons-outlined" color="primary"
|
||||||
|
style="font-size: 128px; height: 128px; width: 128px;">videogame_asset_off
|
||||||
|
</mat-icon>
|
||||||
<h1>Your game library is empty!</h1>
|
<h1>Your game library is empty!</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content" fxLayout="row" fxFlexFill="100" *ngIf="!this.loading && this.detectedGames.length > 0">
|
<div class="content" fxLayout="row" fxLayout.lt-lg="column" fxFlexFill="100"
|
||||||
|
*ngIf="!this.loading && !this.gameLibraryIsEmpty">
|
||||||
<div fxFlex="10" fxHide fxShow.gt-md><!--SPACER--></div>
|
<div fxFlex="10" fxHide fxShow.gt-md><!--SPACER--></div>
|
||||||
<div fxFlex="0 1 20"><!--FILTER--></div>
|
|
||||||
|
<div fxFlex.gt-md="0 1 15" fxLayout="column" fxLayoutGap="16px" fxLayoutAlign.lt-lg="start center" fxFlex.lt-lg="100" [ngClass.gt-md]="'sticky'">
|
||||||
|
|
||||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="6px">
|
||||||
|
<mat-icon matTooltip="Search for games by title">search</mat-icon>
|
||||||
|
<mat-form-field fxFlex="80" class="filter-category-content">
|
||||||
|
<input type="text" matInput [matAutocomplete]="auto" [(ngModel)]="searchTerm" (ngModelChange)="filterGames()">
|
||||||
|
<mat-autocomplete #auto="matAutocomplete">
|
||||||
|
<mat-option *ngFor="let game of games" [value]="game.title">
|
||||||
|
{{game.title}}
|
||||||
|
</mat-option>
|
||||||
|
</mat-autocomplete>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="6px">
|
||||||
|
<h3 class="filter-category-title">Gamemodes</h3>
|
||||||
|
<mat-icon matTooltip="Filter may not work correctly, working on a fix" color="warn">error</mat-icon>
|
||||||
|
</div>
|
||||||
|
<div fxLayout="column" class="filter-category-content">
|
||||||
|
<mat-checkbox [(ngModel)]="offlineCoopFilterEnabled" (change)="filterGames()" color="primary">Offline Co-op
|
||||||
|
</mat-checkbox>
|
||||||
|
<mat-checkbox [(ngModel)]="onlineCoopFilterEnabled" (change)="filterGames()" color="primary">Online Co-op
|
||||||
|
</mat-checkbox>
|
||||||
|
<mat-checkbox [(ngModel)]="lanSupportFilterEnabled" (change)="filterGames()" color="primary">LAN Support
|
||||||
|
</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="availableGenres.length > 0">
|
||||||
|
<h3 class="filter-category-title">Genres</h3>
|
||||||
|
<div fxLayout="column" class="filter-category-content">
|
||||||
|
<mat-checkbox *ngFor="let genre of availableGenres" (change)="toggleGenreFilter(genre.slug)"
|
||||||
|
[checked]="activeGenreFilters.includes(genre.slug)"
|
||||||
|
color="primary">{{genre.name}}</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="availableThemes.length > 0">
|
||||||
|
<h3 class="filter-category-title">Themes</h3>
|
||||||
|
<div fxLayout="column" class="filter-category-content">
|
||||||
|
<mat-checkbox *ngFor="let theme of availableThemes" (change)="toggleThemeFilter(theme.slug)"
|
||||||
|
[checked]="activeThemeFilters.includes(theme.slug)"
|
||||||
|
color="primary">{{theme.name}}</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<div fxFlex fxLayout="row wrap" fxLayoutGap="16px grid">
|
<div fxFlex fxLayout="row wrap" fxLayoutGap="16px grid">
|
||||||
<div *ngFor="let game of detectedGames">
|
<div *ngFor="let game of games">
|
||||||
<game-cover [game]="game"></game-cover>
|
<game-cover [game]="game"></game-cover>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div fxFlex="0 1 10" fxHide fxShow.gt-lg><!--SPACER--></div>
|
<div fxFlex="0 1 10" fxHide fxShow.gt-lg><!--SPACER--></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,3 +39,29 @@
|
|||||||
.content {
|
.content {
|
||||||
padding: 16px;
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {GamesService} from "../../services/games.service";
|
||||||
import {DetectedGameDto} from "../../models/dtos/DetectedGameDto";
|
import {DetectedGameDto} from "../../models/dtos/DetectedGameDto";
|
||||||
|
import {GenreDto} from "../../models/dtos/GenreDto";
|
||||||
|
import {ThemeDto} from "../../models/dtos/ThemeDto";
|
||||||
|
import {forkJoin, Observable} from "rxjs";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-gameserver-list',
|
selector: 'app-gameserver-list',
|
||||||
@@ -9,19 +12,98 @@ import {DetectedGameDto} from "../../models/dtos/DetectedGameDto";
|
|||||||
})
|
})
|
||||||
export class LibraryOverviewComponent implements AfterContentInit {
|
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;
|
loading: boolean = true;
|
||||||
|
gameLibraryIsEmpty: boolean = false;
|
||||||
|
|
||||||
constructor(private gameServerService: GamesService) {
|
constructor(private gameServerService: GamesService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterContentInit(): void {
|
ngAfterContentInit(): void {
|
||||||
this.gameServerService.getAllGames().subscribe(
|
this.gameServerService.getAllGames().subscribe(
|
||||||
(detectedGames: DetectedGameDto[]) => {
|
detectedGames => {
|
||||||
this.detectedGames = detectedGames;
|
if(detectedGames.length === 0) {
|
||||||
this.loading = false;
|
this.gameLibraryIsEmpty = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.games = detectedGames;
|
||||||
|
|
||||||
|
let genreObservable: Observable<ThemeDto[]> = this.gameServerService.getAvailableGenres();
|
||||||
|
let themeObservable: Observable<GenreDto[]> = 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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export class GenreDto {
|
export class GenreDto {
|
||||||
slug!: string;
|
slug!: string;
|
||||||
name?: string;
|
name!: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export class ThemeDto {
|
export class ThemeDto {
|
||||||
slug!: string;
|
slug!: string;
|
||||||
name?: string;
|
name!: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {GamesApi} from "../api/GamesApi";
|
import {GamesApi} from "../api/GamesApi";
|
||||||
import {HttpClient} from "@angular/common/http";
|
import {HttpClient} from "@angular/common/http";
|
||||||
import {map, Observable} from "rxjs";
|
import {distinct, map, Observable} from "rxjs";
|
||||||
import {DetectedGameDto} from "../models/dtos/DetectedGameDto";
|
import {DetectedGameDto} from "../models/dtos/DetectedGameDto";
|
||||||
import {GameOverviewDto} from "../models/dtos/GameOverviewDto";
|
import {GameOverviewDto} from "../models/dtos/GameOverviewDto";
|
||||||
|
import {GenreDto} from "../models/dtos/GenreDto";
|
||||||
|
import {ThemeDto} from "../models/dtos/ThemeDto";
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@@ -42,6 +44,34 @@ export class GamesService implements GamesApi {
|
|||||||
return this.http.get<DetectedGameDto>(`${this.apiPath}/game/${slug}`);
|
return this.http.get<DetectedGameDto>(`${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<GenreDto[]> {
|
||||||
|
return this.getAllGames().pipe(
|
||||||
|
map<DetectedGameDto[], GenreDto[]>(
|
||||||
|
games => {
|
||||||
|
let availableGenresMap: Map<string, GenreDto> = new Map<string, GenreDto>;
|
||||||
|
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<ThemeDto[]> {
|
||||||
|
return this.getAllGames().pipe(
|
||||||
|
map<DetectedGameDto[], ThemeDto[]>(
|
||||||
|
games => {
|
||||||
|
let availableThemesMap: Map<string, ThemeDto> = new Map<string, GenreDto>;
|
||||||
|
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 {
|
downloadGame(slug: String): void {
|
||||||
window.open(`v1${this.apiPath}/game/${slug}/download`, '_top');
|
window.open(`v1${this.apiPath}/game/${slug}/download`, '_top');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,13 @@
|
|||||||
@include mat.all-component-themes($custom-theme);
|
@include mat.all-component-themes($custom-theme);
|
||||||
|
|
||||||
/* You can add global styles to this file, and also import other style files */
|
/* You can add global styles to this file, and also import other style files */
|
||||||
|
|
||||||
html, body { height: 100%; }
|
html, body { height: 100%; }
|
||||||
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
|
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
|
||||||
|
|
||||||
|
html {
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
.snackbar-dark {
|
.snackbar-dark {
|
||||||
color: white;
|
color: white;
|
||||||
$config: mat.get-color-config($custom-theme);
|
$config: mat.get-color-config($custom-theme);
|
||||||
|
|||||||
Reference in New Issue
Block a user