Merge pull request #16 from Kareadita/feature/scan-library

Library, Series, Volumes, oh my!
This commit is contained in:
Joseph Milazzo 2021-01-02 18:51:29 -06:00 committed by GitHub
commit 7642ffa912
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 293 additions and 19 deletions

View file

@ -16,7 +16,7 @@ export class AdminGuard implements CanActivate {
// this automaticallys subs due to being router guard
return this.accountService.currentUser$.pipe(
map((user: User) => {
if (user && user.roles.includes('Admin')) {
if (this.accountService.hasAdminRole(user)) {
return true;
}

View file

@ -1,4 +1,5 @@
export interface Library {
id: number;
name: string;
coverImage: string;
type: any;

11
src/app/_models/series.ts Normal file
View file

@ -0,0 +1,11 @@
import { Volume } from './volume';
export interface Series {
id: number;
name: string;
originalName: string;
sortName: string;
summary: string;
coverImage: string;
volumes: Volume[];
}

View file

@ -0,0 +1,6 @@
export interface Volume {
id: number;
number: string;
files: Array<string>;
coverImage: string;
}

View file

@ -21,6 +21,10 @@ export class AccountService {
constructor(private httpClient: HttpClient) {
}
hasAdminRole(user: User) {
return user && user.roles.includes('Admin');
}
login(model: any): Observable<any> {
return this.httpClient.post<User>(this.baseUrl + 'account/login', model).pipe(
map((response: User) => {

View file

@ -26,12 +26,13 @@ export class LibraryService {
}
getLibrariesForMember(username: string) {
return this.httpClient.get<Library[]>(this.baseUrl + 'library/' + username);
return this.httpClient.get<Library[]>(this.baseUrl + 'library/libraries-for?username=' + username);
}
updateLibrariesForMember(username: string, selectedLibraries: Library[]) {
return this.httpClient.post(this.baseUrl + '/library/update-for', {username, selectedLibraries});
return this.httpClient.post(this.baseUrl + 'library/update-for', {username, selectedLibraries});
}
}

View file

@ -0,0 +1,27 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from 'src/environments/environment';
import { Series } from '../_models/series';
import { Volume } from '../_models/volume';
@Injectable({
providedIn: 'root'
})
export class SeriesService {
baseUrl = environment.apiUrl;
constructor(private httpClient: HttpClient) { }
getSeriesForLibrary(libraryId: number) {
return this.httpClient.get<Series[]>(this.baseUrl + 'library/series?libraryId=' + libraryId);
}
getSeries(seriesId: number) {
return this.httpClient.get<Series>(this.baseUrl + 'series/' + seriesId);
}
getVolumes(seriesId: number) {
return this.httpClient.get<Volume[]>(this.baseUrl + 'series/volumes?seriesId=' + seriesId);
}
}

View file

@ -13,7 +13,6 @@
{{member.username | titlecase}} <span *ngIf="member.isAdmin" class="badge badge-pill badge-secondary">Admin</span>
<div class="pull-right" *ngIf="canEditMember(member)">
<button class="btn btn-danger mr-2" (click)="deleteUser(member)"><i class="fa fa-trash" title="Delete {{member.username | titlecase}}"></i></button>
<!-- <button class="btn btn-primary" (click)="openChangeRole(member)"><i class="fa fa-pencil"></i>{{member.roles.includes('Admin') ? 'Demote' : 'Promote'}}</button> -->
<button class="btn btn-primary" (click)="openEditLibraryAccess(member)"><i class="fa fa-pencil" title="Edit {{member.username | titlecase}}"></i></button>
</div>
</h4>

View file

@ -61,11 +61,8 @@ export class ManageUsersComponent implements OnInit {
});
}
openChangeRole(member: Member) {
}
deleteUser(member: Member) {
// TODO: Use a modal for this confirm
if (confirm('Are you sure you want to delete this user?')) {
this.memberService.deleteMember(member.username).subscribe(() => {
this.loadMembers();

View file

@ -1,8 +1,9 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { LibraryDetailComponent } from './library-detail/library-detail.component';
import { LibraryComponent } from './library/library.component';
import { AdminGuard } from './_guards/admin.guard';
import { SeriesDetailComponent } from './series-detail/series-detail.component';
const routes: Routes = [
{path: '', component: HomeComponent},
@ -11,7 +12,8 @@ const routes: Routes = [
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
},
{path: 'library', component: LibraryComponent},
{path: 'home', component: HomeComponent},
{path: 'library/:id', component: LibraryDetailComponent}, // NOTE: Should I put a guard up to prevent unauthorized access to libraries and series?
{path: 'series/:id', component: SeriesDetailComponent},
{path: '**', component: HomeComponent, pathMatch: 'full'}
];

View file

@ -15,6 +15,8 @@ import { ToastrModule } from 'ngx-toastr';
import { ErrorInterceptor } from './_interceptors/error.interceptor';
import { LibraryComponent } from './library/library.component';
import { SharedModule } from './shared/shared.module';
import { LibraryDetailComponent } from './library-detail/library-detail.component';
import { SeriesDetailComponent } from './series-detail/series-detail.component';
@ -25,6 +27,8 @@ import { SharedModule } from './shared/shared.module';
NavHeaderComponent,
UserLoginComponent,
LibraryComponent,
LibraryDetailComponent,
SeriesDetailComponent,
],
imports: [
HttpClientModule,

View file

@ -0,0 +1,12 @@
<div class="container">
<h2>Title (Manga/Recently Added)</h2>
<div class="row">
<div class="col-md-2" *ngFor="let manga of series">
<app-card-item [title]="manga.name" (clicked)="seriesClicked(manga)"></app-card-item>
</div>
</div>
<ng-container *ngIf="series.length === 0">
<!-- Put a cricket here -->
Nothing here....
</ng-container>
</div>

View file

@ -0,0 +1,4 @@
.card {
height: 400;
width: 200;
}

View file

@ -0,0 +1,38 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Library } from '../_models/library';
import { Series } from '../_models/series';
import { SeriesService } from '../_services/series.service';
@Component({
selector: 'app-library-detail',
templateUrl: './library-detail.component.html',
styleUrls: ['./library-detail.component.scss']
})
export class LibraryDetailComponent implements OnInit {
libraryId!: number;
series: Series[] = [];
constructor(private route: ActivatedRoute, private router: Router, private seriesService: SeriesService) {
const routeId = this.route.snapshot.paramMap.get('id');
if (routeId === null) {
console.error('No library id was passed. Redirecting to home');
this.router.navigateByUrl('/home');
return;
}
this.libraryId = parseInt(routeId, 10);
this.seriesService.getSeriesForLibrary(this.libraryId).subscribe(series => {
this.series = series;
});
}
ngOnInit(): void {
}
seriesClicked(series: Series) {
this.router.navigateByUrl('/series/' + series.id);
}
}

View file

@ -1 +1,7 @@
<p>library works!</p>
<h2>Libraries</h2>
<div class="row">
<div class="col-sm-6 col-md-4 col-lg-2" *ngFor="let library of libraries">
<app-card-item [imageUrl]="library.coverImage" [title]="library.name" (clicked)="handleNavigation($event, library)" [actions]="actions" [entity]="library"></app-card-item>
</div>
</div>

View file

@ -1,5 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { take } from 'rxjs/operators';
import { CardItemAction } from '../shared/card-item/card-item.component';
import { Library } from '../_models/library';
import { User } from '../_models/user';
import { AccountService } from '../_services/account.service';
@ -15,16 +17,29 @@ export class LibraryComponent implements OnInit {
user: User | undefined;
libraries: Library[] = [];
actions: CardItemAction[] = [];
constructor(public accountService: AccountService, private memberService: MemberService, private libraryService: LibraryService) { }
constructor(public accountService: AccountService, private libraryService: LibraryService, private router: Router) { }
ngOnInit(): void {
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
this.user = user;
// this.libraryService.getLibrariesForUser(this.user.username).subscribe(libraries => {
// this.libraries = libraries;
// });
if (this.accountService.hasAdminRole(user)) {
this.actions = [
{title: 'Scan Library', callback: (data: Library) => {
console.log('You tried to scan library: ' + data.name);
}}
];
}
this.libraryService.getLibrariesForMember(this.user.username).subscribe(libraries => {
this.libraries = libraries;
console.log('Libraries: ', this.libraries);
});
});
}
handleNavigation(event: any, library: Library) {
this.router.navigateByUrl('/library/' + library.id);
}
}

View file

@ -0,0 +1,29 @@
<div class="container" *ngIf="series !== undefined">
<div class="row">
<div class="col-md-2">
<app-card-item [imageUrl]="series.coverImage === null ? 'assets/images/image-placeholder.jpg' : series.coverImage"></app-card-item>
<button class="btn btn-primary">Read</button>
</div>
<div class="col-md-10">
<h2>{{series.name | titlecase}}</h2>
<div class="row">
<ngb-rating></ngb-rating>
</div>
<div class="row">
</div>
<div class="row">
<p>{{series?.summary}}</p>
</div>
</div>
</div>
<h4 class="mt-3">Volumes</h4>
<div class="row mt-3">
<div class="col-md-2" *ngFor="let volume of volumes">
<app-card-item [entity]="volume" [title]="'Volume ' + volume.number" (click)="openVolume(volume)"></app-card-item>
</div>
</div>
</div>

View file

@ -0,0 +1,40 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Series } from '../_models/series';
import { Volume } from '../_models/volume';
import { SeriesService } from '../_services/series.service';
@Component({
selector: 'app-series-detail',
templateUrl: './series-detail.component.html',
styleUrls: ['./series-detail.component.scss']
})
export class SeriesDetailComponent implements OnInit {
series: Series | undefined;
volumes: Volume[] = [];
constructor(private route: ActivatedRoute, private seriesService: SeriesService) { }
ngOnInit(): void {
const routeId = this.route.snapshot.paramMap.get('id');
if (routeId === null) {
console.error('No library id was passed. Redirecting to home');
//this.router.navigateByUrl('/home');
return;
}
const seriesId = parseInt(routeId, 10);
this.seriesService.getSeries(seriesId).subscribe(series => {
this.series = series;
this.seriesService.getVolumes(this.series.id).subscribe(volumes => {
this.volumes = volumes;
});
});
}
openVolume(volume: Volume) {
alert('TODO: Let user read Manga');
}
}

View file

@ -0,0 +1,17 @@
<div class="card" style="width: 18rem;">
<img (click)="handleClick()" class="card-img-top" src="{{isNullOrEmpty(imageUrl) ? placeholderImage : imageUrl}}" alt="{{title}}">
<div class="card-body text-center" *ngIf="title.length > 0 || actions.length > 0">
<h5 class="card-title" (click)="handleClick()">{{title}}</h5>
<ng-container *ngIf="actions.length > 0">
<div class="col">
<div ngbDropdown class="d-inline-block">
<button class="btn" id="actions-{{title}}" ngbDropdownToggle><i class="fa fa-ellipsis-v" aria-hidden="true"></i></button>
<div ngbDropdownMenu attr.aria-labelledby="actions-{{title}}">
<button ngbDropdownItem *ngFor="let action of actions" (click)="performAction($event, action)">{{action.title}}</button>
</div>
</div>
</div>
</ng-container>
</div>
</div>

View file

@ -0,0 +1,13 @@
.card {
margin: 5px;
max-width: 130px;
max-height: 195px;
}
.card-body {
padding: 5px !important;
}
.dropdown-toggle:after {
content: none !important;
}

View file

@ -0,0 +1,44 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
export interface CardItemAction {
title: string;
callback: (data: any) => void;
}
@Component({
selector: 'app-card-item',
templateUrl: './card-item.component.html',
styleUrls: ['./card-item.component.scss']
})
export class CardItemComponent implements OnInit {
@Input() imageUrl = '';
@Input() title = '';
@Input() actions: CardItemAction[] = []; // TODO: Create a factory that generates actions based on if admin, etc. for each card type.
@Input() entity: any; // This is the entity we are representing. It will be returned if an action is executed.
@Output() clicked = new EventEmitter<string>();
placeholderImage = 'assets/images/image-placeholder.jpg';
constructor() { }
ngOnInit(): void {
}
handleClick() {
this.clicked.emit(this.title);
}
isNullOrEmpty(val: string) {
return val === null || val === undefined || val === '';
}
performAction(event: any, action: CardItemAction) {
event.stopPropagation();
if (typeof action.callback === 'function') {
action.callback(this.entity);
}
}
}

View file

@ -2,17 +2,21 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RegisterMemberComponent } from './register-member/register-member.component';
import { ReactiveFormsModule } from '@angular/forms';
import { CardItemComponent } from './card-item/card-item.component';
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
@NgModule({
declarations: [RegisterMemberComponent],
declarations: [RegisterMemberComponent, CardItemComponent],
imports: [
CommonModule,
ReactiveFormsModule
ReactiveFormsModule,
NgbDropdownModule
],
exports: [
RegisterMemberComponent
RegisterMemberComponent,
CardItemComponent
]
})
export class SharedModule { }

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB