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:
parent
ab0f13ef74
commit
9d7476a367
79 changed files with 3026 additions and 157 deletions
8
UI/Web/src/app/_models/device/device-platform.ts
Normal file
8
UI/Web/src/app/_models/device/device-platform.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export enum DevicePlatform {
|
||||
Custom = 0,
|
||||
PocketBook = 1,
|
||||
Kindle = 2,
|
||||
Kobo = 3
|
||||
}
|
||||
|
||||
export const devicePlatforms = [DevicePlatform.Custom, DevicePlatform.Kindle, DevicePlatform.Kobo, DevicePlatform.PocketBook];
|
||||
9
UI/Web/src/app/_models/device/device.ts
Normal file
9
UI/Web/src/app/_models/device/device.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { DevicePlatform } from "./device-platform";
|
||||
|
||||
export interface Device {
|
||||
id: number;
|
||||
name: string;
|
||||
platform: DevicePlatform;
|
||||
emailAddress: string;
|
||||
lastUsed: string;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable, OnDestroy } from '@angular/core';
|
||||
import { Observable, of, ReplaySubject, Subject } from 'rxjs';
|
||||
import { of, ReplaySubject, Subject } from 'rxjs';
|
||||
import { filter, map, switchMap, takeUntil } from 'rxjs/operators';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { Preferences } from '../_models/preferences/preferences';
|
||||
|
|
@ -10,6 +10,7 @@ import { EVENTS, MessageHubService } from './message-hub.service';
|
|||
import { ThemeService } from './theme.service';
|
||||
import { InviteUserResponse } from '../_models/invite-user-response';
|
||||
import { UserUpdateEvent } from '../_models/events/user-update-event';
|
||||
import { DeviceService } from './device.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
|
@ -66,7 +67,7 @@ export class AccountService implements OnDestroy {
|
|||
return this.httpClient.get<string[]>(this.baseUrl + 'account/roles');
|
||||
}
|
||||
|
||||
login(model: {username: string, password: string}): Observable<any> {
|
||||
login(model: {username: string, password: string}) {
|
||||
return this.httpClient.post<User>(this.baseUrl + 'account/login', model).pipe(
|
||||
map((response: User) => {
|
||||
const user = response;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { map, Observable, shareReplay } from 'rxjs';
|
||||
import { Chapter } from '../_models/chapter';
|
||||
import { CollectionTag } from '../_models/collection-tag';
|
||||
import { Device } from '../_models/device/device';
|
||||
import { Library } from '../_models/library';
|
||||
import { ReadingList } from '../_models/reading-list';
|
||||
import { Series } from '../_models/series';
|
||||
import { Volume } from '../_models/volume';
|
||||
import { AccountService } from './account.service';
|
||||
import { DeviceService } from './device.service';
|
||||
|
||||
export enum Action {
|
||||
AddTo = -2,
|
||||
Others = -1,
|
||||
Submenu = -1,
|
||||
/**
|
||||
* Mark entity as read
|
||||
*/
|
||||
|
|
@ -78,14 +80,26 @@ export enum Action {
|
|||
* Remove from user's Want to Read List
|
||||
*/
|
||||
RemoveFromWantToReadList = 16,
|
||||
/**
|
||||
* Send to a device
|
||||
*/
|
||||
SendTo = 17,
|
||||
}
|
||||
|
||||
export interface ActionItem<T> {
|
||||
title: string;
|
||||
action: Action;
|
||||
callback: (action: Action, data: T) => void;
|
||||
callback: (action: ActionItem<T>, data: T) => void;
|
||||
requiresAdmin: boolean;
|
||||
children: Array<ActionItem<T>>;
|
||||
/**
|
||||
* Indicates that there exists a separate list will be loaded from an API
|
||||
*/
|
||||
dynamicList?: Observable<{title: string, data: any}[]> | undefined;
|
||||
/**
|
||||
* Extra data that needs to be sent back from the card item. Used mainly for dynamicList. This will be the item from dyanamicList return
|
||||
*/
|
||||
_extra?: any;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
|
|
@ -109,7 +123,7 @@ export class ActionFactoryService {
|
|||
isAdmin = false;
|
||||
hasDownloadRole = false;
|
||||
|
||||
constructor(private accountService: AccountService) {
|
||||
constructor(private accountService: AccountService, private deviceService: DeviceService) {
|
||||
this.accountService.currentUser$.subscribe((user) => {
|
||||
if (user) {
|
||||
this.isAdmin = this.accountService.hasAdminRole(user);
|
||||
|
|
@ -123,35 +137,35 @@ export class ActionFactoryService {
|
|||
});
|
||||
}
|
||||
|
||||
getLibraryActions(callback: (action: Action, library: Library) => void) {
|
||||
getLibraryActions(callback: (action: ActionItem<Library>, library: Library) => void) {
|
||||
return this.applyCallbackToList(this.libraryActions, callback);
|
||||
}
|
||||
|
||||
getSeriesActions(callback: (action: Action, series: Series) => void) {
|
||||
getSeriesActions(callback: (action: ActionItem<Series>, series: Series) => void) {
|
||||
return this.applyCallbackToList(this.seriesActions, callback);
|
||||
}
|
||||
|
||||
getVolumeActions(callback: (action: Action, volume: Volume) => void) {
|
||||
getVolumeActions(callback: (action: ActionItem<Volume>, volume: Volume) => void) {
|
||||
return this.applyCallbackToList(this.volumeActions, callback);
|
||||
}
|
||||
|
||||
getChapterActions(callback: (action: Action, chapter: Chapter) => void) {
|
||||
getChapterActions(callback: (action: ActionItem<Chapter>, chapter: Chapter) => void) {
|
||||
return this.applyCallbackToList(this.chapterActions, callback);
|
||||
}
|
||||
|
||||
getCollectionTagActions(callback: (action: Action, collectionTag: CollectionTag) => void) {
|
||||
getCollectionTagActions(callback: (action: ActionItem<CollectionTag>, collectionTag: CollectionTag) => void) {
|
||||
return this.applyCallbackToList(this.collectionTagActions, callback);
|
||||
}
|
||||
|
||||
getReadingListActions(callback: (action: Action, readingList: ReadingList) => void) {
|
||||
getReadingListActions(callback: (action: ActionItem<ReadingList>, readingList: ReadingList) => void) {
|
||||
return this.applyCallbackToList(this.readingListActions, callback);
|
||||
}
|
||||
|
||||
getBookmarkActions(callback: (action: Action, series: Series) => void) {
|
||||
getBookmarkActions(callback: (action: ActionItem<Series>, series: Series) => void) {
|
||||
return this.applyCallbackToList(this.bookmarkActions, callback);
|
||||
}
|
||||
|
||||
dummyCallback(action: Action, data: any) {}
|
||||
dummyCallback(action: ActionItem<any>, data: any) {}
|
||||
|
||||
_resetActions() {
|
||||
this.libraryActions = [
|
||||
|
|
@ -163,7 +177,7 @@ export class ActionFactoryService {
|
|||
children: [],
|
||||
},
|
||||
{
|
||||
action: Action.Others,
|
||||
action: Action.Submenu,
|
||||
title: 'Others',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: true,
|
||||
|
|
@ -212,7 +226,7 @@ export class ActionFactoryService {
|
|||
children: [],
|
||||
},
|
||||
{
|
||||
action: Action.AddTo,
|
||||
action: Action.Submenu,
|
||||
title: 'Add to',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false,
|
||||
|
|
@ -262,7 +276,7 @@ export class ActionFactoryService {
|
|||
children: [],
|
||||
},
|
||||
{
|
||||
action: Action.Others,
|
||||
action: Action.Submenu,
|
||||
title: 'Others',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false,
|
||||
|
|
@ -315,7 +329,7 @@ export class ActionFactoryService {
|
|||
children: [],
|
||||
},
|
||||
{
|
||||
action: Action.AddTo,
|
||||
action: Action.Submenu,
|
||||
title: 'Add to',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false,
|
||||
|
|
@ -368,7 +382,7 @@ export class ActionFactoryService {
|
|||
children: [],
|
||||
},
|
||||
{
|
||||
action: Action.AddTo,
|
||||
action: Action.Submenu,
|
||||
title: 'Add to',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false,
|
||||
|
|
@ -397,6 +411,24 @@ export class ActionFactoryService {
|
|||
requiresAdmin: false,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
action: Action.Submenu,
|
||||
title: 'Send To',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false,
|
||||
children: [
|
||||
{
|
||||
action: Action.SendTo,
|
||||
title: '',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false,
|
||||
dynamicList: this.deviceService.devices$.pipe(map(devices => devices.map(d => {
|
||||
return {'title': d.name, 'data': d};
|
||||
}), shareReplay())),
|
||||
children: []
|
||||
}
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
this.readingListActions = [
|
||||
|
|
@ -441,7 +473,7 @@ export class ActionFactoryService {
|
|||
];
|
||||
}
|
||||
|
||||
private applyCallback(action: ActionItem<any>, callback: (action: Action, data: any) => void) {
|
||||
private applyCallback(action: ActionItem<any>, callback: (action: ActionItem<any>, data: any) => void) {
|
||||
action.callback = callback;
|
||||
|
||||
if (action.children === null || action.children?.length === 0) return;
|
||||
|
|
@ -451,7 +483,7 @@ export class ActionFactoryService {
|
|||
});
|
||||
}
|
||||
|
||||
private applyCallbackToList(list: Array<ActionItem<any>>, callback: (action: Action, data: any) => void): Array<ActionItem<any>> {
|
||||
private applyCallbackToList(list: Array<ActionItem<any>>, callback: (action: ActionItem<any>, data: any) => void): Array<ActionItem<any>> {
|
||||
const actions = list.map((a) => {
|
||||
return { ...a };
|
||||
});
|
||||
|
|
|
|||
48
UI/Web/src/app/_services/device.service.ts
Normal file
48
UI/Web/src/app/_services/device.service.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ReplaySubject, shareReplay, switchMap, take, tap } from 'rxjs';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { Device } from '../_models/device/device';
|
||||
import { DevicePlatform } from '../_models/device/device-platform';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DeviceService {
|
||||
|
||||
baseUrl = environment.apiUrl;
|
||||
|
||||
private devicesSource: ReplaySubject<Device[]> = new ReplaySubject<Device[]>(1);
|
||||
public devices$ = this.devicesSource.asObservable().pipe(shareReplay());
|
||||
|
||||
|
||||
constructor(private httpClient: HttpClient) {
|
||||
this.httpClient.get<Device[]>(this.baseUrl + 'device', {}).subscribe(data => {
|
||||
this.devicesSource.next(data);
|
||||
});
|
||||
}
|
||||
|
||||
createDevice(name: string, platform: DevicePlatform, emailAddress: string) {
|
||||
return this.httpClient.post(this.baseUrl + 'device/create', {name, platform, emailAddress}, {responseType: 'text' as 'json'});
|
||||
}
|
||||
|
||||
updateDevice(id: number, name: string, platform: DevicePlatform, emailAddress: string) {
|
||||
return this.httpClient.post(this.baseUrl + 'device/update', {id, name, platform, emailAddress}, {responseType: 'text' as 'json'});
|
||||
}
|
||||
|
||||
deleteDevice(id: number) {
|
||||
return this.httpClient.delete(this.baseUrl + 'device?deviceId=' + id);
|
||||
}
|
||||
|
||||
getDevices() {
|
||||
return this.httpClient.get<Device[]>(this.baseUrl + 'device', {}).pipe(tap(data => {
|
||||
this.devicesSource.next(data);
|
||||
}));
|
||||
}
|
||||
|
||||
sendTo(chapterId: number, deviceId: number) {
|
||||
return this.httpClient.post(this.baseUrl + 'device/send-to', {deviceId, chapterId}, {responseType: 'text' as 'json'});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -2,8 +2,11 @@
|
|||
<form [formGroup]="settingsForm" *ngIf="serverSettings !== undefined">
|
||||
<h4>Email Services (SMTP)</h4>
|
||||
<p>Kavita comes out of the box with an email service to power flows like invite user, forgot password, etc. Emails sent via our service are deleted immediately. You can use your own
|
||||
email service, by setting up <a href="https://github.com/Kareadita/KavitaEmail" target="_blank" rel="noopener noreferrer">Kavita Email</a> service. Set the url of the email service and use the Test button to ensure it works. At any time you can reset to the default. There is no way to disable emails although you are not required to use a
|
||||
valid email address for users. Confirmation links will always be saved to logs and presented in the UI. Emails will not be sent if you are not accessing Kavita via a publically reachable url.
|
||||
email service, by setting up <a href="https://github.com/Kareadita/KavitaEmail" target="_blank" rel="noopener noreferrer">Kavita Email</a> service. Set the url of the email service and use the Test button to ensure it works.
|
||||
At any time you can reset to the default. There is no way to disable emails for authentication, although you are not required to use a
|
||||
valid email address for users. Confirmation links will always be saved to logs and presented in the UI.
|
||||
Registration/Confirmation emails will not be sent if you are not accessing Kavita via a publically reachable url.
|
||||
<span class="text-warning">If you want Send To device to work, you must host your own email service.</span>
|
||||
</p>
|
||||
<div class="mb-3">
|
||||
<label for="settings-emailservice" class="form-label">Email Service Url</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="emailServiceTooltip" role="button" tabindex="0"></i>
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@
|
|||
<label for="logging-level-port" class="form-label">Logging Level</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="loggingLevelTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #loggingLevelTooltip>Use debug to help identify issues. Debug can eat up a lot of disk space.</ng-template>
|
||||
<span class="visually-hidden" id="logging-level-port-help">Port the server listens on.</span>
|
||||
<select id="logging-level-port" aria-describedby="logging-level-port-help" class="form-select" aria-describedby="settings-tasks-scan-help" formControlName="loggingLevel">
|
||||
<select id="logging-level-port" aria-describedby="logging-level-port-help" class="form-select" formControlName="loggingLevel">
|
||||
<option *ngFor="let level of logLevels" [value]="level">{{level | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { JumpKey } from '../_models/jumpbar/jump-key';
|
|||
import { Pagination } from '../_models/pagination';
|
||||
import { Series } from '../_models/series';
|
||||
import { FilterEvent, SeriesFilter } from '../_models/series-filter';
|
||||
import { Action } from '../_services/action-factory.service';
|
||||
import { Action, ActionItem } from '../_services/action-factory.service';
|
||||
import { ActionService } from '../_services/action.service';
|
||||
import { EVENTS, Message, MessageHubService } from '../_services/message-hub.service';
|
||||
import { SeriesService } from '../_services/series.service';
|
||||
|
|
@ -34,11 +34,11 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
|
|||
filterActive: boolean = false;
|
||||
jumpbarKeys: Array<JumpKey> = [];
|
||||
|
||||
bulkActionCallback = (action: Action, data: any) => {
|
||||
bulkActionCallback = (action: ActionItem<any>, data: any) => {
|
||||
const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('series');
|
||||
const selectedSeries = this.series.filter((series, index: number) => selectedSeriesIndexies.includes(index + ''));
|
||||
|
||||
switch (action) {
|
||||
switch (action.action) {
|
||||
case Action.AddToReadingList:
|
||||
this.actionService.addMultipleSeriesToReadingList(selectedSeries, (success) => {
|
||||
if (success) this.bulkSelectionService.deselectAll();
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { NavService } from './_services/nav.service';
|
|||
import { filter } from 'rxjs/operators';
|
||||
import { NgbModal, NgbRatingConfig } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { DeviceService } from './_services/device.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
|
|
|
|||
|
|
@ -12,15 +12,18 @@ import { ErrorInterceptor } from './_interceptors/error.interceptor';
|
|||
import { SAVER, getSaver } from './shared/_providers/saver.provider';
|
||||
import { SidenavModule } from './sidenav/sidenav.module';
|
||||
import { NavModule } from './nav/nav.module';
|
||||
import { DevicesComponent } from './devices/devices.component';
|
||||
|
||||
|
||||
// Disable Web Animations if the user's browser (such as iOS 12.5.5) does not support this.
|
||||
const disableAnimations = !('animate' in document.documentElement);
|
||||
if (disableAnimations) console.error("Web Animations have been disabled as your current browser does not support this.");
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
DevicesComponent,
|
||||
],
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
|
|
|
|||
|
|
@ -89,8 +89,8 @@ export class BookmarksComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
async handleAction(action: Action, series: Series) {
|
||||
switch (action) {
|
||||
async handleAction(action: ActionItem<Series>, series: Series) {
|
||||
switch (action.action) {
|
||||
case(Action.Delete):
|
||||
this.clearBookmarks(series);
|
||||
break;
|
||||
|
|
@ -105,12 +105,12 @@ export class BookmarksComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
bulkActionCallback = async (action: Action, data: any) => {
|
||||
bulkActionCallback = async (action: ActionItem<any>, data: any) => {
|
||||
const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('bookmark');
|
||||
const selectedSeries = this.series.filter((series, index: number) => selectedSeriesIndexies.includes(index + ''));
|
||||
const seriesIds = selectedSeries.map(item => item.id);
|
||||
|
||||
switch (action) {
|
||||
switch (action.action) {
|
||||
case Action.DownloadBookmark:
|
||||
this.downloadService.download('bookmark', this.bookmarks.filter(bmk => seriesIds.includes(bmk.seriesId)), (d) => {
|
||||
if (!d) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@ export class AllCollectionsComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
handleCollectionActionCallback(action: Action, collectionTag: CollectionTag) {
|
||||
switch (action) {
|
||||
handleCollectionActionCallback(action: ActionItem<CollectionTag>, collectionTag: CollectionTag) {
|
||||
switch (action.action) {
|
||||
case(Action.Edit):
|
||||
const modalRef = this.modalService.open(EditCollectionTagsComponent, { size: 'lg', scrollable: true });
|
||||
modalRef.componentInstance.tag = collectionTag;
|
||||
|
|
|
|||
|
|
@ -58,11 +58,11 @@ export class CollectionDetailComponent implements OnInit, OnDestroy, AfterConten
|
|||
|
||||
private onDestory: Subject<void> = new Subject<void>();
|
||||
|
||||
bulkActionCallback = (action: Action, data: any) => {
|
||||
bulkActionCallback = (action: ActionItem<any>, data: any) => {
|
||||
const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('series');
|
||||
const selectedSeries = this.series.filter((series, index: number) => selectedSeriesIndexies.includes(index + ''));
|
||||
|
||||
switch (action) {
|
||||
switch (action.action) {
|
||||
case Action.AddToReadingList:
|
||||
this.actionService.addMultipleSeriesToReadingList(selectedSeries, (success) => {
|
||||
if (success) this.bulkSelectionService.deselectAll();
|
||||
|
|
@ -224,8 +224,8 @@ export class CollectionDetailComponent implements OnInit, OnDestroy, AfterConten
|
|||
this.loadPage();
|
||||
}
|
||||
|
||||
handleCollectionActionCallback(action: Action, collectionTag: CollectionTag) {
|
||||
switch (action) {
|
||||
handleCollectionActionCallback(action: ActionItem<CollectionTag>, collectionTag: CollectionTag) {
|
||||
switch (action.action) {
|
||||
case(Action.Edit):
|
||||
this.openEditCollectionTagModal(this.collectionTag);
|
||||
break;
|
||||
|
|
@ -236,7 +236,7 @@ export class CollectionDetailComponent implements OnInit, OnDestroy, AfterConten
|
|||
|
||||
performAction(action: ActionItem<any>) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action.action, this.collectionTag);
|
||||
action.callback(action, this.collectionTag);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
1
UI/Web/src/app/devices/devices.component.html
Normal file
1
UI/Web/src/app/devices/devices.component.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
<p>devices works!</p>
|
||||
0
UI/Web/src/app/devices/devices.component.scss
Normal file
0
UI/Web/src/app/devices/devices.component.scss
Normal file
15
UI/Web/src/app/devices/devices.component.ts
Normal file
15
UI/Web/src/app/devices/devices.component.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-devices',
|
||||
templateUrl: './devices.component.html',
|
||||
styleUrls: ['./devices.component.scss']
|
||||
})
|
||||
export class DevicesComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -52,11 +52,11 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
|||
active = this.tabs[0];
|
||||
|
||||
|
||||
bulkActionCallback = (action: Action, data: any) => {
|
||||
bulkActionCallback = (action: ActionItem<any>, data: any) => {
|
||||
const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('series');
|
||||
const selectedSeries = this.series.filter((series, index: number) => selectedSeriesIndexies.includes(index + ''));
|
||||
|
||||
switch (action) {
|
||||
switch (action.action) {
|
||||
case Action.AddToReadingList:
|
||||
this.actionService.addMultipleSeriesToReadingList(selectedSeries, (success) => {
|
||||
if (success) this.bulkSelectionService.deselectAll();
|
||||
|
|
@ -197,12 +197,12 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
handleAction(action: Action, library: Library) {
|
||||
handleAction(action: ActionItem<Library>, library: Library) {
|
||||
let lib: Partial<Library> = library;
|
||||
if (library === undefined) {
|
||||
lib = {id: this.libraryId, name: this.libraryName};
|
||||
}
|
||||
switch (action) {
|
||||
switch (action.action) {
|
||||
case(Action.Scan):
|
||||
this.actionService.scanLibrary(lib);
|
||||
break;
|
||||
|
|
@ -216,7 +216,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
|||
|
||||
performAction(action: ActionItem<any>) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action.action, undefined);
|
||||
action.callback(action, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ export class ReadingListDetailComponent implements OnInit {
|
|||
|
||||
performAction(action: ActionItem<any>) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action.action, this.readingList);
|
||||
action.callback(action, this.readingList);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -123,8 +123,8 @@ export class ReadingListDetailComponent implements OnInit {
|
|||
this.router.navigate(this.readerService.getNavigationArray(item.libraryId, item.seriesId, item.chapterId, item.seriesFormat), {queryParams: params});
|
||||
}
|
||||
|
||||
handleReadingListActionCallback(action: Action, readingList: ReadingList) {
|
||||
switch(action) {
|
||||
handleReadingListActionCallback(action: ActionItem<ReadingList>, readingList: ReadingList) {
|
||||
switch(action.action) {
|
||||
case Action.Delete:
|
||||
this.deleteList(readingList);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -44,14 +44,14 @@ export class ReadingListsComponent implements OnInit {
|
|||
.filter(action => this.readingListService.actionListFilter(action, readingList, this.isAdmin));
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<any>, readingList: ReadingList) {
|
||||
performAction(action: ActionItem<ReadingList>, readingList: ReadingList) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action.action, readingList);
|
||||
action.callback(action, readingList);
|
||||
}
|
||||
}
|
||||
|
||||
handleReadingListActionCallback(action: Action, readingList: ReadingList) {
|
||||
switch(action) {
|
||||
handleReadingListActionCallback(action: ActionItem<ReadingList>, readingList: ReadingList) {
|
||||
switch(action.action) {
|
||||
case Action.Delete:
|
||||
this.readingListService.delete(readingList.id).subscribe(() => {
|
||||
this.toastr.success('Reading list deleted');
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ import { PageLayoutMode } from '../_models/page-layout-mode';
|
|||
import { DOCUMENT } from '@angular/common';
|
||||
import { User } from '../_models/user';
|
||||
import { ScrollService } from '../_services/scroll.service';
|
||||
import { DeviceService } from '../_services/device.service';
|
||||
import { Device } from '../_models/device/device';
|
||||
|
||||
interface RelatedSeris {
|
||||
series: Series;
|
||||
|
|
@ -158,7 +160,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
|
|||
isAscendingSort: boolean = false; // TODO: Get this from User preferences
|
||||
user: User | undefined;
|
||||
|
||||
bulkActionCallback = (action: Action, data: any) => {
|
||||
bulkActionCallback = (action: ActionItem<any>, data: any) => {
|
||||
if (this.series === undefined) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -177,7 +179,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
|
|||
const selectedSpecials = this.specials.filter((_chapter, index: number) => selectedSpecialIndexes.includes(index + ''));
|
||||
const chapters = [...selectedChapterIds, ...selectedSpecials];
|
||||
|
||||
switch (action) {
|
||||
switch (action.action) {
|
||||
case Action.AddToReadingList:
|
||||
this.actionService.addMultipleToReadingList(seriesId, selectedVolumeIds, chapters, (success) => {
|
||||
this.actionInProgress = false;
|
||||
|
|
@ -250,7 +252,8 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
|
|||
public imageSerivce: ImageService, private messageHub: MessageHubService,
|
||||
private readingListService: ReadingListService, public navService: NavService,
|
||||
private offcanvasService: NgbOffcanvas, @Inject(DOCUMENT) private document: Document,
|
||||
private changeDetectionRef: ChangeDetectorRef, private scrollService: ScrollService
|
||||
private changeDetectionRef: ChangeDetectorRef, private scrollService: ScrollService,
|
||||
private deviceSerivce: DeviceService
|
||||
) {
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
|
|
@ -330,10 +333,10 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
|
|||
this.changeDetectionRef.markForCheck();
|
||||
}
|
||||
|
||||
handleSeriesActionCallback(action: Action, series: Series) {
|
||||
handleSeriesActionCallback(action: ActionItem<Series>, series: Series) {
|
||||
this.actionInProgress = true;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
switch(action) {
|
||||
switch(action.action) {
|
||||
case(Action.MarkAsRead):
|
||||
this.actionService.markSeriesAsRead(series, (series: Series) => {
|
||||
this.actionInProgress = false;
|
||||
|
|
@ -400,8 +403,8 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
|
|||
}
|
||||
}
|
||||
|
||||
handleVolumeActionCallback(action: Action, volume: Volume) {
|
||||
switch(action) {
|
||||
handleVolumeActionCallback(action: ActionItem<Volume>, volume: Volume) {
|
||||
switch(action.action) {
|
||||
case(Action.MarkAsRead):
|
||||
this.markVolumeAsRead(volume);
|
||||
break;
|
||||
|
|
@ -424,8 +427,8 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
|
|||
}
|
||||
}
|
||||
|
||||
handleChapterActionCallback(action: Action, chapter: Chapter) {
|
||||
switch (action) {
|
||||
handleChapterActionCallback(action: ActionItem<Chapter>, chapter: Chapter) {
|
||||
switch (action.action) {
|
||||
case(Action.MarkAsRead):
|
||||
this.markChapterAsRead(chapter);
|
||||
break;
|
||||
|
|
@ -441,6 +444,14 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
|
|||
case(Action.IncognitoRead):
|
||||
this.openChapter(chapter, true);
|
||||
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;
|
||||
}
|
||||
|
|
@ -748,7 +759,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
|
|||
|
||||
performAction(action: ActionItem<any>) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action.action, this.series);
|
||||
action.callback(action, this.series);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -76,8 +76,8 @@ export class SideNavComponent implements OnInit, OnDestroy {
|
|||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
handleAction(action: Action, library: Library) {
|
||||
switch (action) {
|
||||
handleAction(action: ActionItem<Library>, library: Library) {
|
||||
switch (action.action) {
|
||||
case(Action.Scan):
|
||||
this.actionService.scanLibrary(library);
|
||||
break;
|
||||
|
|
@ -95,7 +95,7 @@ export class SideNavComponent implements OnInit, OnDestroy {
|
|||
|
||||
performAction(action: ActionItem<Library>, library: Library) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action.action, library);
|
||||
action.callback(action, library);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
19
UI/Web/src/app/user-settings/_pipes/device-platform.pipe.ts
Normal file
19
UI/Web/src/app/user-settings/_pipes/device-platform.pipe.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { DevicePlatform } from 'src/app/_models/device/device-platform';
|
||||
|
||||
@Pipe({
|
||||
name: 'devicePlatform'
|
||||
})
|
||||
export class DevicePlatformPipe implements PipeTransform {
|
||||
|
||||
transform(value: DevicePlatform): string {
|
||||
switch(value) {
|
||||
case DevicePlatform.Kindle: return 'Kindle';
|
||||
case DevicePlatform.Kobo: return 'Kobo';
|
||||
case DevicePlatform.PocketBook: return 'PocketBook';
|
||||
case DevicePlatform.Custom: return 'Custom';
|
||||
default: return value + '';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<div class="card">
|
||||
<form [formGroup]="settingsForm" class="card-body">
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-md-3 col-sm-12 pe-2">
|
||||
<label for="settings-name" class="form-label">Device Name</label>
|
||||
<input id="settings-name" class="form-control" formControlName="name" type="text">
|
||||
<ng-container *ngIf="settingsForm.get('name')?.errors as errors">
|
||||
<p class="invalid-feedback" *ngIf="errors.required">
|
||||
This field is required
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 col-sm-12 pe-2">
|
||||
<label for="email" class="form-label">Email</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="emailTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #emailTooltip>This email will be used to accept the file via Send To</ng-template>
|
||||
<span class="visually-hidden" id="email-help">The number of backups to maintain. Default is 30, minumum is 1, maximum is 30.</span>
|
||||
<input id="email" aria-describedby="email-help" class="form-control" formControlName="email" type="email" placeholder="@kindle.com">
|
||||
<ng-container *ngIf="settingsForm.get('email')?.errors as errors">
|
||||
<p class="invalid-feedback" *ngIf="errors.email">
|
||||
This must be a valid email
|
||||
</p>
|
||||
<p class="invalid-feedback" *ngIf="errors.required">
|
||||
This field is required
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 col-sm-12 pe-2">
|
||||
<label for="device-platform" class="form-label">Device Platform</label>
|
||||
<select id="device-platform" aria-describedby="device-platform-help" class="form-select" formControlName="platform">
|
||||
<option *ngFor="let patform of devicePlatforms" [value]="patform">{{patform | devicePlatform}}</option>
|
||||
</select>
|
||||
<ng-container *ngIf="settingsForm.get('platform')?.errors as errors">
|
||||
<p class="invalid-feedback" *ngIf="errors.required">
|
||||
This field is required
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end">
|
||||
<button type="submit" class="flex-fill btn btn-primary" (click)="addDevice()" [disabled]="!settingsForm.dirty">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { Device } from 'src/app/_models/device/device';
|
||||
import { DevicePlatform, devicePlatforms } from 'src/app/_models/device/device-platform';
|
||||
import { DeviceService } from 'src/app/_services/device.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-device',
|
||||
templateUrl: './edit-device.component.html',
|
||||
styleUrls: ['./edit-device.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class EditDeviceComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
@Input() device: Device | undefined;
|
||||
|
||||
@Output() deviceAdded: EventEmitter<void> = new EventEmitter();
|
||||
@Output() deviceUpdated: EventEmitter<Device> = new EventEmitter();
|
||||
|
||||
settingsForm: FormGroup = new FormGroup({});
|
||||
devicePlatforms = devicePlatforms;
|
||||
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
|
||||
constructor(public deviceService: DeviceService, private toastr: ToastrService,
|
||||
private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.settingsForm.addControl('name', new FormControl(this.device?.name || '', [Validators.required]));
|
||||
this.settingsForm.addControl('email', new FormControl(this.device?.emailAddress || '', [Validators.required, Validators.email]));
|
||||
this.settingsForm.addControl('platform', new FormControl(this.device?.platform || DevicePlatform.Custom, [Validators.required]));
|
||||
|
||||
// If user has filled in email and the platform hasn't been explicitly updated, try to update it for them
|
||||
this.settingsForm.get('email')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(email => {
|
||||
if (this.settingsForm.get('platform')?.dirty) return;
|
||||
if (email === null || email === undefined || email === '') return;
|
||||
if (email.endsWith('@kindle.com')) this.settingsForm.get('platform')?.setValue(DevicePlatform.Kindle);
|
||||
else if (email.endsWith('@pbsync.com')) this.settingsForm.get('platform')?.setValue(DevicePlatform.PocketBook);
|
||||
else this.settingsForm.get('platform')?.setValue(DevicePlatform.Custom);
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (this.device) {
|
||||
this.settingsForm.get('name')?.setValue(this.device.name);
|
||||
this.settingsForm.get('email')?.setValue(this.device.emailAddress);
|
||||
this.settingsForm.get('platform')?.setValue(this.device.platform);
|
||||
this.cdRef.markForCheck();
|
||||
this.settingsForm.markAsPristine();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
addDevice() {
|
||||
if (this.device !== undefined) {
|
||||
this.deviceService.updateDevice(this.device.id, this.settingsForm.value.name, this.settingsForm.value.platform, this.settingsForm.value.email).subscribe(() => {
|
||||
this.settingsForm.reset();
|
||||
this.toastr.success('Device updated');
|
||||
this.cdRef.markForCheck();
|
||||
this.deviceUpdated.emit();
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
this.deviceService.createDevice(this.settingsForm.value.name, this.settingsForm.value.platform, this.settingsForm.value.email).subscribe(() => {
|
||||
this.settingsForm.reset();
|
||||
this.toastr.success('Device created');
|
||||
this.cdRef.markForCheck();
|
||||
this.deviceAdded.emit();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-8"><h3>Device Manager</h3></div>
|
||||
<div class="col-4">
|
||||
<button class="btn btn-primary float-end" (click)="collapse.toggle()" [attr.aria-expanded]="!addDeviceIsCollapsed"
|
||||
aria-controls="collapseExample">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i> Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
This section is for you to setup devices that cannot connect to Kavita via a web browser and instead have an email address that accepts files.
|
||||
</p>
|
||||
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="addDeviceIsCollapsed">
|
||||
<app-edit-device [device]="device" (deviceAdded)="loadDevices()" (deviceUpdated)="loadDevices()"></app-edit-device>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mt-2">
|
||||
<h4>Devices</h4>
|
||||
<p *ngIf="devices.length == 0">
|
||||
There are no devices setup yet
|
||||
</p>
|
||||
<ng-container *ngFor="let device of devices">
|
||||
<div class="card col-auto me-3 mb-3" style="width: 18rem;">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{device.name | sentenceCase}}</h5>
|
||||
Platform: <h6 class="card-subtitle mb-2 text-muted">{{device.platform | devicePlatform}}</h6>
|
||||
Email: <h6 class="card-subtitle mb-2 text-muted">{{device.emailAddress}}</h6>
|
||||
|
||||
<button class="btn btn-danger me-2" (click)="deleteDevice(device)">Delete</button>
|
||||
<button class="btn btn-primary" (click)="editDevice(device)">Edit</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { Device } from 'src/app/_models/device/device';
|
||||
import { DevicePlatform, devicePlatforms } from 'src/app/_models/device/device-platform';
|
||||
import { DeviceService } from 'src/app/_services/device.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-devices',
|
||||
templateUrl: './manage-devices.component.html',
|
||||
styleUrls: ['./manage-devices.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ManageDevicesComponent implements OnInit, OnDestroy {
|
||||
|
||||
devices: Array<Device> = [];
|
||||
addDeviceIsCollapsed: boolean = true;
|
||||
device: Device | undefined;
|
||||
|
||||
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
|
||||
constructor(public deviceService: DeviceService, private toastr: ToastrService,
|
||||
private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadDevices();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
loadDevices() {
|
||||
this.addDeviceIsCollapsed = true;
|
||||
this.device = undefined;
|
||||
this.cdRef.markForCheck();
|
||||
this.deviceService.getDevices().subscribe(devices => {
|
||||
this.devices = devices;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
deleteDevice(device: Device) {
|
||||
this.deviceService.deleteDevice(device.id).subscribe(() => {
|
||||
const index = this.devices.indexOf(device);
|
||||
this.devices.splice(index, 1);
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
editDevice(device: Device) {
|
||||
this.device = device;
|
||||
this.addDeviceIsCollapsed = false;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
|
@ -335,6 +335,9 @@
|
|||
<ng-container *ngIf="tab.fragment === 'theme'">
|
||||
<app-theme-manager></app-theme-manager>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="tab.fragment === 'devices'">
|
||||
<app-manage-devices></app-manage-devices>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||
{title: 'Password', fragment: 'password'},
|
||||
{title: '3rd Party Clients', fragment: 'clients'},
|
||||
{title: 'Theme', fragment: 'theme'},
|
||||
{title: 'Devices', fragment: 'devices'},
|
||||
];
|
||||
active = this.tabs[0];
|
||||
opdsEnabled: boolean = false;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { UserPreferencesComponent } from './user-preferences/user-preferences.component';
|
||||
import { NgbAccordionModule, NgbNavModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NgbAccordionModule, NgbCollapseModule, NgbNavModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { UserSettingsRoutingModule } from './user-settings-routing.module';
|
||||
import { ApiKeyComponent } from './api-key/api-key.component';
|
||||
|
|
@ -10,7 +10,9 @@ import { SiteThemeProviderPipe } from './_pipes/site-theme-provider.pipe';
|
|||
import { ThemeManagerComponent } from './theme-manager/theme-manager.component';
|
||||
import { ColorPickerModule } from 'ngx-color-picker';
|
||||
import { SidenavModule } from '../sidenav/sidenav.module';
|
||||
|
||||
import { ManageDevicesComponent } from './manage-devices/manage-devices.component';
|
||||
import { DevicePlatformPipe } from './_pipes/device-platform.pipe';
|
||||
import { EditDeviceComponent } from './edit-device/edit-device.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
|
|
@ -19,6 +21,9 @@ import { SidenavModule } from '../sidenav/sidenav.module';
|
|||
ApiKeyComponent,
|
||||
ThemeManagerComponent,
|
||||
SiteThemeProviderPipe,
|
||||
ManageDevicesComponent,
|
||||
DevicePlatformPipe,
|
||||
EditDeviceComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
|
@ -27,6 +32,7 @@ import { SidenavModule } from '../sidenav/sidenav.module';
|
|||
NgbAccordionModule,
|
||||
NgbNavModule,
|
||||
NgbTooltipModule,
|
||||
NgbCollapseModule,
|
||||
|
||||
ColorPickerModule, // User prefernces background color
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { JumpKey } from 'src/app/_models/jumpbar/jump-key';
|
|||
import { Pagination } from 'src/app/_models/pagination';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { SeriesFilter, FilterEvent } from 'src/app/_models/series-filter';
|
||||
import { Action } from 'src/app/_services/action-factory.service';
|
||||
import { Action, 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 { MessageHubService, EVENTS } from 'src/app/_services/message-hub.service';
|
||||
|
|
@ -48,11 +48,11 @@ export class WantToReadComponent implements OnInit, OnDestroy {
|
|||
private onDestroy: Subject<void> = new Subject<void>();
|
||||
trackByIdentity = (index: number, item: Series) => `${item.name}_${item.localizedName}_${item.pagesRead}`;
|
||||
|
||||
bulkActionCallback = (action: Action, data: any) => {
|
||||
bulkActionCallback = (action: ActionItem<any>, data: any) => {
|
||||
const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('series');
|
||||
const selectedSeries = this.series.filter((series, index: number) => selectedSeriesIndexies.includes(index + ''));
|
||||
|
||||
switch (action) {
|
||||
switch (action.action) {
|
||||
case Action.RemoveFromWantToReadList:
|
||||
this.actionService.removeMultipleSeriesFromWantToReadList(selectedSeries.map(s => s.id), () => {
|
||||
this.bulkSelectionService.deselectAll();
|
||||
|
|
|
|||
BIN
UI/Web/src/assets/images/sendto/kindle-icon.png
Normal file
BIN
UI/Web/src/assets/images/sendto/kindle-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
UI/Web/src/assets/images/sendto/pocketbook-icon.png
Normal file
BIN
UI/Web/src/assets/images/sendto/pocketbook-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 468 B |
Loading…
Add table
Add a link
Reference in a new issue