Smart Filters & Dashboard Customization (#2282)

Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
Joe Milazzo 2023-09-12 11:24:47 -07:00 committed by GitHub
parent 3d501c9532
commit 84f85b4f24
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
92 changed files with 7149 additions and 555 deletions

View file

@ -0,0 +1,32 @@
<ng-container *transloco="let t; read: 'customize-dashboard-modal'">
<div class="modal-header">
<h4 class="modal-title">{{t('title')}}</h4>
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>
</div>
<div class="modal-body">
<app-draggable-ordered-list [items]="items" (orderUpdated)="orderUpdated($event)" [accessibilityMode]="accessibilityMode"
[showRemoveButton]="false">
<ng-template #draggableItem let-position="idx" let-item>
<app-stream-list-item [item]="item" [position]="position" (hide)="updateVisibility($event, position)"></app-stream-list-item>
</ng-template>
</app-draggable-ordered-list>
<h5>Smart Filters</h5>
<ul class="list-group filter-list">
<li class="filter list-group-item" *ngFor="let filter of smartFilters">
{{filter.name}}
<button class="btn btn-icon" (click)="addFilterToStream(filter)">
<i class="fa fa-plus" aria-hidden="true"></i>
Add
</button>
</li>
<li class="list-group-item" *ngIf="smartFilters.length === 0">
All Smart filters added to Dashboard or none created yet.
</li>
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" (click)="close()">{{t('close')}}</button>
</div>
</ng-container>

View file

@ -0,0 +1,24 @@
::ng-deep .drag-handle {
margin-top: 100% !important;
}
app-stream-list-item {
flex-grow: 1;
}
.filter-list {
margin: 0;
padding:0;
.filter {
padding: 0.5rem 1rem;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
border-radius: 5px;
margin: 5px 0;
color: var(--list-group-hover-text-color);
background-color: var(--list-group-hover-bg-color);
}
}

View file

@ -0,0 +1,72 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject} from '@angular/core';
import {CommonModule} from '@angular/common';
import {SafeHtmlPipe} from "../../../pipe/safe-html.pipe";
import {TranslocoDirective} from "@ngneat/transloco";
import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap";
import {
DraggableOrderedListComponent,
IndexUpdateEvent
} from "../../../reading-list/_components/draggable-ordered-list/draggable-ordered-list.component";
import {
ReadingListItemComponent
} from "../../../reading-list/_components/reading-list-item/reading-list-item.component";
import {forkJoin} from "rxjs";
import {FilterService} from "../../../_services/filter.service";
import {StreamListItemComponent} from "../stream-list-item/stream-list-item.component";
import {SmartFilter} from "../../../_models/metadata/v2/smart-filter";
import {DashboardService} from "../../../_services/dashboard.service";
import {DashboardStream} from "../../../_models/dashboard/dashboard-stream";
@Component({
selector: 'app-customize-dashboard-modal',
standalone: true,
imports: [CommonModule, SafeHtmlPipe, TranslocoDirective, DraggableOrderedListComponent, ReadingListItemComponent, StreamListItemComponent],
templateUrl: './customize-dashboard-modal.component.html',
styleUrls: ['./customize-dashboard-modal.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CustomizeDashboardModalComponent {
items: DashboardStream[] = [];
smartFilters: SmartFilter[] = [];
accessibilityMode: boolean = false;
private readonly dashboardService = inject(DashboardService);
private readonly filterService = inject(FilterService);
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));
this.smartFilters = results[1].filter(d => !smartFilterStreams.has(d.name));
this.cdRef.markForCheck();
});
}
addFilterToStream(filter: SmartFilter) {
this.dashboardService.createDashboardStream(filter.id).subscribe(stream => {
this.smartFilters = this.smartFilters.filter(d => d.name !== filter.name);
this.items.push(stream);
this.cdRef.detectChanges();
});
}
orderUpdated(event: IndexUpdateEvent) {
this.dashboardService.updateDashboardStreamPosition(event.item.name, event.item.id, event.fromPosition, event.toPosition).subscribe();
}
updateVisibility(item: DashboardStream, position: number) {
this.items[position].visible = !this.items[position].visible;
this.dashboardService.updateDashboardStream(this.items[position]).subscribe();
this.cdRef.markForCheck();
}
close() {
this.modal.close();
}
}

View file

