Bulk Operations (#596)

* Implemented the ability to perform multi-selections on cards. Basic selection code is done, CSS needed and exposing actions.

* Implemented a bulk selection bar. Added logic to the card on when to force show checkboxes.

* Fixed some bad parsing groups and cases for Comic Chapters.

* Hooked up some bulk actions on series detail page. Not hooked up to backend yet.

* Fixes #593. URI Enocde library names as sometimes they can have & in them.

* Implemented the ability to mark volume/chapters as read/unread.

* Hooked up mark as unread with specials as well.

* Add to reading list hooked up for Series Detail

* Implemented ability to add multiple series to a reading list.

* Implemented bulk selection for series cards

* Added comments to the new code in ReaderService.cs

* Implemented proper styling on bulk operation bar and integrated for collections.

* Fixed an issue with shift clicking

* Cleaned up css of bulk operations bar

* Code cleanup
This commit is contained in:
Joseph Milazzo 2021-09-24 17:27:47 -07:00 committed by GitHub
parent 52c4285168
commit f5229fd0e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 1129 additions and 172 deletions

View file

@ -1,17 +1,18 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Component, HostListener, OnDestroy, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal, NgbRatingConfig } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { Subject } from 'rxjs';
import { finalize, take, takeUntil, takeWhile } from 'rxjs/operators';
import { BulkSelectionService } from '../cards/bulk-selection.service';
import { CardDetailsModalComponent } from '../cards/_modals/card-details-modal/card-details-modal.component';
import { EditSeriesModalComponent } from '../cards/_modals/edit-series-modal/edit-series-modal.component';
import { ConfirmConfig } from '../shared/confirm-dialog/_models/confirm-config';
import { ConfirmService } from '../shared/confirm.service';
import { TagBadgeCursor } from '../shared/tag-badge/tag-badge.component';
import { DownloadService } from '../shared/_services/download.service';
import { UtilityService } from '../shared/_services/utility.service';
import { KEY_CODES, UtilityService } from '../shared/_services/utility.service';
import { ReviewSeriesModalComponent } from '../_modals/review-series-modal/review-series-modal.component';
import { Chapter } from '../_models/chapter';
import { ScanSeriesEvent } from '../_models/events/scan-series-event';
@ -53,6 +54,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
seriesActions: ActionItem<Series>[] = [];
volumeActions: ActionItem<Volume>[] = [];
chapterActions: ActionItem<Chapter>[] = [];
bulkActions: ActionItem<any>[] = [];
hasSpecials = false;
specials: Array<Chapter> = [];
@ -88,6 +90,48 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
*/
trackByChapterIdentity = (index: number, item: Chapter) => `${item.title}_${item.number}_${item.pagesRead}`;
bulkActionCallback = (action: Action, data: any) => {
console.log('handling bulk action callback');
if (this.series === undefined) {
return;
}
const seriesId = this.series.id;
// we need to figure out what is actually selected now
const selectedVolumeIndexes = this.bulkSelectionService.getSelectedCardsForSource('volume');
const selectedChapterIndexes = this.bulkSelectionService.getSelectedCardsForSource('chapter');
const selectedSpecialIndexes = this.bulkSelectionService.getSelectedCardsForSource('special');
const selectedChapterIds = this.chapters.filter((chapter, index: number) => selectedChapterIndexes.includes(index + ''));
const selectedVolumeIds = this.volumes.filter((volume, index: number) => selectedVolumeIndexes.includes(index + ''));
const selectedSpecials = this.specials.filter((chapter, index: number) => selectedSpecialIndexes.includes(index + ''));
const chapters = [...selectedChapterIds, ...selectedSpecials];
switch (action) {
case Action.AddToReadingList:
this.actionService.addMultipleToReadingList(seriesId, selectedVolumeIds, chapters, () => this.actionInProgress = false);
break;
case Action.MarkAsRead:
console.log('marking volumes as read: ', selectedVolumeIds)
console.log('marking chapters as read: ', chapters)
this.actionService.markMultipleAsRead(seriesId, selectedVolumeIds, chapters, () => {
this.setContinuePoint();
this.actionInProgress = false;
});
break;
case Action.MarkAsUnread:
console.log('marking volumes as unread: ', selectedVolumeIds)
console.log('marking chapters as unread: ', chapters)
this.actionService.markMultipleAsUnread(seriesId, selectedVolumeIds, chapters, () => {
this.setContinuePoint();
this.actionInProgress = false;
});
break;
}
}
private onDestroy: Subject<void> = new Subject();
@ -111,7 +155,8 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
private actionFactoryService: ActionFactoryService, private libraryService: LibraryService,
private confirmService: ConfirmService, private titleService: Title,
private downloadService: DownloadService, private actionService: ActionService,
public imageSerivce: ImageService, private messageHub: MessageHubService) {
public imageSerivce: ImageService, private messageHub: MessageHubService,
public bulkSelectionService: BulkSelectionService) {
ratingConfig.max = 5;
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
@ -151,6 +196,20 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
this.onDestroy.complete();
}
@HostListener('document:keydown.shift', ['$event'])
handleKeypress(event: KeyboardEvent) {
if (event.key === KEY_CODES.SHIFT) {
this.bulkSelectionService.isShiftDown = true;
}
}
@HostListener('document:keyup.shift', ['$event'])
handleKeyUp(event: KeyboardEvent) {
if (event.key === KEY_CODES.SHIFT) {
this.bulkSelectionService.isShiftDown = false;
}
}
handleSeriesActionCallback(action: Action, series: Series) {
this.actionInProgress = true;
switch(action) {
@ -281,6 +340,7 @@ 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.getVolumes(this.series.id).subscribe(volumes => {
this.chapters = volumes.filter(v => v.number === 0).map(v => v.chapters || []).flat().sort(this.utilityService.sortChapters);
@ -490,7 +550,6 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
}
downloadSeries() {
this.downloadService.downloadSeriesSize(this.series.id).pipe(take(1)).subscribe(async (size) => {
const wantToDownload = await this.downloadService.confirmSize(size, 'series');
if (!wantToDownload) { return; }