Dashboard Customization Polish (#2295)

This commit is contained in:
Joe Milazzo 2023-09-30 12:33:16 -06:00 committed by GitHub
parent 25e759d301
commit 25ffb2ffe1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 255 additions and 258 deletions

View file

@ -120,7 +120,6 @@ export class AccountService {
const user = response;
if (user) {
this.setCurrentUser(user);
this.messageHub.createHubConnection(user, this.hasAdminRole(user));
}
}),
takeUntilDestroyed(this.destroyRef)
@ -150,7 +149,8 @@ export class AccountService {
this.stopRefreshTokenTimer();
if (this.currentUser) {
this.messageHub.createHubConnection(this.currentUser, this.hasAdminRole(this.currentUser));
this.messageHub.stopHubConnection();
this.messageHub.createHubConnection(this.currentUser);
this.hasValidLicense().subscribe();
this.startRefreshTokenTimer();
}

View file

@ -114,8 +114,6 @@ export class MessageHubService {
*/
public onlineUsers$ = this.onlineUsersSource.asObservable();
isAdmin: boolean = false;
constructor() {}
/**
@ -132,9 +130,7 @@ export class MessageHubService {
return event.event === eventType;
}
createHubConnection(user: User, isAdmin: boolean) {
this.isAdmin = isAdmin;
createHubConnection(user: User) {
this.hubConnection = new HubConnectionBuilder()
.withUrl(this.hubUrl + 'messages', {
accessTokenFactory: () => user.token
@ -186,7 +182,6 @@ export class MessageHubService {
});
this.hubConnection.on(EVENTS.DashboardUpdate, resp => {
console.log('dashboard update event came in')
this.messagesSource.next({
event: EVENTS.DashboardUpdate,
payload: resp.body as DashboardUpdateEvent

View file

@ -92,7 +92,7 @@ export class LicenseComponent implements OnInit {
}
async deleteLicense() {
if (!await this.confirmService.confirm(translate('k+-delete-key'))) {
if (!await this.confirmService.confirm(translate('toasts.k+-delete-key'))) {
return;
}

View file

@ -2,7 +2,7 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, injec
import {Title} from '@angular/platform-browser';
import {Router, RouterLink} from '@angular/router';
import {Observable, of, ReplaySubject, Subject, switchMap} from 'rxjs';
import {map, shareReplay, take, tap, throttleTime} from 'rxjs/operators';
import {debounceTime, map, shareReplay, take, tap, throttleTime} from 'rxjs/operators';
import {FilterUtilitiesService} from 'src/app/shared/_services/filter-utilities.service';
import {Library} from 'src/app/_models/library';
import {RecentlyAddedItem} from 'src/app/_models/recently-added-item';
@ -57,6 +57,7 @@ export class DashboardComponent implements OnInit {
streams: Array<DashboardStream> = [];
genre: Genre | undefined;
refreshStreams$ = new Subject<void>();
refreshStreamsFromDashboardUpdate$ = new Subject<void>();
/**
@ -80,6 +81,13 @@ export class DashboardComponent implements OnInit {
this.loadDashboard();
this.refreshStreamsFromDashboardUpdate$.pipe(takeUntilDestroyed(this.destroyRef), debounceTime(1000),
tap(() => {
console.log('Loading Dashboard')
this.loadDashboard()
}))
.subscribe();
this.refreshStreams$.pipe(takeUntilDestroyed(this.destroyRef), throttleTime(10_000),
tap(() => {
this.loadDashboard()
@ -87,29 +95,12 @@ export class DashboardComponent implements OnInit {
.subscribe();
// TODO: Solve how Websockets will work with these dyanamic streams
this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(res => {
if (res.event === EVENTS.DashboardUpdate) {
console.log('dashboard update triggered')
this.refreshStreams$.next();
this.refreshStreamsFromDashboardUpdate$.next();
} else if (res.event === EVENTS.SeriesAdded) {
// const seriesAddedEvent = res.payload as SeriesAddedEvent;
// this.seriesService.getSeries(seriesAddedEvent.seriesId).subscribe(series => {
// if (this.recentlyAddedSeries.filter(s => s.id === series.id).length > 0) return;
// this.recentlyAddedSeries = [series, ...this.recentlyAddedSeries];
// this.cdRef.markForCheck();
// });
this.refreshStreams$.next();
} else if (res.event === EVENTS.SeriesRemoved) {
//const seriesRemovedEvent = res.payload as SeriesRemovedEvent;
//
// this.inProgress = this.inProgress.filter(item => item.id != seriesRemovedEvent.seriesId);
// this.recentlyAddedSeries = this.recentlyAddedSeries.filter(item => item.id != seriesRemovedEvent.seriesId);
// this.recentlyUpdatedSeries = this.recentlyUpdatedSeries.filter(item => item.seriesId != seriesRemovedEvent.seriesId);
// this.cdRef.markForCheck();
this.refreshStreams$.next();
} else if (res.event === EVENTS.ScanSeries) {
// We don't have events for when series are updated, but we do get events when a scan update occurs. Refresh recentlyAdded at that time.

View file

@ -20,9 +20,9 @@
<span class="visually-hidden">{{t('shortcuts-menu-alt')}}</span>
</button>
<button *ngIf="!bookmarkMode && hasBookmarkRights" class="btn btn-icon" role="checkbox" [attr.aria-checked]="CurrentPageBookmarked"
title="{{CurrentPageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}" (click)="bookmarkPage()">
title="{{t(CurrentPageBookmarked ? 'unbookmark-page-tooltip' : 'bookmark-page-tooltip')}}" (click)="bookmarkPage()">
<i class="{{CurrentPageBookmarked ? 'fa' : 'far'}} fa-bookmark" aria-hidden="true"></i>
<span class="visually-hidden">{{CurrentPageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}</span>
<span class="visually-hidden">{{t(CurrentPageBookmarked ? 'unbookmark-page-tooltip' : 'bookmark-page-tooltip')}}</span>
</button>
</div>
</div>

View file

@ -737,7 +737,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
getPage(pageNum: number, chapterId: number = this.chapterId, forceNew: boolean = false) {
let img;
if (this.bookmarkMode) img = this.cachedImages.find(img => this.readerService.imageUrlToPageNum(img.src) === pageNum);
if (this.bookmarkMode) img = this.cachedImages.find(img => this.readerService.imageUrlToPageNum(img.src) === pageNum);
else img = this.cachedImages.find(img => this.readerService.imageUrlToPageNum(img.src) === pageNum
&& (this.readerService.imageUrlToChapterId(img.src) == chapterId || this.readerService.imageUrlToChapterId(img.src) === -1)
);
@ -1208,9 +1208,14 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
setCanvasImage() {
if (this.cachedImages === undefined) return;
this.canvasImage = this.getPage(this.pageNum, this.chapterId, this.layoutMode !== LayoutMode.Single);
this.canvasImage.addEventListener('load', () => {
if (!this.canvasImage.complete) {
this.canvasImage.addEventListener('load', () => {
this.currentImage.next(this.canvasImage);
}, false);
} else {
this.currentImage.next(this.canvasImage);
}, false);
}
this.cdRef.markForCheck();
}
@ -1329,7 +1334,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
}
// const pages = this.cachedImages.map(img => [this.readerService.imageUrlToChapterId(img.src), this.readerService.imageUrlToPageNum(img.src)]);
//const pages = this.cachedImages.map(img => [this.readerService.imageUrlToChapterId(img.src), this.readerService.imageUrlToPageNum(img.src)]);
// console.log(this.pageNum, ' Prefetched pages: ', pages.map(p => {
// if (this.pageNum === p[1]) return '[' + p + ']';
// return '' + p

View file

@ -36,7 +36,7 @@
<ng-template #mobileView>
<div class="container-fluid">
<div class="row mb-3">
<div class="col-md-2 col-10">
<div class="col-md-4 col-10">
<select class="form-select" formControlName="comparison">
<option *ngFor="let opt of groupOptions" [value]="opt.value">{{opt.title}}</option>
</select>

View file

@ -3,7 +3,7 @@
}
::ng-deep .ngb-dp-content, ::ng-deep .ngb-dp-header, ::ng-deep .dropdown-menu{
::ng-deep .ngb-dp-content, ::ng-deep .ngb-dp-header{
background: var(--bs-body-bg);
color: var(--body-text-color);
}

View file

@ -1,22 +1,26 @@
<ng-container *transloco="let t; read: 'metadata-filter'">
<ng-container *ngIf="toggleService.toggleState$ | async as isOpen">
<div *ngIf="utilityService.getActiveBreakpoint() >= Breakpoint.Tablet">
<div #collapse="ngbCollapse" [ngbCollapse]="!isOpen" (ngbCollapseChange)="setToggle($event)">
<ng-container [ngTemplateOutlet]="filterSection"></ng-container>
</div>
</div>
<div *ngIf="utilityService.getActiveBreakpoint() < Breakpoint.Desktop">
<app-drawer #commentDrawer="drawer" [isOpen]="isOpen" [options]="{topOffset: 56}" (drawerClosed)="toggleService.set(false)">
<h5 header>
{{t('filter-title')}}
</h5>
<div body class="drawer-body">
<!-- TODO: BUG: Filter section is instantiated twice if this isn't ngIf'd -->
<ng-container *ngIf="utilityService.getActiveBreakpoint() as activeBreakpoint">
<div *ngIf="activeBreakpoint >= Breakpoint.Tablet; else mobileView">
<div #collapse="ngbCollapse" [ngbCollapse]="!isOpen" (ngbCollapseChange)="setToggle($event)">
<ng-container [ngTemplateOutlet]="filterSection"></ng-container>
</div>
</app-drawer>
</div>
</div>
<ng-template #mobileView>
<div>
<app-drawer #commentDrawer="drawer" [isOpen]="isOpen" [options]="{topOffset: 56}" (drawerClosed)="toggleService.set(false)" [width]="600">
<h5 header>
{{t('filter-title')}}
</h5>
<div body class="drawer-body">
<!-- TODO: BUG: Filter section is instantiated twice if this isn't ngIf'd -->
<ng-container [ngTemplateOutlet]="filterSection"></ng-container>
</div>
</app-drawer>
</div>
</ng-template>
</ng-container>
</ng-container>
<ng-template #filterSection>
@ -30,13 +34,13 @@
</div>
<form [formGroup]="sortGroup" class="container-fluid">
<div class="row mb-3">
<div class="col-md-2 col-sm-2">
<div class="col-md-2 col-sm-3">
<div class="form-group pe-1">
<label for="limit-to" class="form-label">{{t('limit-label')}}</label>
<input id="limit-to" type="number" inputmode="numeric" class="form-control" formControlName="limitTo">
</div>
</div>
<div class="col-md-3 col-sm-10">
<div class="col-md-3 col-sm-9">
<label for="sort-options" class="form-label">{{t('sort-by-label')}}</label>
<button class="btn btn-sm btn-secondary-outline" (click)="updateSortOrder()" style="height: 25px; padding-bottom: 0;" [disabled]="filterSettings.sortDisabled">
<i class="fa fa-arrow-up" [title]="t('ascending-alt')" *ngIf="isAscendingSort; else descSort"></i>
@ -48,17 +52,16 @@
<option *ngFor="let field of allSortFields" [value]="field">{{field | sortField}}</option>
</select>
</div>
<div class="col-md-3 col-sm-10">
<div class="col-md-4 col-sm-12" [ngClass]="{'mt-3': utilityService.getActiveBreakpoint() <= Breakpoint.Mobile}">
<label for="filter-name" class="form-label">{{t('filter-name-label')}}</label>
<input id="filter-name" type="text" class="form-control" formControlName="name">
<!-- <select2 [data]="smartFilters"-->
<!-- id="filter-name"-->
<!-- formControlName="name"-->
<!-- (update)="updateFilterValue($event)"-->
<!-- (update)="loadSavedFilter($event)"-->
<!-- (autoCreateItem)="createFilterValue($event)"-->
<!-- [autoCreate]="true"-->
<!-- [multiple]="false"-->
<!-- [infiniteScroll]="false"-->
<!-- [hideSelectedItems]="true"-->
<!-- displaySearchStatus="always"-->
<!-- [resettable]="true">-->
<!-- </select2>-->
</div>
@ -73,11 +76,11 @@
</ng-template>
<ng-template #buttons>
<!-- TODO: I might want to put a Clear button which blanks out the whole filter -->
<div class="col-md-2 col-sm-6 mt-4 pt-2 d-flex justify-content-between">
<div class="col-md-6 col-sm-6 mt-4 pt-2 d-flex justify-content-between">
<button class="btn btn-secondary col-6 me-1" (click)="clear()"><i class="fa-solid fa-arrow-rotate-left me-1" aria-hidden="true"></i>{{t('reset')}}</button>
<button class="btn btn-primary col-6" (click)="apply()"><i class="fa-solid fa-play me-1" aria-hidden="true"></i>{{t('apply')}}</button>
</div>
<div class="col-md-1 col-sm-6 mt-4 pt-2">
<div class="col-md-2 col-sm-6 mt-4 pt-2">
<button class="btn btn-primary col-12" (click)="save()" [disabled]="filterSettings.saveDisabled || !this.sortGroup.get('name')?.value">
<i class="fa-solid fa-floppy-disk" aria-hidden="true"></i>
{{t('save')}}

View file

@ -21,7 +21,7 @@ import {SeriesFilterV2} from '../_models/metadata/v2/series-filter-v2';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {TypeaheadComponent} from '../typeahead/_components/typeahead.component';
import {DrawerComponent} from '../shared/drawer/drawer.component';
import {AsyncPipe, NgForOf, NgIf, NgTemplateOutlet} from '@angular/common';
import {AsyncPipe, NgClass, NgForOf, NgIf, NgTemplateOutlet} from '@angular/common';
import {translate, TranslocoModule} from "@ngneat/transloco";
import {SortFieldPipe} from "../pipe/sort-field.pipe";
import {MetadataBuilderComponent} from "./_components/metadata-builder/metadata-builder.component";
@ -30,7 +30,13 @@ import {MetadataService} from "../_services/metadata.service";
import {FilterUtilitiesService} from "../shared/_services/filter-utilities.service";
import {FilterService} from "../_services/filter.service";
import {ToastrService} from "ngx-toastr";
import {Select2Module, Select2Option, Select2UpdateEvent} from "ng-select2-component";
import {
Select2AutoCreateEvent,
Select2Module,
Select2Option,
Select2UpdateEvent,
Select2UpdateValue
} from "ng-select2-component";
import {SmartFilter} from "../_models/metadata/v2/smart-filter";
@Component({
@ -40,7 +46,7 @@ import {SmartFilter} from "../_models/metadata/v2/smart-filter";
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgIf, NgbCollapse, NgTemplateOutlet, DrawerComponent, NgbTooltip, TypeaheadComponent,
ReactiveFormsModule, FormsModule, NgbRating, AsyncPipe, TranslocoModule, SortFieldPipe, MetadataBuilderComponent, NgForOf, Select2Module]
ReactiveFormsModule, FormsModule, NgbRating, AsyncPipe, TranslocoModule, SortFieldPipe, MetadataBuilderComponent, NgForOf, Select2Module, NgClass]
})
export class MetadataFilterComponent implements OnInit {
@ -61,6 +67,7 @@ export class MetadataFilterComponent implements OnInit {
@ContentChild('[ngbCollapse]') collapse!: NgbCollapse;
private readonly destroyRef = inject(DestroyRef);
public readonly utilityService = inject(UtilityService);
public readonly filterUtilitiesService = inject(FilterUtilitiesService);
/**
@ -114,8 +121,36 @@ export class MetadataFilterComponent implements OnInit {
this.loadFromPresetsAndSetup();
}
updateFilterValue(event: Select2UpdateEvent<any>) {
console.log('event: ', event);
loadSavedFilter(event: Select2UpdateEvent<any>) {
// Load the filter from the backend and update the screen
if (event.value === undefined || typeof(event.value) === 'string') return;
const smartFilter = event.value as SmartFilter;
this.filterV2 = this.filterUtilitiesService.decodeSeriesFilter(smartFilter.filter);
this.cdRef.markForCheck();
console.log('update event: ', event);
}
createFilterValue(event: Select2AutoCreateEvent<any>) {
// Create a new name and filter
if (!this.filterV2) return;
this.filterV2.name = event.value;
this.filterService.saveFilter(this.filterV2).subscribe(() => {
const item = {
value: {
filter: this.filterUtilitiesService.encodeSeriesFilter(this.filterV2!),
name: event.value,
} as SmartFilter,
label: event.value
};
this.smartFilters.push(item);
this.sortGroup.get('name')?.setValue(item);
this.cdRef.markForCheck();
this.toastr.success(translate('toasts.smart-filter-updated'));
this.apply();
});
console.log('create event: ', event);
}
@ -204,7 +239,7 @@ export class MetadataFilterComponent implements OnInit {
apply() {
this.applyFilter.emit({isFirst: this.updateApplied === 0, filterV2: this.filterV2!});
if (this.utilityService.getActiveBreakpoint() === Breakpoint.Mobile && this.updateApplied !== 0) {
this.toggleSelected();
}
@ -219,7 +254,7 @@ export class MetadataFilterComponent implements OnInit {
this.filterService.saveFilter(this.filterV2).subscribe(() => {
this.toastr.success(translate('toasts.smart-filter-updated'));
this.apply();
})
});
}
toggleSelected() {
@ -231,5 +266,5 @@ export class MetadataFilterComponent implements OnInit {
this.toggleService.set(!this.filteringCollapsed);
}
protected readonly Breakpoint = Breakpoint;
protected readonly Breakpoint = Breakpoint;
}

View file

@ -15,9 +15,9 @@ export class ProviderNamePipe implements PipeTransform {
return 'MAL';
case ScrobbleProvider.Kavita:
return 'Kavita';
case ScrobbleProvider.GoogleBooks:
return 'Google Books';
}
return '';
}
}

View file

@ -12,7 +12,7 @@ export class DrawerOptions {
@Component({
selector: 'app-drawer',
standalone: true,
imports: [CommonModule, TranslocoDirective],
imports: [CommonModule, TranslocoDirective],
templateUrl: './drawer.component.html',
styleUrls: ['./drawer.component.scss'],
exportAs: "drawer",

View file

@ -17,11 +17,11 @@
{{filter.name}}
<button class="btn btn-icon" (click)="addFilterToStream(filter)">
<i class="fa fa-plus" aria-hidden="true"></i>
Add
{{t('add')}}
</button>
</li>
<li class="list-group-item" *ngIf="smartFilters.length === 0">
All Smart filters added to Dashboard or none created yet.
{{t('no-data')}}
</li>
</ul>
</div>

View file

@ -36,7 +36,6 @@ export class CustomizeDashboardModalComponent {
private readonly cdRef = inject(ChangeDetectorRef);
constructor(public modal: NgbActiveModal) {
forkJoin([this.dashboardService.getDashboardStreams(false), this.filterService.getAllFilters()]).subscribe(results => {
this.items = results[0];
const smartFilterStreams = new Set(results[0].filter(d => !d.isProvided).map(d => d.name));