Reading Lists & More (#564)

* Added continous reading to the book reader. Clicking on the max pages to right of progress bar will now go to last page.

* Forgot a file for continous book reading

* Fixed up some code regarding transitioning between chapters. Arrows now show to represent a chapter transition.

* Laid the foundation for reading lists

* All foundation is laid out. Actions are wired in the UI. Backend repository is setup. Redid the migration to have ReadingList track modification so we can order them for the user.

* Updated add modal to have basic skeleton

* Hooked up ability to fetch reading lists from backend

* Made a huge performance improvement to GetChapterIdsForSeriesAsync() by reducing a JOIN and an iteration loop. Improvement went from 2 seconds -> 200 ms.

* Implemented the ability to add all chapters in a series to a reading list.

* Fixed issue with adding new items to reading list not being in a logical order. Lots of work on getting all the information around the reading list view. Added some foreign keys back to chapter so delete should clean up after itself.

* Added ability to open directly the series

* Reading List Items now have progress attached

* Hooked up list deletion and added a case where if doesn't exist on load, then redirect to library.

* Lots of changes. Introduced a dashboard component for the main app. This will sit on libraries route for now and will have 3 tabs to show different sections.

Moved libraries reel down to bottom as people are more likely to access recently added or in progress than explore their whole library.

Note: Bundles are messed up, they need to be reoptimized and routes need to be updated.

* Added pagination to the reading lists api and implemented a page to show all lists

* Cleaned up old code from all-collections component so now it only handles all collections and doesn't have the old code for an individual collection

* Hooked in actions and navigation on reading lists

* When the user re-arranges items, they are now persisted

* Implemented remove read, but performance is pretty poor. Needs to be optimized.

* Lots of API fixes for adding items to a series, returning items, etc. Committing before fixing incorrect fetches of items for a readingListId.

* Rewrote the joins for GetReadingListItemDtosByIdAsync() to not return extra records.

* Remove bug marker now that it is fixed

* Refactor update-by-series to move more of the code to a re-usable function for update-by-volume/chapter APIs

* Implemented the ability to add via series, volume or chapter.

* Added OPDS support for reading lists. This included adding VolumeId to the ReadingListDto.

* Fixed a bug with deleting items

* After we create a library inform user that a scan has started

* Added some extra help information for users on directory picker, since linux users were getting confused.

* Setup for the reading functionality

* Fixed an issue where opening the edit series modal and pressing save without doing anything would empty collection tags. Would happen often when editing cover images.

* Fixed get-next-chapter for reading list. Refactored all methods to use the new GetUserIdByUsernameAsync(), which is much faster and uses less memory.

* Hooked in prev chapter for continuous reading with reading list

* Hooked up the read code for manga reader and book reader to have list id passed

* Manga reader now functions completely with reading lists

* Implemented reading list and incognito mode into book reader

* Refactored some common reading code into reader service

* Added support for "Series -  - Vol. 03 Ch. 023.5 - Volume 3 Extras.cbz" format that can occur with FMD2.

* Implemented continuous reading with a reading list between different readers. This incurs a 3x performance hit on the book info api.

* style changes. Don't emit an event if position of draggable item hasn't changed

* Styling and added the edit reading list flow.

* Cleaned up some extra spaces when actionables isn't shown. Lots of cleanup for promoted lists.

* Refactored some filter code to a common service

* Added an RBS check in getting Items for a given user.

* Code smells

* More smells
This commit is contained in:
Joseph Milazzo 2021-09-08 10:03:27 -07:00 committed by GitHub
parent d65e49926a
commit cf7a9aa71e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
117 changed files with 7050 additions and 305 deletions

View file

@ -0,0 +1,37 @@
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">Add to Reading List</h4>
<button type="button" class="close" aria-label="Close" (click)="close()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<!-- TODO: Put filter here -->
<ul class="list-group">
<li class="list-group-item clickable" tabindex="0" role="button" *ngFor="let readingList of lists; let i = index" (click)="addToList(readingList)">
<!-- Think about using radio buttons maybe for screen reader-->
{{readingList.title}} <i class="fa fa-angle-double-up" *ngIf="readingList.promoted" title="Promoted"></i>
</li>
<li class="list-group-item" *ngIf="lists.length === 0 && !loading">No lists created yet</li>
<li class="list-group-item" *ngIf="loading">
<div class="spinner-border text-secondary" role="status">
<span class="sr-only">Loading...</span>
</div>
</li>
</ul>
</div>
<div class="modal-footer" style="justify-content: normal">
<form style="width: 100%" [formGroup]="listForm">
<div class="form-row">
<div class="col-md-10">
<label class="sr-only" for="add-rlist">Reading List</label>
<input width="100%" #title ngbAutofocus type="text" class="form-control mb-2" id="add-rlist" formControlName="title">
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary" (click)="create()">Create</button>
</div>
</div>
</form>
</div>

View file

@ -0,0 +1,7 @@
.clickable {
cursor: pointer;
}
.clickable:hover, .clickable:focus {
background-color: lightgreen;
}

View file

@ -0,0 +1,90 @@
import { AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ReadingList } from 'src/app/_models/reading-list';
import { ReadingListService } from 'src/app/_services/reading-list.service';
export enum ADD_FLOW {
Series = 0,
Volume = 1,
Chapter = 2
}
@Component({
selector: 'app-add-to-list-modal',
templateUrl: './add-to-list-modal.component.html',
styleUrls: ['./add-to-list-modal.component.scss']
})
export class AddToListModalComponent implements OnInit, AfterViewInit {
@Input() title!: string;
@Input() seriesId?: number;
@Input() volumeId?: number;
@Input() chapterId?: number;
/**
* Determines which Input is required and which API is used to associate to the Reading List
*/
@Input() type!: ADD_FLOW;
/**
* All existing reading lists sorted by recent use date
*/
lists: Array<any> = [];
loading: boolean = false;
listForm: FormGroup = new FormGroup({});
@ViewChild('title') inputElem!: ElementRef<HTMLInputElement>;
constructor(private modal: NgbActiveModal, private readingListService: ReadingListService) { }
ngOnInit(): void {
this.listForm.addControl('title', new FormControl(this.title, []));
this.loading = true;
this.readingListService.getReadingLists(false).subscribe(lists => {
this.lists = lists.result;
this.loading = false;
});
}
ngAfterViewInit() {
// Shift focus to input
if (this.inputElem) {
this.inputElem.nativeElement.select();
}
}
close() {
this.modal.close();
}
create() {
this.readingListService.createList(this.listForm.value.title).subscribe(list => {
this.addToList(list);
});
}
addToList(readingList: ReadingList) {
if (this.seriesId === undefined) return;
if (this.type === ADD_FLOW.Series) {
this.readingListService.updateBySeries(readingList.id, this.seriesId).subscribe(() => {
this.modal.close();
});
} else if (this.type === ADD_FLOW.Volume && this.volumeId !== undefined) {
this.readingListService.updateByVolume(readingList.id, this.seriesId, this.volumeId).subscribe(() => {
this.modal.close();
});
} else if (this.type === ADD_FLOW.Chapter && this.chapterId !== undefined) {
this.readingListService.updateByChapter(readingList.id, this.seriesId, this.chapterId).subscribe(() => {
this.modal.close();
});
}
}
}

View file

@ -0,0 +1,31 @@
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">Edit {{readingList.title}} Reading List</h4>
<button type="button" class="close" aria-label="Close" (click)="close()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>
This list is currently {{readingList?.promoted ? 'promoted' : 'not promoted'}} (<i class="fa fa-angle-double-up" aria-hidden="true"></i>).
Promotion means that the list can be seen server-wide, not just for admin users. All series that are within this list will still have user-access restrictions placed on them.
</p>
<form [formGroup]="reviewGroup">
<div class="form-group">
<label for="title">Name</label>
<input id="title" class="form-control" formControlName="title" type="text">
</div>
<div class="form-group">
<label for="summary">Summary</label>
<textarea id="summary" class="form-control" formControlName="summary" rows="3"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" (click)="close()">Close</button>
<button type="button" class="btn btn-info" (click)="togglePromotion()">{{readingList.promoted ? 'Demote' : 'Promote'}}</button>
<button type="submit" class="btn btn-primary" [disabled]="reviewGroup.get('title')?.value.trim().length === 0" (click)="save()">Save</button>
</div>

View file

@ -0,0 +1,52 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ReadingList } from 'src/app/_models/reading-list';
import { ReadingListService } from 'src/app/_services/reading-list.service';
@Component({
selector: 'app-edit-reading-list-modal',
templateUrl: './edit-reading-list-modal.component.html',
styleUrls: ['./edit-reading-list-modal.component.scss']
})
export class EditReadingListModalComponent implements OnInit {
@Input() readingList!: ReadingList;
reviewGroup!: FormGroup;
constructor(private ngModal: NgbActiveModal, private readingListService: ReadingListService) { }
ngOnInit(): void {
this.reviewGroup = new FormGroup({
title: new FormControl(this.readingList.title, [Validators.required]),
summary: new FormControl(this.readingList.summary, [])
});
}
close() {
this.ngModal.dismiss(undefined);
}
save() {
if (this.reviewGroup.value.title.trim() === '') return;
const model = {...this.reviewGroup.value, readingListId: this.readingList.id, promoted: this.readingList.promoted};
this.readingListService.update(model).subscribe(() => {
this.readingList.title = model.title;
this.readingList.summary = model.summary;
this.ngModal.close(this.readingList);
});
}
togglePromotion() {
const originalPromotion = this.readingList.promoted;
this.readingList.promoted = !this.readingList.promoted;
const model = {readingListId: this.readingList.id, promoted: this.readingList.promoted};
this.readingListService.update(model).subscribe(res => {
/* No Operation */
}, err => {
this.readingList.promoted = originalPromotion;
});
}
}

