mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
Merge pull request #11 from grimsi/gh-9_ImproveSortingAndFiltering
Improve Sorting and Filtering
This commit is contained in:
+7
-1
@@ -87,6 +87,11 @@
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- File handling -->
|
||||
<dependency>
|
||||
@@ -144,8 +149,9 @@
|
||||
<include>**/*.properties</include>
|
||||
<include>**/*.yml</include>
|
||||
<include>**/*.yaml</include>
|
||||
<include>**/*.json</include>
|
||||
<include>**/*.sql</include>
|
||||
<include>**/*.txt</include>
|
||||
<include>**/*.json</include>
|
||||
<include>**/*.js</include>
|
||||
<include>**/*.css</include>
|
||||
<include>**/*.html</include>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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<File> files;
|
||||
private Long fileSize;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package de.grimsi.gameyfin.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class UsernamePasswordDto {
|
||||
private String username;
|
||||
private String password;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
+4
@@ -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()
|
||||
@@ -49,6 +49,8 @@ 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";
|
||||
import {MatSelectModule} from "@angular/material/select";
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -104,7 +106,9 @@ import { FooterComponent } from './components/footer/footer.component';
|
||||
MatTableFilterModule,
|
||||
MatDividerModule,
|
||||
MatListModule,
|
||||
MatAutocompleteModule
|
||||
MatAutocompleteModule,
|
||||
MatExpansionModule,
|
||||
MatSelectModule
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
@@ -119,7 +123,7 @@ import { FooterComponent } from './components/footer/footer.component';
|
||||
},
|
||||
{
|
||||
provide: MAT_SNACK_BAR_DEFAULT_OPTIONS,
|
||||
useValue: { panelClass: ['snackbar-dark'] },
|
||||
useValue: { panelClass: ['formatted-snackbar'] },
|
||||
}
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
|
||||
<span class="spacer"></span>
|
||||
|
||||
<img class="logo" src="assets/Gameyfin_Logo_256px.png" alt="Gameyfin Logo">
|
||||
<img *ngIf="document.body.style.colorScheme == 'dark'" class="logo" src="assets/Gameyfin_Logo_256px.png" alt="Gameyfin Logo">
|
||||
<img *ngIf="document.body.style.colorScheme == 'light'" class="logo" src="assets/Gameyfin_Logo_256px_dark.png" alt="Gameyfin Logo">
|
||||
|
||||
<button mat-icon-button matTooltip="Reload library" (click)="reloadLibrary()" *ngIf="onLibraryScreen()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {Component, Inject} from '@angular/core';
|
||||
import {LibraryService} from "../../services/library.service";
|
||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
import {timeInterval} from "rxjs";
|
||||
import {Router} from "@angular/router";
|
||||
import {GamesService} from "../../services/games.service";
|
||||
import {ThemingService} from "../../services/theming.service";
|
||||
import {DOCUMENT, Location} from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-header',
|
||||
@@ -12,43 +14,15 @@ import {GamesService} from "../../services/games.service";
|
||||
})
|
||||
export class HeaderComponent {
|
||||
|
||||
darkmodeEnabled: boolean;
|
||||
// Maybe bad practice? IDK, but I need to access the document from the template of this component
|
||||
document: Document = document;
|
||||
|
||||
constructor(private libraryService: LibraryService,
|
||||
private gameService: GamesService,
|
||||
private themingService: ThemingService,
|
||||
private snackBar: MatSnackBar,
|
||||
private router: Router) {
|
||||
|
||||
if(this.getCookie("darkmode") !== null) {
|
||||
this.darkmodeEnabled = this.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.setCookie("darkmode", this.darkmodeEnabled);
|
||||
}
|
||||
|
||||
private setDarkmode(): void {
|
||||
document.body.style.background = "#303030";
|
||||
document.body.style.color = "white";
|
||||
}
|
||||
|
||||
private setLightmode(): void {
|
||||
document.body.style.background = "white";
|
||||
document.body.style.color = "black";
|
||||
private router: Router,
|
||||
private location: Location) {
|
||||
}
|
||||
|
||||
scanLibrary(): void {
|
||||
@@ -57,7 +31,7 @@ export class HeaderComponent {
|
||||
// Refresh the current page "angular style"
|
||||
this.router.navigate([this.router.url]).then(() =>
|
||||
this.snackBar.open(`Library scan completed in ${Math.trunc(value.interval / 1000)} seconds.`, undefined, {duration: 5000})
|
||||
)
|
||||
)
|
||||
},
|
||||
error: error => this.snackBar.open(`Error while scanning library: ${error.error.message}`, undefined, {duration: 5000})
|
||||
})
|
||||
@@ -69,7 +43,7 @@ export class HeaderComponent {
|
||||
}
|
||||
|
||||
goToLibraryScreen(): void {
|
||||
this.router.navigate(['/']);
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
goToLibraryManagementScreen(): void {
|
||||
@@ -84,31 +58,7 @@ export class HeaderComponent {
|
||||
return this.router.url === "/library-management";
|
||||
}
|
||||
|
||||
private setCookie(name: string, value: any): void {
|
||||
let d:Date = new Date();
|
||||
document.cookie = `${name}=${value.toString()};`;
|
||||
toggleTheme(): void {
|
||||
this.themingService.toggleTheme();
|
||||
}
|
||||
|
||||
private getCookie(name: string): string | null {
|
||||
var dc = document.cookie;
|
||||
var prefix = name + "=";
|
||||
var begin = dc.indexOf("; " + prefix);
|
||||
if (begin == -1) {
|
||||
begin = dc.indexOf(prefix);
|
||||
if (begin != 0) return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
begin += 2;
|
||||
var end = document.cookie.indexOf(";", begin);
|
||||
if (end == -1) {
|
||||
end = dc.length;
|
||||
}
|
||||
}
|
||||
// because unescape has been deprecated, replaced with decodeURI
|
||||
//return unescape(dc.substring(begin + prefix.length, end));
|
||||
// @ts-ignore
|
||||
return decodeURI(dc.substring(begin + prefix.length, end));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +1 @@
|
||||
@use 'sass:map';
|
||||
@use '@angular/material' as mat;
|
||||
@import 'src/app/theme/default-theme';
|
||||
@import 'src/app/components/library-overview/library-overview.component';
|
||||
|
||||
mat-tab-group {
|
||||
$config: mat.get-color-config($custom-theme);
|
||||
$background: map.get($config, background);
|
||||
background: mat.get-color-from-palette($background, app-bar);
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import {UnmappedFileDto} from "../../models/dtos/UnmappedFileDto";
|
||||
export class LibraryManagementComponent implements OnInit {
|
||||
loggedIn: boolean = false;
|
||||
|
||||
mappedGames!: DetectedGameDto[];
|
||||
unmappedFiles!: UnmappedFileDto[];
|
||||
mappedGames: DetectedGameDto[] = [];
|
||||
unmappedFiles: UnmappedFileDto[] = [];
|
||||
|
||||
constructor(private gamesService: GamesService,
|
||||
private libraryManagementService: LibraryManagementService) {
|
||||
|
||||
@@ -23,55 +23,99 @@
|
||||
*ngIf="!this.loading && !this.gameLibraryIsEmpty">
|
||||
<div fxFlex="10" fxHide fxShow.gt-md><!--SPACER--></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 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]="librarySearchAutocomplete" [(ngModel)]="searchTerm" (ngModelChange)="filterGames()">
|
||||
<mat-card fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="6px" class="mat-card-48">
|
||||
<button mat-icon-button *ngIf="searchTerm.length > 0" matTooltip="Clear search input"
|
||||
(click)="clearSearchTerm()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
|
||||
<button mat-icon-button *ngIf="searchTerm.length === 0" matTooltip="Search for games by title">
|
||||
<mat-icon>search</mat-icon>
|
||||
</button>
|
||||
|
||||
<mat-form-field fxFlex>
|
||||
<input type="text" matInput [matAutocomplete]="librarySearchAutocomplete" [(ngModel)]="searchTerm"
|
||||
(ngModelChange)="refreshLibraryView()">
|
||||
<mat-autocomplete #librarySearchAutocomplete="matAutocomplete">
|
||||
<mat-option *ngFor="let game of games" [value]="game.title">
|
||||
{{game.title}}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</mat-card>
|
||||
|
||||
<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-card fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="20px" class="mat-card-48">
|
||||
<h3 class="filter-category-title" style="white-space: nowrap; padding-left: 6px">Sort by: </h3>
|
||||
<mat-select [(value)]="selectedSortOption" (valueChange)="refreshLibraryView()">
|
||||
<mat-option *ngFor="let sortOption of sortOptions" [value]="sortOption">
|
||||
{{sortOption.title}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-card>
|
||||
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title 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>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div fxLayout="column">
|
||||
<mat-checkbox [(ngModel)]="offlineCoopFilterEnabled" (change)="refreshLibraryView()" color="primary">Offline
|
||||
Co-op
|
||||
</mat-checkbox>
|
||||
<mat-checkbox [(ngModel)]="onlineCoopFilterEnabled" (change)="filterGames()" color="primary">Online Co-op
|
||||
<mat-checkbox [(ngModel)]="onlineCoopFilterEnabled" (change)="refreshLibraryView()" color="primary">Online
|
||||
Co-op
|
||||
</mat-checkbox>
|
||||
<mat-checkbox [(ngModel)]="lanSupportFilterEnabled" (change)="filterGames()" color="primary">LAN Support
|
||||
<mat-checkbox [(ngModel)]="lanSupportFilterEnabled" (change)="refreshLibraryView()" color="primary">LAN
|
||||
Support
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<div *ngIf="availableGenres.length > 0">
|
||||
<h3 class="filter-category-title">Genres</h3>
|
||||
<div fxLayout="column" class="filter-category-content">
|
||||
<mat-expansion-panel *ngIf="availableGenres.length > 0">
|
||||
<mat-expansion-panel-header>
|
||||
<h3 class="filter-category-title">Genres</h3>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div fxLayout="column">
|
||||
<mat-checkbox *ngFor="let genre of availableGenres" (change)="toggleGenreFilter(genre.slug)"
|
||||
[checked]="activeGenreFilters.includes(genre.slug)"
|
||||
color="primary">{{genre.name}}</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<div *ngIf="availableThemes.length > 0">
|
||||
<h3 class="filter-category-title">Themes</h3>
|
||||
<div fxLayout="column" class="filter-category-content">
|
||||
<mat-expansion-panel *ngIf="availableThemes.length > 0">
|
||||
<mat-expansion-panel-header>
|
||||
<h3 class="filter-category-title">Themes</h3>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div fxLayout="column">
|
||||
<mat-checkbox *ngFor="let theme of availableThemes" (change)="toggleThemeFilter(theme.slug)"
|
||||
[checked]="activeThemeFilters.includes(theme.slug)"
|
||||
color="primary">{{theme.name}}</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<mat-expansion-panel *ngIf="availablePlayerPerspectives.length > 0">
|
||||
<mat-expansion-panel-header>
|
||||
<h3 class="filter-category-title">Player Perspectives</h3>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div fxLayout="column">
|
||||
<mat-checkbox *ngFor="let playerPerspective of availablePlayerPerspectives" (change)="togglePlayerPerspectiveFilter(playerPerspective.slug)"
|
||||
[checked]="activePlayerPerspectiveFilters.includes(playerPerspective.slug)"
|
||||
color="primary">{{playerPerspective.name}}</mat-checkbox>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</div>
|
||||
|
||||
<div fxFlex="0 1 1"><!--SPACER--></div>
|
||||
|
||||
<div fxFlex fxLayout="row wrap" fxLayoutGap="16px grid">
|
||||
<div *ngFor="let game of games">
|
||||
<game-cover [game]="game"></game-cover>
|
||||
|
||||
@@ -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 {
|
||||
@@ -44,18 +38,18 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.filter-category-content {
|
||||
margin-left: 6px;
|
||||
.mat-card-48 {
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
::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;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
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} from "rxjs";
|
||||
import {SortDirection} from "@angular/material/sort";
|
||||
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;
|
||||
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,27 +29,49 @@ 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;
|
||||
activeThemeFilters: string[] = [];
|
||||
activeGenreFilters: string[] = [];
|
||||
activePlayerPerspectiveFilters: string[] = [];
|
||||
|
||||
games: DetectedGameDto[] = [];
|
||||
availableGenres: GenreDto[] = [];
|
||||
availableThemes: ThemeDto[] = [];
|
||||
availablePlayerPerspectives: PlayerPerspectiveDto[] = [];
|
||||
|
||||
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 {
|
||||
this.gameServerService.getAllGames().subscribe(
|
||||
detectedGames => {
|
||||
if(detectedGames.length === 0) {
|
||||
if (detectedGames.length === 0) {
|
||||
this.gameLibraryIsEmpty = true;
|
||||
this.loading = false;
|
||||
return;
|
||||
@@ -42,43 +81,80 @@ export class LibraryOverviewComponent implements AfterContentInit {
|
||||
|
||||
let genreObservable: Observable<ThemeDto[]> = this.gameServerService.getAvailableGenres();
|
||||
let themeObservable: Observable<GenreDto[]> = this.gameServerService.getAvailableThemes();
|
||||
let playerPerspectiveObservable: Observable<PlayerPerspectiveDto[]> = this.gameServerService.getAvailablePlayerPerspectives();
|
||||
|
||||
forkJoin([themeObservable, genreObservable]).subscribe(result => {
|
||||
this.availableThemes = result[0];
|
||||
this.availableGenres = result[1];
|
||||
this.filterGames();
|
||||
this.loading = false;
|
||||
forkJoin([genreObservable, themeObservable, playerPerspectiveObservable]).subscribe(result => {
|
||||
this.availableGenres = result[0];
|
||||
this.availableThemes = result[1];
|
||||
this.availablePlayerPerspectives = result[2];
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
filterGames(): void {
|
||||
this.gameServerService.getAllGames().subscribe(games => {
|
||||
let filteredGames: DetectedGameDto[] = games;
|
||||
async refreshLibraryView(): Promise<void> {
|
||||
let games: DetectedGameDto[] = await firstValueFrom(this.gameServerService.getAllGames());
|
||||
this.games = this.sortGames(this.filterGames(games));
|
||||
this.saveStateToRoute();
|
||||
}
|
||||
|
||||
if(this.searchTerm.trim().toLowerCase().length > 0) {
|
||||
filteredGames = filteredGames.filter(game => game.title.trim().toLowerCase().includes(this.searchTerm.trim().toLowerCase()));
|
||||
}
|
||||
clearSearchTerm(): void {
|
||||
this.searchTerm = "";
|
||||
this.refreshLibraryView();
|
||||
}
|
||||
|
||||
if(this.offlineCoopFilterEnabled || this.onlineCoopFilterEnabled || this.lanSupportFilterEnabled) {
|
||||
filteredGames = filteredGames.filter(game => (game.offlineCoop === this.offlineCoopFilterEnabled || game.onlineCoop === this.onlineCoopFilterEnabled || game.lanSupport === this.lanSupportFilterEnabled));
|
||||
}
|
||||
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.activeGenreFilters.length > 0) {
|
||||
filteredGames = filteredGames.filter(game => this.activeGenreFilters.every(activeGenreFilter => game.genres?.map(g => g.slug).includes(activeGenreFilter)));
|
||||
}
|
||||
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.activeThemeFilters.length > 0) {
|
||||
filteredGames = filteredGames.filter(game => this.activeThemeFilters.every(activeThemeFilter => game.themes?.map(g => g.slug).includes(activeThemeFilter)));
|
||||
}
|
||||
if (this.activeGenreFilters.length > 0) {
|
||||
games = games.filter(game => this.activeGenreFilters.every(activeGenreFilter => game.genres?.map(g => g.slug).includes(activeGenreFilter)));
|
||||
}
|
||||
|
||||
this.games = filteredGames;
|
||||
})
|
||||
if (this.activeThemeFilters.length > 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -89,11 +165,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) {
|
||||
@@ -104,7 +180,67 @@ export class LibraryOverviewComponent implements AfterContentInit {
|
||||
this.activeThemeFilters.push(slug);
|
||||
}
|
||||
|
||||
this.filterGames();
|
||||
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();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {Component, OnInit} from '@angular/core';
|
||||
selector: 'app-navbar-layout',
|
||||
template: `
|
||||
<div class="main-container" fxLayout="column">
|
||||
<div fxFlex="none" style="position: sticky; top: 0; z-index: 99999">
|
||||
<div fxFlex="none" style="position: sticky; top: 0; z-index: 999">
|
||||
<app-header></app-header>
|
||||
</div>
|
||||
<div fxFlex>
|
||||
|
||||
@@ -30,4 +30,5 @@ export class DetectedGameDto {
|
||||
path!: string;
|
||||
diskSize!: number;
|
||||
confirmedMatch!: boolean | undefined;
|
||||
addedToLibrary!: Date;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export class PlayerPerspectiveDto {
|
||||
slug!: string;
|
||||
name?: string;
|
||||
name!: string;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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<DetectedGameDto[], ThemeDto[]>(
|
||||
games => {
|
||||
let availableThemesMap: Map<string, ThemeDto> = new Map<string, GenreDto>;
|
||||
let availableThemesMap: Map<string, ThemeDto> = new Map<string, ThemeDto>;
|
||||
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<PlayerPerspectiveDto[]> {
|
||||
return this.getAllGames().pipe(
|
||||
map<DetectedGameDto[], PlayerPerspectiveDto[]>(
|
||||
games => {
|
||||
let availablePlayerPerspectivesMap: Map<string, PlayerPerspectiveDto> = new Map<string, PlayerPerspectiveDto>;
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
@@ -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
|
||||
)
|
||||
));
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
@@ -4,7 +4,8 @@
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
// Plus imports for other components in your app.
|
||||
@import "src/app/theme/default-theme";
|
||||
@import "src/app/themes/light-theme";
|
||||
@import "src/app/themes/dark-theme";
|
||||
// Include the common styles for Angular Material. We include this here so that you only
|
||||
// have to load a single css file for Angular Material in your app.
|
||||
// Be sure that you only ever include this mixin once!
|
||||
@@ -13,7 +14,11 @@
|
||||
// Include theme styles for core and each component used in your app.
|
||||
// Alternatively, you can import and @include the theme mixins for each component
|
||||
// that you are using.
|
||||
@include mat.all-component-themes($custom-theme);
|
||||
@include mat.all-component-themes($light-theme);
|
||||
|
||||
.darkMode {
|
||||
@include mat.all-component-colors($dark-theme);
|
||||
}
|
||||
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
html, body { height: 100%; }
|
||||
@@ -23,12 +28,7 @@ html {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.snackbar-dark {
|
||||
color: white;
|
||||
$config: mat.get-color-config($custom-theme);
|
||||
$background: map.get($config, background);
|
||||
background: mat.get-color-from-palette($background, app-bar);
|
||||
|
||||
.formatted-snackbar {
|
||||
// add support for formatting (newlines)
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user