Series Detail Refactor (#1118)
* Fixed a bug where reading list and collection's summary wouldn't render newlines * Moved all the logic in the UI for Series Detail into the backend (messy code). We are averaging 400ms max with much optimizations available. Next step is to refactor out of controller and provide unit tests. * Unit tests for CleanSpecialTitle * Laid out foundation for testing major code in SeriesController. * Refactored code so that read doesn't need to be disabled on page load. SeriesId doesn't need the series to actually load. * Removed old property from Volume * Changed tagbadge font size to rem. * Refactored some methods from SeriesController.cs into SeriesService.cs * UpdateRating unit tested * Wrote unit tests for SeriesDetail * Worked up some code where books are rendered only as volumes. However, looks like I will need to use Chapters to better support series_index as floats. * Refactored Series Detail to change Volume Name on Book libraries to have book name and series_index. * Some cleanup on the code * DeleteMultipleSeries test is hard. Going to skip. * Removed some debug code and make all tabs Books for Book library Type
This commit is contained in:
parent
58b1d0df8a
commit
d291eb809d
20 changed files with 944 additions and 187 deletions
12
UI/Web/src/app/_models/series-detail/series-detail.ts
Normal file
12
UI/Web/src/app/_models/series-detail/series-detail.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { Chapter } from "../chapter";
|
||||
import { Volume } from "../volume";
|
||||
|
||||
/**
|
||||
* This is built for Series Detail itself
|
||||
*/
|
||||
export interface SeriesDetail {
|
||||
specials: Array<Chapter>;
|
||||
chapters: Array<Chapter>;
|
||||
volumes: Array<Volume>;
|
||||
storylineChapters: Array<Chapter>;
|
||||
}
|
|
@ -4,7 +4,6 @@ export interface Volume {
|
|||
id: number;
|
||||
number: number;
|
||||
name: string;
|
||||
coverImage: string;
|
||||
created: string;
|
||||
lastModified: string;
|
||||
pages: number;
|
||||
|
|
|
@ -8,6 +8,7 @@ import { CollectionTag } from '../_models/collection-tag';
|
|||
import { PaginatedResult } from '../_models/pagination';
|
||||
import { RecentlyAddedItem } from '../_models/recently-added-item';
|
||||
import { Series } from '../_models/series';
|
||||
import { SeriesDetail } from '../_models/series-detail/series-detail';
|
||||
import { SeriesFilter } from '../_models/series-filter';
|
||||
import { SeriesGroup } from '../_models/series-group';
|
||||
import { SeriesMetadata } from '../_models/series-metadata';
|
||||
|
@ -185,6 +186,10 @@ export class SeriesService {
|
|||
);
|
||||
}
|
||||
|
||||
getSeriesDetail(seriesId: number) {
|
||||
return this.httpClient.get<SeriesDetail>(this.baseUrl + 'series/series-detail?seriesId=' + seriesId);
|
||||
}
|
||||
|
||||
_addPaginationIfExists(params: HttpParams, pageNum?: number, itemsPerPage?: number) {
|
||||
if (pageNum !== null && pageNum !== undefined && itemsPerPage !== null && itemsPerPage !== undefined) {
|
||||
params = params.append('pageNumber', pageNum + '');
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<app-read-more [text]="collectionTag.summary" [maxLength]="250"></app-read-more>
|
||||
<app-read-more [text]="summary" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -40,6 +40,7 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
|||
isAdmin: boolean = false;
|
||||
filter: SeriesFilter | undefined = undefined;
|
||||
filterSettings: FilterSettings = new FilterSettings();
|
||||
summary: string = '';
|
||||
|
||||
private onDestory: Subject<void> = new Subject<void>();
|
||||
|
||||
|
@ -149,6 +150,7 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
|||
return;
|
||||
}
|
||||
this.collectionTag = matchingTags[0];
|
||||
this.summary = (this.collectionTag.summary === null ? '' : this.collectionTag.summary).replace(/\n/g, '<br>');
|
||||
this.tagImage = this.imageService.randomize(this.imageService.getCollectionCoverImage(this.collectionTag.id));
|
||||
this.titleService.setTitle('Kavita - ' + this.collectionTag.title + ' Collection');
|
||||
});
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
</div>
|
||||
<!-- Summary row-->
|
||||
<div class="row g-0 mt-2">
|
||||
<app-read-more [text]="readingList.summary" [maxLength]="250"></app-read-more>
|
||||
<app-read-more [text]="readingListSummary" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -34,6 +34,8 @@ export class ReadingListDetailComponent implements OnInit {
|
|||
hasDownloadingRole: boolean = false;
|
||||
downloadInProgress: boolean = false;
|
||||
|
||||
readingListSummary: string = '';
|
||||
|
||||
libraryTypes: {[key: number]: LibraryType} = {};
|
||||
|
||||
get MangaFormat(): typeof MangaFormat {
|
||||
|
@ -77,6 +79,7 @@ export class ReadingListDetailComponent implements OnInit {
|
|||
return;
|
||||
}
|
||||
this.readingList = readingList;
|
||||
this.readingListSummary = (this.readingList.summary === null ? '' : this.readingList.summary).replace(/\n/g, '<br>');
|
||||
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
|
@ -113,6 +116,7 @@ export class ReadingListDetailComponent implements OnInit {
|
|||
this.actionService.editReadingList(readingList, (readingList: ReadingList) => {
|
||||
// Reload information around list
|
||||
this.readingList = readingList;
|
||||
this.readingListSummary = (this.readingList.summary === null ? '' : this.readingList.summary).replace(/\n/g, '<br>');
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
</div>
|
||||
<div class="row g-0">
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-primary" (click)="read()" [disabled]="isLoading">
|
||||
<button class="btn btn-primary" (click)="read()">
|
||||
<span>
|
||||
<i class="fa {{showBook ? 'fa-book-open' : 'fa-book'}}"></i>
|
||||
</span>
|
||||
|
@ -63,7 +63,7 @@
|
|||
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="nav nav-tabs mb-2" [destroyOnHide]="false" (navChange)="onNavChange($event)">
|
||||
<li [ngbNavItem]="TabID.Specials" *ngIf="hasSpecials">
|
||||
<a ngbNavLink>Specials</a>
|
||||
<a ngbNavLink>{{libraryType === LibraryType.Book ? 'Books': 'Specials'}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="row g-0">
|
||||
<ng-container *ngFor="let chapter of specials; let idx = index; trackBy: trackByChapterIdentity">
|
||||
|
@ -92,7 +92,7 @@
|
|||
</ng-template>
|
||||
</li>
|
||||
<li [ngbNavItem]="TabID.Volumes" *ngIf="hasNonSpecialVolumeChapters">
|
||||
<a ngbNavLink>Volumes</a>
|
||||
<a ngbNavLink>{{libraryType === LibraryType.Book ? 'Books': 'Volumes'}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="row g-0">
|
||||
<ng-container *ngFor="let volume of volumes; let idx = index; trackBy: trackByVolumeIdentity">
|
||||
|
|
|
@ -46,6 +46,10 @@ enum TabID {
|
|||
})
|
||||
export class SeriesDetailComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* Series Id. Set at load before UI renders
|
||||
*/
|
||||
seriesId!: number;
|
||||
series!: Series;
|
||||
volumes: Volume[] = [];
|
||||
chapters: Chapter[] = [];
|
||||
|
@ -185,34 +189,26 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
return;
|
||||
}
|
||||
|
||||
// this.messageHub.messages$.pipe(takeUntil(this.onDestroy), takeWhile(e => this.messageHub.isEventType(e, EVENTS.ScanSeries))).subscribe((e) => {
|
||||
// const event = e.payload as ScanSeriesEvent;
|
||||
// if (event.seriesId == this.series.id)
|
||||
// this.loadSeries(seriesId);
|
||||
// this.seriesImage = this.imageService.randomize(this.imageService.getSeriesCoverImage(this.series.id));
|
||||
// this.toastr.success('Scan series completed');
|
||||
// });
|
||||
|
||||
this.messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(event => {
|
||||
if (event.event === EVENTS.SeriesRemoved) {
|
||||
const seriesRemovedEvent = event.payload as SeriesRemovedEvent;
|
||||
if (seriesRemovedEvent.seriesId === this.series.id) {
|
||||
if (seriesRemovedEvent.seriesId === this.seriesId) {
|
||||
this.toastr.info('This series no longer exists');
|
||||
this.router.navigateByUrl('/libraries');
|
||||
}
|
||||
} else if (event.event === EVENTS.ScanSeries) {
|
||||
const seriesCoverUpdatedEvent = event.payload as ScanSeriesEvent;
|
||||
if (seriesCoverUpdatedEvent.seriesId === this.series.id) {
|
||||
this.loadSeries(seriesId);
|
||||
this.seriesImage = this.imageService.randomize(this.imageService.getSeriesCoverImage(this.series.id)); // NOTE: Is this needed as cover update will update the image for us
|
||||
if (seriesCoverUpdatedEvent.seriesId === this.seriesId) {
|
||||
this.loadSeries(this.seriesId);
|
||||
this.seriesImage = this.imageService.randomize(this.imageService.getSeriesCoverImage(this.seriesId)); // NOTE: Is this needed as cover update will update the image for us
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const seriesId = parseInt(routeId, 10);
|
||||
this.seriesId = parseInt(routeId, 10);
|
||||
this.libraryId = parseInt(libraryId, 10);
|
||||
this.seriesImage = this.imageService.getSeriesCoverImage(seriesId);
|
||||
this.loadSeries(seriesId);
|
||||
this.seriesImage = this.imageService.getSeriesCoverImage(this.seriesId);
|
||||
this.loadSeries(this.seriesId);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
@ -288,7 +284,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
this.openViewInfo(volume);
|
||||
break;
|
||||
case(Action.AddToReadingList):
|
||||
this.actionService.addVolumeToReadingList(volume, this.series.id, () => {/* No Operation */ });
|
||||
this.actionService.addVolumeToReadingList(volume, this.seriesId, () => {/* No Operation */ });
|
||||
break;
|
||||
case(Action.IncognitoRead):
|
||||
if (volume.chapters != undefined && volume.chapters?.length >= 1) {
|
||||
|
@ -312,7 +308,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
this.openViewInfo(chapter);
|
||||
break;
|
||||
case(Action.AddToReadingList):
|
||||
this.actionService.addChapterToReadingList(chapter, this.series.id, () => {/* No Operation */ });
|
||||
this.actionService.addChapterToReadingList(chapter, this.seriesId, () => {/* No Operation */ });
|
||||
break;
|
||||
case(Action.IncognitoRead):
|
||||
this.openChapter(chapter, true);
|
||||
|
@ -336,6 +332,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
this.coverImageOffset = 0;
|
||||
|
||||
this.seriesService.getMetadata(seriesId).subscribe(metadata => this.seriesMetadata = metadata);
|
||||
this.setContinuePoint();
|
||||
|
||||
forkJoin([
|
||||
this.libraryService.getLibraryType(this.libraryId),
|
||||
|
@ -354,30 +351,15 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
this.volumeActions = this.actionFactoryService.getVolumeActions(this.handleVolumeActionCallback.bind(this));
|
||||
this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this));
|
||||
|
||||
this.seriesService.getSeriesDetail(this.seriesId).subscribe(detail => {
|
||||
this.hasSpecials = detail.specials.length > 0
|
||||
this.specials = detail.specials;
|
||||
|
||||
this.seriesService.getVolumes(this.series.id).subscribe(volumes => {
|
||||
this.volumes = volumes; // volumes are already be sorted in the backend
|
||||
const vol0 = this.volumes.filter(v => v.number === 0);
|
||||
this.storyChapters = vol0.map(v => v.chapters || []).flat().sort(this.utilityService.sortChapters);
|
||||
this.chapters = volumes.map(v => v.chapters || []).flat().sort(this.utilityService.sortChapters).filter(c => !c.isSpecial || isNaN(parseInt(c.range, 10)));
|
||||
|
||||
|
||||
this.setContinuePoint();
|
||||
|
||||
|
||||
const specials = this.storyChapters.filter(c => c.isSpecial || isNaN(parseInt(c.range, 10)));
|
||||
this.hasSpecials = specials.length > 0
|
||||
if (this.hasSpecials) {
|
||||
this.specials = specials
|
||||
.map(c => {
|
||||
c.title = this.utilityService.cleanSpecialTitle(c.title);
|
||||
c.range = this.utilityService.cleanSpecialTitle(c.range);
|
||||
return c;
|
||||
});
|
||||
}
|
||||
this.chapters = detail.chapters;
|
||||
this.volumes = detail.volumes;
|
||||
this.storyChapters = detail.storylineChapters;
|
||||
|
||||
this.updateSelectedTab();
|
||||
|
||||
this.isLoading = false;
|
||||
});
|
||||
}, err => {
|
||||
|
@ -422,8 +404,8 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
setContinuePoint() {
|
||||
this.readerService.hasSeriesProgress(this.series.id).subscribe(hasProgress => this.hasReadingProgress = hasProgress);
|
||||
this.readerService.getCurrentChapter(this.series.id).subscribe(chapter => this.currentlyReadingChapter = chapter);
|
||||
this.readerService.hasSeriesProgress(this.seriesId).subscribe(hasProgress => this.hasReadingProgress = hasProgress);
|
||||
this.readerService.getCurrentChapter(this.seriesId).subscribe(chapter => this.currentlyReadingChapter = chapter);
|
||||
}
|
||||
|
||||
markVolumeAsRead(vol: Volume) {
|
||||
|
@ -431,7 +413,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
return;
|
||||
}
|
||||
|
||||
this.actionService.markVolumeAsRead(this.series.id, vol, () => {
|
||||
this.actionService.markVolumeAsRead(this.seriesId, vol, () => {
|
||||
this.setContinuePoint();
|
||||
this.actionInProgress = false;
|
||||
});
|
||||
|
@ -442,7 +424,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
return;
|
||||
}
|
||||
|
||||
this.actionService.markVolumeAsUnread(this.series.id, vol, () => {
|
||||
this.actionService.markVolumeAsUnread(this.seriesId, vol, () => {
|
||||
this.setContinuePoint();
|
||||
this.actionInProgress = false;
|
||||
});
|
||||
|
@ -453,7 +435,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
return;
|
||||
}
|
||||
|
||||
this.actionService.markChapterAsRead(this.series.id, chapter, () => {
|
||||
this.actionService.markChapterAsRead(this.seriesId, chapter, () => {
|
||||
this.setContinuePoint();
|
||||
this.actionInProgress = false;
|
||||
});
|
||||
|
@ -464,14 +446,21 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
return;
|
||||
}
|
||||
|
||||
this.actionService.markChapterAsUnread(this.series.id, chapter, () => {
|
||||
this.actionService.markChapterAsUnread(this.seriesId, chapter, () => {
|
||||
this.setContinuePoint();
|
||||
this.actionInProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
read() {
|
||||
if (this.currentlyReadingChapter !== undefined) { this.openChapter(this.currentlyReadingChapter); }
|
||||
if (this.currentlyReadingChapter !== undefined) {
|
||||
this.openChapter(this.currentlyReadingChapter);
|
||||
return;
|
||||
}
|
||||
|
||||
this.readerService.getCurrentChapter(this.seriesId).subscribe(chapter => {
|
||||
this.openChapter(chapter);
|
||||
});
|
||||
}
|
||||
|
||||
updateRating(rating: any) {
|
||||
|
@ -509,7 +498,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
// If user has progress on the volume, load them where they left off
|
||||
if (volume.pagesRead < volume.pages && volume.pagesRead > 0) {
|
||||
// Find the continue point chapter and load it
|
||||
this.readerService.getCurrentChapter(this.series.id).subscribe(chapter => this.openChapter(chapter));
|
||||
this.readerService.getCurrentChapter(this.seriesId).subscribe(chapter => this.openChapter(chapter));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -540,10 +529,10 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
modalRef.closed.subscribe((closeResult: {success: boolean, series: Series, coverImageUpdate: boolean}) => {
|
||||
window.scrollTo(0, 0);
|
||||
if (closeResult.success) {
|
||||
this.loadSeries(this.series.id);
|
||||
this.loadSeries(this.seriesId);
|
||||
if (closeResult.coverImageUpdate) {
|
||||
// Random triggers a load change without any problems with API
|
||||
this.seriesImage = this.imageService.randomize(this.imageService.getSeriesCoverImage(this.series.id));
|
||||
this.seriesImage = this.imageService.randomize(this.imageService.getSeriesCoverImage(this.seriesId));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -585,7 +574,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
downloadSeries() {
|
||||
this.downloadService.downloadSeriesSize(this.series.id).pipe(take(1)).subscribe(async (size) => {
|
||||
this.downloadService.downloadSeriesSize(this.seriesId).pipe(take(1)).subscribe(async (size) => {
|
||||
const wantToDownload = await this.downloadService.confirmSize(size, 'series');
|
||||
if (!wantToDownload) { return; }
|
||||
this.downloadInProgress = true;
|
||||
|
@ -604,6 +593,10 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
formatVolumeTitle(volume: Volume) {
|
||||
if (this.libraryType === LibraryType.Book) {
|
||||
return volume.name;
|
||||
}
|
||||
|
||||
return 'Volume ' + volume.name;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,13 +3,13 @@
|
|||
margin: 3px 5px 3px 0px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-size: 0.9rem;
|
||||
display: inline-block;
|
||||
cursor: default;
|
||||
width: auto;
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
margin-right: 0px;
|
||||
cursor: pointer;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue