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:
parent
d24620fd15
commit
eddbb7ab18
49 changed files with 1022 additions and 463 deletions
12
UI/Web/src/app/_models/events/file-scan-progress-event.ts
Normal file
12
UI/Web/src/app/_models/events/file-scan-progress-event.ts
Normal 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;
|
||||
}
|
||||
40
UI/Web/src/app/_models/events/notification-container.ts
Normal file
40
UI/Web/src/app/_models/events/notification-container.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
30
UI/Web/src/app/_models/events/notification-progress-event.ts
Normal file
30
UI/Web/src/app/_models/events/notification-progress-event.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -2,4 +2,10 @@ export interface ProgressEvent {
|
|||
libraryId: number;
|
||||
progress: number;
|
||||
eventTime: string;
|
||||
|
||||
// New fields
|
||||
/**
|
||||
* Event type
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
|
@ -1,7 +1,3 @@
|
|||
export interface SiteThemeProgressEvent {
|
||||
totalUpdates: number;
|
||||
currentCount: number;
|
||||
themeName: string;
|
||||
progress: number;
|
||||
eventTime: string;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> 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> 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>
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
@import './theme/components/slider';
|
||||
@import './theme/components/radios';
|
||||
@import './theme/components/selects';
|
||||
@import './theme/components/progress';
|
||||
|
||||
|
||||
@import './theme/utilities/utilities';
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
13
UI/Web/src/theme/components/progress.scss
Normal file
13
UI/Web/src/theme/components/progress.scss
Normal 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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue