From aa72161990627842745a09d61b2440d2593cf51f Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Mon, 25 Jul 2022 21:17:30 +0200 Subject: [PATCH] Finished implementation of frontend functionality. Styling and bugfixing next --- .../config/SecurityConfiguration.java | 14 ---- .../gameyfin/rest/LibraryController.java | 2 + .../rest/LibraryManagementController.java | 5 ++ .../grimsi/gameyfin/service/GameService.java | 20 ++++-- .../de/grimsi/gameyfin/util/FilenameUtil.java | 5 ++ .../src/main/resources/application-dev.yml | 4 +- backend/src/main/resources/config/secure.yml | 14 ---- frontend/src/app/api/LibraryApi.ts | 2 - frontend/src/app/api/LibraryManagementApi.ts | 12 ++++ frontend/src/app/app-routing.module.ts | 18 ++--- frontend/src/app/app.module.ts | 72 ++++++++++--------- .../components/header/header.component.html | 6 +- .../app/components/header/header.component.ts | 10 ++- .../not-implemented.component.html | 1 - .../not-implemented.component.scss | 0 .../not-implemented.component.spec.ts | 25 ------- .../not-implemented.component.ts | 15 ---- .../fullpage-layout.component.spec.ts | 0 .../fullpage-layout.component.ts | 22 ------ frontend/src/app/models/dtos/PathToSlugDto.ts | 11 +++ .../src/app/models/dtos/UnmappedFileDto.ts | 4 ++ frontend/src/app/services/dialog.service.ts | 32 +++++++++ frontend/src/app/services/games.service.ts | 4 +- 23 files changed, 146 insertions(+), 152 deletions(-) delete mode 100644 backend/src/main/resources/config/secure.yml create mode 100644 frontend/src/app/api/LibraryManagementApi.ts delete mode 100644 frontend/src/app/components/not-implemented/not-implemented.component.html delete mode 100644 frontend/src/app/components/not-implemented/not-implemented.component.scss delete mode 100644 frontend/src/app/components/not-implemented/not-implemented.component.spec.ts delete mode 100644 frontend/src/app/components/not-implemented/not-implemented.component.ts delete mode 100644 frontend/src/app/layouts/fullpage-layout/fullpage-layout.component.spec.ts delete mode 100644 frontend/src/app/layouts/fullpage-layout/fullpage-layout.component.ts create mode 100644 frontend/src/app/models/dtos/PathToSlugDto.ts create mode 100644 frontend/src/app/models/dtos/UnmappedFileDto.ts diff --git a/backend/src/main/java/de/grimsi/gameyfin/config/SecurityConfiguration.java b/backend/src/main/java/de/grimsi/gameyfin/config/SecurityConfiguration.java index 69a99b5..e16a3e4 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/config/SecurityConfiguration.java +++ b/backend/src/main/java/de/grimsi/gameyfin/config/SecurityConfiguration.java @@ -32,22 +32,8 @@ public class SecurityConfiguration { @Bean protected SecurityFilterChain httpSecurity(HttpSecurity http) throws Exception { - - // TODO: Try to enable CSRF 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.exceptionHandling() - .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)); - return http.build(); } diff --git a/backend/src/main/java/de/grimsi/gameyfin/rest/LibraryController.java b/backend/src/main/java/de/grimsi/gameyfin/rest/LibraryController.java index 7df7eaf..46b2081 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/rest/LibraryController.java +++ b/backend/src/main/java/de/grimsi/gameyfin/rest/LibraryController.java @@ -4,6 +4,7 @@ import de.grimsi.gameyfin.service.DownloadService; import de.grimsi.gameyfin.service.LibraryService; import lombok.RequiredArgsConstructor; 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.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -17,6 +18,7 @@ import java.util.List; */ @RestController @RequestMapping("/v1/library") +@PreAuthorize("hasAuthority('ADMIN_API_ACCESS')") @RequiredArgsConstructor public class LibraryController { diff --git a/backend/src/main/java/de/grimsi/gameyfin/rest/LibraryManagementController.java b/backend/src/main/java/de/grimsi/gameyfin/rest/LibraryManagementController.java index b55a125..1f1964b 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/rest/LibraryManagementController.java +++ b/backend/src/main/java/de/grimsi/gameyfin/rest/LibraryManagementController.java @@ -28,6 +28,11 @@ public class LibraryManagementController { 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) public DetectedGame confirmMatch(@PathVariable String slug, @RequestParam(required = false, defaultValue = "true") boolean confirm) { return gameService.confirmGame(slug, confirm); diff --git a/backend/src/main/java/de/grimsi/gameyfin/service/GameService.java b/backend/src/main/java/de/grimsi/gameyfin/service/GameService.java index 36f2537..2e2f338 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/service/GameService.java +++ b/backend/src/main/java/de/grimsi/gameyfin/service/GameService.java @@ -6,8 +6,8 @@ import de.grimsi.gameyfin.entities.DetectedGame; import de.grimsi.gameyfin.entities.UnmappableFile; import de.grimsi.gameyfin.igdb.IgdbWrapper; import de.grimsi.gameyfin.mapper.GameMapper; -import de.grimsi.gameyfin.repositories.UnmappableFileRepository; import de.grimsi.gameyfin.repositories.DetectedGameRepository; +import de.grimsi.gameyfin.repositories.UnmappableFileRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; @@ -50,11 +50,21 @@ public class GameService { public List getGameOverviews() { return detectedGameRepository.findAll().stream().map(GameMapper::toGameOverviewDto).toList(); } - + 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); } + public void deleteUnmappedFile(Long id) { + unmappableFileRepository.deleteById(id); + } + public DetectedGame confirmGame(String slug, boolean confirm) { DetectedGame g = getDetectedGame(slug); g.setConfirmedMatch(confirm); @@ -63,17 +73,17 @@ public class GameService { 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)); Optional optionalUnmappableFile = unmappableFileRepository.findByPath(path); Optional optionalDetectedGame = detectedGameRepository.findByPath(path); - if(optionalUnmappableFile.isPresent()) { + if (optionalUnmappableFile.isPresent()) { return mapUnmappableFile(optionalUnmappableFile.get(), slug); } - if(optionalDetectedGame.isPresent()) { + if (optionalDetectedGame.isPresent()) { return mapDetectedGame(optionalDetectedGame.get(), slug); } diff --git a/backend/src/main/java/de/grimsi/gameyfin/util/FilenameUtil.java b/backend/src/main/java/de/grimsi/gameyfin/util/FilenameUtil.java index fda1eae..2c0b8b9 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/util/FilenameUtil.java +++ b/backend/src/main/java/de/grimsi/gameyfin/util/FilenameUtil.java @@ -18,6 +18,11 @@ public class FilenameUtil { } 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()); } diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml index ec7b922..e7120b7 100644 --- a/backend/src/main/resources/application-dev.yml +++ b/backend/src/main/resources/application-dev.yml @@ -1,8 +1,8 @@ gameyfin: user: admin password: 112 - root: C:\Projects\privat\gameyfin-library - #root: \\NAS-Simon\Öffentlich\Spiele + #root: C:\Projects\privat\gameyfin-library + root: \\NAS-Simon\Öffentlich\Spiele cache: ${gameyfin.root}\.gameyfin\cache db: ${gameyfin.root}\.gameyfin\db #db: ./data diff --git a/backend/src/main/resources/config/secure.yml b/backend/src/main/resources/config/secure.yml deleted file mode 100644 index 4445e02..0000000 --- a/backend/src/main/resources/config/secure.yml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/frontend/src/app/api/LibraryApi.ts b/frontend/src/app/api/LibraryApi.ts index d31a42b..fb8df25 100644 --- a/frontend/src/app/api/LibraryApi.ts +++ b/frontend/src/app/api/LibraryApi.ts @@ -1,6 +1,4 @@ import {Observable} from "rxjs"; -import {DetectedGameDto} from "../models/dtos/DetectedGameDto"; -import {GameOverviewDto} from "../models/dtos/GameOverviewDto"; import {HttpResponse} from "@angular/common/http"; export interface LibraryApi { diff --git a/frontend/src/app/api/LibraryManagementApi.ts b/frontend/src/app/api/LibraryManagementApi.ts new file mode 100644 index 0000000..2b3d0ec --- /dev/null +++ b/frontend/src/app/api/LibraryManagementApi.ts @@ -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; + getUnmappedFiles(): Observable; + confirmGameMapping(slug: string): Observable; + deleteGame(slug: string): Observable; + deleteUnmappedFile(id: number): Observable; +} diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 922d108..d6f3d00 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -1,11 +1,10 @@ import {NgModule} from '@angular/core'; 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 {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 {GameDetailViewComponent} from "./components/game-detail-view/game-detail-view.component"; +import {LibraryManagementComponent} from "./components/library-management/library-management.component"; const appRoutes: Routes = [ { @@ -21,19 +20,12 @@ const appRoutes: Routes = [ component: GameDetailViewComponent }, { - path: '', - redirectTo: '/library', - pathMatch: 'full' - } - ] - }, - { - path: '', - component: FullpageLayoutComponent, - children: [ + path: 'library-management', + component: LibraryManagementComponent + }, { path: '', - redirectTo: '/login', + redirectTo: '/library', pathMatch: 'full' } ] diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index e3470fe..5e9fac7 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -2,10 +2,8 @@ import {NgModule} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; import {AppComponent} from './app.component'; -import {FullpageLayoutComponent} from "./layouts/fullpage-layout/fullpage-layout.component"; import {NavbarLayoutComponent} from "./layouts/navbar-layout/navbar-layout.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 {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {FormsModule, ReactiveFormsModule} from "@angular/forms"; @@ -35,53 +33,57 @@ import {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 {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"; @NgModule({ declarations: [ AppComponent, HeaderComponent, - FullpageLayoutComponent, NavbarLayoutComponent, PageNotFoundComponent, - NotImplementedComponent, ErrorDialogComponent, LibraryOverviewComponent, GameCoverComponent, GameDetailViewComponent, GameScreenshotComponent, - GameVideoComponent + GameVideoComponent, + LibraryManagementComponent, + MapGameDialogComponent + ], + 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 ], - 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 - ], providers: [ { provide: HTTP_INTERCEPTORS, diff --git a/frontend/src/app/components/header/header.component.html b/frontend/src/app/components/header/header.component.html index 3cd291b..379e01c 100644 --- a/frontend/src/app/components/header/header.component.html +++ b/frontend/src/app/components/header/header.component.html @@ -5,7 +5,11 @@ - + + diff --git a/frontend/src/app/components/header/header.component.ts b/frontend/src/app/components/header/header.component.ts index 9f19dcd..cf0854c 100644 --- a/frontend/src/app/components/header/header.component.ts +++ b/frontend/src/app/components/header/header.component.ts @@ -19,7 +19,7 @@ export class HeaderComponent { reloadLibrary(): 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}), - 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(['/']); } + goToLibraryManagementScreen(): void { + this.router.navigate(['/library-management']); + } + notOnLibraryScreen(): boolean { return !(this.router.url === "/library"); } + onLibraryManagementScreen(): boolean { + return this.router.url === "/library-management"; + } + } diff --git a/frontend/src/app/components/not-implemented/not-implemented.component.html b/frontend/src/app/components/not-implemented/not-implemented.component.html deleted file mode 100644 index e279cdb..0000000 --- a/frontend/src/app/components/not-implemented/not-implemented.component.html +++ /dev/null @@ -1 +0,0 @@ -

This component is currently not implemented.

diff --git a/frontend/src/app/components/not-implemented/not-implemented.component.scss b/frontend/src/app/components/not-implemented/not-implemented.component.scss deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/src/app/components/not-implemented/not-implemented.component.spec.ts b/frontend/src/app/components/not-implemented/not-implemented.component.spec.ts deleted file mode 100644 index f93563a..0000000 --- a/frontend/src/app/components/not-implemented/not-implemented.component.spec.ts +++ /dev/null @@ -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; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [ NotImplementedComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(NotImplementedComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/frontend/src/app/components/not-implemented/not-implemented.component.ts b/frontend/src/app/components/not-implemented/not-implemented.component.ts deleted file mode 100644 index 78fe135..0000000 --- a/frontend/src/app/components/not-implemented/not-implemented.component.ts +++ /dev/null @@ -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() { - } - -} diff --git a/frontend/src/app/layouts/fullpage-layout/fullpage-layout.component.spec.ts b/frontend/src/app/layouts/fullpage-layout/fullpage-layout.component.spec.ts deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/src/app/layouts/fullpage-layout/fullpage-layout.component.ts b/frontend/src/app/layouts/fullpage-layout/fullpage-layout.component.ts deleted file mode 100644 index 74584e4..0000000 --- a/frontend/src/app/layouts/fullpage-layout/fullpage-layout.component.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {Component, OnInit} from '@angular/core'; - -@Component({ - selector: 'app-fullpage-layout', - template: ` -
-
- -
-
- `, - styles: [] -}) -export class FullpageLayoutComponent implements OnInit { - - constructor() { - } - - ngOnInit() { - } - -} diff --git a/frontend/src/app/models/dtos/PathToSlugDto.ts b/frontend/src/app/models/dtos/PathToSlugDto.ts new file mode 100644 index 0000000..9783d4c --- /dev/null +++ b/frontend/src/app/models/dtos/PathToSlugDto.ts @@ -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; + } +} diff --git a/frontend/src/app/models/dtos/UnmappedFileDto.ts b/frontend/src/app/models/dtos/UnmappedFileDto.ts new file mode 100644 index 0000000..6350d42 --- /dev/null +++ b/frontend/src/app/models/dtos/UnmappedFileDto.ts @@ -0,0 +1,4 @@ +export class UnmappedFileDto { + id!: number; + path!: string; +} diff --git a/frontend/src/app/services/dialog.service.ts b/frontend/src/app/services/dialog.service.ts index 071b449..0428514 100644 --- a/frontend/src/app/services/dialog.service.ts +++ b/frontend/src/app/services/dialog.service.ts @@ -1,6 +1,9 @@ import {Injectable} from '@angular/core'; import {MatDialog, MatDialogConfig} from '@angular/material/dialog'; 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({ providedIn: 'root' @@ -24,4 +27,33 @@ export class DialogService { 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); + } + } diff --git a/frontend/src/app/services/games.service.ts b/frontend/src/app/services/games.service.ts index 727418d..9a82f53 100644 --- a/frontend/src/app/services/games.service.ts +++ b/frontend/src/app/services/games.service.ts @@ -1,7 +1,7 @@ import {Injectable} from '@angular/core'; import {GamesApi} from "../api/GamesApi"; import {HttpClient} from "@angular/common/http"; -import {Observable} from "rxjs"; +import {map, Observable} from "rxjs"; import {DetectedGameDto} from "../models/dtos/DetectedGameDto"; import {GameOverviewDto} from "../models/dtos/GameOverviewDto"; @@ -16,7 +16,7 @@ export class GamesService implements GamesApi { } getAllGames(): Observable { - return this.http.get(this.apiPath); + return this.http.get(this.apiPath).pipe(map(games => games.sort((g1, g2) => g1.title.localeCompare(g2.title)))); } getGame(slug: String): Observable {