Event Widget Updates + Format Downloads + Scanner Work (#3024)

This commit is contained in:
Joe Milazzo 2024-06-27 16:35:50 -05:00 committed by GitHub
parent 30a8a2555f
commit a427d02ed1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 971 additions and 694 deletions

View file

@ -1,139 +1,118 @@
<ng-container *transloco="let t; read: 'events-widget'">
<ng-container *ngIf="isAdmin$ | async">
<ng-container *ngIf="downloadService.activeDownloads$ | async as activeDownloads">
<ng-container *ngIf="errors$ | async as errors">
<ng-container *ngIf="infos$ | async as infos">
<button type="button" class="btn btn-icon" [ngClass]="{'colored': activeEvents > 0 || activeDownloads.length > 0, 'colored-error': errors.length > 0,
'colored-info': infos.length > 0 && errors.length === 0}"
[ngbPopover]="popContent" [title]="t('title-alt')" placement="bottom" [popoverClass]="'nav-events'" [autoClose]="'outside'">
<i aria-hidden="true" class="fa fa-wave-square nav"></i>
</button>
</ng-container>
</ng-container>
</ng-container>
@if (isAdmin$ | async) {
@if (downloadService.activeDownloads$ | async; as activeDownloads) {
@if (errors$ | async; as errors) {
@if (infos$ | async; as infos) {
@if (messageHub.onlineUsers$ | async; as onlineUsers) {
<button type="button" class="btn btn-icon"
[ngbPopover]="popContent" [title]="t('title-alt')"
placement="bottom" [popoverClass]="'nav-events'"
[autoClose]="'outside'">
@if (onlineUsers.length > 1) {
<span class="me-2" [ngClass]="{'colored': activeEvents > 0 || activeDownloads.length > 0 || updateAvailable}">{{onlineUsers.length}}</span>
}
<i aria-hidden="true" class="fa fa-wave-square nav" [ngClass]="{'colored': activeEvents > 0 || activeDownloads.length > 0 || updateAvailable}"></i>
@if (errors.length > 0) {
<i aria-hidden="true" class="fa fa-circle-exclamation nav widget-button--indicator error"></i>
} @else if (infos.length > 0) {
<i aria-hidden="true" class="fa fa-circle-info nav widget-button--indicator info"></i>
} @else if (activeEvents > 0 || activeDownloads.length > 0) {
<div class="nav widget-button--indicator spinner-border spinner-border-sm"></div>
} @else if (updateAvailable) {
<i aria-hidden="true" class="fa fa-circle-arrow-up nav widget-button--indicator update"></i>
}
</button>
}
}
}
}
<ng-template #popContent>
<ul class="list-group list-group-flush dark-menu">
<ng-container *ngIf="errors$ | async as errors">
<ng-container *ngIf="infos$ | async as infos">
<li class="list-group-item dark-menu-item clickable" *ngIf="errors.length > 0 || infos.length > 0" (click)="clearAllErrorOrInfos()">
{{t('dismiss-all')}}
</li>
</ng-container>
</ng-container>
@if (debugMode) {
<ng-container>
<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>
<li class="list-group-item dark-menu-item info">
<div>
<div class="h6 mb-1"><i class="fa-solid fa-circle-info me-2"></i>Scan didn't run becasuse nothing to do</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>
<li class="list-group-item dark-menu-item">
<div class="d-inline-flex">
<span class="download">
<app-circular-loader [currentValue]="25" fontSize="16px" [showIcon]="true" width="25px" height="unset" [center]="false"></app-circular-loader>
<span class="visually-hidden" role="status">
10% downloaded
</span>
</span>
<span class="h6 mb-1">Downloading {{'series' | sentenceCase}}</span>
</div>
<div class="accent-text">PDFs</div>
</li>
</ng-container>
@if(errors$ | async; as errors) {
@if(infos$ | async; as infos) {
@if (errors.length > 0 || infos.length > 0) {
<li class="list-group-item dark-menu-item clickable" (click)="clearAllErrorOrInfos()">
{{t('dismiss-all')}}
</li>
}
}
}
<!-- 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>
@if (message.subTitle !== '') {
<div class="accent-text mb-1" [title]="message.subTitle">{{message.subTitle}}</div>
}
@if (message.name === EVENTS.ScanProgress && message.body.leftToProcess > 0) {
<div class="accent-text mb-1" [title]="t('left-to-process', {leftToProcess: message.body.leftToProcess})">{{t('left-to-process', {leftToProcess: message.body.leftToProcess})}}</div>
}
<div class="progress-container row g-0 align-items-center">
@if(message.progress === 'indeterminate') {
<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>
<ng-template #progressEvent>
@if (progressEvents$ | async; as progressUpdates) {
@for (message of progressUpdates; track message) {
@if (message.progress === 'indeterminate' || message.progress === 'none') {
<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 !== ''" [title]="message.subTitle">{{message.subTitle}}</div>
@if (message.subTitle !== '') {
<div class="accent-text mb-1" [title]="message.subTitle">{{message.subTitle}}</div>
}
@if (message.name === EVENTS.ScanProgress && message.body.leftToProcess > 0) {
<div class="accent-text mb-1" [title]="t('left-to-process', {leftToProcess: message.body.leftToProcess})">
{{t('left-to-process', {leftToProcess: message.body.leftToProcess})}}
</div>
}
<div class="progress-container row g-0 align-items-center">
@if(message.progress === 'indeterminate') {
<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>
} @else {
<li class="list-group-item dark-menu-item">
<div class="h6 mb-1">{{message.title}}</div>
@if (message.subTitle !== '') {
<div class="accent-text mb-1" [title]="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 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 me-1" aria-hidden="true"></i>{{t('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>
@if (singleUpdates$ | async; as singleUpdates) {
@for(singleUpdate of singleUpdates; track singleUpdate) {
@if (singleUpdate.name === EVENTS.UpdateAvailable) {
<li class="list-group-item dark-menu-item update-available" (click)="handleUpdateAvailableClick(singleUpdate)">
<i class="fa fa-chevron-circle-up me-1" aria-hidden="true"></i>{{t('update-available')}}
</li>
} @else {
<li class="list-group-item dark-menu-item update-available">
<div>{{singleUpdate.title}}</div>
@if (singleUpdate.subTitle !== '') {
<div class="accent-text">{{singleUpdate.subTitle}}</div>
}
</li>
}
}
}
<!-- Active Downloads by the user-->
<ng-container *ngIf="downloadService.activeDownloads$ | async as activeDownloads">
<ng-container *ngFor="let download of activeDownloads">
@if (downloadService.activeDownloads$ | async; as activeDownloads) {
@for(download of activeDownloads; track download) {
<li class="list-group-item dark-menu-item">
<div class="h6 mb-1">{{t('downloading-item', {item: download.entityType | sentenceCase})}}</div>
<div class="accent-text mb-1" *ngIf="download.subTitle !== ''" [title]="download.subTitle">{{download.subTitle}}</div>
@if (download.subTitle !== '') {
<div class="accent-text mb-1" [title]="download.subTitle">{{download.subTitle}}</div>
}
<div class="progress-container row g-0 align-items-center">
<div class="col-2">{{download.progress}}%</div>
<div class="col-10 progress" style="height: 5px;">
@ -141,57 +120,49 @@
</div>
</div>
</li>
</ng-container>
@if(activeDownloads.length > 1) {
<li class="list-group-item dark-menu-item">{{activeDownloads.length}} downloads in Queue</li>
}
</ng-container>
@if(activeDownloads.length > 1) {
<li class="list-group-item dark-menu-item">{{t('download-in-queue', {num: activeDownloads.length})}}</li>
}
}
<!-- Errors -->
<ng-container *ngIf="errors$ | async as errors">
<ng-container *ngFor="let error of errors">
@if (errors$ | async; as errors) {
@for (error of errors; track error) {
<li class="list-group-item dark-menu-item error" role="alert" (click)="seeMore(error)">
<div>
<div class="h6 mb-1"><i class="fa-solid fa-triangle-exclamation me-2"></i>{{error.title}}</div>
<div class="h6 mb-1"><i class="fa-solid fa-triangle-exclamation me-2" aria-hidden="true"></i>{{error.title}}</div>
<div class="accent-text mb-1">{{t('more-info')}}</div>
</div>
<button type="button" class="btn-close float-end" [attr.aria-label]="t('close')" (click)="removeErrorOrInfo(error, $event)"></button>
</li>
</ng-container>
</ng-container>
}
}
<!-- Infos -->
<ng-container *ngIf="infos$ | async as infos">
<ng-container *ngFor="let info of infos">
@if (infos$ | async; as infos) {
@for (info of infos; track info) {
<li class="list-group-item dark-menu-item info" role="alert" (click)="seeMore(info)">
<div>
<div class="h6 mb-1"><i class="fa-solid fa-circle-info me-2"></i>{{info.title}}</div>
<div class="h6 mb-1"><i class="fa-solid fa-circle-info me-2" aria-hidden="true"></i>{{info.title}}</div>
<div class="accent-text mb-1">{{t('more-info')}}</div>
</div>
<button type="button" class="btn-close float-end" [attr.aria-label]="t('close')" (click)="removeErrorOrInfo(info, $event)"></button>
</li>
</ng-container>
</ng-container>
<!-- Online Users -->
@if (messageHub.onlineUsers$ | async; as onlineUsers) {
@if (onlineUsers.length > 1) {
<li class="list-group-item dark-menu-item">
<div>{{t('users-online-count', {num: onlineUsers.length})}}</div>
</li>
}
@if (debugMode) {
<li class="list-group-item dark-menu-item">{{t('active-events-title')}} {{activeEvents}}</li>
}
}
<ng-container *ngIf="downloadService.activeDownloads$ | async as activeDownloads">
<li class="list-group-item dark-menu-item" *ngIf="activeEvents === 0 && activeDownloads.length === 0">{{t('no-data')}}</li>
</ng-container>
@if (downloadService.activeDownloads$ | async; as activeDownloads) {
@if (errors$ | async; as errors) {
@if (infos$ | async; as infos) {
@if (infos.length === 0 && errors.length === 0 && activeDownloads.length === 0 && activeEvents === 0) {
<li class="list-group-item dark-menu-item">{{t('no-data')}}</li>
}
}
}
}
</ul>
</ng-template>
</ng-container>
}
</ng-container>

View file

@ -14,6 +14,26 @@
border-bottom-color: transparent;
}
.colored {
color: var(--event-widget-activity-bg-color) !important;
}
.widget-button--indicator {
position: absolute;
top: 30px;
color: var(--event-widget-activity-bg-color);
&.error {
color: var(--event-widget-error-bg-color) !important;
}
&.info {
color: var(--event-widget-info-bg-color) !important;
}
&.update {
color: var(--event-widget-update-bg-color) !important;
}
}
::ng-deep .nav-events {
.popover-body {
@ -56,67 +76,56 @@
.btn-icon {
color: white;
color: var(--event-widget-text-color);
}
.colored {
background-color: var(--primary-color);
border-radius: 60px;
}
.colored-error {
background-color: var(--error-color) !important;
border-radius: 60px;
}
.colored-info {
background-color: var(--event-widget-info-bg-color) !important;
border-radius: 60px;
}
.update-available {
.dark-menu-item {
&.update-available {
cursor: pointer;
i.fa {
color: var(--primary-color) !important;
color: var(--primary-color) !important;
}
color: var(--primary-color);
}
}
.error {
&.error {
cursor: pointer;
position: relative;
.h6 {
color: var(--error-color);
color: var(--event-widget-error-bg-color);
}
i.fa {
color: var(--primary-color) !important;
color: var(--primary-color) !important;
}
.btn-close {
top: 5px;
right: 10px;
font-size: 11px;
position: absolute;
top: 5px;
right: 10px;
font-size: 11px;
position: absolute;
}
}
}
.info {
&.info {
cursor: pointer;
position: relative;
.h6 {
color: var(--event-widget-info-bg-color);
color: var(--event-widget-info-bg-color);
}
i.fa {
color: var(--primary-color) !important;
color: var(--primary-color) !important;
}
.btn-close {
top: 10px;
right: 10px;
font-size: 11px;
position: absolute;
top: 10px;
right: 10px;
font-size: 11px;
position: absolute;
}
}
}

View file

@ -25,7 +25,7 @@ import { EVENTS, Message, MessageHubService } from 'src/app/_services/message-hu
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import { SentenceCasePipe } from '../../../_pipes/sentence-case.pipe';
import { CircularLoaderComponent } from '../../../shared/circular-loader/circular-loader.component';
import { NgIf, NgClass, NgStyle, NgFor, AsyncPipe } from '@angular/common';
import { NgClass, NgStyle, AsyncPipe } from '@angular/common';
import {TranslocoDirective} from "@ngneat/transloco";
@Component({
@ -34,12 +34,20 @@ import {TranslocoDirective} from "@ngneat/transloco";
styleUrls: ['./events-widget.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgIf, NgClass, NgbPopover, NgStyle, CircularLoaderComponent, NgFor, AsyncPipe, SentenceCasePipe, TranslocoDirective]
imports: [NgClass, NgbPopover, NgStyle, CircularLoaderComponent, AsyncPipe, SentenceCasePipe, TranslocoDirective]
})
export class EventsWidgetComponent implements OnInit, OnDestroy {
@Input({required: true}) user!: User;
public readonly downloadService = inject(DownloadService);
public readonly messageHub = inject(MessageHubService);
private readonly modalService = inject(NgbModal);
private readonly accountService = inject(AccountService);
private readonly confirmService = inject(ConfirmService);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly destroyRef = inject(DestroyRef);
@Input({required: true}) user!: User;
isAdmin$: Observable<boolean> = of(false);
/**
@ -60,17 +68,15 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
private updateNotificationModalRef: NgbModalRef | null = null;
activeEvents: number = 0;
/**
* Intercepts from Single Updates to show an extra indicator to the user
*/
updateAvailable: boolean = false;
debugMode: boolean = false;
protected readonly EVENTS = EVENTS;
public readonly downloadService = inject(DownloadService);
constructor(public messageHub: MessageHubService, private modalService: NgbModal,
private accountService: AccountService, private confirmService: ConfirmService,
private readonly cdRef: ChangeDetectorRef) {
}
ngOnDestroy(): void {
this.progressEventsSource.complete();
@ -115,6 +121,9 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
values.push(message);
this.singleUpdateSource.next(values);
this.activeEvents += 1;
if (event.payload.name === EVENTS.UpdateAvailable) {
this.updateAvailable = true;
}
this.cdRef.markForCheck();
break;
case 'started':