View file

@ -0,0 +1,20 @@
<div cdkDropList class="{{items.length > 0 ? 'example-list list-group-flush' : ''}}" (cdkDropListDropped)="drop($event)">
<div class="example-box" *ngFor="let item of items; index as i" cdkDrag [cdkDragData]="item" cdkDragBoundary=".example-list">
<div class="mr-3 align-middle">
<i class="fa fa-grip-vertical drag-handle" aria-hidden="true" cdkDragHandle></i>
<label for="reorder-{{i}}" class="sr-only">Reorder</label>
<input *ngIf="accessibilityMode" id="reorder-{{i}}" type="number" min="0" [max]="items.length - 1" [value]="i" style="width: 40px" (keydown.enter)="updateIndex(i, item)" aria-describedby="instructions">
</div>
<ng-container style="display: inline-block" [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
<button class="btn btn-icon pull-right" (click)="removeItem(item, i)">
<i class="fa fa-times" aria-hidden="true"></i>
<span class="sr-only" attr.aria-labelledby="item.id--{{i}}">Remove item</span>
</button>
</div>
</div>
<p class="sr-only" id="instructions">
</p>

View file

@ -0,0 +1,53 @@
.example-list {
min-width: 500px;
max-width: 100%;
//border: solid 1px #ccc;
min-height: 60px;
display: block;
//background: white;
border-radius: 4px;
overflow: hidden;
}
.example-box {
padding: 20px 10px;
border-bottom: solid 1px #ccc;
//color: rgba(0, 0, 0, 0.87);
display: flex;
flex-direction: row;
//align-items: center;
//justify-content: space-between;
box-sizing: border-box;
//background: white;
font-size: 14px;
.drag-handle {
cursor: move;
font-size: 24px;
margin-top: 215%;
}
}
.cdk-drag-preview {
box-sizing: border-box;
border-radius: 4px;
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12);
}
.cdk-drag-placeholder {
opacity: 0;
}
.cdk-drag-animating {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.example-box:last-child {
border: none;
}
.example-list.cdk-drop-list-dragging .example-box:not(.cdk-drag-placeholder) {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}

View file

@ -0,0 +1,63 @@
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateRef } from '@angular/core';
export interface IndexUpdateEvent {
fromPosition: number;
toPosition: number;
item: any;
}
export interface ItemRemoveEvent {
position: number;
item: any;
}
@Component({
selector: 'app-dragable-ordered-list',
templateUrl: './dragable-ordered-list.component.html',
styleUrls: ['./dragable-ordered-list.component.scss']
})
export class DragableOrderedListComponent implements OnInit {
@Input() accessibilityMode: boolean = false;
@Input() items: Array<any> = [];
@Output() orderUpdated: EventEmitter<IndexUpdateEvent> = new EventEmitter<IndexUpdateEvent>();
@Output() itemRemove: EventEmitter<ItemRemoveEvent> = new EventEmitter<ItemRemoveEvent>();
@ContentChild('draggableItem') itemTemplate!: TemplateRef<any>;
constructor() { }
ngOnInit(): void {
}
drop(event: CdkDragDrop<string[]>) {
if (event.previousIndex === event.currentIndex) return;
moveItemInArray(this.items, event.previousIndex, event.currentIndex);
this.orderUpdated.emit({
fromPosition: event.previousIndex,
toPosition: event.currentIndex,
item: this.items[event.currentIndex]
});
}
updateIndex(previousIndex: number, item: any) {
// get the new value of the input
var inputElem = <HTMLInputElement>document.querySelector('#reorder-' + previousIndex);
const newIndex = parseInt(inputElem.value, 10);
if (previousIndex === newIndex) return;
moveItemInArray(this.items, previousIndex, newIndex);
this.orderUpdated.emit({
fromPosition: previousIndex,
toPosition: newIndex,
item: this.items[newIndex]
});
}
removeItem(item: any, position: number) {
this.itemRemove.emit({
position,
item
});
}
}

