mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-15 08:15:37 +00:00
Polishing and code clean-up
This commit is contained in:
@@ -2,6 +2,7 @@ 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";
|
||||
|
||||
export interface LibraryManagementApi {
|
||||
mapGame(pathToSlugDto: PathToSlugDto): Observable<DetectedGameDto>;
|
||||
@@ -9,4 +10,5 @@ export interface LibraryManagementApi {
|
||||
confirmGameMapping(slug: string, confirm: boolean): Observable<DetectedGameDto>;
|
||||
deleteGame(slug: string): Observable<Response>;
|
||||
deleteUnmappedFile(id: number): Observable<Response>;
|
||||
getAutocompleteSuggestions(searchTerm: string, limit: number): Observable<AutocompleteSuggestionDto[]>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {ActivatedRoute, NavigationEnd, Router} from "@angular/router";
|
||||
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',
|
||||
@@ -7,16 +9,16 @@ import {ActivatedRoute, NavigationEnd, Router} from "@angular/router";
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'frontend';
|
||||
mySubscription;
|
||||
|
||||
constructor(private router: Router, private activatedRoute: ActivatedRoute){
|
||||
constructor(private router: Router, private title: Title) {
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
this.mySubscription = this.router.events.subscribe((event) => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ import { UnmappedFilesTableComponent } from './components/unmapped-files-table/u
|
||||
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';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -63,7 +65,9 @@ import {MatAutocompleteModule} from "@angular/material/autocomplete";
|
||||
LibraryManagementComponent,
|
||||
MapGameDialogComponent,
|
||||
MappedGamesTableComponent,
|
||||
UnmappedFilesTableComponent
|
||||
UnmappedFilesTableComponent,
|
||||
NgModelChangeDebouncedDirective,
|
||||
FooterComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<p>© {{date| date:'yyyy'}} grimsi | <a href="{{githubUrl}}" target="_blank">GitHub</a></p>
|
||||
@@ -0,0 +1,9 @@
|
||||
@use 'sass:map';
|
||||
@use '@angular/material' as mat;
|
||||
@import '../../theme/default-theme';
|
||||
|
||||
a {
|
||||
$config: mat.get-color-config($custom-theme);
|
||||
$primary-palette: map.get($config, 'primary');
|
||||
color: mat.get-color-from-palette($primary-palette, 500);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-footer',
|
||||
templateUrl: './footer.component.html',
|
||||
styleUrls: ['./footer.component.scss']
|
||||
})
|
||||
export class FooterComponent implements OnInit {
|
||||
|
||||
githubUrl: string = "https://github.com/grimsi/gameyfin";
|
||||
date: Date = new Date();
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<mat-toolbar style="position: sticky; top: 0; z-index: 99999">
|
||||
<mat-toolbar>
|
||||
<button mat-icon-button matTooltip="Home" (click)="goToLibraryScreen()" *ngIf="!onLibraryScreen()">
|
||||
<mat-icon>home</mat-icon>
|
||||
</button>
|
||||
|
||||
@@ -1,27 +1,3 @@
|
||||
.menu-item-icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 45px;
|
||||
padding-right: 30px;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.drop-down {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.mat-tab-nav-bar, .mat-tab-links, .mat-tab-link {
|
||||
height: 64px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
#username {
|
||||
margin-right: 10px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,12 @@ export class HeaderComponent {
|
||||
|
||||
scanLibrary(): void {
|
||||
this.libraryService.scanLibrary().pipe(timeInterval()).subscribe({
|
||||
next: value => this.snackBar.open(`Library scan completed in ${Math.trunc(value.interval / 1000)} seconds.`, undefined, {duration: 2000}),
|
||||
next: value => {
|
||||
// Refresh the current page "angular style"
|
||||
this.router.navigate([this.router.url]).then(() =>
|
||||
this.snackBar.open(`Library scan completed in ${Math.trunc(value.interval / 1000)} seconds.`, undefined, {duration: 5000})
|
||||
)
|
||||
},
|
||||
error: error => this.snackBar.open(`Error while scanning library: ${error.error.message}`, undefined, {duration: 5000})
|
||||
})
|
||||
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})
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="6px">
|
||||
<mat-icon matTooltip="Search for games by title">search</mat-icon>
|
||||
<mat-form-field fxFlex="80" class="filter-category-content">
|
||||
<input type="text" matInput [matAutocomplete]="auto" [(ngModel)]="searchTerm" (ngModelChange)="filterGames()">
|
||||
<mat-autocomplete #auto="matAutocomplete">
|
||||
<input type="text" matInput [matAutocomplete]="librarySearchAutocomplete" [(ngModel)]="searchTerm" (ngModelChange)="filterGames()">
|
||||
<mat-autocomplete #librarySearchAutocomplete="matAutocomplete">
|
||||
<mat-option *ngFor="let game of games" [value]="game.title">
|
||||
{{game.title}}
|
||||
</mat-option>
|
||||
|
||||
@@ -3,13 +3,28 @@
|
||||
<form fxLayout="column" fxLayoutAlign="space-evenly stretch">
|
||||
|
||||
<p>Path: {{path}}</p>
|
||||
|
||||
<mat-form-field>
|
||||
<input matInput type="text" placeholder="IGDB Slug" [formControl]="newSlugInput" [value]="currentSlug"/>
|
||||
<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)}})
|
||||
</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">Cancel</button>
|
||||
<button mat-raised-button (click)="submit()" [disabled]="newSlugInput?.value?.length < 1" color="primary">OK</button>
|
||||
<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,8 +1,10 @@
|
||||
import {Component, Inject, OnInit} from '@angular/core';
|
||||
import {FormBuilder, FormControl} from "@angular/forms";
|
||||
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',
|
||||
@@ -12,26 +14,71 @@ import {PathToSlugDto} from "../../models/dtos/PathToSlugDto";
|
||||
export class MapGameDialogComponent implements OnInit {
|
||||
|
||||
path: string;
|
||||
currentSlug?: string;
|
||||
newSlugInput: FormControl;
|
||||
slug: string;
|
||||
|
||||
constructor(private fb: FormBuilder,
|
||||
private libraryManagementService: LibraryManagementService,
|
||||
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.currentSlug = data.slug;
|
||||
this.newSlugInput = new FormControl(this.currentSlug);
|
||||
this.slug = data.slug ?? '';
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.loadInitialSuggestions();
|
||||
}
|
||||
|
||||
submit(): void {
|
||||
this.libraryManagementService.mapGame(new PathToSlugDto(this.newSlugInput.value, this.path)).subscribe({
|
||||
this.submitLoading = true;
|
||||
this.libraryManagementService.mapGame(new PathToSlugDto(this.slug, this.path)).subscribe({
|
||||
next: () => this.dialogRef.close(true),
|
||||
error: () => this.dialogRef.close(false)
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,9 @@ export class MappedGamesTableComponent implements AfterViewInit, OnChanges {
|
||||
}
|
||||
|
||||
openCorrectMappingDialog(mappedGame: DetectedGameDto): void {
|
||||
this.dialogService.correctGameMappingDialog(mappedGame);
|
||||
this.dialogService.correctGameMappingDialog(mappedGame).subscribe(gameSuccessfullyMapped => {
|
||||
if (gameSuccessfullyMapped) this.refreshMappedGamesList();
|
||||
})
|
||||
}
|
||||
|
||||
private refreshData(newData: DetectedGameDto[]): void {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export class Config {
|
||||
public static baseTitle = 'Game-Radar';
|
||||
public static baseTitle = 'Gameyfin';
|
||||
public static apiBasePath = '/v1';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { NgModelChangeDebouncedDirective } from './ng-model-change-debounced.directive';
|
||||
|
||||
describe('NgModelChangeDebouncedDirective', () => {
|
||||
it('should create an instance', () => {
|
||||
const directive = new NgModelChangeDebouncedDirective();
|
||||
expect(directive).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -3,16 +3,23 @@ import {Component, OnInit} from '@angular/core';
|
||||
@Component({
|
||||
selector: 'app-navbar-layout',
|
||||
template: `
|
||||
<div fxFlexFill>
|
||||
<app-header></app-header>
|
||||
<div fxLayout="column" fxLayoutAlign="space-around stretch">
|
||||
<div fxFlex>
|
||||
<router-outlet class="hidden-router"></router-outlet>
|
||||
</div>
|
||||
<div class="main-container" fxLayout="column">
|
||||
<div fxFlex="none" style="position: sticky; top: 0; z-index: 99999">
|
||||
<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: []
|
||||
styles: [`
|
||||
.main-container {
|
||||
min-height: 100vh;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class NavbarLayoutComponent implements OnInit {
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export class AutocompleteSuggestionDto {
|
||||
slug!: string;
|
||||
title!: string;
|
||||
releaseDate!: number;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {PathToSlugDto} from "../models/dtos/PathToSlugDto";
|
||||
import {UnmappedFileDto} from "../models/dtos/UnmappedFileDto";
|
||||
import {LibraryManagementApi} from "../api/LibraryManagementApi";
|
||||
import {GamesService} from "./games.service";
|
||||
import {AutocompleteSuggestionDto} from "../models/dtos/AutocompleteSuggestionDto";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -42,4 +43,11 @@ export class LibraryManagementService implements LibraryManagementApi {
|
||||
return this.http.delete<Response>(`${this.apiPath}/delete-unmapped-file/${id}`);
|
||||
}
|
||||
|
||||
getAutocompleteSuggestions(searchTerm: string, limit: number): Observable<AutocompleteSuggestionDto[]> {
|
||||
let queryParams = new HttpParams();
|
||||
queryParams = queryParams.append("searchTerm", searchTerm);
|
||||
queryParams = queryParams.append("limit", limit);
|
||||
|
||||
return this.http.get<AutocompleteSuggestionDto[]>(`${this.apiPath}/autocomplete-suggestions`, {params:queryParams})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user