Finished implementation of frontend functionality.

Styling and bugfixing next
This commit is contained in:
grimsi
2022-07-25 21:17:30 +02:00
parent 57377036c4
commit aa72161990
23 changed files with 146 additions and 152 deletions
@@ -32,22 +32,8 @@ public class SecurityConfiguration {
@Bean @Bean
protected SecurityFilterChain httpSecurity(HttpSecurity http) throws Exception { protected SecurityFilterChain httpSecurity(HttpSecurity http) throws Exception {
// TODO: Try to enable CSRF
http.csrf().disable(); http.csrf().disable();
// Allow GET-Requests on *all* URLs (Frontend will handle 404 and permission)
// except paths under "/v1/library-management"
http.authorizeRequests()
.antMatchers("**").permitAll()
.antMatchers("/v1/library-management").authenticated()
.anyRequest().denyAll();
http.httpBasic(Customizer.withDefaults()); http.httpBasic(Customizer.withDefaults());
http.exceptionHandling()
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
return http.build(); return http.build();
} }
@@ -4,6 +4,7 @@ import de.grimsi.gameyfin.service.DownloadService;
import de.grimsi.gameyfin.service.LibraryService; import de.grimsi.gameyfin.service.LibraryService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
@@ -17,6 +18,7 @@ import java.util.List;
*/ */
@RestController @RestController
@RequestMapping("/v1/library") @RequestMapping("/v1/library")
@PreAuthorize("hasAuthority('ADMIN_API_ACCESS')")
@RequiredArgsConstructor @RequiredArgsConstructor
public class LibraryController { public class LibraryController {
@@ -28,6 +28,11 @@ public class LibraryManagementController {
gameService.deleteGame(slug); gameService.deleteGame(slug);
} }
@DeleteMapping(value = "/delete-unmapped-file/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public void deleteUnmappedFile(@PathVariable Long id) {
gameService.deleteUnmappedFile(id);
}
@GetMapping(value = "/confirm-game/{slug}", produces = MediaType.APPLICATION_JSON_VALUE) @GetMapping(value = "/confirm-game/{slug}", produces = MediaType.APPLICATION_JSON_VALUE)
public DetectedGame confirmMatch(@PathVariable String slug, @RequestParam(required = false, defaultValue = "true") boolean confirm) { public DetectedGame confirmMatch(@PathVariable String slug, @RequestParam(required = false, defaultValue = "true") boolean confirm) {
return gameService.confirmGame(slug, confirm); return gameService.confirmGame(slug, confirm);
@@ -6,8 +6,8 @@ import de.grimsi.gameyfin.entities.DetectedGame;
import de.grimsi.gameyfin.entities.UnmappableFile; import de.grimsi.gameyfin.entities.UnmappableFile;
import de.grimsi.gameyfin.igdb.IgdbWrapper; import de.grimsi.gameyfin.igdb.IgdbWrapper;
import de.grimsi.gameyfin.mapper.GameMapper; import de.grimsi.gameyfin.mapper.GameMapper;
import de.grimsi.gameyfin.repositories.UnmappableFileRepository;
import de.grimsi.gameyfin.repositories.DetectedGameRepository; import de.grimsi.gameyfin.repositories.DetectedGameRepository;
import de.grimsi.gameyfin.repositories.UnmappableFileRepository;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -52,9 +52,19 @@ public class GameService {
} }
public void deleteGame(String slug) { public void deleteGame(String slug) {
DetectedGame gameToBeDeleted = getDetectedGame(slug);
// Add the path of the game to be deleted to the unmappable files
// so it doesn't get re-indexed on the next library scan
unmappableFileRepository.save(new UnmappableFile(gameToBeDeleted.getPath()));
detectedGameRepository.deleteById(slug); detectedGameRepository.deleteById(slug);
} }
public void deleteUnmappedFile(Long id) {
unmappableFileRepository.deleteById(id);
}
public DetectedGame confirmGame(String slug, boolean confirm) { public DetectedGame confirmGame(String slug, boolean confirm) {
DetectedGame g = getDetectedGame(slug); DetectedGame g = getDetectedGame(slug);
g.setConfirmedMatch(confirm); g.setConfirmedMatch(confirm);
@@ -63,17 +73,17 @@ public class GameService {
public DetectedGame mapPathToGame(String path, String slug) { public DetectedGame mapPathToGame(String path, String slug) {
if(detectedGameRepository.existsBySlug(slug)) if (detectedGameRepository.existsBySlug(slug))
throw new ResponseStatusException(HttpStatus.CONFLICT, "Game with slug '%s' already exists in database.".formatted(slug)); throw new ResponseStatusException(HttpStatus.CONFLICT, "Game with slug '%s' already exists in database.".formatted(slug));
Optional<UnmappableFile> optionalUnmappableFile = unmappableFileRepository.findByPath(path); Optional<UnmappableFile> optionalUnmappableFile = unmappableFileRepository.findByPath(path);
Optional<DetectedGame> optionalDetectedGame = detectedGameRepository.findByPath(path); Optional<DetectedGame> optionalDetectedGame = detectedGameRepository.findByPath(path);
if(optionalUnmappableFile.isPresent()) { if (optionalUnmappableFile.isPresent()) {
return mapUnmappableFile(optionalUnmappableFile.get(), slug); return mapUnmappableFile(optionalUnmappableFile.get(), slug);
} }
if(optionalDetectedGame.isPresent()) { if (optionalDetectedGame.isPresent()) {
return mapDetectedGame(optionalDetectedGame.get(), slug); return mapDetectedGame(optionalDetectedGame.get(), slug);
} }
@@ -18,6 +18,11 @@ public class FilenameUtil {
} }
public static String getFilenameWithoutExtension(Path p) { public static String getFilenameWithoutExtension(Path p) {
// If the path points to a folder, return the folder name
// Folders like "Counter Strike 1.6" would otherwise be returned as "Counter Strike 1"
if(p.toFile().isDirectory()) return FilenameUtils.getName(p.toString());
return FilenameUtils.getBaseName(p.toString()); return FilenameUtils.getBaseName(p.toString());
} }
@@ -1,8 +1,8 @@
gameyfin: gameyfin:
user: admin user: admin
password: 112 password: 112
root: C:\Projects\privat\gameyfin-library #root: C:\Projects\privat\gameyfin-library
#root: \\NAS-Simon\Öffentlich\Spiele root: \\NAS-Simon\Öffentlich\Spiele
cache: ${gameyfin.root}\.gameyfin\cache cache: ${gameyfin.root}\.gameyfin\cache
db: ${gameyfin.root}\.gameyfin\db db: ${gameyfin.root}\.gameyfin\db
#db: ./data #db: ./data
@@ -1,14 +0,0 @@
server:
error.include-stacktrace: never
spring:
mvc:
async.request-timeout: -1
jackson.default-property-inclusion: non_null
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate.ddl-auto: update
open-in-view: true
properties:
hibernate:
event.merge.entity_copy_observer: allow
-2
View File
@@ -1,6 +1,4 @@
import {Observable} from "rxjs"; import {Observable} from "rxjs";
import {DetectedGameDto} from "../models/dtos/DetectedGameDto";
import {GameOverviewDto} from "../models/dtos/GameOverviewDto";
import {HttpResponse} from "@angular/common/http"; import {HttpResponse} from "@angular/common/http";
export interface LibraryApi { export interface LibraryApi {
@@ -0,0 +1,12 @@
import {PathToSlugDto} from "../models/dtos/PathToSlugDto";
import {Observable} from "rxjs";
import {DetectedGameDto} from "../models/dtos/DetectedGameDto";
import {UnmappedFileDto} from "../models/dtos/UnmappedFileDto";
export interface LibraryManagementApi {
mapGame(pathToSlugDto: PathToSlugDto): Observable<DetectedGameDto>;
getUnmappedFiles(): Observable<UnmappedFileDto[]>;
confirmGameMapping(slug: string): Observable<DetectedGameDto>;
deleteGame(slug: string): Observable<Response>;
deleteUnmappedFile(id: number): Observable<Response>;
}
+4 -12
View File
@@ -1,11 +1,10 @@
import {NgModule} from '@angular/core'; import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router'; import {RouterModule, Routes} from '@angular/router';
import {FullpageLayoutComponent} from "./layouts/fullpage-layout/fullpage-layout.component";
import {PageNotFoundComponent} from "./components/page-not-found/page-not-found.component"; import {PageNotFoundComponent} from "./components/page-not-found/page-not-found.component";
import {NavbarLayoutComponent} from "./layouts/navbar-layout/navbar-layout.component"; import {NavbarLayoutComponent} from "./layouts/navbar-layout/navbar-layout.component";
import {NotImplementedComponent} from "./components/not-implemented/not-implemented.component";
import {LibraryOverviewComponent} from "./components/library-overview/library-overview.component"; import {LibraryOverviewComponent} from "./components/library-overview/library-overview.component";
import {GameDetailViewComponent} from "./components/game-detail-view/game-detail-view.component"; import {GameDetailViewComponent} from "./components/game-detail-view/game-detail-view.component";
import {LibraryManagementComponent} from "./components/library-management/library-management.component";
const appRoutes: Routes = [ const appRoutes: Routes = [
{ {
@@ -21,19 +20,12 @@ const appRoutes: Routes = [
component: GameDetailViewComponent component: GameDetailViewComponent
}, },
{ {
path: '', path: 'library-management',
redirectTo: '/library', component: LibraryManagementComponent
pathMatch: 'full'
}
]
}, },
{ {
path: '', path: '',
component: FullpageLayoutComponent, redirectTo: '/library',
children: [
{
path: '',
redirectTo: '/login',
pathMatch: 'full' pathMatch: 'full'
} }
] ]
+9 -7
View File
@@ -2,10 +2,8 @@ import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser'; import {BrowserModule} from '@angular/platform-browser';
import {AppComponent} from './app.component'; import {AppComponent} from './app.component';
import {FullpageLayoutComponent} from "./layouts/fullpage-layout/fullpage-layout.component";
import {NavbarLayoutComponent} from "./layouts/navbar-layout/navbar-layout.component"; import {NavbarLayoutComponent} from "./layouts/navbar-layout/navbar-layout.component";
import {PageNotFoundComponent} from "./components/page-not-found/page-not-found.component"; import {PageNotFoundComponent} from "./components/page-not-found/page-not-found.component";
import {NotImplementedComponent} from "./components/not-implemented/not-implemented.component";
import {HeaderComponent} from "./components/header/header.component"; import {HeaderComponent} from "./components/header/header.component";
import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {FormsModule, ReactiveFormsModule} from "@angular/forms"; import {FormsModule, ReactiveFormsModule} from "@angular/forms";
@@ -35,23 +33,26 @@ import {MatSnackBarModule} from '@angular/material/snack-bar';
import {MatGridListModule} from "@angular/material/grid-list"; import {MatGridListModule} from "@angular/material/grid-list";
import {GameScreenshotComponent} from './components/game-screenshot/game-screenshot.component'; import {GameScreenshotComponent} from './components/game-screenshot/game-screenshot.component';
import {YouTubePlayerModule} from "@angular/youtube-player"; import {YouTubePlayerModule} from "@angular/youtube-player";
import { GameVideoComponent } from './components/game-video/game-video.component'; import {GameVideoComponent} from './components/game-video/game-video.component';
import {MatChipsModule} from "@angular/material/chips"; 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";
@NgModule({ @NgModule({
declarations: [ declarations: [
AppComponent, AppComponent,
HeaderComponent, HeaderComponent,
FullpageLayoutComponent,
NavbarLayoutComponent, NavbarLayoutComponent,
PageNotFoundComponent, PageNotFoundComponent,
NotImplementedComponent,
ErrorDialogComponent, ErrorDialogComponent,
LibraryOverviewComponent, LibraryOverviewComponent,
GameCoverComponent, GameCoverComponent,
GameDetailViewComponent, GameDetailViewComponent,
GameScreenshotComponent, GameScreenshotComponent,
GameVideoComponent GameVideoComponent,
LibraryManagementComponent,
MapGameDialogComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@@ -80,7 +81,8 @@ import {MatChipsModule} from "@angular/material/chips";
FlexLayoutModule, FlexLayoutModule,
GridModule, GridModule,
YouTubePlayerModule, YouTubePlayerModule,
MatChipsModule MatChipsModule,
MatTooltipModule
], ],
providers: [ providers: [
{ {
@@ -5,7 +5,11 @@
<span class="spacer"></span> <span class="spacer"></span>
<button mat-icon-button (click)="reloadLibrary()"> <button mat-icon-button (click)="reloadLibrary()" *ngIf="onLibraryManagementScreen()">
<mat-icon>youtube_searched_for</mat-icon> <mat-icon>youtube_searched_for</mat-icon>
</button> </button>
<button mat-icon-button (click)="goToLibraryManagementScreen()">
<mat-icon>settings</mat-icon>
</button>
</mat-toolbar> </mat-toolbar>
@@ -19,7 +19,7 @@ export class HeaderComponent {
reloadLibrary(): void { reloadLibrary(): void {
this.libraryService.scanLibrary().pipe(timeInterval()).subscribe({ 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 => this.snackBar.open(`Library scan completed in ${Math.trunc(value.interval / 1000)} seconds.`, undefined, {duration: 2000}),
error: error => this.snackBar.open(`Error while scanning library: ${error}`, undefined, {duration: 5000}) error: error => this.snackBar.open(`Error while scanning library: ${error.error.message}`, undefined, {duration: 5000})
}) })
} }
@@ -27,8 +27,16 @@ export class HeaderComponent {
this.router.navigate(['/']); this.router.navigate(['/']);
} }
goToLibraryManagementScreen(): void {
this.router.navigate(['/library-management']);
}
notOnLibraryScreen(): boolean { notOnLibraryScreen(): boolean {
return !(this.router.url === "/library"); return !(this.router.url === "/library");
} }
onLibraryManagementScreen(): boolean {
return this.router.url === "/library-management";
}
} }
@@ -1 +0,0 @@
<h2 fxLayoutAlign="center center">This component is currently not implemented.</h2>
@@ -1,25 +0,0 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { NotImplementedComponent } from './not-implemented.component';
describe('NotImplementedComponent', () => {
let component: NotImplementedComponent;
let fixture: ComponentFixture<NotImplementedComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ NotImplementedComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(NotImplementedComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,15 +0,0 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-not-implemented',
templateUrl: './not-implemented.component.html',
styleUrls: ['./not-implemented.component.scss']
})
export class NotImplementedComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}
@@ -1,22 +0,0 @@
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-fullpage-layout',
template: `
<div fxLayout="column" fxFlexFill>
<div fxFlex>
<router-outlet class="hidden-router"></router-outlet>
</div>
</div>
`,
styles: []
})
export class FullpageLayoutComponent implements OnInit {
constructor() {
}
ngOnInit() {
}
}
@@ -0,0 +1,11 @@
import {FormControl} from "@angular/forms";
export class PathToSlugDto {
slug: string;
path: string;
constructor(slug: string, path: string) {
this.slug = slug;
this.path = path;
}
}
@@ -0,0 +1,4 @@
export class UnmappedFileDto {
id!: number;
path!: string;
}
@@ -1,6 +1,9 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {MatDialog, MatDialogConfig} from '@angular/material/dialog'; import {MatDialog, MatDialogConfig} from '@angular/material/dialog';
import {ErrorDialogComponent} from '../components/error-dialog/error-dialog.component'; import {ErrorDialogComponent} from '../components/error-dialog/error-dialog.component';
import {DetectedGameDto} from "../models/dtos/DetectedGameDto";
import {MapGameDialogComponent} from "../components/map-game-dialog/map-game-dialog.component";
import {UnmappedFileDto} from "../models/dtos/UnmappedFileDto";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@@ -24,4 +27,33 @@ export class DialogService {
this.dialog.open(ErrorDialogComponent, dialogConfig); this.dialog.open(ErrorDialogComponent, dialogConfig);
} }
public correctGameMappingDialog(game: DetectedGameDto): void {
const dialogConfig = new MatDialogConfig();
dialogConfig.disableClose = true;
dialogConfig.autoFocus = true;
dialogConfig.closeOnNavigation = true;
dialogConfig.data = {
path: game.path,
slug: game.slug
};
this.dialog.open(MapGameDialogComponent, dialogConfig);
}
public mapUnmappedGameDialog(unmappedFile: UnmappedFileDto): void {
const dialogConfig = new MatDialogConfig();
dialogConfig.disableClose = true;
dialogConfig.autoFocus = true;
dialogConfig.closeOnNavigation = true;
dialogConfig.data = {
path: unmappedFile.path
};
this.dialog.open(MapGameDialogComponent, dialogConfig);
}
} }
+2 -2
View File
@@ -1,7 +1,7 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {GamesApi} from "../api/GamesApi"; import {GamesApi} from "../api/GamesApi";
import {HttpClient} from "@angular/common/http"; import {HttpClient} from "@angular/common/http";
import {Observable} from "rxjs"; import {map, Observable} from "rxjs";
import {DetectedGameDto} from "../models/dtos/DetectedGameDto"; import {DetectedGameDto} from "../models/dtos/DetectedGameDto";
import {GameOverviewDto} from "../models/dtos/GameOverviewDto"; import {GameOverviewDto} from "../models/dtos/GameOverviewDto";
@@ -16,7 +16,7 @@ export class GamesService implements GamesApi {
} }
getAllGames(): Observable<DetectedGameDto[]> { getAllGames(): Observable<DetectedGameDto[]> {
return this.http.get<DetectedGameDto[]>(this.apiPath); return this.http.get<DetectedGameDto[]>(this.apiPath).pipe(map(games => games.sort((g1, g2) => g1.title.localeCompare(g2.title))));
} }
getGame(slug: String): Observable<DetectedGameDto> { getGame(slug: String): Observable<DetectedGameDto> {