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:
parent
52c4285168
commit
f5229fd0e6
38 changed files with 1129 additions and 172 deletions
147
UI/Web/src/app/cards/bulk-selection.service.ts
Normal file
147
UI/Web/src/app/cards/bulk-selection.service.ts
Normal file
|
@ -0,0 +1,147 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { NavigationStart, Router } from '@angular/router';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { Action, ActionFactoryService } from '../_services/action-factory.service';
|
||||
|
||||
type DataSource = 'volume' | 'chapter' | 'special' | 'series';
|
||||
|
||||
/**
|
||||
* Responsible for handling selections on cards. Can handle multiple card sources next to each other in different loops.
|
||||
* This will clear selections between pages.
|
||||
*
|
||||
* Remakrs: Page which renders cards is responsible for listening for shift keydown/keyup and updating our state variable.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class BulkSelectionService {
|
||||
|
||||
private debug: boolean = false;
|
||||
private prevIndex: number = 0;
|
||||
private prevDataSource!: DataSource;
|
||||
private selectedCards: { [key: string]: {[key: number]: boolean} } = {};
|
||||
private dataSourceMax: { [key: string]: number} = {};
|
||||
public isShiftDown: boolean = false;
|
||||
|
||||
constructor(private router: Router, private actionFactory: ActionFactoryService) {
|
||||
router.events
|
||||
.pipe(filter(event => event instanceof NavigationStart))
|
||||
.subscribe((event) => {
|
||||
this.deselectAll();
|
||||
this.dataSourceMax = {};
|
||||
this.prevIndex = 0;
|
||||
});
|
||||
}
|
||||
|
||||
handleCardSelection(dataSource: DataSource, index: number, maxIndex: number, wasSelected: boolean) {
|
||||
if (this.isShiftDown) {
|
||||
|
||||
if (dataSource === this.prevDataSource) {
|
||||
this.debugLog('Selecting ' + dataSource + ' cards from ' + this.prevIndex + ' to ' + index);
|
||||
this.selectCards(dataSource, this.prevIndex, index, !wasSelected);
|
||||
} else {
|
||||
const isForwardSelection = index < this.prevIndex;
|
||||
|
||||
if (isForwardSelection) {
|
||||
this.debugLog('Selecting ' + this.prevDataSource + ' cards from ' + this.prevIndex + ' to ' + this.dataSourceMax[this.prevDataSource]);
|
||||
this.selectCards(this.prevDataSource, this.prevIndex, this.dataSourceMax[this.prevDataSource], !wasSelected);
|
||||
this.debugLog('Selecting ' + dataSource + ' cards from ' + 0 + ' to ' + index);
|
||||
this.selectCards(dataSource, 0, index, !wasSelected);
|
||||
} else {
|
||||
this.debugLog('Selecting ' + this.prevDataSource + ' cards from ' + 0 + ' to ' + this.prevIndex);
|
||||
this.selectCards(this.prevDataSource, this.prevIndex, 0, !wasSelected);
|
||||
this.debugLog('Selecting ' + dataSource + ' cards from ' + index + ' to ' + maxIndex);
|
||||
this.selectCards(dataSource, index, maxIndex, !wasSelected);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.debugLog('Selecting ' + dataSource + ' cards at ' + index);
|
||||
this.selectCards(dataSource, index, index, !wasSelected);
|
||||
}
|
||||
this.prevIndex = index;
|
||||
this.prevDataSource = dataSource;
|
||||
this.dataSourceMax[dataSource] = maxIndex;
|
||||
}
|
||||
|
||||
isCardSelected(dataSource: DataSource, index: number) {
|
||||
if (this.selectedCards.hasOwnProperty(dataSource) && this.selectedCards[dataSource].hasOwnProperty(index)) {
|
||||
return this.selectedCards[dataSource][index];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
selectCards(dataSource: DataSource, from: number, to: number, value: boolean) {
|
||||
if (!this.selectedCards.hasOwnProperty(dataSource)) {
|
||||
this.selectedCards[dataSource] = {};
|
||||
}
|
||||
|
||||
if (from === to) {
|
||||
this.selectedCards[dataSource][to] = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (from > to) {
|
||||
for (let i = to; i <= from; i++) {
|
||||
this.selectedCards[dataSource][i] = value;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = from; i <= to; i++) {
|
||||
this.selectedCards[dataSource][i] = value;
|
||||
}
|
||||
}
|
||||
|
||||
deselectAll() {
|
||||
this.selectedCards = {};
|
||||
}
|
||||
|
||||
hasSelections() {
|
||||
const keys = Object.keys(this.selectedCards);
|
||||
return keys.filter(key => {
|
||||
return Object.values(this.selectedCards[key]).filter(item => item).length > 0;
|
||||
}).length > 0;
|
||||
}
|
||||
|
||||
totalSelections() {
|
||||
let sum = 0;
|
||||
const keys = Object.keys(this.selectedCards);
|
||||
keys.forEach(key => {
|
||||
sum += Object.values(this.selectedCards[key]).filter(item => item).length;
|
||||
});
|
||||
return sum;
|
||||
}
|
||||
|
||||
getSelectedCardsForSource(dataSource: DataSource) {
|
||||
if (!this.selectedCards.hasOwnProperty(dataSource)) return [];
|
||||
|
||||
let ret = [];
|
||||
for(let k in this.selectedCards[dataSource]) {
|
||||
if (this.selectedCards[dataSource][k]) {
|
||||
ret.push(k);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
getActions(callback: (action: Action, data: any) => void) {
|
||||
// checks if series is present. If so, returns only series actions
|
||||
// else returns volume/chapter items
|
||||
const allowedActions = [Action.AddToReadingList, Action.MarkAsRead, Action.MarkAsUnread];
|
||||
if (Object.keys(this.selectedCards).filter(item => item === 'series').length > 0) {
|
||||
return this.actionFactory.getSeriesActions(callback).filter(item => allowedActions.includes(item.action));
|
||||
}
|
||||
|
||||
return this.actionFactory.getVolumeActions(callback).filter(item => allowedActions.includes(item.action));
|
||||
}
|
||||
|
||||
private debugLog(message: string, extraData?: any) {
|
||||
if (!this.debug) return;
|
||||
|
||||
if (extraData !== undefined) {
|
||||
console.log(message, extraData);
|
||||
} else {
|
||||
console.log(message);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue