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
This commit is contained in:
Joseph Milazzo 2022-09-23 17:41:29 -05:00 committed by GitHub
parent ab0f13ef74
commit 9d7476a367
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
79 changed files with 3026 additions and 157 deletions

View file

@ -11,7 +11,7 @@ import { BulkSelectionService } from '../bulk-selection.service';
})
export class BulkOperationsComponent implements OnInit, OnDestroy {
@Input() actionCallback!: (action: Action, data: any) => void;
@Input() actionCallback!: (action: ActionItem<any>, data: any) => void;
topOffset: number = 56;
hasMarkAsRead: boolean = false;
@ -41,13 +41,13 @@ export class BulkOperationsComponent implements OnInit, OnDestroy {
this.onDestory.complete();
}
handleActionCallback(action: Action, data: any) {
handleActionCallback(action: ActionItem<any>, data: any) {
this.actionCallback(action, data);
}
performAction(action: ActionItem<any>) {
if (typeof action.callback === 'function') {
action.callback(action.action, null);
action.callback(action, null);
}
}

View file

@ -139,7 +139,7 @@ export class BulkSelectionService {
return ret;
}
getActions(callback: (action: Action, data: any) => void) {
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];

View file

@ -8,6 +8,7 @@ import { DownloadService } from 'src/app/shared/_services/download.service';
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
import { Chapter } from 'src/app/_models/chapter';
import { ChapterMetadata } from 'src/app/_models/chapter-metadata';
import { Device } from 'src/app/_models/device/device';
import { LibraryType } from 'src/app/_models/library';
import { MangaFile } from 'src/app/_models/manga-file';
import { MangaFormat } from 'src/app/_models/manga-format';
@ -16,6 +17,7 @@ import { Volume } from 'src/app/_models/volume';
import { AccountService } from 'src/app/_services/account.service';
import { ActionItem, ActionFactoryService, Action } from 'src/app/_services/action-factory.service';
import { ActionService } from 'src/app/_services/action.service';
import { DeviceService } from 'src/app/_services/device.service';
import { ImageService } from 'src/app/_services/image.service';
import { LibraryService } from 'src/app/_services/library.service';
import { MetadataService } from 'src/app/_services/metadata.service';
@ -100,7 +102,8 @@ export class CardDetailDrawerComponent implements OnInit, OnDestroy {
private accountService: AccountService, private actionFactoryService: ActionFactoryService,
private actionService: ActionService, private router: Router, private libraryService: LibraryService,
private seriesService: SeriesService, private readerService: ReaderService, public metadataService: MetadataService,
public activeOffcanvas: NgbActiveOffcanvas, private downloadService: DownloadService, private readonly cdRef: ChangeDetectorRef) {
public activeOffcanvas: NgbActiveOffcanvas, private downloadService: DownloadService, private readonly cdRef: ChangeDetectorRef,
private deviceSerivce: DeviceService) {
this.isAdmin$ = this.accountService.currentUser$.pipe(
takeUntil(this.onDestroy),
map(user => (user && this.accountService.hasAdminRole(user)) || false),
@ -166,7 +169,7 @@ export class CardDetailDrawerComponent implements OnInit, OnDestroy {
performAction(action: ActionItem<any>, chapter: Chapter) {
if (typeof action.callback === 'function') {
action.callback(action.action, chapter);
action.callback(action, chapter);
}
}
@ -196,8 +199,8 @@ export class CardDetailDrawerComponent implements OnInit, OnDestroy {
this.actionService.markChapterAsUnread(this.seriesId, chapter, () => { this.cdRef.markForCheck(); });
}
handleChapterActionCallback(action: Action, chapter: Chapter) {
switch (action) {
handleChapterActionCallback(action: ActionItem<Chapter>, chapter: Chapter) {
switch (action.action) {
case(Action.MarkAsRead):
this.markChapterAsRead(chapter);
break;
@ -216,6 +219,14 @@ export class CardDetailDrawerComponent implements OnInit, OnDestroy {
case (Action.Read):
this.readChapter(chapter, false);
break;
case (Action.SendTo):
{
const device = (action._extra.data as Device);
this.deviceSerivce.sendTo(chapter.id, device.id).subscribe(() => {
this.toastr.success('File emailed to ' + device.name);
});
break;
}
default:
break;
}

View file

@ -135,7 +135,7 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges {
performAction(action: ActionItem<any>) {
if (typeof action.callback === 'function') {
action.callback(action.action, undefined);
action.callback(action, undefined);
}
}

View file

@ -1,6 +1,6 @@
<ng-container *ngIf="actions.length > 0">
<div ngbDropdown container="body" class="d-inline-block">
<button [disabled]="disabled" class="btn {{btnClass}}" id="actions-{{labelBy}}" ngbDropdownToggle (click)="preventClick($event)"><i class="fa {{iconClass}}" aria-hidden="true"></i></button>
<button [disabled]="disabled" class="btn {{btnClass}}" id="actions-{{labelBy}}" ngbDropdownToggle (click)="preventEvent($event)"><i class="fa {{iconClass}}" aria-hidden="true"></i></button>
<div ngbDropdownMenu attr.aria-labelledby="actions-{{labelBy}}">
<ng-container *ngTemplateOutlet="submenu; context: { list: actions }"></ng-container>
</div>
@ -8,10 +8,17 @@
<ng-template #submenu let-list="list">
<ng-container *ngFor="let action of list">
<ng-container *ngIf="action.children === undefined || action?.children?.length === 0 else submenuDropdown">
<button ngbDropdownItem *ngIf="willRenderAction(action)" (click)="performAction($event, action)">{{action.title}}</button>
<ng-container *ngIf="action.dynamicList != undefined; else justItem">
<ng-container *ngFor="let dynamicItem of (action.dynamicList | async)">
<button ngbDropdownItem (click)="performDynamicClick($event, action, dynamicItem)">{{dynamicItem.title}}</button>
</ng-container>
</ng-container>
<ng-template #justItem>
<button ngbDropdownItem *ngIf="willRenderAction(action)" (click)="performAction($event, action)">{{action.title}}</button>
</ng-template>
</ng-container>
<ng-template #submenuDropdown>
<div ngbDropdown #subMenuHover="ngbDropdown" placement="right" (mouseover)="preventClick($event); openSubmenu(action.title, subMenuHover)" (mouseleave)="preventClick($event)">
<div ngbDropdown #subMenuHover="ngbDropdown" placement="right" (mouseover)="preventEvent($event); openSubmenu(action.title, subMenuHover)" (mouseleave)="preventEvent($event)">
<button id="actions-{{action.title}}" class="submenu-toggle" ngbDropdownToggle>{{action.title}} <i class="fa-solid fa-angle-right submenu-icon"></i></button>
<div ngbDropdownMenu attr.aria-labelledby="actions-{{action.title}}">
<ng-container *ngTemplateOutlet="submenu; context: { list: action.children }"></ng-container>

View file

@ -34,13 +34,13 @@ export class CardActionablesComponent implements OnInit {
});
}
preventClick(event: any) {
preventEvent(event: any) {
event.stopPropagation();
event.preventDefault();
}
performAction(event: any, action: ActionItem<any>) {
this.preventClick(event);
this.preventEvent(event);
if (typeof action.callback === 'function') {
this.actionHandler.emit(action);
@ -66,4 +66,9 @@ export class CardActionablesComponent implements OnInit {
subMenu.open();
}
performDynamicClick(event: any, action: ActionItem<any>, dynamicItem: any) {
action._extra = dynamicItem;
this.performAction(event, action);
}
}

View file

@ -280,12 +280,6 @@ export class CardItemComponent implements OnInit, OnDestroy {
performAction(action: ActionItem<any>) {
if (action.action == Action.Download) {
// if (this.download$ !== null) {
// this.toastr.info('Download is already in progress. Please wait.');
// return;
// }
if (this.utilityService.isVolume(this.entity)) {
const volume = this.utilityService.asVolume(this.entity);
this.downloadService.download('volume', volume);
@ -300,7 +294,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
}
if (typeof action.callback === 'function') {
action.callback(action.action, this.entity);
action.callback(action, this.entity);
}
}

View file

@ -134,7 +134,7 @@ export class ListItemComponent implements OnInit, OnDestroy {
}
if (typeof action.callback === 'function') {
action.callback(action.action, this.entity);
action.callback(action, this.entity);
}
}
}

View file

@ -64,7 +64,7 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
ngOnChanges(changes: any) {
if (this.data) {
this.actions = this.actionFactoryService.getSeriesActions((action: Action, series: Series) => this.handleSeriesActionCallback(action, series));
this.actions = this.actionFactoryService.getSeriesActions((action: ActionItem<Series>, series: Series) => this.handleSeriesActionCallback(action, series));
this.cdRef.markForCheck();
}
}
@ -74,8 +74,8 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
this.onDestroy.complete();
}
handleSeriesActionCallback(action: Action, series: Series) {
switch (action) {
handleSeriesActionCallback(action: ActionItem<Series>, series: Series) {
switch (action.action) {
case(Action.MarkAsRead):
this.markAsRead(series);
break;