mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-14 16:20:04 +00:00
Refactored and improved dark-mode implementation
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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} from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-header',
|
||||
@@ -12,43 +14,14 @@ 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";
|
||||
}
|
||||
|
||||
scanLibrary(): void {
|
||||
@@ -57,7 +30,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})
|
||||
})
|
||||
@@ -84,31 +57,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,19 +23,28 @@
|
||||
*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="100" class="filter-category-content">
|
||||
<input type="text" matInput [matAutocomplete]="librarySearchAutocomplete" [(ngModel)]="searchTerm" (ngModelChange)="filterGames()">
|
||||
<mat-card fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="6px" style="height: 48px">
|
||||
<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>
|
||||
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
@@ -46,11 +55,11 @@
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div fxLayout="column">
|
||||
<mat-checkbox [(ngModel)]="offlineCoopFilterEnabled" (change)="filterGames()" color="primary">Offline Co-op
|
||||
<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>
|
||||
</mat-expansion-panel>
|
||||
@@ -80,7 +89,7 @@
|
||||
</mat-expansion-panel>
|
||||
</div>
|
||||
|
||||
<div fxFlex="0 1 1" fxHide fxShow.gt-lg><!--SPACER--></div>
|
||||
<div fxFlex="0 1 1"><!--SPACER--></div>
|
||||
|
||||
<div fxFlex fxLayout="row wrap" fxLayoutGap="16px grid">
|
||||
<div *ngFor="let game of games">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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