Event Widget Update (#1098)

* Took care of some notes in the code

* Fixed an issue where Extra might get flagged as special too early, if in a word like Extraordinary

* Moved Tag cleanup code into Scanner service. Added a SplitQuery to another heavy API. Refactored Scan loop to remove parallelism and use async instead.

* Lots of rework on the codebase to support detailed messages and easier management of message sending. Need to take a break on this work.

* Progress is being made, but slowly. Code is broken in this commit.

* Progress is being made, but slowly. Code is broken in this commit.

* Fixed merge issue

* Fixed unit tests

* CoverUpdate is now hooked into new ProgressEvent structure

* Refactored code to remove custom observables and have everything use standard messages$

* Refactored a ton of instances to NotificationProgressEvent style and tons of the UI to respect that too. UI is still a bit buggy, but wholistically the work is done.

* Working much better. Sometimes events come in too fast. Currently cover update progress doesn't display on UI

* Fixed unit tests

* Removed SignalREvent to minimize internal event types. Updated the UI to use progress bars. Finished SiteThemeService.

* Merged metadata refresh progress events and changed library scan events to merge cleaner in the UI

* Changed RefreshMetadataProgress to CoverUpdateProgress to reflect the event better.

* Theme Cleanup (#1089)

* Fixed e-ink theme not properly applying correctly

* Fixed some seed changes. Changed card checkboxes to use our themed ones

* Fixed recently added carousel not going to recently-added page

* Fixed an issue where no results found would show when searching for a library name

* Cleaned up list a bit, typeahead dropdown still needs work

* Added a TODO to streamline series-card component

* Removed ng-lazyload-image module since we don't use it. We use lazysizes

* Darken card on hover

* Fixing accordion focus style

* ux pass updates

- Fixed typeahead width
- Fixed changelog download buttons
- Fixed a select
- Fixed various input box-shadows
- Fixed all anchors to only have underline on hover
- Added navtab hover and active effects

* more ux pass

- Fixed spacing on theme cards
- Fixed some light theme issues
- Exposed text-muted-color for theme card subtitle color

* UX pass fixes

- Changed back to bright green for primary on dark theme
- Changed fa icon to black on e-ink

* Merged changelog component

* Fixed anchor buttons text decoration

* Changed nav tabs to have a background color instead of open active state

* When user is not authenticated, make sure we set default theme (dark)

* Cleanup on carousel

* Updated Users tab to use small buttons with icons to align with Library tab

* Cleaned up brand to not underline, removed default link underline on hover in dropdown and pill tabs

* Fixed collection detail posters not rendering

Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>

* Bump versions by dotnet-bump-version.

* Tweaked some of the emitting code

* Some css, but pretty bad. Robbie please save me

* Removed a todo

* styling update

* Only send filename on FileScanProgress

* Some console.log spam cleanup

* Various updates

* Show events widget activity based on activeEvents

* progress bar color updates

* Code cleanup

Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
Joseph Milazzo 2022-02-18 18:57:37 -08:00 committed by GitHub
parent d24620fd15
commit eddbb7ab18
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 1022 additions and 463 deletions

View file

@ -0,0 +1,12 @@
/**
* Represents a file being scanned during a Library Scan
*/
export interface FileScanProgressEvent {
// libraryId: number;
// libraryName: string;
// fileName: string;
title: string;
subtitle: string;
eventTime: string;
}

View file

@ -0,0 +1,40 @@
export interface NotificationContainer<T> {
/**
* Represents underlying type of event
*/
type: string;
/**
* How many events are in this object
*/
size: number;
events: Array<T>;
}
export interface ActivityNotification {
type: string; // library.update.section
/**
* If this notification has some sort of cancellable operation
*/
cancellable: boolean;
userId: number;
/**
* Main action title ie) Scanning LIBRARY_NAME
*/
title: string;
/**
* Detail information about action. ie) Series Name
*/
subtitle: string;
/**
* Progress of this action [0-100]
*/
progress: number;
/**
* Any additional context backend needs to send to UI
*/
context: {
libraryId: number;
};
}

View file

@ -0,0 +1,30 @@
export interface NotificationProgressEvent {
/**
* Payload of the event subtype
*/
body: any;
/**
* Subtype event
*/
name: string;
/**
* Title to display in events widget
*/
title: string;
/**
* Optional subtitle to display. Defaults to empty string
*/
subTitle: string;
/**
* Type of event. Helps events widget to understand how to handle said event
*/
eventType: 'single' | 'started' | 'updated' | 'ended';
/**
* Type of progress. Helps widget understand how to display spinner
*/
progress: 'none' | 'indeterminate' | 'determinate';
/**
* When event was sent
*/
eventTime: string;
}

View file

@ -2,4 +2,10 @@ export interface ProgressEvent {
libraryId: number;
progress: number;
eventTime: string;
// New fields
/**
* Event type
*/
name: string;
}

View file

@ -1,7 +1,3 @@
export interface SiteThemeProgressEvent {
totalUpdates: number;
currentCount: number;
themeName: string;
progress: number;
eventTime: string;
}

View file

@ -1,19 +1,16 @@
import { EventEmitter, Injectable } from '@angular/core';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr';
import { ToastrService } from 'ngx-toastr';
import { BehaviorSubject, ReplaySubject } from 'rxjs';
import { environment } from 'src/environments/environment';
import { ProgressEvent } from '../_models/events/scan-library-progress-event';
import { ScanSeriesEvent } from '../_models/events/scan-series-event';
import { SeriesAddedEvent } from '../_models/events/series-added-event';
import { NotificationProgressEvent } from '../_models/events/notification-progress-event';
import { SiteThemeProgressEvent } from '../_models/events/site-theme-progress-event';
import { User } from '../_models/user';
export enum EVENTS {
UpdateAvailable = 'UpdateAvailable',
ScanSeries = 'ScanSeries',
RefreshMetadataProgress = 'RefreshMetadataProgress',
SeriesAdded = 'SeriesAdded',
SeriesRemoved = 'SeriesRemoved',
ScanLibraryProgress = 'ScanLibraryProgress',
@ -21,8 +18,22 @@ export enum EVENTS {
SeriesAddedToCollection = 'SeriesAddedToCollection',
ScanLibraryError = 'ScanLibraryError',
BackupDatabaseProgress = 'BackupDatabaseProgress',
/**
* A subtype of NotificationProgress that represents maintenance cleanup on server-owned resources
*/
CleanupProgress = 'CleanupProgress',
/**
* A subtype of NotificationProgress that represnts a user downloading a file or group of files
*/
DownloadProgress = 'DownloadProgress',
/**
* A generic progress event
*/
NotificationProgress = 'NotificationProgress',
/**
* A subtype of NotificationProgress that represents the underlying file being processed during a scan
*/
FileScanProgress = 'FileScanProgress',
/**
* A custom user site theme is added or removed during a scan
*/
@ -30,7 +41,11 @@ export enum EVENTS {
/**
* A cover is updated
*/
CoverUpdate = 'CoverUpdate'
CoverUpdate = 'CoverUpdate',
/**
* A subtype of NotificationProgress that represents a file being processed for cover image extraction
*/
CoverUpdateProgress = 'CoverUpdateProgress',
}
export interface Message<T> {
@ -38,6 +53,7 @@ export interface Message<T> {
payload: T;
}
@Injectable({
providedIn: 'root'
})
@ -46,19 +62,36 @@ export class MessageHubService {
private hubConnection!: HubConnection;
private messagesSource = new ReplaySubject<Message<any>>(1);
public messages$ = this.messagesSource.asObservable();
private onlineUsersSource = new BehaviorSubject<string[]>([]);
onlineUsers$ = this.onlineUsersSource.asObservable();
public scanSeries: EventEmitter<ScanSeriesEvent> = new EventEmitter<ScanSeriesEvent>();
public scanLibrary: EventEmitter<ProgressEvent> = new EventEmitter<ProgressEvent>(); // TODO: Refactor this name to be generic
public seriesAdded: EventEmitter<SeriesAddedEvent> = new EventEmitter<SeriesAddedEvent>();
/**
* Any events that come from the backend
*/
public messages$ = this.messagesSource.asObservable();
/**
* Users that are online
*/
public onlineUsers$ = this.onlineUsersSource.asObservable();
isAdmin: boolean = false;
constructor(private toastr: ToastrService, private router: Router) {
}
/**
* Tests that an event is of the type passed
* @param event
* @param eventType
* @returns
*/
public isEventType(event: Message<any>, eventType: EVENTS) {
if (event.event == EVENTS.NotificationProgress) {
const notification = event.payload as NotificationProgressEvent;
return notification.eventType.toLowerCase() == eventType.toLowerCase();
}
return event.event === eventType;
}
createHubConnection(user: User, isAdmin: boolean) {
@ -85,7 +118,6 @@ export class MessageHubService {
event: EVENTS.ScanSeries,
payload: resp.body
});
this.scanSeries.emit(resp.body);
});
this.hubConnection.on(EVENTS.ScanLibraryProgress, resp => {
@ -93,34 +125,13 @@ export class MessageHubService {
event: EVENTS.ScanLibraryProgress,
payload: resp.body
});
this.scanLibrary.emit(resp.body);
});
this.hubConnection.on(EVENTS.BackupDatabaseProgress, resp => {
this.messagesSource.next({
event: EVENTS.BackupDatabaseProgress,
payload: resp.body
});
});
this.hubConnection.on(EVENTS.CleanupProgress, resp => {
this.hubConnection.on(EVENTS.NotificationProgress, (resp: NotificationProgressEvent) => {
this.messagesSource.next({
event: EVENTS.CleanupProgress,
payload: resp.body
});
});
this.hubConnection.on(EVENTS.DownloadProgress, resp => {
this.messagesSource.next({
event: EVENTS.DownloadProgress,
payload: resp.body
});
});
this.hubConnection.on(EVENTS.RefreshMetadataProgress, resp => {
this.messagesSource.next({
event: EVENTS.RefreshMetadataProgress,
payload: resp.body
event: EVENTS.NotificationProgress,
payload: resp
});
});
@ -144,6 +155,7 @@ export class MessageHubService {
payload: resp.body
});
if (this.isAdmin) {
// TODO: Just show the error, RBS is done in eventhub
this.toastr.error('Library Scan had a critical error. Some series were not saved. Check logs');
}
});
@ -153,7 +165,6 @@ export class MessageHubService {
event: EVENTS.SeriesAdded,
payload: resp.body
});
this.seriesAdded.emit(resp.body);
});
this.hubConnection.on(EVENTS.SeriesRemoved, resp => {
@ -163,14 +174,6 @@ export class MessageHubService {
});
});
// this.hubConnection.on(EVENTS.RefreshMetadata, resp => {
// this.messagesSource.next({
// event: EVENTS.RefreshMetadata,
// payload: resp.body
// });
// this.refreshMetadata.emit(resp.body); // TODO: Remove this
// });
this.hubConnection.on(EVENTS.CoverUpdate, resp => {
this.messagesSource.next({
event: EVENTS.CoverUpdate,
@ -195,5 +198,5 @@ export class MessageHubService {
sendMessage(methodName: string, body?: any) {
return this.hubConnection.invoke(methodName, body);
}
}

View file

@ -2,12 +2,13 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { Subject } from 'rxjs';
import { take, takeUntil } from 'rxjs/operators';
import { take, takeUntil, takeWhile } from 'rxjs/operators';
import { ConfirmService } from 'src/app/shared/confirm.service';
import { ProgressEvent } from 'src/app/_models/events/scan-library-progress-event';
import { NotificationProgressEvent } from 'src/app/_models/events/notification-progress-event';
import { ProgressEvent } from 'src/app/_models/events/progress-event';
import { Library, LibraryType } from 'src/app/_models/library';
import { LibraryService } from 'src/app/_services/library.service';
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
import { EVENTS, Message, MessageHubService } from 'src/app/_services/message-hub.service';
import { LibraryEditorModalComponent } from '../_modals/library-editor-modal/library-editor-modal.component';
@Component({
@ -37,18 +38,20 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
this.getLibraries();
// when a progress event comes in, show it on the UI next to library
this.hubService.messages$.pipe(takeUntil(this.onDestroy)).subscribe((event) => {
if (event.event !== EVENTS.ScanLibraryProgress) return;
this.hubService.messages$.pipe(takeUntil(this.onDestroy), takeWhile(event => event.event === EVENTS.NotificationProgress))
.subscribe((event: Message<NotificationProgressEvent>) => {
if (event.event !== EVENTS.NotificationProgress && (event.payload as NotificationProgressEvent).name === EVENTS.ScanSeries) return;
console.log('scan event: ', event.payload);
// TODO: Refactor this to use EventyType on NotificationProgress interface rather than float comparison
const scanEvent = event.payload as ProgressEvent;
const scanEvent = event.payload.body as ProgressEvent;
this.scanInProgress[scanEvent.libraryId] = {progress: scanEvent.progress !== 1};
if (scanEvent.progress === 0) {
this.scanInProgress[scanEvent.libraryId].timestamp = scanEvent.eventTime;
}
if (this.scanInProgress[scanEvent.libraryId].progress === false && scanEvent.progress === 1) {
if (this.scanInProgress[scanEvent.libraryId].progress === false && (scanEvent.progress === 1 || event.payload.eventType === 'ended')) {
this.libraryService.getLibraries().pipe(take(1)).subscribe(libraries => {
const newLibrary = libraries.find(lib => lib.id === scanEvent.libraryId);
const existingLibrary = this.libraries.find(lib => lib.id === scanEvent.libraryId);

View file

@ -13,7 +13,7 @@ import { Series } from '../_models/series';
import { FilterEvent, SeriesFilter } from '../_models/series-filter';
import { ActionItem, Action } from '../_services/action-factory.service';
import { ActionService } from '../_services/action.service';
import { MessageHubService } from '../_services/message-hub.service';
import { EVENTS, Message, MessageHubService } from '../_services/message-hub.service';
import { SeriesService } from '../_services/series.service';
@Component({
@ -82,7 +82,8 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
}
ngOnInit(): void {
this.hubService.seriesAdded.pipe(debounceTime(6000), takeUntil(this.onDestroy)).subscribe((event: SeriesAddedEvent) => {
this.hubService.messages$.pipe(debounceTime(6000), takeUntil(this.onDestroy)).subscribe((event: Message<any>) => {
if (event.event !== EVENTS.SeriesAdded) return;
this.loadPage();
});
}

View file

@ -108,7 +108,7 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
ngOnInit(): void {
this.collectionTagActions = this.actionFactoryService.getCollectionTagActions(this.handleCollectionActionCallback.bind(this));
this.messageHub.messages$.pipe(takeWhile(event => event.event === EVENTS.SeriesAddedToCollection), takeUntil(this.onDestory), debounceTime(2000)).subscribe(event => {
this.messageHub.messages$.pipe(takeUntil(this.onDestory), debounceTime(2000)).subscribe(event => {
if (event.event == EVENTS.SeriesAddedToCollection) {
const collectionEvent = event.payload as SeriesAddedToCollectionEvent;
if (collectionEvent.tagId === this.collectionTag.id) {

View file

@ -14,7 +14,7 @@ import { FilterEvent, SeriesFilter } from '../_models/series-filter';
import { Action, ActionFactoryService, ActionItem } from '../_services/action-factory.service';
import { ActionService } from '../_services/action.service';
import { LibraryService } from '../_services/library.service';
import { MessageHubService } from '../_services/message-hub.service';
import { EVENTS, MessageHubService } from '../_services/message-hub.service';
import { SeriesService } from '../_services/series.service';
@Component({
@ -92,12 +92,13 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.utilityService.filterPresetsFromUrl(this.route.snapshot, this.seriesService.createSeriesFilter());
this.filterSettings.presets.libraries = [this.libraryId];
//this.loadPage();
}
ngOnInit(): void {
this.hubService.seriesAdded.pipe(takeWhile(event => event.libraryId === this.libraryId), debounceTime(6000), takeUntil(this.onDestroy)).subscribe((event: SeriesAddedEvent) => {
this.hubService.messages$.pipe(debounceTime(6000), takeUntil(this.onDestroy)).subscribe((event) => {
if (event.event !== EVENTS.SeriesAdded) return;
const seriesAdded = event.payload as SeriesAddedEvent;
if (seriesAdded.libraryId !== this.libraryId) return;
this.loadPage();
});
}

View file

@ -1,25 +1,90 @@
<ng-container *ngIf="isAdmin">
<button type="button" class="btn btn-icon {{(progressEventsSource.getValue().length > 0 || updateAvailable) ? 'colored' : ''}}"
<button type="button" class="btn btn-icon {{activeEvents > 0 ? 'colored' : ''}}"
[ngbPopover]="popContent" title="Activity" placement="bottom" [popoverClass]="'nav-events'">
<i aria-hidden="true" class="fa fa-wave-square nav"></i>
</button>
<ng-template #popContent>
<ul class="list-group list-group-flush dark-menu">
<li class="list-group-item dark-menu-item" *ngFor="let event of progressEvents$ | async">
<div class="spinner-border text-primary small-spinner"
role="status" title="Started at {{event.timestamp | date: 'short'}}"
attr.aria-valuetext="{{prettyPrintProgress(event.progress)}}%" [attr.aria-valuenow]="prettyPrintProgress(event.progress)">
<span class="visually-hidden">Scan for {{event.libraryName}} in progress</span>
</div>
{{prettyPrintProgress(event.progress)}}%
{{prettyPrintEvent(event.eventType, event)}} {{event.libraryName}}
</li>
<li class="list-group-item dark-menu-item" *ngIf="progressEventsSource.getValue().length === 0 && !updateAvailable">Not much going on here</li>
<li class="list-group-item dark-menu-item update-available" *ngIf="updateAvailable" (click)="handleUpdateAvailableClick()">
<i class="fa fa-chevron-circle-up" aria-hidden="true"></i>&nbsp;Update available
</li>
<ng-container *ngIf="debugMode">
<li class="list-group-item dark-menu-item">
<!-- <div class="spinner-grow text-primary small-spinner" role="status"></div> -->
<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>
</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>
<!-- 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>
</ng-container>
</ul>
</ng-template>
</ng-container>

View file

@ -8,7 +8,7 @@
.dark-menu-item {
color: var(--body-text-color);
background-color: rgb(1, 4, 9);
border-color: rgba(1, 4, 9, 0.5);
border-color: rgba(53, 53, 53, 0.5);
}
// Popovers need to be their own component
@ -16,17 +16,37 @@
border-bottom-color: transparent;
}
.nav-events {
background-color: var(--navbar-bg-color);
::ng-deep .nav-events {
.popover-body {
min-width: 250px;
max-width: 250px;
padding: 0px;
box-shadow: 0px 0px 12px rgb(0 0 0 / 75%);
}
.popover {
min-width: 300px;
}
}
// .nav-events {
// background-color: white;
// }
.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 rgba(255, 255, 255, 1); // TODO: Used in nav as well, move to dark for btn-icon focus
box-shadow: 0 0 0 0.1rem var(--navbar-btn-hover-outline-color);
}
.small-spinner {
@ -36,9 +56,6 @@
.nav-events .popover-body {
padding: 0px;
}
.btn-icon {
color: white;

View file

@ -1,25 +1,16 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { BehaviorSubject, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { UpdateNotificationModalComponent } from '../shared/update-notification/update-notification-modal.component';
import { ProgressEvent } from '../_models/events/scan-library-progress-event';
import { NotificationProgressEvent } from '../_models/events/notification-progress-event';
import { UpdateVersionEvent } from '../_models/events/update-version-event';
import { User } from '../_models/user';
import { AccountService } from '../_services/account.service';
import { LibraryService } from '../_services/library.service';
import { EVENTS, Message, MessageHubService } from '../_services/message-hub.service';
interface ProcessedEvent {
eventType: string;
timestamp?: string;
progress: number;
libraryId: number;
libraryName: string;
}
type ProgressType = EVENTS.ScanLibraryProgress | EVENTS.RefreshMetadataProgress | EVENTS.BackupDatabaseProgress | EVENTS.CleanupProgress;
const acceptedEvents = [EVENTS.ScanLibraryProgress, EVENTS.RefreshMetadataProgress, EVENTS.BackupDatabaseProgress, EVENTS.CleanupProgress, EVENTS.DownloadProgress, EVENTS.SiteThemeProgress];
// TODO: Rename this to events widget
@Component({
@ -28,37 +19,48 @@ const acceptedEvents = [EVENTS.ScanLibraryProgress, EVENTS.RefreshMetadataProgre
styleUrls: ['./nav-events-toggle.component.scss']
})
export class NavEventsToggleComponent implements OnInit, OnDestroy {
@Input() user!: User;
isAdmin: boolean = false;
private readonly onDestroy = new Subject<void>();
/**
* Events that come through and are merged (ie progress event gets merged into a progress event)
* Progress events (Event Type: 'started', 'ended', 'updated' that have progress property)
*/
progressEventsSource = new BehaviorSubject<ProcessedEvent[]>([]);
progressEventsSource = new BehaviorSubject<NotificationProgressEvent[]>([]);
progressEvents$ = this.progressEventsSource.asObservable();
updateAvailable: boolean = false;
updateBody: any;
singleUpdateSource = new BehaviorSubject<NotificationProgressEvent[]>([]);
singleUpdates$ = this.singleUpdateSource.asObservable();
private updateNotificationModalRef: NgbModalRef | null = null;
constructor(private messageHub: MessageHubService, private libraryService: LibraryService, private modalService: NgbModal, private accountService: AccountService) { }
activeEvents: number = 0;
debugMode: boolean = false;
get EVENTS() {
return EVENTS;
}
constructor(public messageHub: MessageHubService, private modalService: NgbModal, private accountService: AccountService) { }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
this.progressEventsSource.complete();
this.singleUpdateSource.complete();
}
ngOnInit(): void {
// Debounce for testing. Kavita's too fast
this.messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(event => {
if (acceptedEvents.includes(event.event)) {
this.processProgressEvent(event, event.event);
} else if (event.event === EVENTS.UpdateAvailable) {
this.updateAvailable = true;
this.updateBody = event.payload;
if (event.event.endsWith('error')) {
// TODO: Show an error handle
} else if (event.event === EVENTS.NotificationProgress) {
this.processNotificationProgressEvent(event);
}
});
this.accountService.currentUser$.pipe(takeUntil(this.onDestroy)).subscribe(user => {
@ -70,32 +72,49 @@ export class NavEventsToggleComponent implements OnInit, OnDestroy {
});
}
processNotificationProgressEvent(event: Message<NotificationProgressEvent>) {
const message = event.payload as NotificationProgressEvent;
let data;
processProgressEvent(event: Message<ProgressEvent>, eventType: string) {
const scanEvent = event.payload as ProgressEvent;
this.libraryService.getLibraryNames().subscribe(names => {
const data = this.progressEventsSource.getValue();
const index = data.findIndex(item => item.eventType === eventType && item.libraryId === event.payload.libraryId);
if (index >= 0) {
data.splice(index, 1);
}
if (scanEvent.progress !== 1) {
const libraryName = names[scanEvent.libraryId] || '';
const newEvent = {eventType: eventType, timestamp: scanEvent.eventTime, progress: scanEvent.progress, libraryId: scanEvent.libraryId, libraryName, rawBody: event.payload};
data.push(newEvent);
}
this.progressEventsSource.next(data);
});
switch (event.payload.eventType) {
case 'single':
const values = this.singleUpdateSource.getValue();
values.push(message);
this.singleUpdateSource.next(values);
this.activeEvents += 1;
break;
case 'started':
data = this.progressEventsSource.getValue();
data.push(message);
this.progressEventsSource.next(data);
this.activeEvents += 1;
break;
case 'updated':
data = this.progressEventsSource.getValue();
const index = data.findIndex(m => m.name === message.name);
if (index < 0) {
data.push(message);
} else {
data[index] = message;
}
this.progressEventsSource.next(data);
break;
case 'ended':
data = this.progressEventsSource.getValue();
data = data.filter(m => m.name !== message.name); // This does not work // && m.title !== message.title
this.progressEventsSource.next(data);
this.activeEvents = Math.max(this.activeEvents - 1, 0);
break;
default:
break;
}
}
handleUpdateAvailableClick() {
handleUpdateAvailableClick(message: NotificationProgressEvent) {
if (this.updateNotificationModalRef != null) { return; }
this.updateNotificationModalRef = this.modalService.open(UpdateNotificationModalComponent, { scrollable: true, size: 'lg' });
this.updateNotificationModalRef.componentInstance.updateData = this.updateBody;
this.updateNotificationModalRef.componentInstance.updateData = message.body as UpdateVersionEvent;
this.updateNotificationModalRef.closed.subscribe(() => {
this.updateNotificationModalRef = null;
});
@ -107,16 +126,4 @@ export class NavEventsToggleComponent implements OnInit, OnDestroy {
prettyPrintProgress(progress: number) {
return Math.trunc(progress * 100);
}
prettyPrintEvent(eventType: string, event: any) {
switch(eventType) {
case (EVENTS.ScanLibraryProgress): return 'Scanning ';
case (EVENTS.RefreshMetadataProgress): return 'Refreshing Covers for ';
case (EVENTS.CleanupProgress): return 'Clearing Cache';
case (EVENTS.BackupDatabaseProgress): return 'Backing up Database';
case (EVENTS.DownloadProgress): return event.rawBody.userName.charAt(0).toUpperCase() + event.rawBody.userName.substr(1) + ' is downloading ' + event.rawBody.downloadName;
default: return eventType;
}
}
}

View file

@ -12,7 +12,7 @@ import { Series } from '../_models/series';
import { FilterEvent, SeriesFilter } from '../_models/series-filter';
import { Action } from '../_services/action-factory.service';
import { ActionService } from '../_services/action.service';
import { MessageHubService } from '../_services/message-hub.service';
import { EVENTS, Message, MessageHubService } from '../_services/message-hub.service';
import { SeriesService } from '../_services/series.service';
/**
@ -63,7 +63,10 @@ export class RecentlyAddedComponent implements OnInit, OnDestroy {
}
ngOnInit() {
this.hubService.seriesAdded.pipe(takeWhile(event => event.libraryId === this.libraryId), debounceTime(6000), takeUntil(this.onDestroy)).subscribe((event: SeriesAddedEvent) => {
this.hubService.messages$.pipe(debounceTime(6000), takeUntil(this.onDestroy)).subscribe((event) => {
if (event.event !== EVENTS.SeriesAdded) return;
const seriesAdded = event.payload as SeriesAddedEvent;
if (seriesAdded.libraryId !== this.libraryId) return;
this.loadPage();
});
}

View file

@ -4,7 +4,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal, NgbNavChangeEvent, NgbRatingConfig } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { forkJoin, Subject } from 'rxjs';
import { finalize, take, takeUntil, takeWhile } from 'rxjs/operators';
import { finalize, map, take, takeUntil, takeWhile } from 'rxjs/operators';
import { BulkSelectionService } from '../cards/bulk-selection.service';
import { CardDetailsModalComponent } from '../cards/_modals/card-details-modal/card-details-modal.component';
import { EditSeriesModalComponent } from '../cards/_modals/edit-series-modal/edit-series-modal.component';
@ -185,12 +185,13 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
return;
}
this.messageHub.scanSeries.pipe(takeUntil(this.onDestroy)).subscribe((event: ScanSeriesEvent) => {
if (event.seriesId == this.series.id)
this.loadSeries(seriesId);
this.seriesImage = this.imageService.randomize(this.imageService.getSeriesCoverImage(this.series.id));
this.toastr.success('Scan series completed');
});
// this.messageHub.messages$.pipe(takeUntil(this.onDestroy), takeWhile(e => this.messageHub.isEventType(e, EVENTS.ScanSeries))).subscribe((e) => {
// const event = e.payload as ScanSeriesEvent;
// if (event.seriesId == this.series.id)
// this.loadSeries(seriesId);
// this.seriesImage = this.imageService.randomize(this.imageService.getSeriesCoverImage(this.series.id));
// this.toastr.success('Scan series completed');
// });
this.messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(event => {
if (event.event === EVENTS.SeriesRemoved) {
@ -203,6 +204,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
const seriesCoverUpdatedEvent = event.payload as ScanSeriesEvent;
if (seriesCoverUpdatedEvent.seriesId === this.series.id) {
this.loadSeries(seriesId);
this.seriesImage = this.imageService.randomize(this.imageService.getSeriesCoverImage(this.series.id)); // NOTE: Is this needed as cover update will update the image for us
}
}
});

View file

@ -56,7 +56,6 @@ export class ImageComponent implements OnChanges, OnDestroy {
//...seriesId=123&random=
const id = tokens[0].replace(enityType + 'Id=', '');
if (id === (updateEvent.id + '')) {
console.log('Image url: ', this.imageUrl, ' matches update event: ', updateEvent);
this.imageUrl = this.imageService.randomize(this.imageUrl);
}
}

View file

@ -5,6 +5,7 @@ import { DomSanitizer } from '@angular/platform-browser';
import { map, ReplaySubject, Subject, takeUntil } from 'rxjs';
import { environment } from 'src/environments/environment';
import { ConfirmService } from './shared/confirm.service';
import { NotificationProgressEvent } from './_models/events/notification-progress-event';
import { SiteThemeProgressEvent } from './_models/events/site-theme-progress-event';
import { SiteTheme, ThemeProvider } from './_models/preferences/site-theme';
import { EVENTS, MessageHubService } from './_services/message-hub.service';
@ -41,10 +42,13 @@ export class ThemeService implements OnDestroy {
this.getThemes();
messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(message => {
if (message.event === EVENTS.SiteThemeProgress) {
if ((message.payload as SiteThemeProgressEvent).progress === 1) {
this.getThemes().subscribe(() => {});
}
if (message.event !== EVENTS.NotificationProgress) return;
const notificationEvent = (message.payload as NotificationProgressEvent);
if (notificationEvent.name !== EVENTS.SiteThemeProgress) return;
if (notificationEvent.eventType === 'ended') {
this.getThemes().subscribe(() => {});
}
});
}
@ -59,7 +63,6 @@ export class ThemeService implements OnDestroy {
}
isDarkTheme() {
console.log('color scheme: ', getComputedStyle(this.document.body).getPropertyValue('--color-scheme').trim().toLowerCase());
return this.getColorScheme().toLowerCase() === 'dark';
}

View file

@ -34,6 +34,7 @@
@import './theme/components/slider';
@import './theme/components/radios';
@import './theme/components/selects';
@import './theme/components/progress';
@import './theme/utilities/utilities';

View file

@ -1,5 +1,22 @@
.popover {
background-color: var(--popover-bg-color);
border-color: var(--popover-border-color);
}
.bs-popover-bottom {
> .popover-arrow {
&::before {
border-bottom-color: var(--popover-outerarrow-color);
}
&::after {
border-bottom-color: var(--popover-arrow-color);
}
}
}
.popover-body {
background-color: var(--popover-body-bg-color);
color: var(--popover-body-text-color)
}
}

View file

@ -0,0 +1,13 @@
.progress {
background-color: var(--progress-bg-color);
}
.progress-bar {
background-color: var(--progress-bar-color);
}
.progress-bar-striped {
background-image: var(--progress-striped-animated-color);
background-color: unset;
}

View file

@ -96,6 +96,10 @@
/* Popover */
--popover-body-bg-color: var(--navbar-bg-color);
--popover-body-text-color: var(--navbar-text-color);
--popover-outerarrow-color: transparent;
--popover-arrow-color: transparent;
--popover-bg-color: black;
--popover-border-color: black;
/* Pagination */
--pagination-active-link-border-color: var(--primary-color);
@ -106,9 +110,14 @@
--pagination-link-bg-color: rgba(1, 4, 9, 0.5);
--pagination-focus-border-color: var(--primary-color);
/* Progress Bar */
--progress-striped-animated-color: linear-gradient(45deg, rgba(74,198,148, 0.75) 25%, rgba(51, 138, 103, 0.75) 25%, rgba(51, 138, 103, 0.75) 50%, rgba(74,198,148, 0.75) 50%, rgba(74,198,148, 0.75) 75%, rgba(51, 138, 103, 0.75) 75%, rgba(51, 138, 103, 0.75));
--progress-bg-color: var(--nav-header-bg-color);
--progress-bar-color: var(--primary-color-dark-shade);
/* Dropdown */
--dropdown-item-hover-text-color: white;
--dropdown-item-hover-bg-color: var(--primary-color);
--dropdown-item-hover-bg-color: var(--primary-color-dark-shade);
--dropdown-item-text-color: var(--navbar-text-color);
--dropdown-item-bg-color: var(--navbar-bg-color);
--dropdown-overlay-color: rgba(0,0,0,0.5);