View file

@ -0,0 +1,58 @@
<div class="container mt-2" *ngIf="readingList">
<div class="row mb-3">
<div class="col-md-10 col-xs-8 col-sm-6">
<div class="row no-gutters">
<h2 style="display: inline-block">
<span *ngIf="actions.length > 0">
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="readingList.title"></app-card-actionables>&nbsp;
</span>
{{readingList.title}}&nbsp;<span *ngIf="readingList?.promoted">(<i class="fa fa-angle-double-up" aria-hidden="true"></i>)</span>&nbsp;
<span class="badge badge-primary badge-pill" attr.aria-label="{{items.length}} total items">{{items.length}}</span>
</h2>
</div>
<div class="row no-gutters">
<div class="mr-2">
<button class="btn btn-primary" title="Read" (click)="read()">
<span>
<i class="fa fa-book-open" aria-hidden="true"></i>
<span class="read-btn--text">&nbsp;Read</span>
</span>
</button>
</div>
<div>
<button class="btn btn-secondary" (click)="removeRead()" [disabled]="readingList?.promoted && !this.isAdmin">
<span>
<i class="fa fa-check"></i>
</span>
<span class="read-btn--text">&nbsp;Remove Read</span>
</button>
</div>
</div>
<p class="mt-2" *ngIf="readingList.summary.length > 0">{{readingList.summary}}</p>
</div>
</div>
<div *ngIf="items.length === 0">
No chapters added
</div>
<!-- NOTE: It might be nice to have a switch for the accessibility toggle -->
<app-dragable-ordered-list [items]="items" (orderUpdated)="orderUpdated($event)" (itemRemove)="itemRemoved($event)">
<ng-template #draggableItem let-item let-position="idx">
<div class="media" style="width: 100%;">
<img width="74px" style="width: 74px;" class="img-top lazyload mr-3" [src]="imageService.placeholderImage" [attr.data-src]="imageService.getChapterCoverImage(item.chapterId)"
(error)="imageService.updateErroredImage($event)">
<div class="media-body">
<h5 class="mt-0 mb-1" id="item.id--{{position}}">{{formatTitle(item)}}</h5>
<i class="fa {{utilityService.mangaFormatIcon(item.seriesFormat)}}" aria-hidden="true" *ngIf="item.seriesFormat != MangaFormat.UNKNOWN" title="{{utilityService.mangaFormat(item.seriesFormat)}}"></i><span class="sr-only">{{utilityService.mangaFormat(item.seriesFormat)}}</span>&nbsp;
<a href="/library/{{item.libraryId}}/series/{{item.seriesId}}">{{item.seriesName}}</a>
<span *ngIf="item.promoted">
<i class="fa fa-angle-double-up" aria-hidden="true"></i>
</span>
</div>
<div class="pull-right" *ngIf="item.pagesRead === item.pagesTotal"><i class="fa fa-check-square" aria-label="Read"></i></div>
</div>
</ng-template>
</app-dragable-ordered-list>
</div>

