From acd9e79fce694eb504801063224dbd588abae9d0 Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Sun, 14 Aug 2022 15:26:09 +0200 Subject: [PATCH 1/6] Added field "addedToLibrary" to DetectedGame Integrated Flyway for DB Migrations Removed unused DTO classes --- backend/pom.xml | 8 +- .../config/SecurityConfiguration.java | 1 + .../java/de/grimsi/gameyfin/dto/GameDto.java | 24 --- .../gameyfin/dto/UsernamePasswordDto.java | 9 -- .../gameyfin/entities/DetectedGame.java | 4 + .../src/main/resources/config/database.yml | 4 +- .../V1_0_0__Initial_Database_Setup.sql | 148 ++++++++++++++++++ ...d_Field_addedToLibrary_to_DetectedGame.sql | 4 + .../src/app/models/dtos/DetectedGameDto.ts | 1 + 9 files changed, 168 insertions(+), 35 deletions(-) delete mode 100644 backend/src/main/java/de/grimsi/gameyfin/dto/GameDto.java delete mode 100644 backend/src/main/java/de/grimsi/gameyfin/dto/UsernamePasswordDto.java create mode 100644 backend/src/main/resources/db/migration/V1_0_0__Initial_Database_Setup.sql create mode 100644 backend/src/main/resources/db/migration/V1_1_0__Add_Field_addedToLibrary_to_DetectedGame.sql diff --git a/backend/pom.xml b/backend/pom.xml index 6906b6d..c795310 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -87,6 +87,11 @@ h2 runtime + + org.flywaydb + flyway-core + + @@ -144,8 +149,9 @@ **/*.properties **/*.yml **/*.yaml - **/*.json + **/*.sql **/*.txt + **/*.json **/*.js **/*.css **/*.html diff --git a/backend/src/main/java/de/grimsi/gameyfin/config/SecurityConfiguration.java b/backend/src/main/java/de/grimsi/gameyfin/config/SecurityConfiguration.java index 22d2812..889989b 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/config/SecurityConfiguration.java +++ b/backend/src/main/java/de/grimsi/gameyfin/config/SecurityConfiguration.java @@ -35,6 +35,7 @@ public class SecurityConfiguration { @Bean protected SecurityFilterChain httpSecurity(HttpSecurity http) throws Exception { http.csrf().disable(); + http.headers().frameOptions().disable(); http.httpBasic(Customizer.withDefaults()); return http.build(); } diff --git a/backend/src/main/java/de/grimsi/gameyfin/dto/GameDto.java b/backend/src/main/java/de/grimsi/gameyfin/dto/GameDto.java deleted file mode 100644 index 8f72c3a..0000000 --- a/backend/src/main/java/de/grimsi/gameyfin/dto/GameDto.java +++ /dev/null @@ -1,24 +0,0 @@ -package de.grimsi.gameyfin.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.File; -import java.time.Instant; -import java.util.List; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class GameDto { - private String name; - private String publisher; - private String slug; - private Instant releaseDate; - - private List files; - private Long fileSize; -} diff --git a/backend/src/main/java/de/grimsi/gameyfin/dto/UsernamePasswordDto.java b/backend/src/main/java/de/grimsi/gameyfin/dto/UsernamePasswordDto.java deleted file mode 100644 index 2f16123..0000000 --- a/backend/src/main/java/de/grimsi/gameyfin/dto/UsernamePasswordDto.java +++ /dev/null @@ -1,9 +0,0 @@ -package de.grimsi.gameyfin.dto; - -import lombok.Data; - -@Data -public class UsernamePasswordDto { - private String username; - private String password; -} diff --git a/backend/src/main/java/de/grimsi/gameyfin/entities/DetectedGame.java b/backend/src/main/java/de/grimsi/gameyfin/entities/DetectedGame.java index 5d27d91..7435cba 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/entities/DetectedGame.java +++ b/backend/src/main/java/de/grimsi/gameyfin/entities/DetectedGame.java @@ -3,6 +3,7 @@ package de.grimsi.gameyfin.entities; import lombok.*; import org.hibernate.Hibernate; +import org.hibernate.annotations.CreationTimestamp; import javax.persistence.*; import java.time.Instant; @@ -86,6 +87,9 @@ public class DetectedGame { @Column(columnDefinition = "boolean default false") private boolean confirmedMatch; + @CreationTimestamp + private Instant addedToLibrary; + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/backend/src/main/resources/config/database.yml b/backend/src/main/resources/config/database.yml index 0e02ae7..7551f4d 100644 --- a/backend/src/main/resources/config/database.yml +++ b/backend/src/main/resources/config/database.yml @@ -9,7 +9,9 @@ spring: entity_copy_observer: allow database-platform: org.hibernate.dialect.H2Dialect hibernate: - ddl-auto: update + ddl-auto: none + flyway: + baseline-on-migrate: true datasource: username: gfadmin password: gameyfin diff --git a/backend/src/main/resources/db/migration/V1_0_0__Initial_Database_Setup.sql b/backend/src/main/resources/db/migration/V1_0_0__Initial_Database_Setup.sql new file mode 100644 index 0000000..1c3687f --- /dev/null +++ b/backend/src/main/resources/db/migration/V1_0_0__Initial_Database_Setup.sql @@ -0,0 +1,148 @@ +-- Automatically generated by JPA + +-- Hibernate sequence +create sequence HIBERNATE_SEQUENCE start with 1 increment by 1; + +-- Detected Games +create table DETECTED_GAME +( + slug varchar(255) not null, + category varchar(255), + confirmed_match boolean default false, + cover_id varchar(255) not null, + critics_rating integer, + disk_size bigint not null, + lan_support boolean not null, + max_players integer not null, + offline_coop boolean not null, + online_coop boolean not null, + path varchar(255) not null, + release_date timestamp, + summary CLOB, + title varchar(255) not null, + total_rating integer, + user_rating integer, + primary key (slug) +); + +-- Companies +create table COMPANY +( + slug varchar(255) not null, + logo_id varchar(255), + name varchar(255) not null, + primary key (slug) +); + +-- Genres +create table GENRE +( + slug varchar(255) not null, + name varchar(255), + primary key (slug) +); + +-- Themes +create table THEME +( + slug varchar(255) not null, + name varchar(255), + primary key (slug) +); + +-- Keywords +create table KEYWORD +( + slug varchar(255) not null, + name varchar(255), + primary key (slug) +); + +-- Player Perspectives +create table PLAYER_PERSPECTIVE +( + slug varchar(255) not null, + name varchar(255), + primary key (slug) +); + +-- Unmappable files +create table UNMAPPABLE_FILE +( + id bigint not null, + path varchar(255), + primary key (id) +); + +-- Game <-> Companies +create table DETECTED_GAME_COMPANIES +( + detected_game_slug varchar(255) not null, + companies_slug varchar(255) not null +); +alter table DETECTED_GAME_COMPANIES + add constraint companies_company_slug foreign key (companies_slug) references company; +alter table DETECTED_GAME_COMPANIES + add constraint companies_detected_game_slug foreign key (detected_game_slug) references detected_game; + +-- Game <-> Genres +create table DETECTED_GAME_GENRES +( + detected_game_slug varchar(255) not null, + genres_slug varchar(255) not null +); +alter table DETECTED_GAME_GENRES + add constraint genres_genre_slug foreign key (genres_slug) references genre; +alter table DETECTED_GAME_GENRES + add constraint genres_detected_game_slug foreign key (detected_game_slug) references detected_game; + +-- Game <-> Themes +create table DETECTED_GAME_THEMES +( + detected_game_slug varchar(255) not null, + themes_slug varchar(255) not null +); +alter table DETECTED_GAME_THEMES + add constraint themes_theme_slug foreign key (themes_slug) references theme; +alter table DETECTED_GAME_THEMES + add constraint themes_detected_game_slug foreign key (detected_game_slug) references detected_game; + +-- Game <-> Keywords +create table DETECTED_GAME_KEYWORDS +( + detected_game_slug varchar(255) not null, + keywords_slug varchar(255) not null +); +alter table DETECTED_GAME_KEYWORDS + add constraint keywords_keyword_slug foreign key (keywords_slug) references keyword; +alter table DETECTED_GAME_KEYWORDS + add constraint keywords_detected_game_slug foreign key (detected_game_slug) references detected_game; + +-- Game <-> Player Perspectives +create table DETECTED_GAME_PLAYER_PERSPECTIVES +( + detected_game_slug varchar(255) not null, + player_perspectives_slug varchar(255) not null +); +alter table DETECTED_GAME_PLAYER_PERSPECTIVES + add constraint player_perspectives_player_perspective_slug foreign key (player_perspectives_slug) references player_perspective; +alter table DETECTED_GAME_PLAYER_PERSPECTIVES + add constraint player_perspectives_detected_game_slug foreign key (detected_game_slug) references detected_game; + +-- Game <-> Videos +create table DETECTED_GAME_VIDEO_IDS +( + detected_game_slug varchar(255) not null, + video_ids varchar(255) +); +alter table DETECTED_GAME_VIDEO_IDS + add constraint video_ids_detected_game_slug foreign key (detected_game_slug) references detected_game; + +-- Game <-> Screenshots +create table DETECTED_GAME_SCREENSHOT_IDS +( + detected_game_slug varchar(255) not null, + screenshot_ids varchar(255) +); +alter table DETECTED_GAME_SCREENSHOT_IDS + add constraint screenshot_ids_detected_game_slug foreign key (detected_game_slug) references detected_game; diff --git a/backend/src/main/resources/db/migration/V1_1_0__Add_Field_addedToLibrary_to_DetectedGame.sql b/backend/src/main/resources/db/migration/V1_1_0__Add_Field_addedToLibrary_to_DetectedGame.sql new file mode 100644 index 0000000..e75fe44 --- /dev/null +++ b/backend/src/main/resources/db/migration/V1_1_0__Add_Field_addedToLibrary_to_DetectedGame.sql @@ -0,0 +1,4 @@ +-- Add field "addedToLibrary" to the "DetectedGame" table with the default value of CURRENT_TIMESTAMP() + +alter table DETECTED_GAME +add added_to_library timestamp not null default CURRENT_TIMESTAMP() \ No newline at end of file diff --git a/frontend/src/app/models/dtos/DetectedGameDto.ts b/frontend/src/app/models/dtos/DetectedGameDto.ts index 543c091..ccb7bff 100644 --- a/frontend/src/app/models/dtos/DetectedGameDto.ts +++ b/frontend/src/app/models/dtos/DetectedGameDto.ts @@ -30,4 +30,5 @@ export class DetectedGameDto { path!: string; diskSize!: number; confirmedMatch!: boolean | undefined; + addedToLibrary!: Date; } From 1f24aa73e50e72c6fca8ceb958003f70cef061ae Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Sun, 14 Aug 2022 15:46:45 +0200 Subject: [PATCH 2/6] Moved filters into expansion panels --- frontend/src/app/app.module.ts | 76 ++++++++++--------- .../library-overview.component.html | 44 ++++++----- .../library-overview.component.scss | 4 - 3 files changed, 66 insertions(+), 58 deletions(-) diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index ac2dae5..35513e0 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -49,6 +49,7 @@ import {MatListModule} from "@angular/material/list"; import {MatAutocompleteModule} from "@angular/material/autocomplete"; import { NgModelChangeDebouncedDirective } from './directives/ng-model-change-debounced.directive'; import { FooterComponent } from './components/footer/footer.component'; +import {MatExpansionModule} from "@angular/material/expansion"; @NgModule({ declarations: [ @@ -69,43 +70,44 @@ import { FooterComponent } from './components/footer/footer.component'; NgModelChangeDebouncedDirective, FooterComponent ], - 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 - ], + 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, + MatExpansionModule + ], providers: [ { provide: HTTP_INTERCEPTORS, 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 173a6ef..adf3dc4 100644 --- a/frontend/src/app/components/library-overview/library-overview.component.html +++ b/frontend/src/app/components/library-overview/library-overview.component.html @@ -27,7 +27,7 @@
search - + @@ -37,12 +37,15 @@
-
-
-

Gamemodes

- error -
-
+ + + +

Gamemodes

+ error +
+
+ +
Offline Co-op Online Co-op @@ -50,28 +53,35 @@ LAN Support
-
+ -
-

Genres

-
+ + +

Genres

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

Themes

-
+ + +

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 92d78a1..d5703ae 100644 --- a/frontend/src/app/components/library-overview/library-overview.component.scss +++ b/frontend/src/app/components/library-overview/library-overview.component.scss @@ -44,10 +44,6 @@ 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'); From 9ff6d76cf293e842117e444996ed760a4f58fbea Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Mon, 15 Aug 2022 14:31:44 +0200 Subject: [PATCH 3/6] Refactored and improved dark-mode implementation --- frontend/src/app/app.module.ts | 2 +- .../components/footer/footer.component.scss | 4 +- .../components/header/header.component.html | 3 +- .../app/components/header/header.component.ts | 69 +++--------------- .../library-management.component.scss | 9 --- .../library-management.component.ts | 4 +- .../library-overview.component.html | 29 +++++--- .../library-overview.component.scss | 16 ++-- .../library-overview.component.ts | 11 ++- .../src/app/services/cookie.service.spec.ts | 16 ++++ frontend/src/app/services/cookie.service.ts | 34 +++++++++ .../src/app/services/theming.service.spec.ts | 16 ++++ frontend/src/app/services/theming.service.ts | 53 ++++++++++++++ .../dark-theme.scss} | 2 +- frontend/src/app/themes/light-theme.scss | 14 ++++ .../src/assets/Gameyfin_Logo_256px_dark.png | Bin 0 -> 3006 bytes frontend/src/styles.scss | 16 ++-- 17 files changed, 192 insertions(+), 106 deletions(-) create mode 100644 frontend/src/app/services/cookie.service.spec.ts create mode 100644 frontend/src/app/services/cookie.service.ts create mode 100644 frontend/src/app/services/theming.service.spec.ts create mode 100644 frontend/src/app/services/theming.service.ts rename frontend/src/app/{theme/default-theme.scss => themes/dark-theme.scss} (90%) create mode 100644 frontend/src/app/themes/light-theme.scss create mode 100644 frontend/src/assets/Gameyfin_Logo_256px_dark.png diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 35513e0..cf3c62d 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -121,7 +121,7 @@ import {MatExpansionModule} from "@angular/material/expansion"; }, { provide: MAT_SNACK_BAR_DEFAULT_OPTIONS, - useValue: { panelClass: ['snackbar-dark'] }, + useValue: { panelClass: ['formatted-snackbar'] }, } ], bootstrap: [AppComponent] diff --git a/frontend/src/app/components/footer/footer.component.scss b/frontend/src/app/components/footer/footer.component.scss index b7d02fb..47698af 100644 --- a/frontend/src/app/components/footer/footer.component.scss +++ b/frontend/src/app/components/footer/footer.component.scss @@ -1,9 +1,9 @@ @use 'sass:map'; @use '@angular/material' as mat; -@import '../../theme/default-theme'; +@import 'src/app/themes/light-theme'; a { - $config: mat.get-color-config($custom-theme); + $config: mat.get-color-config($light-theme); $primary-palette: map.get($config, 'primary'); color: mat.get-color-from-palette($primary-palette, 500); } diff --git a/frontend/src/app/components/header/header.component.html b/frontend/src/app/components/header/header.component.html index dd877db..ab0068b 100644 --- a/frontend/src/app/components/header/header.component.html +++ b/frontend/src/app/components/header/header.component.html @@ -5,7 +5,8 @@ - + + + + + + + {{game.title}} -
+ @@ -46,11 +55,11 @@
- Offline Co-op + Offline Co-op - Online Co-op + Online Co-op - LAN Support + LAN Support
@@ -80,7 +89,7 @@
-
+
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 d5703ae..28825b2 100644 --- a/frontend/src/app/components/library-overview/library-overview.component.scss +++ b/frontend/src/app/components/library-overview/library-overview.component.scss @@ -1,6 +1,6 @@ @use 'sass:map'; @use '@angular/material' as mat; -@import '../../theme/default-theme'; +@import 'src/app/themes/dark-theme'; .fullscreen-overlay { position: absolute; @@ -19,21 +19,15 @@ @include mat.elevation(16); - $config: mat.get-color-config($custom-theme); - $background: map.get($config, background); position: absolute; right: 56px; top: 72px; width: 250px; border-radius: 6px; - background: mat.get-color-from-palette($background, app-bar); - border-color: mat.get-color-from-palette($background, app-bar); - border-style: solid; - color: white; p { padding: 0 12px 12px 16px; - } + } } .content { @@ -45,13 +39,13 @@ } ::ng-deep .mat-checkbox-frame { - $config: mat.get-color-config($custom-theme); + $config: mat.get-color-config($dark-theme); $primary-palette: map.get($config, 'primary'); - border-color: mat.get-color-from-palette($primary-palette, 500); + border-color: mat.get-color-from-palette($primary-palette, 500) !important; } ::ng-deep .mat-form-field-underline { - $config: mat.get-color-config($custom-theme); + $config: mat.get-color-config($dark-theme); $primary-palette: map.get($config, 'primary'); background-color: mat.get-color-from-palette($primary-palette, 500) !important; } 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 774ad40..e5b176f 100644 --- a/frontend/src/app/components/library-overview/library-overview.component.ts +++ b/frontend/src/app/components/library-overview/library-overview.component.ts @@ -46,13 +46,22 @@ export class LibraryOverviewComponent implements AfterContentInit { forkJoin([themeObservable, genreObservable]).subscribe(result => { this.availableThemes = result[0]; this.availableGenres = result[1]; - this.filterGames(); + this.refreshLibraryView(); this.loading = false; }); } ); } + refreshLibraryView(): void { + this.filterGames(); + } + + clearSearchTerm(): void { + this.searchTerm = ""; + this.refreshLibraryView(); + } + filterGames(): void { this.gameServerService.getAllGames().subscribe(games => { let filteredGames: DetectedGameDto[] = games; diff --git a/frontend/src/app/services/cookie.service.spec.ts b/frontend/src/app/services/cookie.service.spec.ts new file mode 100644 index 0000000..43ea274 --- /dev/null +++ b/frontend/src/app/services/cookie.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { CookieService } from './cookie.service'; + +describe('CookieService', () => { + let service: CookieService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(CookieService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/cookie.service.ts b/frontend/src/app/services/cookie.service.ts new file mode 100644 index 0000000..00af0e3 --- /dev/null +++ b/frontend/src/app/services/cookie.service.ts @@ -0,0 +1,34 @@ +import {Injectable} from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class CookieService { + + constructor() { + } + + setCookie(name: string, value: any): void { + document.cookie = `${name}=${value.toString()};`; + } + + getCookie(name: string): string | null { + let end; + const dc = document.cookie; + const prefix = name + "="; + let begin = dc.indexOf("; " + prefix); + + if (begin == -1) { + begin = dc.indexOf(prefix); + if (begin != 0) return null; + } else { + begin += 2; + end = document.cookie.indexOf(";", begin); + if (end == -1) { + end = dc.length; + } + } + + return decodeURI(dc.substring(begin + prefix.length, end)); + } +} diff --git a/frontend/src/app/services/theming.service.spec.ts b/frontend/src/app/services/theming.service.spec.ts new file mode 100644 index 0000000..f932a5e --- /dev/null +++ b/frontend/src/app/services/theming.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ThemingService } from './theming.service'; + +describe('ThemingService', () => { + let service: ThemingService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ThemingService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/theming.service.ts b/frontend/src/app/services/theming.service.ts new file mode 100644 index 0000000..787718b --- /dev/null +++ b/frontend/src/app/services/theming.service.ts @@ -0,0 +1,53 @@ +import {Injectable} from '@angular/core'; +import {OverlayContainer} from "@angular/cdk/overlay"; +import {CookieService} from "./cookie.service"; + +@Injectable({ + providedIn: 'root' +}) +export class ThemingService { + + private darkmodeEnabled!: boolean; + private darkmodeClassName: string = 'darkMode'; + + constructor(private cookieService: CookieService, + private overlay: OverlayContainer) { + if (this.cookieService.getCookie("darkmode") !== null) { + this.darkmodeEnabled = this.cookieService.getCookie("darkmode") === "true"; + } else if (window.matchMedia) { + this.darkmodeEnabled = window.matchMedia('(prefers-color-scheme: dark)').matches; + } else { + this.darkmodeEnabled = false; + } + + this.setTheme(); + } + + toggleTheme(): void { + this.darkmodeEnabled = !this.darkmodeEnabled; + this.setTheme(); + } + + private setTheme(): void { + this.darkmodeEnabled ? this.setDarkmode() : this.setLightmode(); + this.cookieService.setCookie("darkmode", this.darkmodeEnabled); + } + + private setDarkmode(): void { + document.body.classList.add(this.darkmodeClassName); + document.body.style.colorScheme = "dark"; + document.body.style.background = "#303030"; + document.body.style.color = "white"; + + this.overlay.getContainerElement().classList.add(this.darkmodeClassName); + } + + private setLightmode(): void { + document.body.classList.remove(this.darkmodeClassName); + document.body.style.colorScheme = "light"; + document.body.style.background = "white"; + document.body.style.color = "black"; + + this.overlay.getContainerElement().classList.remove(this.darkmodeClassName); + } +} diff --git a/frontend/src/app/theme/default-theme.scss b/frontend/src/app/themes/dark-theme.scss similarity index 90% rename from frontend/src/app/theme/default-theme.scss rename to frontend/src/app/themes/dark-theme.scss index 93993e4..e0a9323 100644 --- a/frontend/src/app/theme/default-theme.scss +++ b/frontend/src/app/themes/dark-theme.scss @@ -5,7 +5,7 @@ $custom-theme-primary: mat.define-palette(mat.$green-palette); $custom-theme-accent: mat.define-palette(mat.$grey-palette, A200, A100, A400); $custom-theme-warn: mat.define-palette(mat.$red-palette); -$custom-theme: mat.define-dark-theme(( +$dark-theme: mat.define-dark-theme(( color: ( primary: $custom-theme-primary, accent: $custom-theme-accent, diff --git a/frontend/src/app/themes/light-theme.scss b/frontend/src/app/themes/light-theme.scss new file mode 100644 index 0000000..83897e1 --- /dev/null +++ b/frontend/src/app/themes/light-theme.scss @@ -0,0 +1,14 @@ +@use '@angular/material' as mat; +@import "@angular/material/theming"; + +$custom-theme-primary: mat.define-palette(mat.$green-palette); +$custom-theme-accent: mat.define-palette(mat.$grey-palette, A200, A100, A400); +$custom-theme-warn: mat.define-palette(mat.$red-palette); + +$light-theme: mat.define-light-theme(( + color: ( + primary: $custom-theme-primary, + accent: $custom-theme-accent, + warn: $custom-theme-warn + ) +)); diff --git a/frontend/src/assets/Gameyfin_Logo_256px_dark.png b/frontend/src/assets/Gameyfin_Logo_256px_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..b6be344adb9bb372cf785587cb104d13ac9c76bc GIT binary patch literal 3006 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 Date: Mon, 15 Aug 2022 15:49:19 +0200 Subject: [PATCH 4/6] Implemented sorting for library overview --- frontend/src/app/app.module.ts | 78 +++++++-------- .../library-overview.component.html | 23 ++++- .../library-overview.component.scss | 4 + .../library-overview.component.ts | 97 +++++++++++++------ .../navbar-layout/navbar-layout.component.ts | 2 +- 5 files changed, 132 insertions(+), 72 deletions(-) diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index cf3c62d..bc2d9d4 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -50,6 +50,7 @@ import {MatAutocompleteModule} from "@angular/material/autocomplete"; import { NgModelChangeDebouncedDirective } from './directives/ng-model-change-debounced.directive'; import { FooterComponent } from './components/footer/footer.component'; import {MatExpansionModule} from "@angular/material/expansion"; +import {MatSelectModule} from "@angular/material/select"; @NgModule({ declarations: [ @@ -70,44 +71,45 @@ import {MatExpansionModule} from "@angular/material/expansion"; NgModelChangeDebouncedDirective, FooterComponent ], - 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, - MatExpansionModule - ], + 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, + MatExpansionModule, + MatSelectModule + ], providers: [ { provide: HTTP_INTERCEPTORS, 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 8f84cca..88a3c49 100644 --- a/frontend/src/app/components/library-overview/library-overview.component.html +++ b/frontend/src/app/components/library-overview/library-overview.component.html @@ -26,8 +26,9 @@
- - @@ -46,6 +47,15 @@ + +

Sort by:

+ + + {{sortOption.title}} + + +
+ @@ -55,11 +65,14 @@
- Offline Co-op + Offline + Co-op - Online Co-op + Online + Co-op - LAN Support + LAN + Support
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 28825b2..c3d9f7e 100644 --- a/frontend/src/app/components/library-overview/library-overview.component.scss +++ b/frontend/src/app/components/library-overview/library-overview.component.scss @@ -38,6 +38,10 @@ margin-bottom: 0; } +.mat-card-48 { + height: 48px; +} + ::ng-deep .mat-checkbox-frame { $config: mat.get-color-config($dark-theme); $primary-palette: map.get($config, 'primary'); 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 e5b176f..8e9c1b9 100644 --- a/frontend/src/app/components/library-overview/library-overview.component.ts +++ b/frontend/src/app/components/library-overview/library-overview.component.ts @@ -1,9 +1,22 @@ -import {AfterContentInit, AfterViewInit, Component, Input} from '@angular/core'; +import {AfterContentInit, Component} 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"; +import {firstValueFrom, forkJoin, Observable, pipe} from "rxjs"; +import {SortDirection} from "@angular/material/sort"; + +class SortOption { + title: string; + field: string; + direction: SortDirection; + + constructor(title: string, field: string, direction: SortDirection) { + this.title = title; + this.field = field; + this.direction = direction; + } +} @Component({ selector: 'app-gameserver-list', @@ -12,7 +25,24 @@ import {forkJoin, Observable} from "rxjs"; }) export class LibraryOverviewComponent implements AfterContentInit { + defaultSortOption: SortOption = new SortOption("Title (A-Z)", "title", "asc"); + + sortOptions: SortOption[] = [ + this.defaultSortOption, + new SortOption("Title (Z-A)", "title", "desc"), + + new SortOption("Release (newest first)", "releaseDate", "desc"), + new SortOption("Release (oldest first)", "releaseDate", "asc"), + + new SortOption("Added to library (newest first)", "addedToLibrary", "desc"), + new SortOption("Added to library (oldest first)", "addedToLibrary", "asc"), + + new SortOption("Rating (highest first)", "totalRating", "desc"), + new SortOption("Rating (lowest first)", "totalRating", "asc") + ]; + searchTerm: string = ""; + selectedSortOption: SortOption = this.defaultSortOption; offlineCoopFilterEnabled: boolean = false; onlineCoopFilterEnabled: boolean = false; lanSupportFilterEnabled: boolean = false; @@ -32,7 +62,7 @@ export class LibraryOverviewComponent implements AfterContentInit { ngAfterContentInit(): void { this.gameServerService.getAllGames().subscribe( detectedGames => { - if(detectedGames.length === 0) { + if (detectedGames.length === 0) { this.gameLibraryIsEmpty = true; this.loading = false; return; @@ -46,15 +76,15 @@ export class LibraryOverviewComponent implements AfterContentInit { forkJoin([themeObservable, genreObservable]).subscribe(result => { this.availableThemes = result[0]; this.availableGenres = result[1]; - this.refreshLibraryView(); - this.loading = false; + this.refreshLibraryView().then(() => this.loading = false); }); } ); } - refreshLibraryView(): void { - this.filterGames(); + async refreshLibraryView(): Promise { + let games: DetectedGameDto[] = await firstValueFrom(this.gameServerService.getAllGames()); + this.games = this.sortGames(this.filterGames(games)); } clearSearchTerm(): void { @@ -62,32 +92,43 @@ export class LibraryOverviewComponent implements AfterContentInit { this.refreshLibraryView(); } - filterGames(): void { - this.gameServerService.getAllGames().subscribe(games => { - let filteredGames: DetectedGameDto[] = games; + filterGames(games: DetectedGameDto[]): DetectedGameDto[] { + if (this.searchTerm.trim().toLowerCase().length > 0) { + games = games.filter(game => game.title.trim().toLowerCase().includes(this.searchTerm.trim().toLowerCase())); + } - 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) { + games = games.filter(game => (game.offlineCoop === this.offlineCoopFilterEnabled || game.onlineCoop === this.onlineCoopFilterEnabled || game.lanSupport === this.lanSupportFilterEnabled)); + } - 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) { + games = games.filter(game => this.activeGenreFilters.every(activeGenreFilter => game.genres?.map(g => g.slug).includes(activeGenreFilter))); + } - 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) { + games = games.filter(game => this.activeThemeFilters.every(activeThemeFilter => game.themes?.map(g => g.slug).includes(activeThemeFilter))); + } - if(this.activeThemeFilters.length > 0) { - filteredGames = filteredGames.filter(game => this.activeThemeFilters.every(activeThemeFilter => game.themes?.map(g => g.slug).includes(activeThemeFilter))); - } + return games; + } - this.games = filteredGames; - }) + sortGames(games: DetectedGameDto[]): DetectedGameDto[] { + games = games.sort((g1, g2) => { + // @ts-ignore + let f1 = g1[this.selectedSortOption.field]; + // @ts-ignore + let f2 = g2[this.selectedSortOption.field]; + + if(f1 > f2) return 1; + if(f1 < f2) return -1; + return 0; + }); + if(this.selectedSortOption.direction === "desc") games = games.reverse(); + return games; } toggleGenreFilter(slug: string): void { - if(this.activeGenreFilters.includes(slug)) { + if (this.activeGenreFilters.includes(slug)) { const index = this.activeGenreFilters.indexOf(slug, 0); if (index > -1) { @@ -98,11 +139,11 @@ export class LibraryOverviewComponent implements AfterContentInit { this.activeGenreFilters.push(slug); } - this.filterGames(); + this.refreshLibraryView(); } toggleThemeFilter(slug: string) { - if(this.activeThemeFilters.includes(slug)) { + if (this.activeThemeFilters.includes(slug)) { const index = this.activeThemeFilters.indexOf(slug, 0); if (index > -1) { @@ -113,7 +154,7 @@ export class LibraryOverviewComponent implements AfterContentInit { this.activeThemeFilters.push(slug); } - this.filterGames(); + this.refreshLibraryView(); } } diff --git a/frontend/src/app/layouts/navbar-layout/navbar-layout.component.ts b/frontend/src/app/layouts/navbar-layout/navbar-layout.component.ts index 9ce31f1..397b20a 100644 --- a/frontend/src/app/layouts/navbar-layout/navbar-layout.component.ts +++ b/frontend/src/app/layouts/navbar-layout/navbar-layout.component.ts @@ -4,7 +4,7 @@ import {Component, OnInit} from '@angular/core'; selector: 'app-navbar-layout', template: `
-
+
From b6626c9d4faac0a68ca80ffb508b54d2a30cdcec Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Mon, 15 Aug 2022 16:10:12 +0200 Subject: [PATCH 5/6] Added filter for player perspective --- .../library-overview.component.html | 12 +++++++ .../library-overview.component.ts | 32 +++++++++++++++++-- .../app/models/dtos/PlayerPerspectiveDto.ts | 2 +- frontend/src/app/services/games.service.ts | 18 ++++++++++- 4 files changed, 59 insertions(+), 5 deletions(-) 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 88a3c49..d741353 100644 --- a/frontend/src/app/components/library-overview/library-overview.component.html +++ b/frontend/src/app/components/library-overview/library-overview.component.html @@ -100,6 +100,18 @@ color="primary">{{theme.name}}
+ + + +

Player Perspectives

+
+ +
+ {{playerPerspective.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 8e9c1b9..a8efcd1 100644 --- a/frontend/src/app/components/library-overview/library-overview.component.ts +++ b/frontend/src/app/components/library-overview/library-overview.component.ts @@ -5,6 +5,8 @@ import {GenreDto} from "../../models/dtos/GenreDto"; import {ThemeDto} from "../../models/dtos/ThemeDto"; import {firstValueFrom, forkJoin, Observable, pipe} from "rxjs"; import {SortDirection} from "@angular/material/sort"; +import {CompanyDto} from "../../models/dtos/CompanyDto"; +import {PlayerPerspectiveDto} from "../../models/dtos/PlayerPerspectiveDto"; class SortOption { title: string; @@ -48,10 +50,12 @@ export class LibraryOverviewComponent implements AfterContentInit { lanSupportFilterEnabled: boolean = false; activeThemeFilters: string[] = []; activeGenreFilters: string[] = []; + activePlayerPerspectiveFilters: string[] = []; games: DetectedGameDto[] = []; availableGenres: GenreDto[] = []; availableThemes: ThemeDto[] = []; + availablePlayerPerspectives: PlayerPerspectiveDto[] = []; loading: boolean = true; gameLibraryIsEmpty: boolean = false; @@ -72,10 +76,13 @@ export class LibraryOverviewComponent implements AfterContentInit { let genreObservable: Observable = this.gameServerService.getAvailableGenres(); let themeObservable: Observable = this.gameServerService.getAvailableThemes(); + let playerPerspectiveObservable: Observable = this.gameServerService.getAvailablePlayerPerspectives(); + + forkJoin([genreObservable, themeObservable, playerPerspectiveObservable]).subscribe(result => { + this.availableGenres = result[0]; + this.availableThemes = result[1]; + this.availablePlayerPerspectives = result[2]; - forkJoin([themeObservable, genreObservable]).subscribe(result => { - this.availableThemes = result[0]; - this.availableGenres = result[1]; this.refreshLibraryView().then(() => this.loading = false); }); } @@ -109,6 +116,10 @@ export class LibraryOverviewComponent implements AfterContentInit { games = games.filter(game => this.activeThemeFilters.every(activeThemeFilter => game.themes?.map(g => g.slug).includes(activeThemeFilter))); } + if (this.activePlayerPerspectiveFilters.length > 0) { + games = games.filter(game => this.activePlayerPerspectiveFilters.every(activePlayerPerspectiveFilter => game.playerPerspectives?.map(g => g.slug).includes(activePlayerPerspectiveFilter))); + } + return games; } @@ -157,4 +168,19 @@ export class LibraryOverviewComponent implements AfterContentInit { this.refreshLibraryView(); } + togglePlayerPerspectiveFilter(slug: string) { + if (this.activePlayerPerspectiveFilters.includes(slug)) { + + const index = this.activePlayerPerspectiveFilters.indexOf(slug, 0); + if (index > -1) { + this.activePlayerPerspectiveFilters.splice(index, 1); + } + + } else { + this.activePlayerPerspectiveFilters.push(slug); + } + + this.refreshLibraryView(); + } + } diff --git a/frontend/src/app/models/dtos/PlayerPerspectiveDto.ts b/frontend/src/app/models/dtos/PlayerPerspectiveDto.ts index ff0ee82..e4f7e0d 100644 --- a/frontend/src/app/models/dtos/PlayerPerspectiveDto.ts +++ b/frontend/src/app/models/dtos/PlayerPerspectiveDto.ts @@ -1,4 +1,4 @@ export class PlayerPerspectiveDto { 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 c91f940..87bd6f4 100644 --- a/frontend/src/app/services/games.service.ts +++ b/frontend/src/app/services/games.service.ts @@ -6,6 +6,8 @@ import {DetectedGameDto} from "../models/dtos/DetectedGameDto"; import {GameOverviewDto} from "../models/dtos/GameOverviewDto"; import {GenreDto} from "../models/dtos/GenreDto"; import {ThemeDto} from "../models/dtos/ThemeDto"; +import {CompanyDto} from "../models/dtos/CompanyDto"; +import {PlayerPerspectiveDto} from "../models/dtos/PlayerPerspectiveDto"; @Injectable({ providedIn: 'root' @@ -64,7 +66,7 @@ export class GamesService implements GamesApi { return this.getAllGames().pipe( map( games => { - let availableThemesMap: Map = new Map; + 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)); } @@ -72,6 +74,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 + getAvailablePlayerPerspectives(): Observable { + return this.getAllGames().pipe( + map( + games => { + let availablePlayerPerspectivesMap: Map = new Map; + games.map(game => game.playerPerspectives === undefined ? [] : game.playerPerspectives).flat().forEach(playerPerspective => availablePlayerPerspectivesMap.set(playerPerspective.slug, playerPerspective)); + return Array.from(availablePlayerPerspectivesMap.values()).sort((t1, t2) => t1.name.localeCompare(t2.name)); + } + ) + ); + } + downloadGame(slug: String): void { window.open(`v1${this.apiPath}/game/${slug}/download`, '_top'); } From b58af494e9d00da1b50fad285bf1ac35b8c9e435 Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Mon, 15 Aug 2022 17:09:59 +0200 Subject: [PATCH 6/6] Implement loading and saving state from/to URL --- .../app/components/header/header.component.ts | 7 +- .../library-overview.component.ts | 76 +++++++++++++++++-- 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/components/header/header.component.ts b/frontend/src/app/components/header/header.component.ts index 130ca42..5c8c67f 100644 --- a/frontend/src/app/components/header/header.component.ts +++ b/frontend/src/app/components/header/header.component.ts @@ -5,7 +5,7 @@ import {timeInterval} from "rxjs"; import {Router} from "@angular/router"; import {GamesService} from "../../services/games.service"; import {ThemingService} from "../../services/theming.service"; -import {DOCUMENT} from '@angular/common'; +import {DOCUMENT, Location} from '@angular/common'; @Component({ selector: 'app-header', @@ -21,7 +21,8 @@ export class HeaderComponent { private gameService: GamesService, private themingService: ThemingService, private snackBar: MatSnackBar, - private router: Router) { + private router: Router, + private location: Location) { } scanLibrary(): void { @@ -42,7 +43,7 @@ export class HeaderComponent { } goToLibraryScreen(): void { - this.router.navigate(['/']); + this.location.back(); } goToLibraryManagementScreen(): void { 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 a8efcd1..c40b4f7 100644 --- a/frontend/src/app/components/library-overview/library-overview.component.ts +++ b/frontend/src/app/components/library-overview/library-overview.component.ts @@ -3,10 +3,12 @@ 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 {firstValueFrom, forkJoin, Observable, pipe} from "rxjs"; +import {firstValueFrom, forkJoin, Observable} from "rxjs"; import {SortDirection} from "@angular/material/sort"; -import {CompanyDto} from "../../models/dtos/CompanyDto"; import {PlayerPerspectiveDto} from "../../models/dtos/PlayerPerspectiveDto"; +import {ActivatedRoute, Params, Router} from "@angular/router"; +import {Location} from "@angular/common"; +import {HttpParams} from "@angular/common/http"; class SortOption { title: string; @@ -60,7 +62,10 @@ export class LibraryOverviewComponent implements AfterContentInit { loading: boolean = true; gameLibraryIsEmpty: boolean = false; - constructor(private gameServerService: GamesService) { + constructor(private gameServerService: GamesService, + private route: ActivatedRoute, + private router: Router, + private location: Location) { } ngAfterContentInit(): void { @@ -83,7 +88,16 @@ export class LibraryOverviewComponent implements AfterContentInit { this.availableThemes = result[1]; this.availablePlayerPerspectives = result[2]; - this.refreshLibraryView().then(() => this.loading = false); + this.route.queryParams.subscribe(params => { + if (params['search'] !== undefined) this.searchTerm = params['search']; + if (params['sort'] !== undefined) this.selectedSortOption = this.matchSelectedSortOptionFromParam(params['sort']); + if (params['gamemodes'] !== undefined) this.setSelectedGamemodesFromParam(params['gamemodes']); + if (params['genres'] !== undefined) this.activeGenreFilters = this.matchSelectedFilters(this.availableGenres, params['genres']); + if (params['themes'] !== undefined) this.activeThemeFilters = this.matchSelectedFilters(this.availableThemes, params['themes']); + if (params['playerPerspectives'] !== undefined) this.activePlayerPerspectiveFilters = this.matchSelectedFilters(this.availablePlayerPerspectives, params['playerPerspectives']); + + this.refreshLibraryView().then(() => this.loading = false); + }); }); } ); @@ -91,7 +105,8 @@ export class LibraryOverviewComponent implements AfterContentInit { async refreshLibraryView(): Promise { let games: DetectedGameDto[] = await firstValueFrom(this.gameServerService.getAllGames()); - this.games = this.sortGames(this.filterGames(games)); + this.games = this.sortGames(this.filterGames(games)); + this.saveStateToRoute(); } clearSearchTerm(): void { @@ -130,11 +145,11 @@ export class LibraryOverviewComponent implements AfterContentInit { // @ts-ignore let f2 = g2[this.selectedSortOption.field]; - if(f1 > f2) return 1; - if(f1 < f2) return -1; + if (f1 > f2) return 1; + if (f1 < f2) return -1; return 0; }); - if(this.selectedSortOption.direction === "desc") games = games.reverse(); + if (this.selectedSortOption.direction === "desc") games = games.reverse(); return games; } @@ -183,4 +198,49 @@ export class LibraryOverviewComponent implements AfterContentInit { this.refreshLibraryView(); } + private saveStateToRoute(): void { + let stateParams: Params = {}; + + if (this.searchTerm.trim().length > 0) stateParams['search'] = this.searchTerm; + if (this.selectedSortOption !== this.defaultSortOption) stateParams['sort'] = this.toParam(this.selectedSortOption); + if (this.getActiveGameModesFilters().length > 0) stateParams['gamemodes'] = this.getActiveGameModesFilters().join(','); + if (this.activeGenreFilters.length > 0) stateParams['genres'] = this.activeGenreFilters.join(','); + if (this.activeThemeFilters.length > 0) stateParams['themes'] = this.activeThemeFilters.join(','); + if (this.activePlayerPerspectiveFilters.length > 0) stateParams['playerPerspectives'] = this.activePlayerPerspectiveFilters.join(','); + + const url = this.router.createUrlTree([], {relativeTo: this.route, queryParams: stateParams}).toString(); + this.location.go(url); + } + + private toParam(sortOption: SortOption): string { + return `${sortOption.field}_${sortOption.direction}`; + } + + private matchSelectedSortOptionFromParam(sortParam: string): SortOption { + return this.sortOptions.find(s => sortParam === this.toParam(s)) ?? this.defaultSortOption; + } + + private matchSelectedFilters(options: any[], paramString: string): string[] { + let params: string[] = paramString.split(","); + return options.filter(o => params.includes(o.slug)).map(o => o.slug); + } + + private getActiveGameModesFilters(): string[] { + let activeFilters: string[] = []; + + if (this.offlineCoopFilterEnabled) activeFilters.push('offlineCoop'); + if (this.onlineCoopFilterEnabled) activeFilters.push('onlineCoop'); + if (this.lanSupportFilterEnabled) activeFilters.push('lanSupport'); + + return activeFilters; + } + + private setSelectedGamemodesFromParam(paramString: string): void { + let params: string[] = paramString.split(","); + + if (params.includes('offlineCoop')) this.offlineCoopFilterEnabled = true; + if (params.includes('onlineCoop')) this.onlineCoopFilterEnabled = true; + if (params.includes('lanSupport')) this.lanSupportFilterEnabled = true; + } + }