Start development of v2

This commit is contained in:
grimsi
2024-02-04 12:21:07 +01:00
parent 8baf2e776b
commit fc84f92e23
253 changed files with 479 additions and 24885 deletions
-16
View File
@@ -1,16 +0,0 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false
-44
View File
@@ -1,44 +0,0 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db
/node/
/.angular/
-27
View File
@@ -1,27 +0,0 @@
# Frontend
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.7.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
-111
View File
@@ -1,111 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"frontend": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/frontend",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "1mb",
"maximumError": "2mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "frontend:build:production"
},
"development": {
"proxyConfig": "src/proxy.conf.json",
"buildTarget": "frontend:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "frontend:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
}
}
}
}
},
"cli": {
"analytics": false
}
}
-44
View File
@@ -1,44 +0,0 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/frontend'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};
-12879
View File
File diff suppressed because it is too large Load Diff
-43
View File
@@ -1,43 +0,0 @@
{
"name": "frontend",
"version": "1.4.6-SNAPSHOT",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "^17.1.1",
"@angular/cdk": "^16.2.7",
"@angular/common": "^17.1.1",
"@angular/compiler": "^17.1.1",
"@angular/core": "^17.1.1",
"@angular/forms": "^17.1.1",
"@angular/material": "^16.2.9",
"@angular/platform-browser": "^17.1.1",
"@angular/platform-browser-dynamic": "^17.1.1",
"@angular/router": "^17.1.1",
"@angular/youtube-player": "^17.1.1",
"@ngbracket/ngx-layout": "^17.0.1",
"mat-table-filter": "^15.0.0",
"rxjs": "~7.8.1",
"tslib": "^2.6.2",
"zone.js": "~0.14.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.1.1",
"@angular/cli": "~17.1.1",
"@angular/compiler-cli": "^17.1.1",
"@types/jasmine": "~5.1.2",
"jasmine-core": "~4.6.0",
"karma": "~6.4.2",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.1",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "^5.3.3"
}
}
-94
View File
@@ -1,94 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>gameyfin</artifactId>
<groupId>de.grimsi</groupId>
<version>1.4.6-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>gameyfin-frontend</artifactId>
<packaging>jar</packaging>
<properties>
<frontend-maven-plugin.version>1.15.0</frontend-maven-plugin.version>
</properties>
<build>
<resources>
<resource>
<directory>./dist/frontend</directory>
<targetPath>static</targetPath>
</resource>
</resources>
<plugins>
<!-- clean the dist directory used by Angular -->
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<configuration>
<filesets>
<fileset>
<directory>dist</directory>
</fileset>
</filesets>
</configuration>
</plugin>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>${frontend-maven-plugin.version}</version>
<executions>
<!-- Install node and npm -->
<execution>
<id>Install Node and NPM</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
<configuration>
<nodeVersion>v20.11.0</nodeVersion>
</configuration>
</execution>
<!-- clean install -->
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<!-- Use legacy peer dependency handling because "mat-table-filter" is unmaintained -->
<arguments>install --legacy-peer-deps</arguments>
</configuration>
</execution>
<!-- Update frontend version in package.json -->
<execution>
<id>npm version</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>version ${project.version} --allow-same-version</arguments>
</configuration>
</execution>
<!-- build app -->
<execution>
<id>npm run build</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run build --omit=dev</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
-11
View File
@@ -1,11 +0,0 @@
import {Observable} from "rxjs";
import {DetectedGameDto} from "../models/dtos/DetectedGameDto";
import {GameOverviewDto} from "../models/dtos/GameOverviewDto";
export interface GamesApi {
getAllGames(): Observable<DetectedGameDto[]>;
getGame(slug: String): Observable<DetectedGameDto>;
getGameOverviews(): Observable<GameOverviewDto[]>;
getAllGameMappings(): Observable<Map<string, string>>;
refreshGame(slug: String): Observable<DetectedGameDto>;
}
-12
View File
@@ -1,12 +0,0 @@
import {Observable} from "rxjs";
import {LibraryScanResultDto} from "../models/dtos/LibraryScanResultDto";
import {ImageDownloadResultDto} from "../models/dtos/ImageDownloadResultDto";
import {LibraryScanRequestDto} from "../models/dtos/LibraryScanRequestDto";
export interface LibraryApi {
scanLibrary(mappedLibrary: LibraryScanRequestDto): Observable<LibraryScanResultDto>;
downloadImages(): Observable<ImageDownloadResultDto>;
getFiles(): Observable<string[]>;
}
@@ -1,16 +0,0 @@
import {PathToSlugDto} from "../models/dtos/PathToSlugDto";
import {Observable} from "rxjs";
import {DetectedGameDto} from "../models/dtos/DetectedGameDto";
import {UnmappedFileDto} from "../models/dtos/UnmappedFileDto";
import {AutocompleteSuggestionDto} from "../models/dtos/AutocompleteSuggestionDto";
import {LibraryDto} from "../models/dtos/LibraryDto";
export interface LibraryManagementApi {
mapGame(pathToSlugDto: PathToSlugDto): Observable<DetectedGameDto>;
getUnmappedFiles(): Observable<UnmappedFileDto[]>;
confirmGameMapping(slug: string, confirm: boolean): Observable<DetectedGameDto>;
deleteGame(slug: string): Observable<Response>;
deleteUnmappedFile(id: number): Observable<Response>;
getAutocompleteSuggestions(searchTerm: string, limit: number): Observable<AutocompleteSuggestionDto[]>;
getLibraries(): Observable<LibraryDto[]>;
}
-50
View File
@@ -1,50 +0,0 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {PageNotFoundComponent} from "./components/page-not-found/page-not-found.component";
import {NavbarLayoutComponent} from "./layouts/navbar-layout/navbar-layout.component";
import {LibraryOverviewComponent} from "./components/library-overview/library-overview.component";
import {GameDetailViewComponent} from "./components/game-detail-view/game-detail-view.component";
import {LibraryManagementComponent} from "./components/library-management/library-management.component";
import {MappedGamesTableComponent} from "./components/mapped-games-table/mapped-games-table.component";
const appRoutes: Routes = [
{
path: '',
component: NavbarLayoutComponent,
children: [
{
path: 'library',
component: LibraryOverviewComponent
},
{
path: 'game/:slug',
component: GameDetailViewComponent
},
{
path: 'library-management',
component: LibraryManagementComponent
},
{
path: 'test',
component: MappedGamesTableComponent
},
{
path: '',
redirectTo: '/library',
pathMatch: 'full'
}
]
},
{
path: '**',
component: PageNotFoundComponent
}
];
@NgModule({
imports: [RouterModule.forRoot(appRoutes, { scrollPositionRestoration: 'enabled' })],
exports: [RouterModule]
})
export class AppRoutingModule {
}
View File
-5
View File
@@ -1,5 +0,0 @@
<div fxLayout="column" fxFlexFill>
<div fxFlex>
<router-outlet class="hidden-router"></router-outlet>
</div>
</div>
-31
View File
@@ -1,31 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'frontend'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('frontend');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.content span').textContent).toContain('frontend app is running!');
});
});
-24
View File
@@ -1,24 +0,0 @@
import {Component} from '@angular/core';
import {NavigationEnd, Router} from "@angular/router";
import {Config} from "./config/Config";
import {Title} from "@angular/platform-browser";
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
constructor(private router: Router, private title: Title) {
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
// Trick the Router into believing it's last link wasn't previously loaded
this.router.navigated = false;
}
});
title.setTitle(Config.baseTitle);
}
}
-140
View File
@@ -1,140 +0,0 @@
import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {AppComponent} from './app.component';
import {NavbarLayoutComponent} from "./layouts/navbar-layout/navbar-layout.component";
import {PageNotFoundComponent} from "./components/page-not-found/page-not-found.component";
import {HeaderComponent} from "./components/header/header.component";
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {MatFormFieldModule} from "@angular/material/form-field";
import {MatCardModule} from "@angular/material/card";
import {MatTabsModule} from "@angular/material/tabs";
import {MatToolbarModule} from "@angular/material/toolbar";
import {MatMenuModule} from "@angular/material/menu";
import {MatIconModule} from "@angular/material/icon";
import {AppRoutingModule} from "./app-routing.module";
import {HTTP_INTERCEPTORS, HttpClientModule} from "@angular/common/http";
import {ErrorInterceptor} from "./interceptor/error.interceptor";
import {ApiUrlInterceptor} from "./interceptor/api-url.interceptor";
import {ErrorDialogComponent} from "./components/error-dialog/error-dialog.component";
import {MatDialogModule} from "@angular/material/dialog";
import {MatButtonModule} from "@angular/material/button";
import {MatInputModule} from "@angular/material/input";
import {FlexLayoutModule, FlexModule, GridModule} from "@ngbracket/ngx-layout";
import {LibraryOverviewComponent} from './components/library-overview/library-overview.component';
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
import {MatTableModule} from "@angular/material/table";
import {MatPaginatorModule} from "@angular/material/paginator";
import {MatSortModule} from "@angular/material/sort";
import {GameCoverComponent} from './components/game-cover/game-cover.component';
import {GameDetailViewComponent} from './components/game-detail-view/game-detail-view.component';
import {MAT_SNACK_BAR_DEFAULT_OPTIONS, MatSnackBarModule} from '@angular/material/snack-bar';
import {MatGridListModule} from "@angular/material/grid-list";
import {GameScreenshotComponent} from './components/game-screenshot/game-screenshot.component';
import {YouTubePlayerModule} from "@angular/youtube-player";
import {GameVideoComponent} from './components/game-video/game-video.component';
import {MatChipsModule} from "@angular/material/chips";
import { LibraryManagementComponent } from './components/library-management/library-management.component';
import {MatTooltipModule} from "@angular/material/tooltip";
import {MapGameDialogComponent} from "./components/map-game-dialog/map-game-dialog.component";
import {MapLibraryDialogComponent} from "./components/map-library-dialog/map-library-dialog.component";
import {MatSlideToggleModule} from "@angular/material/slide-toggle";
import {MatCheckboxModule} from "@angular/material/checkbox";
import {A11yModule} from "@angular/cdk/a11y";
import { MappedGamesTableComponent } from './components/mapped-games-table/mapped-games-table.component';
import { MappedLibrariesTableComponent } from './components/mapped-libraries-table/mapped-libraries-table.component';
import {MatTableFilterModule} from "mat-table-filter";
import { UnmappedFilesTableComponent } from './components/unmapped-files-table/unmapped-files-table.component';
import {MatDividerModule} from "@angular/material/divider";
import {MatListModule} from "@angular/material/list";
import {MatAutocompleteModule} from "@angular/material/autocomplete";
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";
import {MatProgressBarModule} from "@angular/material/progress-bar";
import { ProgressBarColorDirective } from './directives/progress-bar-color.directive';
@NgModule({
declarations: [
AppComponent,
HeaderComponent,
NavbarLayoutComponent,
PageNotFoundComponent,
ErrorDialogComponent,
LibraryOverviewComponent,
GameCoverComponent,
GameDetailViewComponent,
GameScreenshotComponent,
GameVideoComponent,
LibraryManagementComponent,
MapGameDialogComponent,
MapLibraryDialogComponent,
MappedGamesTableComponent,
MappedLibrariesTableComponent,
UnmappedFilesTableComponent,
NgModelChangeDebouncedDirective,
ProgressBarColorDirective,
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,
MatSelectModule,
MatProgressBarModule,
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: ApiUrlInterceptor,
multi: true
},
{
provide: HTTP_INTERCEPTORS,
useClass: ErrorInterceptor,
multi: true
},
{
provide: MAT_SNACK_BAR_DEFAULT_OPTIONS,
useValue: { panelClass: ['formatted-snackbar'] },
}
],
bootstrap: [AppComponent]
})
export class AppModule {
}
@@ -1,25 +0,0 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ErrorDialogComponent } from './error-dialog.component';
describe('ErrorDialogComponent', () => {
let component: ErrorDialogComponent;
let fixture: ComponentFixture<ErrorDialogComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ ErrorDialogComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ErrorDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,31 +0,0 @@
import {Component, Inject, OnInit} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
@Component({
selector: 'app-error-dialog',
template: `
<h1 mat-dialog-title>Error</h1>
<mat-dialog-content [innerHTML]="message"></mat-dialog-content>
<mat-dialog-actions style="justify-content: end">
<button mat-raised-button color="primary" (click)="onClick()">OK</button>
</mat-dialog-actions>
`,
styles: []
})
export class ErrorDialogComponent implements OnInit {
message: string;
constructor(public dialogRef: MatDialogRef<ErrorDialogComponent>,
@Inject(MAT_DIALOG_DATA) data: any) {
this.message = data.message;
}
ngOnInit() {
}
onClick(): void {
this.dialogRef.close();
}
}
@@ -1 +0,0 @@
<p>&copy; {{date| date:'yyyy'}} grimsi | Gameyfin v{{gameyfinVersion}} | <a href="{{githubUrl}}" target="_blank">GitHub</a></p>
@@ -1,9 +0,0 @@
@use 'sass:map';
@use '@angular/material' as mat;
@import 'src/app/themes/light-theme';
a {
$config: mat.get-color-config($light-theme);
$primary-palette: map.get($config, 'primary');
color: mat.get-color-from-palette($primary-palette, 500);
}
@@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FooterComponent } from './footer.component';
describe('FooterComponent', () => {
let component: FooterComponent;
let fixture: ComponentFixture<FooterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ FooterComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(FooterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,20 +0,0 @@
import { Component, OnInit } from '@angular/core';
import packageJson from 'package.json';
@Component({
selector: 'app-footer',
templateUrl: './footer.component.html',
styleUrls: ['./footer.component.scss']
})
export class FooterComponent implements OnInit {
githubUrl: string = "https://github.com/grimsi/gameyfin";
gameyfinVersion: string = packageJson.version;
date: Date = new Date();
constructor() { }
ngOnInit(): void {
}
}
@@ -1,5 +0,0 @@
<a routerLink="/game/{{game.slug}}">
<div class="game-cover-container shine enlarge" [style.background-image]="'url(v1/images/' + game.coverId + ')'" fxLayoutAlign="center end">
<h2 *ngIf="game.coverId === 'nocover'" class="no-link-stlying">{{game.title}}</h2>
</div>
</a>
@@ -1,59 +0,0 @@
@use '@angular/material' as mat;
.game-cover-container {
height: 352px;
width: 264px;
background-repeat: no-repeat;
background-position: center bottom;
@include mat.elevation(4);
}
.enlarge {
transition: transform 280ms ease-out;
&:hover,
&:focus {
transform: scale(1.05);
}
}
.shine {
position: relative;
overflow: hidden;
&::before {
background: linear-gradient(
to right,
fade_out(#fff, 1) 0%,
fade_out(#fff, 0.7) 100%
);
content: "";
display: block;
height: 100%;
left: -100%;
position: absolute;
top: 0;
transform: skewX(-25deg);
width: 50%;
z-index: 2;
}
&:hover,
&:focus {
&::before {
animation: shine 0.85s;
}
}
@keyframes shine {
100% {
left: 125%;
}
}
}
.no-link-styling a:hover, a:visited, a:link, a:active {
text-decoration: none;
color: black;
}
@@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GameCoverComponent } from './game-cover.component';
describe('GameCoverComponent', () => {
let component: GameCoverComponent;
let fixture: ComponentFixture<GameCoverComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ GameCoverComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(GameCoverComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,19 +0,0 @@
import {Component, Input, OnInit} from '@angular/core';
import {DetectedGameDto} from "../../models/dtos/DetectedGameDto";
@Component({
selector: 'game-cover',
templateUrl: './game-cover.component.html',
styleUrls: ['./game-cover.component.scss']
})
export class GameCoverComponent implements OnInit {
@Input() game!: DetectedGameDto;
constructor() {
}
ngOnInit(): void {
}
}
@@ -1,112 +0,0 @@
<div fxLayout="row" fxLayoutAlign="center" style="margin-top: 16px;"
*ngIf="this.game !== null && this.game !== undefined">
<div fxLayout="column" fxFlex="0 1 75" fxLayoutGap="16px" fxFlex.lt-lg="95">
<div fxLayout="row" fxLayout.lt-lg="column" fxLayoutGap="16px">
<mat-card>
<mat-card-content fxLayout="row" fxLayout.lt-lg="column" fxLayoutGap="16px">
<div fxLayoutAlign="start" fxLayoutAlign.lt-lg="center">
<img src="v1/images/{{game.coverId}}" style="max-height: 352px" alt="Game cover">
</div>
<div fxLayout="column" fxLayoutGap="8px" fxLayoutAlign="space-between">
<div fxLayoutGap="8px">
<h1 style="display: inline-block">{{game.title}}</h1>
<h3 style="display: inline-block; font-style: italic">{{game.releaseDate | date: 'yyyy'}}</h3>
<h2>Description</h2>
<p>{{game.summary}}</p>
</div>
<div *ngIf="companiesWithLogo.length > 0">
<h2>Developed by</h2>
<div fxLayout="row wrap" fxLayoutGap="8px grid">
<div *ngFor="let company of companiesWithLogo" class="company-logos">
<img src="v1/images/{{company.logoId}}" alt="{{company.name}}" [matTooltip]="company.name">
</div>
</div>
</div>
</div>
</mat-card-content>
</mat-card>
<div fxFlex><!-- Spacer --></div>
<div fxLayout="column" fxFlex="40" fxLayoutGap="16px">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<button mat-fab color="secondary" (click)="refreshGame()" title="Refresh metadata">
<mat-icon>refresh</mat-icon>
</button>
<button mat-fab color="primary" (click)="downloadGame()">
<mat-icon>download</mat-icon>
</button>
<b style="font-size: 24px">Download ({{bytesAsHumanReadableString(game.diskSize)}})</b>
</div>
<div fxLayout="column" fxLayoutGap="24px">
<div *ngIf="game.genres !== undefined && game.genres.length > 0">
<h2>Genres</h2>
<mat-chip-listbox>
<mat-chip-option *ngFor="let genre of game.genres"
(click)="goToLibraryWithFilter('genres', genre.slug)">{{genre.name}}</mat-chip-option>
</mat-chip-listbox>
</div>
<div *ngIf="game.themes !== undefined && game.themes.length > 0">
<h2>Themes</h2>
<mat-chip-listbox>
<mat-chip-option *ngFor="let theme of game.themes"
(click)="goToLibraryWithFilter('themes', theme.slug)">{{theme.name}}</mat-chip-option>
</mat-chip-listbox>
</div>
<div *ngIf="game.platforms !== undefined && game.platforms.length > 0">
<h2>Platforms</h2>
<mat-chip-listbox>
<mat-chip-option *ngFor="let platform of game.platforms" [disabled]="game.library == undefined || !hasPlatform(game.library, platform)"
(click)="goToLibraryWithFilter('platforms', platform.slug)">{{platform.name}}</mat-chip-option>
</mat-chip-listbox>
</div>
</div>
<div fxFlex fxLayout="row" fxLayoutGap="16px">
<div fxFlex="40" *ngIf="game.criticsRating !== undefined && game.criticsRating > 0">
<h2>Critics Rating <span style="font-weight: normal; font-size: medium">({{game.criticsRating}}/100)</span>
</h2>
<mat-progress-bar mode="determinate" [value]="game.criticsRating"
[progressBarColor]="mapRatingToColor(game.criticsRating)"></mat-progress-bar>
</div>
<div fxFlex="40" *ngIf="game.userRating !== undefined && game.userRating > 0">
<h2>User Rating <span style="font-weight: normal; font-size: medium">({{game.userRating}}/100)</span></h2>
<mat-progress-bar mode="determinate" [value]="game.userRating"
[progressBarColor]="mapRatingToColor(game.userRating)"></mat-progress-bar>
</div>
</div>
</div>
</div>
<div id="game-media" fxLayout="column" fxLayoutGap="16px">
<div *ngIf="game.screenshotIds !== undefined && game.screenshotIds.length > 0">
<h2>Screenshots</h2>
<mat-grid-list [cols]="gridColumnCount" gutterSize="8px" rowHeight="312px">
<mat-grid-tile *ngFor="let screenshotId of game.screenshotIds">
<game-screenshot [screenshotId]="screenshotId"></game-screenshot>
</mat-grid-tile>
</mat-grid-list>
</div>
<div *ngIf="game.videoIds !== undefined && game.videoIds.length > 0">
<h2>Videos</h2>
<mat-grid-list [cols]="gridColumnCount" gutterSize="8px" rowHeight="312px">
<mat-grid-tile *ngFor="let videoId of game.videoIds" style="width: 555px; height: 312px;">
<game-video [videoId]="videoId" [width]="555" [height]="312"></game-video>
</mat-grid-tile>
</mat-grid-list>
</div>
</div>
</div>
</div>
@@ -1,4 +0,0 @@
.company-logos img {
max-height: 52px;
max-width: 260px;
}
@@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GameDetailViewComponent } from './game-detail-view.component';
describe('GameDetailViewComponent', () => {
let component: GameDetailViewComponent;
let fixture: ComponentFixture<GameDetailViewComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ GameDetailViewComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(GameDetailViewComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,119 +0,0 @@
import {Component, HostListener} from '@angular/core';
import {ActivatedRoute, Params, Router} from "@angular/router";
import {DetectedGameDto} from "../../models/dtos/DetectedGameDto";
import {GamesService} from "../../services/games.service";
import {CompanyDto} from "../../models/dtos/CompanyDto";
import {LibraryDto} from "../../models/dtos/LibraryDto";
import {PlatformDto} from "../../models/dtos/PlatformDto";
@Component({
selector: 'app-game-detail-view',
templateUrl: './game-detail-view.component.html',
styleUrls: ['./game-detail-view.component.scss']
})
export class GameDetailViewComponent {
game!: DetectedGameDto;
companiesWithLogo: CompanyDto[]= [];
gridColumnCount: number;
constructor(private route: ActivatedRoute,
private router: Router,
private gamesService: GamesService) {
this.gamesService.getGame(this.route.snapshot.params['slug']).subscribe({
next: game => {
this.game = game;
if(game.companies !== undefined) {
this.companiesWithLogo = game.companies.filter(c => c.logoId !== undefined && c.logoId.length > 0);
}
},
error: error => {
if (error.status === 404) {
this.router.navigate(['/library']);
} else {
console.error(error);
}
}
});
this.gridColumnCount = this.calculateColumnCount();
}
@HostListener('window:resize', ['$event'])
onResize() {
this.gridColumnCount = this.calculateColumnCount();
}
public downloadGame(): void {
this.gamesService.downloadGame(this.game.slug);
}
public refreshGame(): void {
this.gamesService.refreshGame(this.game.slug).subscribe({
next: game => {
this.game = game;
if(game.companies !== undefined) {
this.companiesWithLogo = game.companies.filter(c => c.logoId !== undefined && c.logoId.length > 0);
}
},
error: error => {
if (error.status === 404) {
this.router.navigate(['/library']);
} else {
console.error(error);
}
}
});
}
public bytesAsHumanReadableString(bytes: number): string {
const thresh = 1024;
if (Math.abs(bytes) < thresh) {
return bytes + ' B';
}
const units = ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const dp = 1;
let u = -1;
const r = 10 ** dp;
do {
bytes /= thresh;
++u;
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
return bytes.toFixed(dp) + ' ' + units[u];
}
goToLibraryWithFilter(field: string, value: string) {
let params: Params = {};
params[field] = value;
this.router.navigate(['/library'], {queryParams: params});
}
mapRatingToColor(rating: number): string {
if (rating >= 75) return '#388e3c';
if (rating >= 50) return '#fbc02d';
if (rating >= 25) return '#f57c00';
return '#d32f2f';
}
private calculateColumnCount(): number {
const elementWidth: number = 555;
const containerWidth: number | undefined = document.getElementById('game-media')?.offsetWidth;
const defaultColumnCount = 3;
if (containerWidth === undefined) return defaultColumnCount
if (containerWidth < elementWidth) return 1;
return Math.floor(containerWidth / elementWidth);
}
hasPlatform(library: LibraryDto, platform: PlatformDto) {
return library.platforms.some((libPlatform) => libPlatform.slug == platform.slug)
}
}
@@ -1 +0,0 @@
<img src="v1/images/{{screenshotId}}" alt="Screenshot">
@@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GameScreenshotComponent } from './game-screenshot.component';
describe('GameScreenshotComponent', () => {
let component: GameScreenshotComponent;
let fixture: ComponentFixture<GameScreenshotComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ GameScreenshotComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(GameScreenshotComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,17 +0,0 @@
import {Component, Input, OnInit} from '@angular/core';
@Component({
selector: 'game-screenshot',
templateUrl: './game-screenshot.component.html',
styleUrls: ['./game-screenshot.component.scss']
})
export class GameScreenshotComponent implements OnInit {
@Input() screenshotId!: string;
constructor() { }
ngOnInit(): void {
}
}
@@ -1 +0,0 @@
<youtube-player [videoId]="videoId" [width]="width" [height]="height"></youtube-player>
@@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GameVideoComponent } from './game-video.component';
describe('GameVideoComponent', () => {
let component: GameVideoComponent;
let fixture: ComponentFixture<GameVideoComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ GameVideoComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(GameVideoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,22 +0,0 @@
import {Component, Input, OnInit} from '@angular/core';
@Component({
selector: 'game-video',
templateUrl: './game-video.component.html',
styleUrls: ['./game-video.component.scss']
})
export class GameVideoComponent implements OnInit {
@Input() videoId!: string;
@Input() height!: number;
@Input() width!: number;
constructor() { }
ngOnInit(): void {
const tag = document.createElement('script');
tag.src = 'https://www.youtube.com/iframe_api';
document.body.appendChild(tag);
}
}
@@ -1,26 +0,0 @@
<mat-toolbar>
<button mat-icon-button matTooltip="Home" (click)="goToLibraryScreen()" *ngIf="!onLibraryScreen()">
<mat-icon>home</mat-icon>
</button>
<span class="spacer"></span>
<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>
</button>
<button mat-icon-button matTooltip="Toggle dark mode" (click)="toggleTheme()">
<mat-icon>brightness_medium</mat-icon>
</button>
<button mat-icon-button matTooltip="Scan library" (click)="scanLibrary()" *ngIf="onLibraryManagementScreen()">
<mat-icon>youtube_searched_for</mat-icon>
</button>
<button mat-icon-button matTooltip="Library management" (click)="goToLibraryManagementScreen()" *ngIf="!onLibraryManagementScreen()">
<mat-icon>settings</mat-icon>
</button>
</mat-toolbar>
@@ -1,11 +0,0 @@
.spacer {
flex: 1 1 auto;
}
.logo {
height: 36px;
position: absolute;
right: 0;
left: 0;
margin: 0 auto;
}
@@ -1,25 +0,0 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { HeaderComponent } from './header.component';
describe('HeaderComponent', () => {
let component: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ HeaderComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,82 +0,0 @@
import {Component} from '@angular/core';
import {LibraryService} from "../../services/library.service";
import {MatSnackBar} from '@angular/material/snack-bar';
import {Router} from "@angular/router";
import {GamesService} from "../../services/games.service";
import {ThemingService} from "../../services/theming.service";
import {Location} from '@angular/common';
import {LibraryScanRequestDto} from "../../models/dtos/LibraryScanRequestDto";
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss']
})
export class HeaderComponent {
// 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,
private location: Location) {
}
scanLibrary(): void {
let request = new LibraryScanRequestDto();
request.downloadImages = true;
this.libraryService.scanLibrary(request).subscribe({
next: result => {
// Refresh the current page "angular style"
this.router.navigate([this.router.url]).then(() => {
const snackBarDuration: number = 10000;
let snackbarContent: string = 'Library scan completed in ' + result.scanDuration + ' seconds:\n' +
'- ' + result.newGames + ' new games\n' +
'- ' + result.deletedGames + ' games removed\n' +
'- ' + result.newUnmappableFiles + ' files/folders could not be mapped\n' +
'- ' + result.totalGames + ' games currently in your library';
if (result.companyLogoDownloads !== undefined && result.coverDownloads !== undefined && result.screenshotDownloads !== undefined) {
snackbarContent = snackbarContent.concat('\n' +
'- ' + result.coverDownloads + ' covers downloaded\n' +
'- ' + result.screenshotDownloads + ' screenshots downloaded\n' +
'- ' + result.companyLogoDownloads + ' company logos downloaded');
}
this.snackBar.open(snackbarContent, undefined, {duration: snackBarDuration});
}
)
},
error: error => this.snackBar.open(`Error while scanning library: ${error.error.message}`, undefined, {duration: 5000})
})
this.snackBar.open('Library scan started in the background. This could take some time.\nYou will get another notification once it\'s done', undefined, {duration: 5000})
}
reloadLibrary(): void {
this.gameService.getAllGames(true).subscribe(() => this.router.navigate(['/library']));
}
goToLibraryScreen(): void {
this.location.back();
}
goToLibraryManagementScreen(): void {
this.router.navigate(['/library-management']);
}
onLibraryScreen(): boolean {
return this.router.url.startsWith("/library?") || this.router.url === "/library";
}
onLibraryManagementScreen(): boolean {
return this.router.url === "/library-management";
}
toggleTheme(): void {
this.themingService.toggleTheme();
}
}
@@ -1,36 +0,0 @@
<div fxFlexFill>
<div *ngIf="loggedIn && (this.mappedLibraries.length > 0)" fxFlex fxLayoutAlign="center start">
<mat-tab-group>
<mat-tab label="Library mappings">
<mapped-libraries-table [mappedLibraries]="mappedLibraries"></mapped-libraries-table>
</mat-tab>
<mat-tab label="Game mappings">
<mapped-games-table [mappedGames]="mappedGames"></mapped-games-table>
</mat-tab>
<mat-tab label="Unmapped files">
<unmapped-files-table [unmappedFiles]="unmappedFiles"></unmapped-files-table>
</mat-tab>
</mat-tab-group>
</div>
<div *ngIf="!loggedIn" fxFlex fxLayout="column" fxLayoutAlign="center center">
<div fxLayout="column" fxLayoutAlign="center center">
<mat-icon fontSet="material-icons-outlined" color="primary" style="font-size: 128px; height: 128px; width: 128px;">
lock
</mat-icon>
<h1>Please log in to manage your game library</h1>
</div>
</div>
<div *ngIf="loggedIn && this.mappedLibraries.length === 0" fxFlex fxLayout="column" fxLayoutAlign="center center">
<div class="library-management-hint" fxLayout="column" fxLayoutAlign="start end">
<mat-icon fontSet="material-icons-outlined">north_east</mat-icon>
<p>Use the library management to scan your file system for games</p>
</div>
<div fxLayout="column" fxLayoutAlign="center center">
<mat-icon fontSet="material-icons-outlined" color="primary" style="font-size: 128px; height: 128px; width: 128px;">videogame_asset_off</mat-icon>
<h1>Your game library is empty!</h1>
</div>
</div>
</div>
@@ -1 +0,0 @@
@import 'src/app/components/library-overview/library-overview.component';
@@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LibraryManagementComponent } from './library-management.component';
describe('LibraryManagementComponent', () => {
let component: LibraryManagementComponent;
let fixture: ComponentFixture<LibraryManagementComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ LibraryManagementComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(LibraryManagementComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,36 +0,0 @@
import {Component, OnInit} from '@angular/core';
import {GamesService} from "../../services/games.service";
import {LibraryManagementService} from "../../services/library-management.service";
import {DetectedGameDto} from "../../models/dtos/DetectedGameDto";
import {UnmappedFileDto} from "../../models/dtos/UnmappedFileDto";
import {LibraryDto} from "../../models/dtos/LibraryDto";
@Component({
selector: 'app-library-management',
templateUrl: './library-management.component.html',
styleUrls: ['./library-management.component.scss']
})
export class LibraryManagementComponent implements OnInit {
loggedIn: boolean = false;
mappedGames: DetectedGameDto[] = [];
unmappedFiles: UnmappedFileDto[] = [];
mappedLibraries: LibraryDto[] = [];
constructor(private gamesService: GamesService,
private libraryManagementService: LibraryManagementService) {
}
ngOnInit(): void {
this.gamesService.getAllGames().subscribe(games => this.mappedGames = games);
this.libraryManagementService.getUnmappedFiles().subscribe(uf => {
this.unmappedFiles = uf;
this.loggedIn = true;
});
this.libraryManagementService.getLibraries().subscribe(libraries => {
this.mappedLibraries = libraries;
this.loggedIn = true;
});
}
}
@@ -1,147 +0,0 @@
<div fxFlexFill>
<div class="fullscreen-overlay" *ngIf="this.loading" fxLayout="column" fxLayoutAlign="center center">
<mat-spinner></mat-spinner>
<h2>Loading library...</h2>
</div>
<div *ngIf="!this.loading && this.gameLibraryIsEmpty" fxFlex fxLayout="column" fxLayoutAlign="center center">
<div class="library-management-hint" fxLayout="column" fxLayoutAlign="start end">
<mat-icon fontSet="material-icons-outlined">north_east</mat-icon>
<p>Use the library management to scan your file system for games</p>
</div>
<div fxLayout="column" fxLayoutAlign="center center">
<mat-icon fontSet="material-icons-outlined" color="primary"
style="font-size: 128px; height: 128px; width: 128px;">
videogame_asset_off
</mat-icon>
<h1>Your game library is empty!</h1>
</div>
</div>
<div class="content" fxLayout="row" fxLayout.lt-lg="column" fxFlexFill="100"
*ngIf="!this.loading && !this.gameLibraryIsEmpty">
<div fxFlex="10" fxHide fxShow.gt-md><!--SPACER--></div>
<div fxFlex.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">
<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"
style="pointer-events: none">
<mat-icon>search</mat-icon>
</button>
<mat-form-field appearance="outline" subscriptSizing="dynamic" fxFlex>
<input matInput [matAutocomplete]="librarySearchAutocomplete" [(ngModel)]="searchTerm"
(ngModelChange)="refreshLibraryView()">
<mat-autocomplete #librarySearchAutocomplete="matAutocomplete" hideSingleSelectionIndicator panelWidth="auto">
<mat-option *ngFor="let game of games" [value]="game.title">
{{game.title}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
</div>
<mat-card>
<mat-card-content fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="20px">
<h3 class="filter-category-title" style="white-space: nowrap; padding-left: 6px">Sort by: </h3>
<mat-select [(value)]="selectedSortOption" (valueChange)="refreshLibraryView()" hideSingleSelectionIndicator>
<mat-option *ngFor="let sortOption of sortOptions" [value]="sortOption">
{{sortOption.title}}
</mat-option>
</mat-select>
</mat-card-content>
</mat-card>
<mat-expansion-panel [expanded]="filterExpansionState.gamemodes">
<mat-expansion-panel-header>
<mat-panel-title fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="6px">
<h3 class="filter-category-title">Gamemodes</h3>
</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)="refreshLibraryView()" color="primary">Online
Co-op
</mat-checkbox>
<mat-checkbox [(ngModel)]="lanSupportFilterEnabled" (change)="refreshLibraryView()" color="primary">LAN
Support
</mat-checkbox>
</div>
</mat-expansion-panel>
<mat-expansion-panel *ngIf="availableGenres.length > 0" [expanded]="filterExpansionState.genres">
<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>
</mat-expansion-panel>
<mat-expansion-panel *ngIf="availableThemes.length > 0" [expanded]="filterExpansionState.themes">
<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>
</mat-expansion-panel>
<mat-expansion-panel *ngIf="availablePlayerPerspectives.length > 0"
[expanded]="filterExpansionState.playerPerspectives">
<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>
<mat-expansion-panel *ngIf="availablePlatforms.length > 0" [expanded]="filterExpansionState.platforms">
<mat-expansion-panel-header>
<h3 class="filter-category-title">Platforms</h3>
</mat-expansion-panel-header>
<div fxLayout="column">
<mat-checkbox *ngFor="let platform of availablePlatforms" (change)="togglePlatformFilter(platform.slug)"
[checked]="activePlatformFilters.includes(platform.slug)"
color="primary">{{platform.name}}</mat-checkbox>
</div>
</mat-expansion-panel>
</div>
<div fxFlex="0 1 1"><!--SPACER--></div>
<div fxFlex fxLayout="column">
<div fxFlex fxLayout="row wrap" fxLayoutGap="16px grid">
<div *ngFor="let game of games">
<game-cover [game]="game"></game-cover>
</div>
</div>
<div fxFlex><!--SPACER--></div>
</div>
<div fxFlex="0 1 10" fxHide fxShow.gt-lg><!--SPACER--></div>
</div>
</div>
@@ -1,59 +0,0 @@
@use 'sass:map';
@use '@angular/material' as mat;
@import 'src/app/themes/dark-theme';
.fullscreen-overlay {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: rgba(0, 0, 0, 0.15);
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
}
.library-management-hint {
@include mat.elevation(16);
position: absolute;
right: 56px;
top: 72px;
width: 250px;
border-radius: 6px;
p {
padding: 0 12px 12px 16px;
}
}
.content {
padding: 16px;
}
.filter-category-title {
margin-bottom: 0;
}
/*TODO(mdc-migration): The following rule targets internal classes of checkbox that may no longer apply for the MDC version.*/
::ng-deep .mat-mdc-text-field-wrapper {
$config: mat.get-color-config($dark-theme);
$primary-palette: map.get($config, 'primary');
border-color: mat.get-color-from-palette($primary-palette, 500) !important;
}
/*TODO(mdc-migration): The following rule targets internal classes of form-field that may no longer apply for the MDC version.*/
::ng-deep .mat-form-field-underline {
$config: mat.get-color-config($dark-theme);
$primary-palette: map.get($config, 'primary');
background-color: mat.get-color-from-palette($primary-palette, 500) !important;
}
.sticky {
position: sticky;
align-self: flex-start;
top: 80px; //64px height of app-header + 16px padding of content
}
@@ -1,25 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LibraryOverviewComponent } from './library-overview.component';
describe('LibraryOverviewComponent', () => {
let component: LibraryOverviewComponent;
let fixture: ComponentFixture<LibraryOverviewComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ LibraryOverviewComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(LibraryOverviewComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,293 +0,0 @@
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 {firstValueFrom, forkJoin, Observable} from "rxjs";
import {SortDirection} from "@angular/material/sort";
import {PlayerPerspectiveDto} from "../../models/dtos/PlayerPerspectiveDto";
import {PlatformDto} from "../../models/dtos/PlatformDto";
import {ActivatedRoute, Params, Router} from "@angular/router";
import {Location} from "@angular/common";
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',
templateUrl: './library-overview.component.html',
styleUrls: ['./library-overview.component.scss']
})
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[] = [];
activePlatformFilters: string[] = [];
filterExpansionState: FilterExpansionState = {};
games: DetectedGameDto[] = [];
availableGenres: GenreDto[] = [];
availableThemes: ThemeDto[] = [];
availablePlayerPerspectives: PlayerPerspectiveDto[] = [];
availablePlatforms: PlatformDto[] = [];
loading: boolean = true;
gameLibraryIsEmpty: boolean = false;
private previousStateParams: Params = {};
constructor(private gameServerService: GamesService,
private route: ActivatedRoute,
private router: Router,
private location: Location) {
}
ngAfterContentInit(): void {
this.gameServerService.getAllGames().subscribe(
detectedGames => {
if (detectedGames.length === 0) {
this.gameLibraryIsEmpty = true;
this.loading = false;
return;
}
this.games = detectedGames;
let genreObservable: Observable<ThemeDto[]> = this.gameServerService.getAvailableGenres();
let themeObservable: Observable<GenreDto[]> = this.gameServerService.getAvailableThemes();
let playerPerspectiveObservable: Observable<PlayerPerspectiveDto[]> = this.gameServerService.getAvailablePlayerPerspectives();
let platformObservable: Observable<PlatformDto[]> = this.gameServerService.getAvailablePlatforms();
forkJoin([genreObservable, themeObservable, playerPerspectiveObservable, platformObservable]).subscribe(result => {
this.availableGenres = result[0];
this.availableThemes = result[1];
this.availablePlayerPerspectives = result[2];
this.availablePlatforms = result[3];
this.previousStateParams = this.route.snapshot.queryParams;
if (this.previousStateParams['search'] !== undefined) this.searchTerm = this.previousStateParams['search'];
if (this.previousStateParams['sort'] !== undefined) this.selectedSortOption = this.matchSelectedSortOptionFromParam(this.previousStateParams['sort']);
if (this.previousStateParams['gamemodes'] !== undefined) this.setSelectedGamemodesFromParam(this.previousStateParams['gamemodes']);
if (this.previousStateParams['genres'] !== undefined) this.activeGenreFilters = this.matchSelectedFilters(this.availableGenres, this.previousStateParams['genres']);
if (this.previousStateParams['themes'] !== undefined) this.activeThemeFilters = this.matchSelectedFilters(this.availableThemes, this.previousStateParams['themes']);
if (this.previousStateParams['playerPerspectives'] !== undefined) this.activePlayerPerspectiveFilters = this.matchSelectedFilters(this.availablePlayerPerspectives, this.previousStateParams['playerPerspectives']);
if (this.previousStateParams['platforms'] !== undefined) this.activePlatformFilters = this.matchSelectedFilters(this.availablePlatforms, this.previousStateParams['platforms']);
this.filterExpansionState = {
gamemodes: this.getActiveGameModesFilters().length > 0,
genres: this.activeGenreFilters.length > 0,
themes: this.activeThemeFilters.length > 0,
playerPerspectives: this.activePlayerPerspectiveFilters.length > 0,
platforms: this.activePlatformFilters.length > 0
}
this.refreshLibraryView().then(() => this.loading = false);
});
}
);
}
async refreshLibraryView(): Promise<void> {
let games: DetectedGameDto[] = await firstValueFrom(this.gameServerService.getAllGames());
this.games = this.sortGames(this.filterGames(games));
this.saveStateToRoute();
}
clearSearchTerm(): void {
this.searchTerm = "";
this.refreshLibraryView();
}
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.offlineCoopFilterEnabled || this.onlineCoopFilterEnabled || this.lanSupportFilterEnabled) {
games = games.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.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)));
}
if (this.activePlatformFilters.length > 0) {
games = games.filter(game => this.activePlatformFilters.some(activePlatformFilter =>
game?.library?.platforms?.map(g => g.slug).includes(activePlatformFilter) && game?.platforms?.map(g => g.slug).includes(activePlatformFilter)));
}
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)) {
const index = this.activeGenreFilters.indexOf(slug, 0);
if (index > -1) {
this.activeGenreFilters.splice(index, 1);
}
} else {
this.activeGenreFilters.push(slug);
}
this.refreshLibraryView();
}
toggleThemeFilter(slug: string) {
if (this.activeThemeFilters.includes(slug)) {
const index = this.activeThemeFilters.indexOf(slug, 0);
if (index > -1) {
this.activeThemeFilters.splice(index, 1);
}
} else {
this.activeThemeFilters.push(slug);
}
this.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();
}
togglePlatformFilter(slug: string): void {
if (this.activePlatformFilters.includes(slug)) {
const index = this.activePlatformFilters.indexOf(slug, 0);
if (index > -1) {
this.activePlatformFilters.splice(index, 1);
}
} else {
this.activePlatformFilters.push(slug);
}
this.refreshLibraryView();
}
private saveStateToRoute(): void {
let newStateParams: Params = {};
if (this.searchTerm.trim().length > 0) newStateParams['search'] = this.searchTerm;
if (this.selectedSortOption !== this.defaultSortOption) newStateParams['sort'] = LibraryOverviewComponent.toParam(this.selectedSortOption);
if (this.getActiveGameModesFilters().length > 0) newStateParams['gamemodes'] = this.getActiveGameModesFilters().join(',');
if (this.activeGenreFilters.length > 0) newStateParams['genres'] = this.activeGenreFilters.join(',');
if (this.activeThemeFilters.length > 0) newStateParams['themes'] = this.activeThemeFilters.join(',');
if (this.activePlayerPerspectiveFilters.length > 0) newStateParams['playerPerspectives'] = this.activePlayerPerspectiveFilters.join(',');
if (this.activePlatformFilters.length > 0) newStateParams['platforms'] = this.activePlatformFilters.join(',');
// only update the route if it changed
if (JSON.stringify(this.previousStateParams) !== JSON.stringify(newStateParams)) {
const url = this.router.createUrlTree([], {relativeTo: this.route, queryParams: newStateParams}).toString();
this.previousStateParams = newStateParams;
this.location.go(url);
}
}
private static toParam(sortOption: SortOption): string {
return `${sortOption.field}_${sortOption.direction}`;
}
private matchSelectedSortOptionFromParam(sortParam: string): SortOption {
return this.sortOptions.find(s => sortParam === LibraryOverviewComponent.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;
}
}
class FilterExpansionState {
gamemodes?: boolean;
genres?: boolean;
themes?: boolean;
playerPerspectives?: boolean;
platforms?: boolean;
}
@@ -1,30 +0,0 @@
<h3 mat-dialog-title>Map game to IGDB slug</h3>
<mat-dialog-content>
<form fxLayout="column" fxLayoutAlign="space-evenly stretch">
<p>Path: {{path}}</p>
<mat-form-field>
<div fxLayout="row">
<input type="text" placeholder="IGDB Slug" matInput [matAutocomplete]="igdbSlugAutocomplete" [(ngModel)]="slug" (ngModelChangeDebounced)="loadSuggestions()" [ngModelOptions]="{standalone: true}">
<mat-spinner *ngIf="suggestionsLoading" [diameter]="16"></mat-spinner>
</div>
<mat-autocomplete #igdbSlugAutocomplete="matAutocomplete">
<mat-option *ngFor="let suggestion of autocompleteSuggestions" [value]="suggestion.slug">
{{suggestion.title}} ({{getFullYearFromTimestamp(suggestion.releaseDate)}}) - {{suggestion.platforms.join(', ')}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-raised-button [mat-dialog-close]="false" color="accent" [disabled]="submitLoading">Cancel</button>
<button mat-raised-button (click)="submit()" [disabled]="slug.length < 1 || submitLoading" color="primary">
<span *ngIf="!submitLoading">OK</span>
<div *ngIf="submitLoading" fxLayout="column" fxLayoutAlign="center center" style="height: 36px;">
<mat-spinner [diameter]="24"></mat-spinner>
</div>
</button>
</mat-dialog-actions>
@@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MapGameDialogComponent } from './map-game-dialog.component';
describe('MapGameDialogComponent', () => {
let component: MapGameDialogComponent;
let fixture: ComponentFixture<MapGameDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MapGameDialogComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(MapGameDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,84 +0,0 @@
import {Component, Inject, OnInit} from '@angular/core';
import {LibraryManagementService} from "../../services/library-management.service";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {PathToSlugDto} from "../../models/dtos/PathToSlugDto";
import {DialogService} from "../../services/dialog.service";
import {ApiErrorResponse} from "../../models/dtos/ApiErrorResponse";
import {AutocompleteSuggestionDto} from "../../models/dtos/AutocompleteSuggestionDto";
@Component({
selector: 'app-map-game-dialog',
templateUrl: './map-game-dialog.component.html',
styleUrls: ['./map-game-dialog.component.scss']
})
export class MapGameDialogComponent implements OnInit {
path: string;
slug: string;
autocompleteSuggestions: AutocompleteSuggestionDto[] = [];
submitLoading: boolean = false;
suggestionsLoading: boolean = false;
constructor(private libraryManagementService: LibraryManagementService,
private dialogService: DialogService,
public dialogRef: MatDialogRef<MapGameDialogComponent>,
@Inject(MAT_DIALOG_DATA) data: any) {
this.path = data.path;
this.slug = data.slug ?? '';
}
ngOnInit() {
this.loadInitialSuggestions();
}
submit(): void {
this.submitLoading = true;
this.libraryManagementService.mapGame(new PathToSlugDto(this.slug, this.path)).subscribe({
next: () => this.dialogRef.close(true),
error: (error: ApiErrorResponse) => {
this.dialogRef.close(false);
this.dialogService.showErrorDialog(error.error.message);
}
}
)
}
loadInitialSuggestions(): void {
this.suggestionsLoading = true;
// Extract the last path element (folder name / file name)
let extractedTitleFromPath: string = this.path.match(/([^\\/]*)[\\/]*$/)![1];
// Match it until the first special characters
extractedTitleFromPath = extractedTitleFromPath.match(/^[a-zA-Z0-9:\- ]+/)![0];
if(extractedTitleFromPath == null) {
this.suggestionsLoading = false;
return;
}
this.libraryManagementService.getAutocompleteSuggestions(extractedTitleFromPath, 10).subscribe({
next: suggestions => {
this.autocompleteSuggestions = suggestions;
this.suggestionsLoading = false;
},
error: () => this.suggestionsLoading = false
})
}
loadSuggestions(): void {
this.suggestionsLoading = true;
this.libraryManagementService.getAutocompleteSuggestions(this.slug, 50).subscribe({
next: suggestions => {
this.autocompleteSuggestions = suggestions;
this.suggestionsLoading = false;
},
error: () => this.suggestionsLoading = false
})
}
getFullYearFromTimestamp(timestamp: number): number {
return new Date(timestamp).getFullYear();
}
}
@@ -1,31 +0,0 @@
<h3 mat-dialog-title>Map path to IGDB platform</h3>
<mat-dialog-content>
<form fxLayout="column" fxLayoutAlign="space-evenly stretch">
<p>Path: {{path}}</p>
<p><a href="https://www.igdb.com/platforms" target="_blank">Available platforms</a></p>
<mat-form-field>
<div fxLayout="row">
<input type="text" placeholder="IGDB Platform Slugs" matInput [matAutocomplete]="igdbPlatformSlugsAutocomplete" [(ngModel)]="slugs" (ngModelChangeDebounced)="loadSuggestions()" [ngModelOptions]="{standalone: true}">
<mat-spinner *ngIf="suggestionsLoading" [diameter]="16"></mat-spinner>
</div>
<mat-autocomplete #igdbPlatformSlugsAutocomplete="matAutocomplete">
<mat-option *ngFor="let suggestion of autocompletePlatformSuggestions" [value]="previousSlugs+suggestion.slug">
{{suggestion.name}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-raised-button [mat-dialog-close]="false" color="accent" [disabled]="submitLoading">Cancel</button>
<button mat-raised-button (click)="submit()" [disabled]="slugs.length < 1 || submitLoading" color="primary">
<span *ngIf="!submitLoading">OK</span>
<div *ngIf="submitLoading" fxLayout="column" fxLayoutAlign="center center" style="height: 36px;">
<mat-spinner [diameter]="24"></mat-spinner>
</div>
</button>
</mat-dialog-actions>
@@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MapLibraryDialogComponent } from './map-library-dialog.component';
describe('MapLibraryDialogComponent', () => {
let component: MapLibraryDialogComponent;
let fixture: ComponentFixture<MapLibraryDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MapLibraryDialogComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(MapLibraryDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,90 +0,0 @@
import {Component, Inject, OnInit} from '@angular/core';
import {LibraryManagementService} from "../../services/library-management.service";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {PathToSlugDto} from "../../models/dtos/PathToSlugDto";
import {DialogService} from "../../services/dialog.service";
import {ApiErrorResponse} from "../../models/dtos/ApiErrorResponse";
import {PlatformDto} from "../../models/dtos/PlatformDto";
@Component({
selector: 'app-map-library-dialog',
templateUrl: './map-library-dialog.component.html',
styleUrls: ['./map-library-dialog.component.scss']
})
export class MapLibraryDialogComponent implements OnInit {
path: string;
slugs: string;
previousSlugs: string;
autocompletePlatformSuggestions: PlatformDto[] = [];
submitLoading: boolean = false;
suggestionsLoading: boolean = false;
constructor(private libraryManagementService: LibraryManagementService,
private dialogService: DialogService,
public dialogRef: MatDialogRef<MapLibraryDialogComponent>,
@Inject(MAT_DIALOG_DATA) data: any) {
this.path = data.path;
this.slugs = data.slugs ?? '';
this.previousSlugs = data.previousSlugs ?? '';
}
ngOnInit() {
this.loadInitialSuggestions();
}
submit(): void {
this.submitLoading = true;
this.libraryManagementService.mapLibrary(new PathToSlugDto(Array.isArray(this.slugs) ? this.slugs.join(',') : this.slugs, this.path)).subscribe({
next: () => this.dialogRef.close(true),
error: (error: ApiErrorResponse) => {
this.dialogRef.close(false);
this.dialogService.showErrorDialog(error.error.message);
}
}
)
}
loadInitialSuggestions(): void {
this.suggestionsLoading = true;
// Extract the last path element (folder name / file name)
let extractedPlatformFromPath: string = this.path.match(/([^\\/]*)[\\/]*$/)![1];
// Match it until the first special characters
extractedPlatformFromPath = extractedPlatformFromPath.match(/^[a-zA-Z0-9:\- ]+/)![0];
if(extractedPlatformFromPath == null) {
this.suggestionsLoading = false;
return;
}
this.libraryManagementService.getPlatforms(extractedPlatformFromPath, 10).subscribe({
next: suggestions => {
this.autocompletePlatformSuggestions = suggestions;
this.suggestionsLoading = false;
},
error: () => this.suggestionsLoading = false
})
}
loadSuggestions(): void {
this.suggestionsLoading = true;
let searchTerm = '';
if (this.slugs.length > 0) {
let slugArray = this.slugs.split(',');
// pop off the search term after the last comma
searchTerm = slugArray.pop() ?? '';
// if we already had slugs in our input field we need to add them back again
this.previousSlugs = (slugArray.length > 0 ? slugArray.join(',') + ',' : '');
}
this.libraryManagementService.getPlatforms(searchTerm, 50).subscribe({
next: suggestions => {
this.autocompletePlatformSuggestions = suggestions;
this.suggestionsLoading = false;
},
error: () => this.suggestionsLoading = false
})
}
}
@@ -1,50 +0,0 @@
<div class="mat-elevation-z8">
<table mat-table matSort matTableFilter [dataSource]="dataSource" [exampleEntity]="filter" [debounceTime]="0">
<!-- Path column -->
<ng-container matColumnDef="path">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Path</th>
<td mat-cell *matCellDef="let element"> {{element.path}} </td>
</ng-container>
<!-- Game column -->
<ng-container matColumnDef="game">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Game</th>
<td mat-cell *matCellDef="let element"> {{element.title}} ({{getFullYearFromTimestamp(element.releaseDate)}})</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>
<button mat-icon-button (click)="toggleShowOnlyUnconfirmedMatches()">
<mat-icon *ngIf="showOnlyUnconfirmedMatches" matTooltip="Show all game mappings" matTooltipPosition="below" color="warn">playlist_add_check_circle</mat-icon>
<mat-icon *ngIf="!showOnlyUnconfirmedMatches" matTooltip="Show only unconfirmed game mappings" matTooltipPosition="below" fontSet="material-icons-outlined">playlist_add_check_circle</mat-icon>
</button>
<button mat-icon-button (click)="refreshMappedGamesList()">
<mat-icon matTooltip="Refresh game list" matTooltipPosition="below">refresh</mat-icon>
</button>
</th>
<!-- Action column -->
<td mat-cell *matCellDef="let element">
<button mat-icon-button (click)="toggleConfirmGameMapping(element)" [color]="element.confirmedMatch ? 'primary' : 'warn'">
<mat-icon [matTooltip]="element.confirmedMatch ? 'Unconfirm match':'Confirm match'" matTooltipPosition="below">check</mat-icon>
</button>
<button mat-icon-button (click)="openCorrectMappingDialog(element)">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button (click)="deleteGameMapping(element)">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator
[length]="dataSource?.data?.length"
[pageIndex]="0"
[pageSize]="15"
[pageSizeOptions]="[10, 15, 25, 50]">
</mat-paginator>
</div>
@@ -1,8 +0,0 @@
table {
width: 50vw;
min-width: 750px;
}
.mat-column-actions {
width: 20%;
}
@@ -1,34 +0,0 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MappedGamesTableComponent } from './mapped-games-table.component';
describe('MappedGamesTableComponent', () => {
let component: MappedGamesTableComponent;
let fixture: ComponentFixture<MappedGamesTableComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ MappedGamesTableComponent ],
imports: [
NoopAnimationsModule,
MatPaginatorModule,
MatSortModule,
MatTableModule,
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MappedGamesTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should compile', () => {
expect(component).toBeTruthy();
});
});
@@ -1,89 +0,0 @@
import {AfterViewInit, Component, Input, OnChanges, SimpleChanges, ViewChild} from '@angular/core';
import {MatPaginator} from '@angular/material/paginator';
import {MatSort} from '@angular/material/sort';
import {MatTable, MatTableDataSource} from '@angular/material/table';
import {DetectedGameDto} from "../../models/dtos/DetectedGameDto";
import {GamesService} from "../../services/games.service";
import {LibraryManagementService} from "../../services/library-management.service";
import {DialogService} from "../../services/dialog.service";
@Component({
selector: 'mapped-games-table',
templateUrl: './mapped-games-table.component.html',
styleUrls: ['./mapped-games-table.component.scss']
})
export class MappedGamesTableComponent implements AfterViewInit, OnChanges {
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
@ViewChild(MatTable) table!: MatTable<DetectedGameDto>;
@Input() mappedGames!: DetectedGameDto[];
dataSource: MatTableDataSource<DetectedGameDto> = new MatTableDataSource();
displayedColumns: string[] = ["path", "game", "actions"];
showOnlyUnconfirmedMatches: boolean = false;
filter: DetectedGameDto = new DetectedGameDto();
constructor(private gamesService: GamesService,
private libraryManagementService: LibraryManagementService,
private dialogService: DialogService) {
}
ngAfterViewInit(): void {
this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = (item: DetectedGameDto, property: string) => {
if (property === 'game') {
return item.title;
}
return (item as any)[property];
};
this.dataSource.paginator = this.paginator;
}
ngOnChanges(changes: SimpleChanges): void {
this.refreshData(changes['mappedGames'].currentValue);
}
refreshMappedGamesList(): void {
this.gamesService.getAllGames(true).subscribe(games => this.refreshData(games));
}
toggleShowOnlyUnconfirmedMatches() {
this.showOnlyUnconfirmedMatches = !this.showOnlyUnconfirmedMatches;
this.filter.confirmedMatch = this.showOnlyUnconfirmedMatches ? false : undefined;
}
getFullYearFromTimestamp(timestamp: number): number {
return new Date(timestamp).getFullYear();
}
toggleConfirmGameMapping(mappedGame: DetectedGameDto): void {
this.libraryManagementService.confirmGameMapping(mappedGame.slug, !mappedGame.confirmedMatch).subscribe(() => {
mappedGame.confirmedMatch = !mappedGame.confirmedMatch;
this.refreshData(this.dataSource.data);
});
}
deleteGameMapping(mappedGame: DetectedGameDto): void {
this.libraryManagementService.deleteGame(mappedGame.slug).subscribe(
() => this.refreshData(this.dataSource.data.filter(game => game !== mappedGame))
);
}
openCorrectMappingDialog(mappedGame: DetectedGameDto): void {
this.dialogService.correctGameMappingDialog(mappedGame).subscribe(gameSuccessfullyMapped => {
if (gameSuccessfullyMapped) this.refreshMappedGamesList();
})
}
private refreshData(newData: DetectedGameDto[]): void {
this.dataSource.data = newData;
// Dirty hack to force a re-render
// Did not find a better solution
this.paginator?._changePageSize(this.paginator?.pageSize);
}
}
@@ -1,43 +0,0 @@
<div class="mat-elevation-z8">
<table mat-table matSort matTableFilter [dataSource]="dataSource" [exampleEntity]="filter" [debounceTime]="0">
<!-- Path column -->
<ng-container matColumnDef="path">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Path</th>
<td mat-cell *matCellDef="let element"> {{element.path}} </td>
</ng-container>
<!-- Platform column -->
<ng-container matColumnDef="platforms">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Platforms</th>
<td mat-cell *matCellDef="let element"><span *ngFor="let item of element.platforms; let isLast=last">{{item.name}}{{isLast ? '' : ', '}}</span></td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>
<button mat-icon-button (click)="refreshMappedLibrariesList()">
<mat-icon matTooltip="Refresh library list" matTooltipPosition="below">refresh</mat-icon>
</button>
</th>
<!-- Action column -->
<td mat-cell *matCellDef="let element">
<button mat-icon-button (click)="openLibraryMappingDialog(element)">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button matTooltip="Scan this library" (click)="scanLibrary(element)">
<mat-icon>youtube_searched_for</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator
[length]="dataSource?.data?.length"
[pageIndex]="0"
[pageSize]="15"
[pageSizeOptions]="[10, 15, 25, 50]">
</mat-paginator>
</div>
@@ -1,8 +0,0 @@
table {
width: 50vw;
min-width: 750px;
}
.mat-column-actions {
width: 20%;
}
@@ -1,34 +0,0 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MappedLibrariesTableComponent } from './mapped-libraries-table.component';
describe('MappedLibrariesTableComponent', () => {
let component: MappedLibrariesTableComponent;
let fixture: ComponentFixture<MappedLibrariesTableComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ MappedLibrariesTableComponent ],
imports: [
NoopAnimationsModule,
MatPaginatorModule,
MatSortModule,
MatTableModule,
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MappedLibrariesTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should compile', () => {
expect(component).toBeTruthy();
});
});
@@ -1,102 +0,0 @@
import {AfterViewInit, Component, Input, OnChanges, SimpleChanges, ViewChild} from '@angular/core';
import {MatPaginator} from '@angular/material/paginator';
import {MatSort} from '@angular/material/sort';
import {
MatTable,
MatTableDataSource
} from '@angular/material/table';
import {LibraryDto} from "../../models/dtos/LibraryDto";
import {LibraryScanRequestDto} from "../../models/dtos/LibraryScanRequestDto";
import {LibraryManagementService} from "../../services/library-management.service";
import {DialogService} from "../../services/dialog.service";
import {MatSnackBar} from '@angular/material/snack-bar';
import {Router} from "@angular/router";
import {LibraryService} from "../../services/library.service";
@Component({
selector: 'mapped-libraries-table',
templateUrl: './mapped-libraries-table.component.html',
styleUrls: ['./mapped-libraries-table.component.scss']
})
export class MappedLibrariesTableComponent implements AfterViewInit, OnChanges {
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
@ViewChild(MatTable) table!: MatTable<LibraryDto>;
@Input() mappedLibraries!: LibraryDto[];
dataSource: MatTableDataSource<LibraryDto> = new MatTableDataSource();
displayedColumns: string[] = ["path", "platforms", "actions"];
filter: LibraryDto = new LibraryDto();
constructor(private libraryManagementService: LibraryManagementService,
private dialogService: DialogService,
private libraryService: LibraryService,
private snackBar: MatSnackBar,
private router: Router) {
}
ngAfterViewInit(): void {
this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = (item: LibraryDto, property: string) => {
return (item as any)[property];
};
this.dataSource.paginator = this.paginator;
}
ngOnChanges(changes: SimpleChanges): void {
this.refreshData(changes['mappedLibraries'].currentValue);
}
refreshMappedLibrariesList(): void {
this.libraryManagementService.getLibraries().subscribe(libraries => this.refreshData(libraries));
}
openLibraryMappingDialog(mappedLibrary: LibraryDto): void {
this.dialogService.libraryMappingDialog(mappedLibrary).subscribe(librarySuccessfullyMapped => {
if (librarySuccessfullyMapped) this.refreshMappedLibrariesList();
})
}
scanLibrary(mappedLibrary: LibraryDto): void {
let request = new LibraryScanRequestDto();
request.path = mappedLibrary.path;
request.downloadImages = true;
this.libraryService.scanLibrary(request).subscribe({
next: result => {
// Refresh the current page "angular style"
this.router.navigate([this.router.url]).then(() => {
const snackBarDuration: number = 10000;
let snackbarContent: string = 'Library scan completed in ' + result.scanDuration + ' seconds:\n' +
'- ' + result.newGames + ' new games\n' +
'- ' + result.deletedGames + ' games removed\n' +
'- ' + result.newUnmappableFiles + ' files/folders could not be mapped\n' +
'- ' + result.totalGames + ' games currently in your library';
if (result.companyLogoDownloads !== undefined && result.coverDownloads !== undefined && result.screenshotDownloads !== undefined) {
snackbarContent = snackbarContent.concat('\n' +
'- ' + result.coverDownloads + ' covers downloaded\n' +
'- ' + result.screenshotDownloads + ' screenshots downloaded\n' +
'- ' + result.companyLogoDownloads + ' company logos downloaded');
}
this.snackBar.open(snackbarContent, undefined, {duration: snackBarDuration});
}
)
},
error: error => this.snackBar.open(`Error while scanning library: ${error.error.message}`, undefined, {duration: 5000})
})
this.snackBar.open('Library scan started in the background. This could take some time.\nYou will get another notification once it\'s done', undefined, {duration: 5000})
}
private refreshData(newData: LibraryDto[]): void {
this.dataSource.data = newData;
// Dirty hack to force a re-render
// Did not find a better solution
this.paginator?._changePageSize(this.paginator?.pageSize);
}
}
@@ -1,4 +0,0 @@
<div fxLayout="column" fxLayoutAlign="center center">
<h1>404</h1>
<p>The page you are looking for does not exist.</p>
</div>
@@ -1,25 +0,0 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { PageNotFoundComponent } from './page-not-found.component';
describe('PageNotFoundComponent', () => {
let component: PageNotFoundComponent;
let fixture: ComponentFixture<PageNotFoundComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ PageNotFoundComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PageNotFoundComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,15 +0,0 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-page-not-found',
templateUrl: './page-not-found.component.html',
styleUrls: ['./page-not-found.component.scss']
})
export class PageNotFoundComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}
@@ -1,36 +0,0 @@
<div class="mat-elevation-z8">
<table mat-table matSort [dataSource]="dataSource">
<!-- Path column -->
<ng-container matColumnDef="path">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Path</th>
<td mat-cell *matCellDef="let element"> {{element.path}} </td>
</ng-container>
<!-- Actions column -->
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>
<button mat-icon-button (click)="refreshUnmappedFilesList()">
<mat-icon>refresh</mat-icon>
</button>
</th>
<td mat-cell *matCellDef="let element">
<button mat-icon-button (click)="openMapUnmappedFileDialog(element)">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button (click)="deleteUnmappedFile(element)">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator
[length]="dataSource?.data?.length"
[pageIndex]="0"
[pageSize]="15"
[pageSizeOptions]="[10, 15, 25, 50]">
</mat-paginator>
</div>
@@ -1,9 +0,0 @@
table {
width: 50vw;
min-width: 750px;
}
.mat-column-actions {
width: 20%;
}
@@ -1,34 +0,0 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { UnmappedFilesTableComponent } from './unmapped-files-table.component';
describe('UnmappedFilesTableComponent', () => {
let component: UnmappedFilesTableComponent;
let fixture: ComponentFixture<UnmappedFilesTableComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ UnmappedFilesTableComponent ],
imports: [
NoopAnimationsModule,
MatPaginatorModule,
MatSortModule,
MatTableModule,
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(UnmappedFilesTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should compile', () => {
expect(component).toBeTruthy();
});
});
@@ -1,62 +0,0 @@
import {AfterViewInit, Component, Input, OnChanges, SimpleChanges, ViewChild} from '@angular/core';
import {MatPaginator} from '@angular/material/paginator';
import {MatSort} from '@angular/material/sort';
import {MatTable, MatTableDataSource} from '@angular/material/table';
import {UnmappedFileDto} from "../../models/dtos/UnmappedFileDto";
import {GamesService} from "../../services/games.service";
import {LibraryManagementService} from "../../services/library-management.service";
import {DialogService} from "../../services/dialog.service";
@Component({
selector: 'unmapped-files-table',
templateUrl: './unmapped-files-table.component.html',
styleUrls: ['./unmapped-files-table.component.scss']
})
export class UnmappedFilesTableComponent implements AfterViewInit, OnChanges {
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
@ViewChild(MatTable) table!: MatTable<UnmappedFileDto>;
@Input() unmappedFiles!: UnmappedFileDto[];
dataSource: MatTableDataSource<UnmappedFileDto> = new MatTableDataSource();
displayedColumns: string[] = ["path", "actions"];
constructor(private gameService: GamesService,
private libraryManagementService: LibraryManagementService,
private dialogService: DialogService) {
}
ngAfterViewInit(): void {
this.dataSource.sort = this.sort;
this.dataSource.paginator = this.paginator;
}
ngOnChanges(changes: SimpleChanges): void {
this.refreshData(changes['unmappedFiles'].currentValue);
}
refreshUnmappedFilesList(): void {
this.libraryManagementService.getUnmappedFiles().subscribe(unmappedFiles => this.refreshData(unmappedFiles));
}
deleteUnmappedFile(unmappedFile: UnmappedFileDto): void {
this.libraryManagementService.deleteUnmappedFile(unmappedFile.id).subscribe(
() => this.refreshData(this.dataSource.data.filter(uf => uf !== unmappedFile))
);
}
openMapUnmappedFileDialog(unmappedFile: UnmappedFileDto): void {
this.dialogService.mapUnmappedGameDialog(unmappedFile).subscribe(gameSuccessfullyMapped => {
if (gameSuccessfullyMapped) this.refreshData(this.dataSource.data.filter(uf => uf !== unmappedFile));
})
}
private refreshData(newData: UnmappedFileDto[]): void {
this.dataSource.data = newData;
// Dirty hack to force a re-render
// Did not find a better solution
this.paginator?._changePageSize(this.paginator?.pageSize);
}
}
-4
View File
@@ -1,4 +0,0 @@
export class Config {
public static baseTitle = 'Gameyfin';
public static apiBasePath = '/v1';
}
@@ -1,8 +0,0 @@
import { NgModelChangeDebouncedDirective } from './ng-model-change-debounced.directive';
describe('NgModelChangeDebouncedDirective', () => {
it('should create an instance', () => {
const directive = new NgModelChangeDebouncedDirective();
expect(directive).toBeTruthy();
});
});
@@ -1,26 +0,0 @@
import {Directive, EventEmitter, Input, OnDestroy, Output} from "@angular/core";
import {debounceTime, distinctUntilChanged, skip, Subscription} from "rxjs";
import {NgModel} from "@angular/forms";
@Directive({
selector: '[ngModelChangeDebounced]',
})
export class NgModelChangeDebouncedDirective implements OnDestroy {
@Output()
ngModelChangeDebounced = new EventEmitter<any>();
@Input()
ngModelChangeDebounceTime = 500; // optional, 500 default
subscription: Subscription;
ngOnDestroy() {
this.subscription.unsubscribe();
}
constructor(private ngModel: NgModel) {
this.subscription = this.ngModel.control.valueChanges.pipe(
skip(1), // skip initial value
distinctUntilChanged(),
debounceTime(this.ngModelChangeDebounceTime)
).subscribe((value) => this.ngModelChangeDebounced.emit(value));
}
}
@@ -1,8 +0,0 @@
import { ProgressBarColorDirective } from './progress-bar-color.directive';
describe('ProgressBarColorDirective', () => {
it('should create an instance', () => {
const directive = new ProgressBarColorDirective();
expect(directive).toBeTruthy();
});
});
@@ -1,38 +0,0 @@
import { Directive, Input, OnChanges, SimpleChanges, ElementRef } from '@angular/core';
@Directive({
selector: '[progressBarColor]'
})
export class ProgressBarColorDirective implements OnChanges{
static counter = 0;
@Input() progressBarColor!: string;
styleEl:HTMLStyleElement = document.createElement('style');
//generate unique attribule which we will use to minimise the scope of our dynamic style
uniqueAttr = `app-progress-bar-color-${ProgressBarColorDirective.counter++}`;
constructor(private el: ElementRef) {
const nativeEl: HTMLElement = this.el.nativeElement;
nativeEl.setAttribute(this.uniqueAttr,'');
nativeEl.appendChild(this.styleEl);
}
ngOnChanges(changes: SimpleChanges): void{
this.updateColor();
}
updateColor(): void{
// update dynamic style with the uniqueAttr
this.styleEl.innerText = `
[${this.uniqueAttr}] .mdc-linear-progress__bar-inner {
border-color: ${this.progressBarColor};
}
[${this.uniqueAttr}] .mdc-linear-progress__buffer-bar {
background-color: ${this.progressBarColor}55 !important;
}
`;
}
}
@@ -1,18 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Config } from '../config/Config';
@Injectable()
export class ApiUrlInterceptor implements HttpInterceptor {
constructor() {
}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
request = request.clone({
url: Config.apiBasePath + request.url
});
return next.handle(request);
}
}
@@ -1,37 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { ApiErrorResponse } from '../models/dtos/ApiErrorResponse';
import { DialogService } from '../services/dialog.service';
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
constructor(private dialogService: DialogService) {
}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe(catchError((err: ApiErrorResponse) => {
switch (err.status) {
case 400:
if (err.error.message === 'Validation error') {
this.dialogService.showErrorDialog(JSON.stringify(err.error.errors));
} else {
this.dialogService.showErrorDialog(err.error.message);
}
break;
case 409:
case 500:
case 401:
this.dialogService.showErrorDialog(err.error.message);
this.dialogService.showErrorDialog(err.error.message);
break;
case 503:
case 504:
this.dialogService.showErrorDialog(`Can't reach the backend at the moment.<br>Please ensure that the backend is running and reload this page`);
break;
}
return throwError(err);
}));
}
}
@@ -1,32 +0,0 @@
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-navbar-layout',
template: `
<div class="main-container" fxLayout="column">
<div fxFlex="none" style="position: sticky; top: 0; z-index: 999">
<app-header></app-header>
</div>
<div fxFlex>
<router-outlet></router-outlet><!-- class="hidden-router" -->
</div>
<div fxLayout="row" fxLayoutAlign="center center">
<app-footer></app-footer>
</div>
</div>
`,
styles: [`
.main-container {
min-height: 100vh;
}
`]
})
export class NavbarLayoutComponent implements OnInit {
constructor() {
}
ngOnInit() {
}
}
@@ -1,12 +0,0 @@
import { HttpErrorResponse } from '@angular/common/http';
export interface ApiErrorResponse extends HttpErrorResponse {
error: {
timestamp: Date;
error: string;
status: number;
errors: object[];
message: string;
path: string;
};
}
@@ -1,6 +0,0 @@
export class AutocompleteSuggestionDto {
slug!: string;
title!: string;
releaseDate!: number;
platforms!: Array<string>;
}
@@ -1,5 +0,0 @@
export class CompanyDto {
slug!: string;
name!: string;
logoId?: string;
}
@@ -1,38 +0,0 @@
import {CompanyDto} from "./CompanyDto";
import {GenreDto} from "./GenreDto";
import {KeywordDto} from "./KeywordDto";
import {PlayerPerspectiveDto} from "./PlayerPerspectiveDto";
import {PlatformDto} from "./PlatformDto";
import {ThemeDto} from "./ThemeDto";
import {LibraryDto} from "./LibraryDto";
export class DetectedGameDto {
slug!: string;
title!: string;
summary?: string;
releaseDate?: Date;
userRating?: number;
criticsRating?: number;
totalRating?: number;
category?: string;
offlineCoop?: boolean;
onlineCoop?: boolean;
lanSupport?: boolean;
maxPlayers?: boolean;
coverId!: string;
screenshotIds?: string[];
videoIds?: string[];
companies?: CompanyDto[];
genres?: GenreDto[];
keywords?: KeywordDto[];
themes?: ThemeDto[];
playerPerspectives?: PlayerPerspectiveDto[];
platforms?: PlatformDto[];
library?: LibraryDto;
path!: string;
diskSize!: number;
confirmedMatch!: boolean | undefined;
addedToLibrary!: Date;
}
@@ -1,5 +0,0 @@
export class GameOverviewDto {
slug!: string;
title!: string;
coverId!: string;
}
-4
View File
@@ -1,4 +0,0 @@
export class GenreDto {
slug!: string;
name!: string;
}
@@ -1,5 +0,0 @@
export class ImageDownloadResultDto {
coverDownloads!: number;
screenshotDownloads!: number;
companyLogoDownloads!: number;
}
@@ -1,4 +0,0 @@
export class KeywordDto {
slug!: string;
name?: string;
}
@@ -1,7 +0,0 @@
import {PlatformDto} from "./PlatformDto";
export class LibraryDto {
path!: string;
platforms!: PlatformDto[];
}
@@ -1,7 +0,0 @@
import {PlatformDto} from "./PlatformDto";
export class LibraryScanRequestDto {
path!: string;
downloadImages!: boolean;
}
@@ -1,10 +0,0 @@
export class LibraryScanResultDto {
newGames!: number;
deletedGames!: number;
newUnmappableFiles!: number;
totalGames!: number;
coverDownloads!: number;
screenshotDownloads!: number;
companyLogoDownloads!: number;
scanDuration!: number;
}
@@ -1,11 +0,0 @@
import {FormControl} from "@angular/forms";
export class PathToSlugDto {
slug: string;
path: string;
constructor(slug: string, path: string) {
this.slug = slug;
this.path = path;
}
}
@@ -1,5 +0,0 @@
export class PlatformDto {
slug!: string;
name!: string;
platformLogoId?: string;
}
@@ -1,4 +0,0 @@
export class PlayerPerspectiveDto {
slug!: string;
name!: string;
}
-4
View File
@@ -1,4 +0,0 @@
export class ThemeDto {
slug!: string;
name!: string;
}
@@ -1,4 +0,0 @@
export class UnmappedFileDto {
id!: number;
path!: string;
}

Some files were not shown because too many files have changed in this diff Show More