Kavita/UI/Web/src/app/cards/bulk-selection.service.ts
Joseph Milazzo 9d7476a367
Send To Device Support (#1557)
* Tweaked the logging output

* Started implementing some basic idea for devices

* Updated Email Service with new API routes

* Implemented basic DB structure and some APIs to prep for the UI and flows.

* Added an abstract class to make Unit testing easier.

* Removed dependency we don't need

* Updated the UI to be able to show devices and add new devices. Email field will update the platform if the user hasn't interacted with it already.

* Added ability to delete a device as well

* Basic ability to send files to devices works

* Refactored Action code to pass ActionItem back and allow for dynamic children based on an Observable (api).

Hooked in ability to send a chapter to a device. There is no logic in the FE to validate type.

* Fixed a broken unit test

* Implemented the ability to edit a device

* Code cleanup

* Fixed a bad success message

* Fixed broken unit test from updating mock layer
2022-09-23 15:41:29 -07:00

166 lines
5.8 KiB
TypeScript

import { Injectable } from '@angular/core';
import { NavigationStart, Router } from '@angular/router';
import { ReplaySubject } from 'rxjs';
import { filter } from 'rxjs/operators';
import { Action, ActionFactoryService, ActionItem } from '../_services/action-factory.service';
type DataSource = 'volume' | 'chapter' | 'special' | 'series' | 'bookmark';
/**
* 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;
private actionsSource = new ReplaySubject<ActionItem<any>[]>(1);
public actions$ = this.actionsSource.asObservable();
private selectionsSource = new ReplaySubject<number>(1);
/**
* Number of active selections
*/
public selections$ = this.selectionsSource.asObservable();
constructor(router: Router, private actionFactory: ActionFactoryService) {
router.events
.pipe(filter(event => event instanceof NavigationStart))
.subscribe(() => {
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;
this.actionsSource.next(this.getActions(() => {}));
}
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;
this.selectionsSource.next(this.totalSelections());
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;
}
this.selectionsSource.next(this.totalSelections());
}
deselectAll() {
this.selectedCards = {};
this.selectionsSource.next(0);
}
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: ActionItem<any>, 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, Action.AddToCollection, Action.Delete, Action.AddToWantToReadList, Action.RemoveFromWantToReadList];
if (Object.keys(this.selectedCards).filter(item => item === 'series').length > 0) {
return this.actionFactory.getSeriesActions(callback).filter(item => allowedActions.includes(item.action));
}
if (Object.keys(this.selectedCards).filter(item => item === 'bookmark').length > 0) {
return this.actionFactory.getBookmarkActions(callback);
}
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);
}
}
}