View file

@ -0,0 +1,153 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr';
import { take } from 'rxjs/operators';
import { ConfirmService } from 'src/app/shared/confirm.service';
import { UtilityService } from 'src/app/shared/_services/utility.service';
import { MangaFormat } from 'src/app/_models/manga-format';
import { ReadingList, ReadingListItem } from 'src/app/_models/reading-list';
import { AccountService } from 'src/app/_services/account.service';
import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service';
import { ActionService } from 'src/app/_services/action.service';
import { ImageService } from 'src/app/_services/image.service';
import { ReaderService } from 'src/app/_services/reader.service';
import { ReadingListService } from 'src/app/_services/reading-list.service';
import { IndexUpdateEvent, ItemRemoveEvent } from '../dragable-ordered-list/dragable-ordered-list.component';
@Component({
selector: 'app-reading-list-detail',
templateUrl: './reading-list-detail.component.html',
styleUrls: ['./reading-list-detail.component.scss']
})
export class ReadingListDetailComponent implements OnInit {
items: Array<ReadingListItem> = [];
listId!: number;
readingList!: ReadingList;
actions: Array<ActionItem<any>> = [];
isAdmin: boolean = false;
isLoading: boolean = false;
get MangaFormat(): typeof MangaFormat {
return MangaFormat;
}
constructor(private route: ActivatedRoute, private router: Router, private readingListService: ReadingListService,
private actionService: ActionService, private actionFactoryService: ActionFactoryService, public utilityService: UtilityService,
public imageService: ImageService, private accountService: AccountService, private toastr: ToastrService, private confirmService: ConfirmService) {}
ngOnInit(): void {
const listId = this.route.snapshot.paramMap.get('id');
if (listId === null) {
this.router.navigateByUrl('/libraries');
return;
}
this.listId = parseInt(listId, 10);
this.readingListService.getReadingList(this.listId).subscribe(readingList => {
if (readingList == null) {
// The list doesn't exist
this.toastr.error('This list doesn\'t exist.');
this.router.navigateByUrl('library');
return;
}
this.readingList = readingList;
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
if (user) {
this.isAdmin = this.accountService.hasAdminRole(user);
this.actions = this.actionFactoryService.getReadingListActions(this.handleReadingListActionCallback.bind(this)).filter(action => this.readingListService.actionListFilter(action, readingList, this.isAdmin));
}
});
});
this.getListItems();
}
getListItems() {
this.isLoading = true;
this.readingListService.getListItems(this.listId).subscribe(items => {
this.items = items;
this.isLoading = false;
});
}
performAction(action: ActionItem<any>) {
// TODO: Try to move performAction into the actionables component. (have default handler in the component, allow for overridding to pass additional context)
if (typeof action.callback === 'function') {
action.callback(action.action, this.readingList);
}
}
handleReadingListActionCallback(action: Action, readingList: ReadingList) {
switch(action) {
case Action.Delete:
this.deleteList(readingList);
break;
case Action.Edit:
this.actionService.editReadingList(readingList, (readingList: ReadingList) => {
// Reload information around list
this.readingList = readingList;
});
break;
}
}
async deleteList(readingList: ReadingList) {
if (!await this.confirmService.confirm('Are you sure you want to delete the reading list? This cannot be undone.')) return;
this.readingListService.delete(readingList.id).subscribe(() => {
this.toastr.success('Reading list deleted');
this.router.navigateByUrl('library#lists');
});
}
formatTitle(item: ReadingListItem) {
if (item.chapterNumber === '0') {
return 'Volume ' + item.volumeNumber;
}
if (item.seriesFormat === MangaFormat.EPUB) {
return 'Volume ' + this.utilityService.cleanSpecialTitle(item.chapterNumber);
}
return 'Chapter ' + item.chapterNumber;
}
orderUpdated(event: IndexUpdateEvent) {
this.readingListService.updatePosition(this.readingList.id, event.item.id, event.fromPosition, event.toPosition).subscribe(() => { /* No Operation */ });
}
itemRemoved(event: ItemRemoveEvent) {
this.readingListService.deleteItem(this.readingList.id, event.item.id).subscribe(() => {
this.items.splice(event.position, 1);
this.toastr.success('Item removed');
});
}
removeRead() {
this.isLoading = true;
this.readingListService.removeRead(this.readingList.id).subscribe(() => {
this.getListItems();
});
}
read() {
let currentlyReadingChapter = this.items[0];
for (let i = 0; i < this.items.length; i++) {
if (this.items[i].pagesRead >= this.items[i].pagesTotal) {
continue;
}
currentlyReadingChapter = this.items[i];
break;
}
if (currentlyReadingChapter.seriesFormat === MangaFormat.EPUB) {
this.router.navigate(['library', currentlyReadingChapter.libraryId, 'series', currentlyReadingChapter.seriesId, 'book', currentlyReadingChapter.chapterId], {queryParams: {readingListId: this.readingList.id}});
} else {
this.router.navigate(['library', currentlyReadingChapter.libraryId, 'series', currentlyReadingChapter.seriesId, 'manga', currentlyReadingChapter.chapterId], {queryParams: {readingListId: this.readingList.id}});
}
}
}