@ -1,18 +1,21 @@
<ng-container *transloco="let t; read: 'side-nav'">
<div class="side-nav" [ngClass]="{'closed' : (navService.sideNavCollapsed$ | async), 'hidden': (navService.sideNavVisibility$ | async) === false, 'no-donate': (accountService.hasValidLicense$ | async) === true}" *ngIf="accountService.currentUser$ | async as user">
<!-- <app-side-nav-item icon="fa-user-circle align-self-center phone-hidden" [title]="user.username | sentenceCase" link="/preferences/">
<ng-container actions>
Todo: This will be customize dashboard/side nav controls
<a href="/preferences/" title="User Settings"><span class="visually-hidden">User Settings</span></a>
</ng-container>
</app-side-nav-item> -->
<!-- <app-side-nav-item icon="fa-user-circle align-self-center phone-hidden" [title]="user.username | sentenceCase" link="/preferences/">-->
<!-- <ng-container actions>-->
<!-- <a href="/preferences/" title="User Settings"><span class="visually-hidden">User Settings</span></a>-->
<!-- </ng-container>-->
<!-- </app-side-nav-item>-->
<app-side-nav-item icon="fa-home" [title]="t('home')" link="/libraries/"></app-side-nav-item>
<app-side-nav-item icon="fa-home" [title]="t('home')" link="/libraries/">
<ng-container actions>
<app-card-actionables [actions]="homeActions" [labelBy]="t('reading-lists')" iconClass="fa-ellipsis-v" (actionHandler)="handleHomeActions()"></app-card-actionables>
</ng-container>
</app-side-nav-item>
<app-side-nav-item icon="fa-star" [title]="t('want-to-read')" link="/want-to-read/"></app-side-nav-item>
<app-side-nav-item icon="fa-list" [title]="t('collections')" link="/collections/"></app-side-nav-item>
<app-side-nav-item icon="fa-list-ol" [title]="t('reading-lists')" link="/lists/">
<ng-container actions>
<app-card-actionables [actions]="readingListActions" labelBy="t('reading-lists')" iconClass="fa-ellipsis-v" (actionHandler)="importCbl()"></app-card-actionables>
<app-card-actionables [actions]="readingListActions" [labelBy]="t('reading-lists')" iconClass="fa-ellipsis-v" (actionHandler)="importCbl()"></app-card-actionables>
</ng-container>
</app-side-nav-item>
<app-side-nav-item icon="fa-bookmark" [title]="t('bookmarks')" link="/bookmarks/"></app-side-nav-item>

View file

@ -27,11 +27,13 @@ import {FilterPipe} from "../../../pipe/filter.pipe";
import {FormsModule} from "@angular/forms";
import {TranslocoDirective} from "@ngneat/transloco";
import {CardActionablesComponent} from "../../../_single-module/card-actionables/card-actionables.component";
import {SentenceCasePipe} from "../../../pipe/sentence-case.pipe";
import {CustomizeDashboardModalComponent} from "../customize-dashboard-modal/customize-dashboard-modal.component";
@Component({
selector: 'app-side-nav',
standalone: true,
imports: [CommonModule, SideNavItemComponent, CardActionablesComponent, FilterPipe, FormsModule, TranslocoDirective],
imports: [CommonModule, SideNavItemComponent, CardActionablesComponent, FilterPipe, FormsModule, TranslocoDirective, SentenceCasePipe],
templateUrl: './side-nav.component.html',
styleUrls: ['./side-nav.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
@ -43,6 +45,7 @@ export class SideNavComponent implements OnInit {
libraries: Library[] = [];
actions: ActionItem<Library>[] = [];
readingListActions = [{action: Action.Import, title: 'import-cbl', children: [], requiresAdmin: true, callback: this.importCbl.bind(this)}];
homeActions = [{action: Action.Edit, title: 'customize', children: [], requiresAdmin: false, callback: this.handleHomeActions.bind(this)}];
filterQuery: string = '';
filterLibrary = (library: Library) => {
return library.name.toLowerCase().indexOf((this.filterQuery || '').toLowerCase()) >= 0;
@ -107,6 +110,12 @@ export class SideNavComponent implements OnInit {
}
}
handleHomeActions() {
this.ngbModal.open(CustomizeDashboardModalComponent, {size: 'xl'});
// TODO: If on /, then refresh the page layout
}
importCbl() {
this.ngbModal.open(ImportCblModalComponent, {size: 'xl'});
}

View file

@ -0,0 +1,23 @@
<ng-container *transloco="let t; read: 'stream-list-item'">
<div class="row pt-2 g-0 list-item">
<div class="g-0">
<h5 class="mb-1 pb-0" id="item.id--{{position}}">
{{item.name}}
<span class="float-end">
<button class="btn btn-icon p-0" (click)="hide.emit(item)">
<i class="me-1" [ngClass]="{'fas fa-eye': item.visible, 'fa-solid fa-eye-slash': !item.visible}" aria-hidden="true"></i>
<span class="visually-hidden">{{t('remove')}}</span>
</button>
</span>
</h5>
<div class="meta">
<div class="ps-1">
{{t(item.isProvided ? 'provided' : 'smart-filter')}}
</div>
<div class="ps-1" *ngIf="!item.isProvided">
<a [href]="'/all-series?' + this.item.smartFilterEncoded" target="_blank">{{t('load-filter')}}</a>
</div>
</div>
</div>
</div>
</ng-container>

View file

@ -0,0 +1,8 @@
.list-item {
height: 60px;
max-height: 60px;
}
.meta {
display: flex;
}

View file

@ -0,0 +1,35 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
inject,
Input,
Output
} from '@angular/core';
import {CommonModule} from '@angular/common';
import {ImageComponent} from "../../../shared/image/image.component";
import {MangaFormatIconPipe} from "../../../pipe/manga-format-icon.pipe";
import {MangaFormatPipe} from "../../../pipe/manga-format.pipe";
import {NgbProgressbar} from "@ng-bootstrap/ng-bootstrap";
import {TranslocoDirective} from "@ngneat/transloco";
import {DashboardStream} from "../../../_models/dashboard/dashboard-stream";
@Component({
selector: 'app-stream-list-item',
standalone: true,
imports: [CommonModule, ImageComponent, MangaFormatIconPipe, MangaFormatPipe, NgbProgressbar, TranslocoDirective],
templateUrl: './stream-list-item.component.html',
styleUrls: ['./stream-list-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class StreamListItemComponent {
@Input({required: true}) item!: DashboardStream;
@Input({required: true}) position: number = 0;
@Output() hide: EventEmitter<DashboardStream> = new EventEmitter<DashboardStream>();
private readonly cdRef = inject(ChangeDetectorRef);
}