Merge pull request #16 from Kareadita/feature/scan-library
Library, Series, Volumes, oh my!
This commit is contained in:
commit
7642ffa912
25 changed files with 293 additions and 19 deletions
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export interface Library {
|
||||
id: number;
|
||||
name: string;
|
||||
coverImage: string;
|
||||
type: any;
|
||||
|
|
|
|||
11
src/app/_models/series.ts
Normal file
11
src/app/_models/series.ts
Normal 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[];
|
||||
}
|
||||
6
src/app/_models/volume.ts
Normal file
6
src/app/_models/volume.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export interface Volume {
|
||||
id: number;
|
||||
number: string;
|
||||
files: Array<string>;
|
||||
coverImage: string;
|
||||
}
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
27
src/app/_services/series.service.ts
Normal file
27
src/app/_services/series.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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'}
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
12
src/app/library-detail/library-detail.component.html
Normal file
12
src/app/library-detail/library-detail.component.html
Normal 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>
|
||||
4
src/app/library-detail/library-detail.component.scss
Normal file
4
src/app/library-detail/library-detail.component.scss
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
.card {
|
||||
height: 400;
|
||||
width: 200;
|
||||
}
|
||||
38
src/app/library-detail/library-detail.component.ts
Normal file
38
src/app/library-detail/library-detail.component.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
29
src/app/series-detail/series-detail.component.html
Normal file
29
src/app/series-detail/series-detail.component.html
Normal 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>
|
||||
0
src/app/series-detail/series-detail.component.scss
Normal file
0
src/app/series-detail/series-detail.component.scss
Normal file
40
src/app/series-detail/series-detail.component.ts
Normal file
40
src/app/series-detail/series-detail.component.ts
Normal 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');
|
||||
}
|
||||
|
||||
}
|
||||
17
src/app/shared/card-item/card-item.component.html
Normal file
17
src/app/shared/card-item/card-item.component.html
Normal 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>
|
||||
13
src/app/shared/card-item/card-item.component.scss
Normal file
13
src/app/shared/card-item/card-item.component.scss
Normal 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;
|
||||
}
|
||||
44
src/app/shared/card-item/card-item.component.ts
Normal file
44
src/app/shared/card-item/card-item.component.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 { }
|
||||
|
|
|
|||
BIN
src/assets/images/image-placeholder.jpg
Normal file
BIN
src/assets/images/image-placeholder.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
Loading…
Add table
Add a link
Reference in a new issue