mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 16:20:04 +00:00
Start development of v2
This commit is contained in:
@@ -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
|
||||
@@ -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/
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
Generated
-12879
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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[]>;
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<div fxLayout="column" fxFlexFill>
|
||||
<div fxFlex>
|
||||
<router-outlet class="hidden-router"></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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!');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>© {{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);
|
||||
}
|
||||
}
|
||||
-43
@@ -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>
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
table {
|
||||
width: 50vw;
|
||||
min-width: 750px;
|
||||
}
|
||||
|
||||
.mat-column-actions {
|
||||
width: 20%;
|
||||
}
|
||||
-34
@@ -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();
|
||||
});
|
||||
});
|
||||
-102
@@ -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%;
|
||||
}
|
||||
|
||||
-34
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user