Library Recomendations (#1236)

* Updated cover regex for finding cover images in archives to ignore back_cover or back-cover

* Fixed an issue where Tags wouldn't save due to not pulling them from the DB.

* Refactored All series to it's own lazy loaded module

* Modularized Dashboard and library detail. Had to change main dashboard page to be libraries. Subject to change.

* Refactored login component into registration module

* Series Detail module created

* Refactored nav stuff into it's own module, not lazy loaded, but self contained.

* Refactored theme component into a dev only module so we don't incur load for temp testing modules

* Finished off modularization code. Only missing thing is to re-introduce some dashboard functionality for library view.

* Implemented a basic recommendation page for library detail
This commit is contained in:
Joseph Milazzo 2022-04-29 17:27:01 -05:00 committed by GitHub
parent 743a3ba935
commit f237aa7ab7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
77 changed files with 1077 additions and 501 deletions

View file

@ -0,0 +1,109 @@
<ng-container *ngIf="isAdmin">
<button type="button" class="btn btn-icon {{activeEvents > 0 ? 'colored' : ''}}"
[ngbPopover]="popContent" title="Activity" placement="bottom" [popoverClass]="'nav-events'" [autoClose]="'outside'">
<i aria-hidden="true" class="fa fa-wave-square nav"></i>
</button>
<ng-template #popContent>
<ul class="list-group list-group-flush dark-menu">
<ng-container *ngIf="debugMode">
<li class="list-group-item dark-menu-item">
<div class="h6 mb-1">Title goes here</div>
<div class="accent-text mb-1">Subtitle goes here</div>
<div class="progress-container row g-0 align-items-center">
<div class="progress" style="height: 5px;">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 100%" [attr.aria-valuenow]="100" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
</li>
<li class="list-group-item dark-menu-item">
<div class="h6 mb-1">Title goes here</div>
<div class="accent-text mb-1">Subtitle goes here</div>
</li>
<li class="list-group-item dark-menu-item">
<div>
<div class="h6 mb-1">Scanning Books</div>
<div class="accent-text mb-1">E:\\Books\\Demon King Daimaou\\Demon King Daimaou - Volume 11.epub</div>
<div class="progress-container row g-0 align-items-center">
<div class="col-2">{{prettyPrintProgress(0.1)}}%</div>
<div class="col-10 progress" style="height: 5px;">
<div class="progress-bar" role="progressbar" [ngStyle]="{'width': 0.1 * 100 + '%'}" [attr.aria-valuenow]="0.1 * 100" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
</div>
</li>
<li class="list-group-item dark-menu-item error">
<div>
<div class="h6 mb-1"><i class="fa-solid fa-triangle-exclamation me-2"></i>There was some library scan error</div>
<div class="accent-text mb-1">Click for more information</div>
</div>
<button type="button" class="btn-close float-end" aria-label="close" ></button>
</li>
</ng-container>
<!-- Progress Events-->
<ng-container *ngIf="progressEvents$ | async as progressUpdates">
<ng-container *ngFor="let message of progressUpdates">
<li class="list-group-item dark-menu-item" *ngIf="message.progress === 'indeterminate' || message.progress === 'none'; else progressEvent">
<div class="h6 mb-1">{{message.title}}</div>
<div class="accent-text mb-1" *ngIf="message.subTitle !== ''">{{message.subTitle}}</div>
<div class="progress-container row g-0 align-items-center">
<div class="progress" style="height: 5px;" *ngIf="message.progress === 'indeterminate'">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 100%" [attr.aria-valuenow]="100" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
</li>
<ng-template #progressEvent>
<li class="list-group-item dark-menu-item">
<div class="h6 mb-1">{{message.title}}</div>
<div class="accent-text mb-1" *ngIf="message.subTitle !== ''">{{message.subTitle}}</div>
<div class="progress-container row g-0 align-items-center">
<div class="col-2">{{prettyPrintProgress(message.body.progress) + '%'}}</div>
<div class="col-10 progress" style="height: 5px;">
<div class="progress-bar" role="progressbar" [ngStyle]="{'width': message.body.progress * 100 + '%'}" [attr.aria-valuenow]="message.body.progress * 100" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
</li>
</ng-template>
</ng-container>
</ng-container>
<!-- Single updates (Informational/Update available)-->
<ng-container *ngIf="singleUpdates$ | async as singleUpdates">
<ng-container *ngFor="let singleUpdate of singleUpdates">
<li class="list-group-item dark-menu-item update-available" *ngIf="singleUpdate.name === EVENTS.UpdateAvailable" (click)="handleUpdateAvailableClick(singleUpdate)">
<i class="fa fa-chevron-circle-up" aria-hidden="true"></i>&nbsp;Update available
</li>
<li class="list-group-item dark-menu-item update-available" *ngIf="singleUpdate.name !== EVENTS.UpdateAvailable">
<div>{{singleUpdate.title}}</div>
<div class="accent-text" *ngIf="singleUpdate.subTitle !== ''">{{singleUpdate.subTitle}}</div>
</li>
</ng-container>
</ng-container>
<!-- Errors -->
<ng-container *ngIf="errors$ | async as errors">
<ng-container *ngFor="let error of errors">
<li class="list-group-item dark-menu-item error" role="alert" (click)="seeMoreError(error)">
<div>
<div class="h6 mb-1"><i class="fa-solid fa-triangle-exclamation me-2"></i>{{error.title}}</div>
<div class="accent-text mb-1">Click for more information</div>
</div>
<button type="button" class="btn-close float-end" aria-label="close" (click)="removeError(error, $event)"></button>
</li>
</ng-container>
</ng-container>
<!-- Online Users -->
<ng-container *ngIf="messageHub.onlineUsers$ | async as onlineUsers">
<li class="list-group-item dark-menu-item" *ngIf="onlineUsers.length > 1">
<div>{{onlineUsers.length}} Users online</div>
</li>
<li class="list-group-item dark-menu-item" *ngIf="activeEvents < 1 && onlineUsers.length <= 1">Not much going on here</li>
<li class="list-group-item dark-menu-item" *ngIf="debugMode">Active Events: {{activeEvents}}</li>
</ng-container>
</ul>
</ng-template>
</ng-container>

View file

@ -0,0 +1,95 @@
.dark-menu {
background-color: var(--navbar-bg-color);
border-color: var(--event-widget-border-color); // rgba(1, 4, 9, 0.5);
}
.dark-menu-item {
color: var(--event-widget-text-color);
background-color: var(--event-widget-item-bg-color); // rgb(1, 4, 9)
border-color: var(--event-widget-item-border-color); // rgba(53, 53, 53, 0.5)
}
// Popovers need to be their own component
::ng-deep .bs-popover-bottom > .popover-arrow::after, .bs-popover-bottom > .popover-arrow::before {
border-bottom-color: transparent;
}
::ng-deep .nav-events {
.popover-body {
min-width: 250px;
max-width: 250px;
padding: 0px;
box-shadow: 0px 0px 12px rgb(0 0 0 / 75%);
max-height: calc(100vh - 60px);
overflow-y: auto;
}
.popover {
min-width: 300px;
}
}
.progress-container {
width: 100%;
}
.progress {
padding: 0;
}
.accent-text {
width: 100%;
text-overflow: ellipsis;
overflow:hidden;
white-space:nowrap;
}
.btn:focus, .btn:hover {
box-shadow: 0 0 0 0.1rem var(--navbar-btn-hover-outline-color);
}
.small-spinner {
width: 1rem;
height: 1rem;
}
.btn-icon {
color: white;
}
.colored {
background-color: var(--primary-color);
border-radius: 60px;
}
.update-available {
cursor: pointer;
i.fa {
color: var(--primary-color) !important;
}
color: var(--primary-color);
}
.error {
cursor: pointer;
position: relative;
.h6 {
color: var(--error-color);
}
i.fa {
color: var(--primary-color) !important;
}
.btn-close {
top: 5px;
right: 10px;
font-size: 11px;
position: absolute;
}
}

View file

@ -0,0 +1,166 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { BehaviorSubject, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { ConfirmConfig } from 'src/app/shared/confirm-dialog/_models/confirm-config';
import { ConfirmService } from 'src/app/shared/confirm.service';
import { UpdateNotificationModalComponent } from 'src/app/shared/update-notification/update-notification-modal.component';
import { ErrorEvent } from 'src/app/_models/events/error-event';
import { NotificationProgressEvent } from 'src/app/_models/events/notification-progress-event';
import { UpdateVersionEvent } from 'src/app/_models/events/update-version-event';
import { User } from 'src/app/_models/user';
import { AccountService } from 'src/app/_services/account.service';
import { EVENTS, Message, MessageHubService } from 'src/app/_services/message-hub.service';
@Component({
selector: 'app-nav-events-toggle',
templateUrl: './events-widget.component.html',
styleUrls: ['./events-widget.component.scss']
})
export class EventsWidgetComponent implements OnInit, OnDestroy {
@Input() user!: User;
isAdmin: boolean = false;
private readonly onDestroy = new Subject<void>();
/**
* Progress events (Event Type: 'started', 'ended', 'updated' that have progress property)
*/
progressEventsSource = new BehaviorSubject<NotificationProgressEvent[]>([]);
progressEvents$ = this.progressEventsSource.asObservable();
singleUpdateSource = new BehaviorSubject<NotificationProgressEvent[]>([]);
singleUpdates$ = this.singleUpdateSource.asObservable();
errorSource = new BehaviorSubject<ErrorEvent[]>([]);
errors$ = this.errorSource.asObservable();
private updateNotificationModalRef: NgbModalRef | null = null;
activeEvents: number = 0;
debugMode: boolean = false;
get EVENTS() {
return EVENTS;
}
constructor(public messageHub: MessageHubService, private modalService: NgbModal,
private accountService: AccountService, private confirmService: ConfirmService) { }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
this.progressEventsSource.complete();
this.singleUpdateSource.complete();
this.errorSource.complete();
}
ngOnInit(): void {
// Debounce for testing. Kavita's too fast
this.messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(event => {
if (event.event === EVENTS.NotificationProgress) {
this.processNotificationProgressEvent(event);
} else if (event.event === EVENTS.Error) {
const values = this.errorSource.getValue();
values.push(event.payload as ErrorEvent);
this.errorSource.next(values);
this.activeEvents += 1;
}
});
this.accountService.currentUser$.pipe(takeUntil(this.onDestroy)).subscribe(user => {
if (user) {
this.isAdmin = this.accountService.hasAdminRole(user);
} else {
this.isAdmin = false;
}
});
}
processNotificationProgressEvent(event: Message<NotificationProgressEvent>) {
const message = event.payload as NotificationProgressEvent;
let data;
let index = -1;
switch (event.payload.eventType) {
case 'single':
const values = this.singleUpdateSource.getValue();
values.push(message);
this.singleUpdateSource.next(values);
this.activeEvents += 1;
break;
case 'started':
// Sometimes we can receive 2 started on long running scans, so better to just treat as a merge then.
data = this.mergeOrUpdate(this.progressEventsSource.getValue(), message);
this.progressEventsSource.next(data);
break;
case 'updated':
data = this.mergeOrUpdate(this.progressEventsSource.getValue(), message);
this.progressEventsSource.next(data);
break;
case 'ended':
data = this.progressEventsSource.getValue();
data = data.filter(m => m.name !== message.name);
this.progressEventsSource.next(data);
this.activeEvents = Math.max(this.activeEvents - 1, 0);
break;
default:
break;
}
}
private mergeOrUpdate(data: NotificationProgressEvent[], message: NotificationProgressEvent) {
const index = data.findIndex(m => m.name === message.name);
// Sometimes we can receive 2 started on long running scans, so better to just treat as a merge then.
if (index < 0) {
data.push(message);
this.activeEvents += 1;
} else {
data[index] = message;
}
return data;
}
handleUpdateAvailableClick(message: NotificationProgressEvent) {
if (this.updateNotificationModalRef != null) { return; }
this.updateNotificationModalRef = this.modalService.open(UpdateNotificationModalComponent, { scrollable: true, size: 'lg' });
this.updateNotificationModalRef.componentInstance.updateData = message.body as UpdateVersionEvent;
this.updateNotificationModalRef.closed.subscribe(() => {
this.updateNotificationModalRef = null;
});
this.updateNotificationModalRef.dismissed.subscribe(() => {
this.updateNotificationModalRef = null;
});
}
async seeMoreError(error: ErrorEvent) {
const config = new ConfirmConfig();
config.buttons = [
{text: 'Dismiss', type: 'primary'},
{text: 'Ok', type: 'secondary'},
];
config.header = error.title;
config.content = error.subTitle;
var result = await this.confirmService.alert(error.subTitle || error.title, config);
if (result) {
this.removeError(error);
}
}
removeError(error: ErrorEvent, event?: MouseEvent) {
if (event) {
event.stopPropagation();
event.preventDefault();
}
let data = this.errorSource.getValue();
data = data.filter(m => m !== error);
this.errorSource.next(data);
this.activeEvents = Math.max(this.activeEvents - 1, 0);
}
prettyPrintProgress(progress: number) {
return Math.trunc(progress * 100);
}
}