View file

@ -0,0 +1,36 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DragableOrderedListComponent } from './dragable-ordered-list/dragable-ordered-list.component';
import { ReadingListDetailComponent } from './reading-list-detail/reading-list-detail.component';
import { ReadingListRoutingModule } from './reading-list.router.module';
import {DragDropModule} from '@angular/cdk/drag-drop';
import { AddToListModalComponent } from './_modals/add-to-list-modal/add-to-list-modal.component';
import { ReactiveFormsModule } from '@angular/forms';
import { CardsModule } from '../cards/cards.module';
import { ReadingListsComponent } from './reading-lists/reading-lists.component';
import { EditReadingListModalComponent } from './_modals/edit-reading-list-modal/edit-reading-list-modal.component';
@NgModule({
declarations: [
DragableOrderedListComponent,
ReadingListDetailComponent,
AddToListModalComponent,
ReadingListsComponent,
EditReadingListModalComponent
],
imports: [
CommonModule,
ReadingListRoutingModule,
ReactiveFormsModule,
DragDropModule,
CardsModule
],
exports: [
AddToListModalComponent,
ReadingListsComponent,
EditReadingListModalComponent
]
})
export class ReadingListModule { }

View file

@ -0,0 +1,24 @@
import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";
import { AuthGuard } from "../_guards/auth.guard";
import { ReadingListDetailComponent } from "./reading-list-detail/reading-list-detail.component";
const routes: Routes = [
{
path: '',
runGuardsAndResolvers: 'always',
canActivate: [AuthGuard], // TODO: Add a guard if they have access to said :id
children: [
{path: '', component: ReadingListDetailComponent, pathMatch: 'full'},
{path: ':id', component: ReadingListDetailComponent, pathMatch: 'full'},
// {path: ':id', component: CollectionDetailComponent},
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ReadingListRoutingModule { }

View file

@ -0,0 +1,11 @@
<app-card-detail-layout header="Reading Lists"
[isLoading]="loadingLists"
[items]="lists"
[actions]="actions"
[pagination]="pagination"
(pageChange)="onPageChange($event)"
>
<ng-template #cardItem let-item let-position="idx">
<app-card-item [title]="item.title" [entity]="item" [actions]="getActions(item)" [supressLibraryLink]="true" [imageUrl]="imageService.placeholderImage" (clicked)="handleClick(item)"></app-card-item>
</ng-template>
</app-card-detail-layout>

View file

@ -0,0 +1,88 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr';
import { take } from 'rxjs/operators';
import { PaginatedResult, Pagination } from 'src/app/_models/pagination';
import { ReadingList } from 'src/app/_models/reading-list';
import { AccountService } from 'src/app/_services/account.service';
import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service';
import { ImageService } from 'src/app/_services/image.service';
import { ReadingListService } from 'src/app/_services/reading-list.service';
@Component({
selector: 'app-reading-lists',
templateUrl: './reading-lists.component.html',
styleUrls: ['./reading-lists.component.scss']
})
export class ReadingListsComponent implements OnInit {
lists: ReadingList[] = [];
loadingLists = false;
pagination!: Pagination;
actions: ActionItem<ReadingList>[] = [];
isAdmin: boolean = false;
constructor(private readingListService: ReadingListService, public imageService: ImageService, private actionFactoryService: ActionFactoryService,
private accountService: AccountService, private toastr: ToastrService, private router: Router) { }
ngOnInit(): void {
this.loadPage();
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
if (user) {
this.isAdmin = this.accountService.hasAdminRole(user);
}
});
}
getActions(readingList: ReadingList) {
return this.actionFactoryService.getReadingListActions(this.handleReadingListActionCallback.bind(this)).filter(action => this.readingListService.actionListFilter(action, readingList, this.isAdmin));
}
performAction(action: ActionItem<any>, readingList: ReadingList) {
// TODO: Try to move performAction into the actionables component. (have default handler in the component, allow for overridding to pass additional context)
if (typeof action.callback === 'function') {
action.callback(action.action, readingList);
}
}
handleReadingListActionCallback(action: Action, readingList: ReadingList) {
switch(action) {
case Action.Delete:
this.readingListService.delete(readingList.id).subscribe(() => {
this.toastr.success('Reading list deleted');
this.loadPage();
});
}
}
getPage() {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('page');
}
loadPage() {
const page = this.getPage();
if (page != null) {
this.pagination.currentPage = parseInt(page, 10);
}
this.loadingLists = true;
this.readingListService.getReadingLists(true, this.pagination?.currentPage, this.pagination?.itemsPerPage).pipe(take(1)).subscribe((readingLists: PaginatedResult<ReadingList[]>) => {
this.lists = readingLists.result;
this.pagination = readingLists.pagination;
this.loadingLists = false;
window.scrollTo(0, 0);
});
}
onPageChange(pagination: Pagination) {
window.history.replaceState(window.location.href, '', window.location.href.split('?')[0] + '?page=' + this.pagination.currentPage);
this.loadPage();
}
handleClick(list: ReadingList) {
this.router.navigateByUrl('lists/' + list.id);
}
}