Misc Bugfixes and Enhancements (#507)
* Removed some extra spam for the console * Implemented the code to update RowVersion, which is our concurrency check * Fixed a critical issue where more than one bookmark could occur for a given chapter due to a race condition. Now we use concurrency checks and we also gracefully allow more than one row, by only grabbing first. * Cleaned up the logic for IHasConcurencyToken and updated the setters to be private. * Lots of comments and when deleting a library, remove any user progress items for which chapters don't exist. * When deleting a Series, cleanup user progress rows. * Now after a scan of library, if a series is removed, collection tags are pruned as well if there are no longer any series bound to it. * Updated the image on the Readme to show a better picture * Small code cleanup to remove null check modifier as I check for null just before then * Fixed images loading multiple times due to using function in binding with random. You can now click chapter images to read that chapter specifically. * Fixed cards being different sizes when read vs unread * Moved over Robbie's workflow changes from notifier. Commented out activity indicators as that is not shipping with this release. * Remove code that isn't needed * Reverted GA * Changed GA to trigger only when HEAD is updated
This commit is contained in:
parent
16faa82d20
commit
3ed99afd32
38 changed files with 1378 additions and 81 deletions
|
@ -5,7 +5,10 @@ export interface Chapter {
|
|||
range: string;
|
||||
number: string;
|
||||
files: Array<MangaFile>;
|
||||
//coverImage: string;
|
||||
/**
|
||||
* This is used in the UI, it is not updated or sent to Backend
|
||||
*/
|
||||
coverImage: string;
|
||||
coverImageLocked: boolean;
|
||||
pages: number;
|
||||
volumeId: number;
|
||||
|
|
|
@ -104,13 +104,6 @@ export class ActionFactoryService {
|
|||
callback: this.dummyCallback,
|
||||
requiresAdmin: true
|
||||
});
|
||||
|
||||
this.volumeActions.push({
|
||||
action: Action.Edit,
|
||||
title: 'Edit',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false
|
||||
});
|
||||
|
||||
this.chapterActions.push({
|
||||
action: Action.Edit,
|
||||
|
@ -203,6 +196,12 @@ export class ActionFactoryService {
|
|||
title: 'Mark as Unread',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false
|
||||
},
|
||||
{
|
||||
action: Action.Edit,
|
||||
title: 'Info',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<li *ngFor="let member of members" class="list-group-item">
|
||||
<div>
|
||||
<h4>
|
||||
<i class="presence fa fa-circle" title="Active" aria-hidden="true" *ngIf="(presence.onlineUsers$ | async)?.includes(member.username)"></i>{{member.username | titlecase}} <span *ngIf="member.isAdmin" class="badge badge-pill badge-secondary">Admin</span>
|
||||
<i class="presence fa fa-circle" title="Active" aria-hidden="true" *ngIf="false && (presence.onlineUsers$ | async)?.includes(member.username)"></i>{{member.username | titlecase}} <span *ngIf="member.isAdmin" class="badge badge-pill badge-secondary">Admin</span>
|
||||
<div class="float-right" *ngIf="canEditMember(member)">
|
||||
<button class="btn btn-danger mr-2" (click)="deleteUser(member)" placement="top" ngbTooltip="Delete User" attr.aria-label="Delete User {{member.username | titlecase}}"><i class="fa fa-trash" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-secondary mr-2" (click)="updatePassword(member)" placement="top" ngbTooltip="Change Password" attr.aria-label="Change Password for {{member.username | titlecase}}"><i class="fa fa-key" aria-hidden="true"></i></button>
|
||||
|
|
|
@ -30,11 +30,20 @@
|
|||
<h4 *ngIf="!utilityService.isChapter(data)">Chapters</h4>
|
||||
<ul class="list-unstyled">
|
||||
<li class="media my-4" *ngFor="let chapter of chapters">
|
||||
<img class="mr-3" style="width: 74px" src="{{imageService.randomize(imageService.getChapterCoverImage(chapter.id))}}">
|
||||
<a (click)="readChapter(chapter)" href="javascript:void(0);" title="Read Chapter {{chapter.number}}">
|
||||
<img class="mr-3" style="width: 74px" [src]="chapter.coverImage">
|
||||
</a>
|
||||
<div class="media-body">
|
||||
<h5 class="mt-0 mb-1">
|
||||
<span *ngIf="chapter.number !== '0'; else specialHeader">
|
||||
Chapter {{formatChapterNumber(chapter)}}
|
||||
<span class="">
|
||||
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions" [labelBy]="'Chapter' + formatChapterNumber(chapter)"></app-card-actionables>
|
||||
</span> Chapter {{formatChapterNumber(chapter)}}
|
||||
<span class="badge badge-primary badge-pill">
|
||||
<span *ngIf="chapter.pagesRead > 0 && chapter.pagesRead < chapter.pages">{{chapter.pagesRead}} / {{chapter.pages}}</span>
|
||||
<span *ngIf="chapter.pagesRead === 0">UNREAD</span>
|
||||
<span *ngIf="chapter.pagesRead === chapter.pages">READ</span>
|
||||
</span>
|
||||
</span>
|
||||
<ng-template #specialHeader>File(s)</ng-template>
|
||||
</h5>
|
||||
|
@ -57,7 +66,7 @@
|
|||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-info" (click)="updateCover()">Update Cover</button>
|
||||
<button type="button" class="btn btn-info" [disabled]="!isAdmin" (click)="updateCover()">Update Cover</button>
|
||||
<button type="submit" class="btn btn-primary" (click)="close()">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { MangaFile } from 'src/app/_models/manga-file';
|
||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||
import { Volume } from 'src/app/_models/volume';
|
||||
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 { UploadService } from 'src/app/_services/upload.service';
|
||||
import { ChangeCoverImageModalComponent } from '../change-cover-image/change-cover-image-modal.component';
|
||||
|
@ -21,6 +25,7 @@ export class CardDetailsModalComponent implements OnInit {
|
|||
|
||||
@Input() parentName = '';
|
||||
@Input() seriesId: number = 0;
|
||||
@Input() libraryId: number = 0;
|
||||
@Input() data!: any; // Volume | Chapter
|
||||
isChapter = false;
|
||||
chapters: Chapter[] = [];
|
||||
|
@ -31,20 +36,34 @@ export class CardDetailsModalComponent implements OnInit {
|
|||
* If a cover image update occured.
|
||||
*/
|
||||
coverImageUpdate: boolean = false;
|
||||
isAdmin: boolean = false;
|
||||
actions: ActionItem<any>[] = [];
|
||||
chapterActions: ActionItem<Chapter>[] = [];
|
||||
|
||||
|
||||
constructor(private modalService: NgbModal, public modal: NgbActiveModal, public utilityService: UtilityService,
|
||||
public imageService: ImageService, private uploadService: UploadService, private toastr: ToastrService) { }
|
||||
public imageService: ImageService, private uploadService: UploadService, private toastr: ToastrService,
|
||||
private accountService: AccountService, private actionFactoryService: ActionFactoryService,
|
||||
private actionService: ActionService, private router: Router) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isChapter = this.utilityService.isChapter(this.data);
|
||||
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
this.isAdmin = this.accountService.hasAdminRole(user);
|
||||
}
|
||||
});
|
||||
|
||||
this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this)).filter(item => item.action !== Action.Edit);
|
||||
|
||||
if (this.isChapter) {
|
||||
this.chapters.push(this.data);
|
||||
} else if (!this.isChapter) {
|
||||
this.chapters.push(...this.data?.chapters);
|
||||
}
|
||||
this.chapters.sort(this.utilityService.sortChapters);
|
||||
this.chapters.forEach(c => c.coverImage = this.imageService.getChapterCoverImage(c.id));
|
||||
// Try to show an approximation of the reading order for files
|
||||
var collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
|
||||
this.chapters.forEach((c: Chapter) => {
|
||||
|
@ -63,10 +82,17 @@ export class CardDetailsModalComponent implements OnInit {
|
|||
return chapter.number;
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<any>, chapter: Chapter) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action.action, chapter);
|
||||
}
|
||||
}
|
||||
|
||||
updateCover() {
|
||||
const modalRef = this.modalService.open(ChangeCoverImageModalComponent, { size: 'lg' }); // scrollable: true, size: 'lg', windowClass: 'scrollable-modal' (these don't work well on mobile)
|
||||
if (this.utilityService.isChapter(this.data)) {
|
||||
const chapter = this.utilityService.asChapter(this.data)
|
||||
chapter.coverImage = this.imageService.getChapterCoverImage(chapter.id);
|
||||
modalRef.componentInstance.chapter = chapter;
|
||||
modalRef.componentInstance.title = 'Select ' + (chapter.isSpecial ? '' : 'Chapter ') + chapter.range + '\'s Cover';
|
||||
} else {
|
||||
|
@ -85,8 +111,52 @@ export class CardDetailsModalComponent implements OnInit {
|
|||
this.uploadService.resetChapterCoverLock(closeResult.chapter.id).subscribe(() => {
|
||||
this.toastr.info('Please refresh in a bit for the cover image to be reflected.');
|
||||
});
|
||||
} else {
|
||||
closeResult.chapter.coverImage = this.imageService.randomize(this.imageService.getChapterCoverImage(closeResult.chapter.id));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
markChapterAsRead(chapter: Chapter) {
|
||||
if (this.seriesId === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionService.markChapterAsRead(this.seriesId, chapter, () => { /* No Action */ });
|
||||
}
|
||||
|
||||
markChapterAsUnread(chapter: Chapter) {
|
||||
if (this.seriesId === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionService.markChapterAsUnread(this.seriesId, chapter, () => { /* No Action */ });
|
||||
}
|
||||
|
||||
handleChapterActionCallback(action: Action, chapter: Chapter) {
|
||||
switch (action) {
|
||||
case(Action.MarkAsRead):
|
||||
this.markChapterAsRead(chapter);
|
||||
break;
|
||||
case(Action.MarkAsUnread):
|
||||
this.markChapterAsUnread(chapter);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
readChapter(chapter: Chapter) {
|
||||
if (chapter.pages === 0) {
|
||||
this.toastr.error('There are no pages. Kavita was not able to read this archive.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (chapter.files.length > 0 && chapter.files[0].format === MangaFormat.EPUB) {
|
||||
this.router.navigate(['library', this.libraryId, 'series', this.seriesId, 'book', chapter.id]);
|
||||
} else {
|
||||
this.router.navigate(['library', this.libraryId, 'series', this.seriesId, 'manga', chapter.id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,10 +53,6 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
private uploadService: UploadService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// this.imageUrls.push({
|
||||
// imageUrl: this.imageService.getSeriesCoverImage(this.series.id),
|
||||
// source: 'Url'
|
||||
// });
|
||||
this.imageUrls.push(this.imageService.getSeriesCoverImage(this.series.id));
|
||||
|
||||
this.libraryService.getLibraryNames().pipe(takeUntil(this.onDestroy)).subscribe(names => {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<div class="card">
|
||||
<div class="overlay" (click)="handleClick()">
|
||||
<img *ngIf="total > 0 || supressArchiveWarning" class="card-img-top lazyload" [src]="imageService.placeholderImage" [attr.data-src]="imageUrl"
|
||||
<img *ngIf="total > 0 || supressArchiveWarning" class="img-top lazyload" [src]="imageService.placeholderImage" [attr.data-src]="imageUrl"
|
||||
(error)="imageService.updateErroredImage($event)" aria-hidden="true" height="230px" width="158px">
|
||||
<img *ngIf="total === 0 && !supressArchiveWarning" class="card-img-top lazyload" [src]="imageService.errorImage" [attr.data-src]="imageUrl"
|
||||
<img *ngIf="total === 0 && !supressArchiveWarning" class="img-top lazyload" [src]="imageService.errorImage" [attr.data-src]="imageUrl"
|
||||
aria-hidden="true" height="230px" width="158px">
|
||||
<div class="progress-banner" *ngIf="read < total && total > 0 && read !== (total -1)">
|
||||
<p><ngb-progressbar type="primary" height="5px" [value]="read" [max]="total"></ngb-progressbar></p>
|
||||
|
|
|
@ -38,7 +38,7 @@ $image-width: 160px;
|
|||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.card-img-top {
|
||||
.img-top {
|
||||
height: $image-height;
|
||||
}
|
||||
|
||||
|
|
|
@ -390,6 +390,7 @@ export class SeriesDetailComponent implements OnInit {
|
|||
modalRef.componentInstance.data = data;
|
||||
modalRef.componentInstance.parentName = this.series?.name;
|
||||
modalRef.componentInstance.seriesId = this.series?.id;
|
||||
modalRef.componentInstance.libraryId = this.series?.libraryId;
|
||||
modalRef.closed.subscribe((result: {coverImageUpdate: boolean}) => {
|
||||
if (result.coverImageUpdate) {
|
||||
this.coverImageOffset += 1;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue