Side Nav Redesign (#2310)
This commit is contained in:
parent
5c2ebb87cc
commit
00dddaefae
88 changed files with 5971 additions and 572 deletions
|
|
@ -1,29 +1,37 @@
|
|||
<ng-container *transloco="let t; read: 'customize-dashboard-modal'">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{t('title')}}</h4>
|
||||
<h4 class="modal-title">{{t('title-' + activeTab)}}</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>
|
||||
{{t('add')}}
|
||||
</button>
|
||||
<div class="modal-body scrollable-modal {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? '' : 'd-flex'}}">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-pills" orientation="{{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'horizontal' : 'vertical'}}" style="min-width: 154px;">
|
||||
<li [ngbNavItem]="TabID.Dashboard">
|
||||
<a ngbNavLink>{{t(TabID.Dashboard)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-customize-dashboard-streams></app-customize-dashboard-streams>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li class="list-group-item" *ngIf="smartFilters.length === 0">
|
||||
{{t('no-data')}}
|
||||
|
||||
<li [ngbNavItem]="TabID.SideNav">
|
||||
<a ngbNavLink>{{t(TabID.SideNav)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-customize-sidenav-streams></app-customize-sidenav-streams>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li [ngbNavItem]="TabID.SmartFilters">
|
||||
<a ngbNavLink>{{t(TabID.SmartFilters)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-manage-smart-filters></app-manage-smart-filters>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li [ngbNavItem]="TabID.ExternalSources">
|
||||
<a ngbNavLink>{{t(TabID.ExternalSources)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-manage-external-sources></app-manage-external-sources>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="nav" class="tab-content {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'mt-3' : 'ms-4 flex-fill'}}"></div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
|
|
|
|||
|
|
@ -1,24 +1 @@
|
|||
::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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject} from '@an
|
|||
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 {NgbActiveModal, NgbNav, NgbNavContent, NgbNavItem, NgbNavLink, NgbNavOutlet} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {
|
||||
DraggableOrderedListComponent,
|
||||
IndexUpdateEvent
|
||||
|
|
@ -12,60 +12,43 @@ import {
|
|||
} 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 {DashboardStreamListItemComponent} from "../dashboard-stream-list-item/dashboard-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";
|
||||
import {Breakpoint, UtilityService} from "../../../shared/_services/utility.service";
|
||||
import {CustomizeDashboardStreamsComponent} from "../customize-dashboard-streams/customize-dashboard-streams.component";
|
||||
import {CustomizeSidenavStreamsComponent} from "../customize-sidenav-streams/customize-sidenav-streams.component";
|
||||
import {ManageExternalSourcesComponent} from "../manage-external-sources/manage-external-sources.component";
|
||||
import {ManageSmartFiltersComponent} from "../manage-smart-filters/manage-smart-filters.component";
|
||||
|
||||
enum TabID {
|
||||
Dashboard = 'dashboard',
|
||||
SideNav = 'sidenav',
|
||||
SmartFilters = 'smart-filters',
|
||||
ExternalSources = 'external-sources'
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-customize-dashboard-modal',
|
||||
standalone: true,
|
||||
imports: [CommonModule, SafeHtmlPipe, TranslocoDirective, DraggableOrderedListComponent, ReadingListItemComponent, StreamListItemComponent],
|
||||
imports: [CommonModule, SafeHtmlPipe, TranslocoDirective, DraggableOrderedListComponent, ReadingListItemComponent, DashboardStreamListItemComponent,
|
||||
NgbNav, NgbNavContent, NgbNavLink, NgbNavItem, NgbNavOutlet, CustomizeDashboardStreamsComponent, CustomizeSidenavStreamsComponent, ManageExternalSourcesComponent, ManageSmartFiltersComponent],
|
||||
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;
|
||||
activeTab = TabID.Dashboard;
|
||||
|
||||
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();
|
||||
}
|
||||
public readonly utilityService = inject(UtilityService);
|
||||
private readonly modal = inject(NgbActiveModal);
|
||||
protected readonly TabID = TabID;
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
|
||||
close() {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
<ng-container *transloco="let t; read: 'customize-dashboard-streams'">
|
||||
<app-draggable-ordered-list [items]="items" (orderUpdated)="orderUpdated($event)" [accessibilityMode]="accessibilityMode"
|
||||
[showRemoveButton]="false">
|
||||
<ng-template #draggableItem let-position="idx" let-item>
|
||||
<app-dashboard-stream-list-item [item]="item" [position]="position" (hide)="updateVisibility($event, position)"></app-dashboard-stream-list-item>
|
||||
</ng-template>
|
||||
</app-draggable-ordered-list>
|
||||
|
||||
<h5>Smart Filters</h5>
|
||||
<form [formGroup]="listForm">
|
||||
<div class="mb-3" *ngIf="smartFilters.length >= 3">
|
||||
<label for="filter" class="form-label">{{t('filter')}}</label>
|
||||
<div class="input-group">
|
||||
<input id="filter" autocomplete="off" class="form-control" formControlName="filterQuery" type="text" aria-describedby="reset-input">
|
||||
<button class="btn btn-outline-secondary" type="button" id="reset-input" (click)="resetFilter()">{{t('clear')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<ul class="list-group filter-list">
|
||||
<li class="filter list-group-item" *ngFor="let filter of smartFilters | filter: filterList">
|
||||
{{filter.name}}
|
||||
<button class="btn btn-icon" (click)="addFilterToStream(filter)">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
{{t('add')}}
|
||||
</button>
|
||||
</li>
|
||||
<li class="list-group-item" *ngIf="smartFilters.length === 0">
|
||||
{{t('no-data')}}
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
::ng-deep .drag-handle {
|
||||
margin-top: 100% !important;
|
||||
}
|
||||
|
||||
app-dashboard-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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {
|
||||
DraggableOrderedListComponent, IndexUpdateEvent
|
||||
} from "../../../reading-list/_components/draggable-ordered-list/draggable-ordered-list.component";
|
||||
import {DashboardStreamListItemComponent} from "../dashboard-stream-list-item/dashboard-stream-list-item.component";
|
||||
import {DashboardStream} from "../../../_models/dashboard/dashboard-stream";
|
||||
import {SmartFilter} from "../../../_models/metadata/v2/smart-filter";
|
||||
import {DashboardService} from "../../../_services/dashboard.service";
|
||||
import {FilterService} from "../../../_services/filter.service";
|
||||
import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {forkJoin} from "rxjs";
|
||||
import {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {CommonStream} from "../../../_models/common-stream";
|
||||
import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms";
|
||||
import {FilterPipe} from "../../../pipe/filter.pipe";
|
||||
|
||||
@Component({
|
||||
selector: 'app-customize-dashboard-streams',
|
||||
standalone: true,
|
||||
imports: [CommonModule, DraggableOrderedListComponent, DashboardStreamListItemComponent, TranslocoDirective, ReactiveFormsModule, FilterPipe],
|
||||
templateUrl: './customize-dashboard-streams.component.html',
|
||||
styleUrls: ['./customize-dashboard-streams.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CustomizeDashboardStreamsComponent {
|
||||
|
||||
items: DashboardStream[] = [];
|
||||
smartFilters: SmartFilter[] = [];
|
||||
accessibilityMode: boolean = false;
|
||||
|
||||
private readonly dashboardService = inject(DashboardService);
|
||||
private readonly filterService = inject(FilterService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
|
||||
listForm: FormGroup = new FormGroup({
|
||||
'filterQuery': new FormControl('', [])
|
||||
});
|
||||
|
||||
filterList = (listItem: SmartFilter) => {
|
||||
const filterVal = (this.listForm.value.filterQuery || '').toLowerCase();
|
||||
return listItem.name.toLowerCase().indexOf(filterVal) >= 0;
|
||||
}
|
||||
resetFilter() {
|
||||
this.listForm.get('filterQuery')?.setValue('');
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
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 = [...this.items, stream];
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
<ng-container *transloco="let t; read: 'customize-sidenav-streams'">
|
||||
<form [formGroup]="listForm">
|
||||
<div class="row g-0 mb-3 justify-content-between">
|
||||
<div class="col-9" *ngIf="items.length >= 3">
|
||||
<label for="sidenav-stream-filter" class="form-label">{{t('filter')}}</label>
|
||||
<div class="input-group">
|
||||
<input id="sidenav-stream-filter" autocomplete="off" class="form-control" formControlName="filterSideNavStream" type="text" aria-describedby="reset-sidenav-stream-input">
|
||||
<button class="btn btn-outline-secondary" type="button" id="reset-sidenav-stream-input" (click)="resetSideNavFilter()">{{t('clear')}}</button>
|
||||
</div>
|
||||
<span role="alert" class="mt-1" *ngIf="listForm.get('filterSideNavStream')?.value">{{t('reorder-when-filter-present')}}</span>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="form-check form-check-inline" style="margin-top: 35px; margin-left: 10px">
|
||||
<input class="form-check-input" type="checkbox" id="accessibility-mode" [value]="accessibilityMode" (change)="updateAccessibilityMode()">
|
||||
<label class="form-check-label" for="accessibility-mode">{{t('order-numbers-label')}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<app-draggable-ordered-list [items]="items | filter: filterSideNavStreams" (orderUpdated)="orderUpdated($event)" [accessibilityMode]="accessibilityMode"
|
||||
[showRemoveButton]="false" [disabled]="listForm.get('filterSideNavStream')?.value">
|
||||
<ng-template #draggableItem let-position="idx" let-item>
|
||||
<app-sidenav-stream-list-item [item]="item" [position]="position" (hide)="updateVisibility($event, position)"></app-sidenav-stream-list-item>
|
||||
</ng-template>
|
||||
</app-draggable-ordered-list>
|
||||
|
||||
<h5>{{t('smart-filters-title')}}</h5>
|
||||
<div class="mb-3" *ngIf="smartFilters.length >= 6">
|
||||
<label for="smart-filter-filter" class="form-label">{{t('filter')}}</label>
|
||||
<div class="input-group">
|
||||
<input id="smart-filter-filter" autocomplete="off" class="form-control" formControlName="filterSmartFilter" type="text" aria-describedby="reset-smart-filter-input">
|
||||
<button class="btn btn-outline-secondary" type="button" id="reset-smart-filter-input" (click)="resetSmartFilterFilter()">{{t('clear')}}</button>
|
||||
</div>
|
||||
<span role="alert" class="mt-1" *ngIf="listForm.get('filterSideNavStream')?.value">{{t('reorder-when-filter-present')}}</span>
|
||||
</div>
|
||||
<ul class="list-group filter-list">
|
||||
<li class="filter list-group-item" *ngFor="let filter of smartFilters | filter: filterSmartFilters">
|
||||
{{filter.name}}
|
||||
<button class="btn btn-icon" (click)="addFilterToStream(filter)">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
{{t('add')}}
|
||||
</button>
|
||||
</li>
|
||||
<li class="list-group-item" *ngIf="smartFilters.length === 0">
|
||||
{{t('no-data')}}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h5 class="mt-3">{{t('external-sources-title')}}</h5>
|
||||
<div class="mb-3" *ngIf="externalSources.length >= 6">
|
||||
<label for="external-source-filter" class="form-label">{{t('filter')}}</label>
|
||||
<div class="input-group">
|
||||
<input id="external-source-filter" autocomplete="off" class="form-control" formControlName="filterSmartFilter" type="text" aria-describedby="reset-external-source-input">
|
||||
<button class="btn btn-outline-secondary" type="button" id="reset-external-source-input" (click)="resetExternalSourceFilter()">{{t('clear')}}</button>
|
||||
</div>
|
||||
<span role="alert" class="mt-1" *ngIf="listForm.get('filterSideNavStream')?.value">{{t('reorder-when-filter-present')}}</span>
|
||||
</div>
|
||||
<ul class="list-group filter-list">
|
||||
<li class="filter list-group-item" *ngFor="let source of externalSources | filter: filterExternalSources">
|
||||
{{source.host}}
|
||||
<button class="btn btn-icon" (click)="addExternalSourceToStream(source)">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
{{t('add')}}
|
||||
</button>
|
||||
</li>
|
||||
<li class="list-group-item" *ngIf="externalSources.length === 0">
|
||||
{{t('no-data-external-source')}}
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</ng-container>
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
::ng-deep .drag-handle {
|
||||
margin-top: 100% !important;
|
||||
}
|
||||
|
||||
app-sidenav-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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {SmartFilter} from "../../../_models/metadata/v2/smart-filter";
|
||||
import {FilterService} from "../../../_services/filter.service";
|
||||
import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {forkJoin} from "rxjs";
|
||||
import {
|
||||
DraggableOrderedListComponent,
|
||||
IndexUpdateEvent
|
||||
} from "../../../reading-list/_components/draggable-ordered-list/draggable-ordered-list.component";
|
||||
import {SideNavStream} from "../../../_models/sidenav/sidenav-stream";
|
||||
import {NavService} from "../../../_services/nav.service";
|
||||
import {DashboardStreamListItemComponent} from "../dashboard-stream-list-item/dashboard-stream-list-item.component";
|
||||
import {CommonStream} from "../../../_models/common-stream";
|
||||
import {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {SidenavStreamListItemComponent} from "../sidenav-stream-list-item/sidenav-stream-list-item.component";
|
||||
import {ExternalSourceService} from "../../../external-source.service";
|
||||
import {ExternalSource} from "../../../_models/sidenav/external-source";
|
||||
import {StreamType} from "../../../_models/dashboard/stream-type.enum";
|
||||
import {SideNavStreamType} from "../../../_models/sidenav/sidenav-stream-type.enum";
|
||||
import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms";
|
||||
import {FilterPipe} from "../../../pipe/filter.pipe";
|
||||
|
||||
@Component({
|
||||
selector: 'app-customize-sidenav-streams',
|
||||
standalone: true,
|
||||
imports: [CommonModule, DraggableOrderedListComponent, DashboardStreamListItemComponent, TranslocoDirective, SidenavStreamListItemComponent, ReactiveFormsModule, FilterPipe],
|
||||
templateUrl: './customize-sidenav-streams.component.html',
|
||||
styleUrls: ['./customize-sidenav-streams.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CustomizeSidenavStreamsComponent {
|
||||
|
||||
items: SideNavStream[] = [];
|
||||
smartFilters: SmartFilter[] = [];
|
||||
externalSources: ExternalSource[] = [];
|
||||
accessibilityMode: boolean = false;
|
||||
|
||||
listForm: FormGroup = new FormGroup({
|
||||
'filterSideNavStream': new FormControl('', []),
|
||||
'filterSmartFilter': new FormControl('', []),
|
||||
'filterExternalSource': new FormControl('', []),
|
||||
});
|
||||
|
||||
filterSideNavStreams = (listItem: SideNavStream) => {
|
||||
const filterVal = (this.listForm.value.filterSideNavStream || '').toLowerCase();
|
||||
return listItem.name.toLowerCase().indexOf(filterVal) >= 0;
|
||||
}
|
||||
|
||||
filterSmartFilters = (listItem: SmartFilter) => {
|
||||
const filterVal = (this.listForm.value.filterSmartFilter || '').toLowerCase();
|
||||
return listItem.name.toLowerCase().indexOf(filterVal) >= 0;
|
||||
}
|
||||
|
||||
filterExternalSources = (listItem: ExternalSource) => {
|
||||
const filterVal = (this.listForm.value.filterExternalSource || '').toLowerCase();
|
||||
return listItem.name.toLowerCase().indexOf(filterVal) >= 0;
|
||||
}
|
||||
|
||||
|
||||
private readonly sideNavService = inject(NavService);
|
||||
private readonly filterService = inject(FilterService);
|
||||
private readonly externalSourceService = inject(ExternalSourceService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
|
||||
constructor(public modal: NgbActiveModal) {
|
||||
forkJoin([this.sideNavService.getSideNavStreams(false),
|
||||
this.filterService.getAllFilters(), this.externalSourceService.getExternalSources()
|
||||
]).subscribe(results => {
|
||||
this.items = results[0];
|
||||
const existingSmartFilterStreams = new Set(results[0].filter(d => !d.isProvided && d.streamType === SideNavStreamType.SmartFilter).map(d => d.name));
|
||||
this.smartFilters = results[1].filter(d => !existingSmartFilterStreams.has(d.name));
|
||||
|
||||
const existingExternalSourceStreams = new Set(results[0].filter(d => !d.isProvided && d.streamType === SideNavStreamType.ExternalSource).map(d => d.name));
|
||||
this.externalSources = results[2].filter(d => !existingExternalSourceStreams.has(d.name));
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
resetSideNavFilter() {
|
||||
this.listForm.get('filterSideNavStream')?.setValue('');
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
resetSmartFilterFilter() {
|
||||
this.listForm.get('filterSmartFilter')?.setValue('');
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
resetExternalSourceFilter() {
|
||||
this.listForm.get('filterExternalSource')?.setValue('');
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
addFilterToStream(filter: SmartFilter) {
|
||||
this.sideNavService.createSideNavStream(filter.id).subscribe(stream => {
|
||||
this.smartFilters = this.smartFilters.filter(d => d.name !== filter.name);
|
||||
this.items = [...this.items, stream];
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
addExternalSourceToStream(externalSource: ExternalSource) {
|
||||
this.sideNavService.createSideNavStreamFromExternalSource(externalSource.id).subscribe(stream => {
|
||||
this.externalSources = this.externalSources.filter(d => d.name !== externalSource.name);
|
||||
this.items = [...this.items, stream];
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
updateAccessibilityMode() {
|
||||
this.accessibilityMode = !this.accessibilityMode;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
||||
orderUpdated(event: IndexUpdateEvent) {
|
||||
this.sideNavService.updateSideNavStreamPosition(event.item.name, event.item.id, event.fromPosition, event.toPosition).subscribe(() => {
|
||||
if (event.fromAccessibilityMode) {
|
||||
this.sideNavService.getSideNavStreams(false).subscribe((data) => {
|
||||
this.items = [...data];
|
||||
this.cdRef.markForCheck();
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateVisibility(item: SideNavStream, position: number) {
|
||||
const stream = this.items.filter(s => s.id == item.id)[0];
|
||||
stream.visible = !stream.visible;
|
||||
this.sideNavService.updateSideNavStream(stream).subscribe();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -2,7 +2,12 @@
|
|||
<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 *ngIf="item.isProvided; else nonProvidedTitle">
|
||||
{{item.name | streamName }}
|
||||
</span>
|
||||
<ng-template #nonProvidedTitle>
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
<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>
|
||||
|
|
@ -14,22 +14,22 @@ 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";
|
||||
import {SideNavStream} from "../../../_models/sidenav/sidenav-stream";
|
||||
import {CommonStream} from "../../../_models/common-stream";
|
||||
import {StreamNamePipe} from "../../../pipe/stream-name.pipe";
|
||||
|
||||
@Component({
|
||||
selector: 'app-stream-list-item',
|
||||
selector: 'app-dashboard-stream-list-item',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ImageComponent, MangaFormatIconPipe, MangaFormatPipe, NgbProgressbar, TranslocoDirective],
|
||||
templateUrl: './stream-list-item.component.html',
|
||||
styleUrls: ['./stream-list-item.component.scss'],
|
||||
imports: [CommonModule, ImageComponent, MangaFormatIconPipe, MangaFormatPipe, NgbProgressbar, TranslocoDirective, StreamNamePipe],
|
||||
templateUrl: './dashboard-stream-list-item.component.html',
|
||||
styleUrls: ['./dashboard-stream-list-item.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class StreamListItemComponent {
|
||||
export class DashboardStreamListItemComponent {
|
||||
@Input({required: true}) item!: DashboardStream;
|
||||
@Input({required: true}) position: number = 0;
|
||||
@Output() hide: EventEmitter<DashboardStream> = new EventEmitter<DashboardStream>();
|
||||
|
||||
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
<ng-container *transloco="let t; read:'edit-external-source-item'">
|
||||
<div class="card mt-2">
|
||||
<div class="card-body">
|
||||
<div class="card-title">
|
||||
<div class="container-fluid row mb-2">
|
||||
<div class="col-9 col-sm-9"><h4 id="anilist-token-header">{{source.name || t('title')}}</h4></div>
|
||||
<div class="col-3 text-end">
|
||||
<button class="btn btn-primary btn-sm me-1" (click)="toggleViewMode()">
|
||||
<ng-container *ngIf="isViewMode; else editMode">
|
||||
<i *ngIf="isViewMode" class="fa-solid fa-pen" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">
|
||||
{{t('edit')}}
|
||||
</span>
|
||||
</ng-container>
|
||||
<ng-template #editMode>
|
||||
{{t('cancel')}}
|
||||
</ng-template>
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm" (click)="delete()">
|
||||
<span class="visually-hidden">{{t('delete')}}</span>
|
||||
<i class="fa-solid fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="isViewMode">
|
||||
<form [formGroup]="formGroup">
|
||||
<div class="form-group mb-3">
|
||||
<label for="host">{{t('name-label')}}</label>
|
||||
<input id="name" class="form-control" formControlName="name" type="text"
|
||||
[class.is-invalid]="formGroup.get('name')?.invalid && formGroup.get('name')?.touched" aria-describedby="name-validations">
|
||||
<div id="name-validations" class="invalid-feedback" *ngIf="hasErrors('name')">
|
||||
<div *ngIf="formGroup.get('name')?.errors?.required">
|
||||
{{t('required')}}
|
||||
</div>
|
||||
<div *ngIf="formGroup.get('name')?.errors?.notUnique">
|
||||
{{t('not-unique')}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-3">
|
||||
<label for="host">{{t('host-label')}}</label>
|
||||
<input id="host" class="form-control" formControlName="host" type="url"
|
||||
[class.is-invalid]="formGroup.get('host')?.invalid && formGroup.get('host')?.touched" aria-describedby="host-validations">
|
||||
<ng-container *ngIf="formGroup.get('host')?.errors as errors">
|
||||
<div id="host-validations" class="invalid-feedback">
|
||||
<div *ngIf="errors.required">
|
||||
{{t('required')}}
|
||||
</div>
|
||||
<div *ngIf="errors.pattern">
|
||||
{{t('pattern')}}
|
||||
</div>
|
||||
<div *ngIf="errors.notUnique">
|
||||
{{t('not-unique')}}
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label for="api-key">{{t('api-key-label')}}</label>
|
||||
<input id="api-key" class="form-control" formControlName="apiKey" type="text"
|
||||
[class.is-invalid]="formGroup.get('apiKey')?.invalid && formGroup.get('apiKey')?.touched" aria-describedby="api-key-validations">
|
||||
<div id="api-key-validations" class="invalid-feedback" *ngIf="hasErrors('apiKey')">
|
||||
<div *ngIf="formGroup.get('apiKey')?.errors?.required">
|
||||
{{t('required')}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
|
||||
<button type="submit" class="flex-fill btn btn-primary" aria-describedby="anilist-token-header" (click)="saveForm()">{{t('save')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</ng-container>
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import {ChangeDetectorRef, Component, DestroyRef, EventEmitter, inject, Input, OnInit, Output} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||
import {ExternalSource} from "../../../_models/sidenav/external-source";
|
||||
import {NgbCollapse} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {translate, TranslocoDirective} from "@ngneat/transloco";
|
||||
import {ExternalSourceService} from "../../../external-source.service";
|
||||
import {distinctUntilChanged, filter, tap} from "rxjs/operators";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {switchMap} from "rxjs";
|
||||
import {ToastrModule, ToastrService} from "ngx-toastr";
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-external-source-item',
|
||||
standalone: true,
|
||||
imports: [CommonModule, NgbCollapse, ReactiveFormsModule, TranslocoDirective],
|
||||
templateUrl: './edit-external-source-item.component.html',
|
||||
styleUrls: ['./edit-external-source-item.component.scss']
|
||||
})
|
||||
export class EditExternalSourceItemComponent implements OnInit {
|
||||
|
||||
@Input({required: true}) source!: ExternalSource;
|
||||
@Output() sourceUpdate = new EventEmitter<ExternalSource>();
|
||||
@Output() sourceDelete = new EventEmitter<ExternalSource>();
|
||||
@Input() isViewMode: boolean = true;
|
||||
|
||||
formGroup: FormGroup = new FormGroup({});
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly externalSourceService = inject(ExternalSourceService);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
|
||||
hasErrors(controlName: string) {
|
||||
const errors = this.formGroup.get(controlName)?.errors;
|
||||
return Object.values(errors || []).filter(v => v).length > 0;
|
||||
}
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.formGroup.addControl('name', new FormControl(this.source.name, [Validators.required]));
|
||||
this.formGroup.addControl('host', new FormControl(this.source.host, [Validators.required, Validators.pattern(/^(http:|https:)+[^\s]+[\w]\/?$/)]));
|
||||
this.formGroup.addControl('apiKey', new FormControl(this.source.apiKey, [Validators.required]));
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
resetForm() {
|
||||
this.formGroup.get('host')?.setValue(this.source.host);
|
||||
this.formGroup.get('name')?.setValue(this.source.name);
|
||||
this.formGroup.get('apiKey')?.setValue(this.source.apiKey);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
saveForm() {
|
||||
if (this.source === undefined) return;
|
||||
|
||||
const model = this.formGroup.value;
|
||||
this.externalSourceService.sourceExists(model.host, model.name, model.apiKey).subscribe(exists => {
|
||||
if (exists) {
|
||||
this.toastr.error(translate('toasts.external-source-already-exists'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.source.id === 0) {
|
||||
// We need to create a new one
|
||||
this.externalSourceService.createSource({id: 0, ...this.formGroup.value}).subscribe((updatedSource) => {
|
||||
this.source = {...updatedSource};
|
||||
this.sourceUpdate.emit(this.source);
|
||||
this.toggleViewMode();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.externalSourceService.updateSource({id: this.source.id, ...this.formGroup.value}).subscribe((updatedSource) => {
|
||||
this.source!.host = this.formGroup.value.host;
|
||||
this.source!.apiKey = this.formGroup.value.apiKey;
|
||||
this.source!.name = this.formGroup.value.name;
|
||||
|
||||
this.sourceUpdate.emit(this.source);
|
||||
this.toggleViewMode();
|
||||
});
|
||||
});
|
||||
}
|
||||
delete() {
|
||||
if (this.source.id === 0) {
|
||||
this.sourceDelete.emit(this.source);
|
||||
if (!this.isViewMode) {
|
||||
this.toggleViewMode();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.externalSourceService.deleteSource(this.source.id).subscribe(() => {
|
||||
this.sourceDelete.emit(this.source);
|
||||
if (!this.isViewMode) {
|
||||
this.toggleViewMode();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleViewMode() {
|
||||
this.isViewMode = !this.isViewMode;
|
||||
if (!this.isViewMode) {
|
||||
this.resetForm();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<ng-container *transloco="let t; read: 'manage-external-sources'">
|
||||
<p>
|
||||
{{t('description')}}
|
||||
<a href="https://wiki.kavitareader.com/en/guides/customization/external-sources" target="_blank" rel="noopener noreferrer">{{t('help-link')}}</a>
|
||||
</p>
|
||||
|
||||
|
||||
<form class="row g-0 justify-content-between mb-3" [formGroup]="listForm">
|
||||
<div class="col-9">
|
||||
<label for="filter" class="form-label">{{t('filter')}}</label>
|
||||
<div class="input-group">
|
||||
<input id="filter" autocomplete="off" class="form-control" formControlName="filterQuery" type="text" aria-describedby="reset-input">
|
||||
<button class="btn btn-outline-secondary" type="button" id="reset-input" (click)="resetFilter()">{{t('clear')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-2 ms-2">
|
||||
<button class="btn btn-primary" style="margin-top: 30px" (click)="addNewExternalSource()">{{t('add-source')}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ng-container *ngFor="let externalSource of externalSources | filter: filterList; let idx = index">
|
||||
<app-edit-external-source-item [source]="externalSource"
|
||||
(sourceUpdate)="updateSource(idx, $event)"
|
||||
(sourceDelete)="deleteSource(idx, $event)"
|
||||
[isViewMode]="externalSource.id !== 0"></app-edit-external-source-item>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject} from '@angular/core';
|
||||
import {CommonModule, NgOptimizedImage} from '@angular/common';
|
||||
import {FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||
import {NgbCollapse, NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {translate, TranslocoDirective} from "@ngneat/transloco";
|
||||
import {AccountService} from "../../../_services/account.service";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {EditExternalSourceItemComponent} from "../edit-external-source-item/edit-external-source-item.component";
|
||||
import {ExternalSource} from "../../../_models/sidenav/external-source";
|
||||
import {ExternalSourceService} from "../../../external-source.service";
|
||||
import {FilterPipe} from "../../../pipe/filter.pipe";
|
||||
import {SmartFilter} from "../../../_models/metadata/v2/smart-filter";
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-external-sources',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, NgOptimizedImage, NgbTooltip, ReactiveFormsModule, TranslocoDirective, NgbCollapse, EditExternalSourceItemComponent, FilterPipe],
|
||||
templateUrl: './manage-external-sources.component.html',
|
||||
styleUrls: ['./manage-external-sources.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ManageExternalSourcesComponent {
|
||||
|
||||
externalSources: Array<ExternalSource> = [];
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly externalSourceService = inject(ExternalSourceService);
|
||||
|
||||
listForm: FormGroup = new FormGroup({
|
||||
'filterQuery': new FormControl('', [])
|
||||
});
|
||||
|
||||
filterList = (listItem: ExternalSource) => {
|
||||
const filterVal = (this.listForm.value.filterQuery || '').toLowerCase();
|
||||
return listItem.name.toLowerCase().indexOf(filterVal) >= 0 || listItem.host.toLowerCase().indexOf(filterVal) >= 0;
|
||||
}
|
||||
|
||||
constructor(public accountService: AccountService) {
|
||||
this.externalSourceService.getExternalSources().subscribe(data => {
|
||||
this.externalSources = data;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
resetFilter() {
|
||||
this.listForm.get('filterQuery')?.setValue('');
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
addNewExternalSource() {
|
||||
this.externalSources.unshift({id: 0, name: '', host: '', apiKey: ''});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateSource(index: number, updatedSource: ExternalSource) {
|
||||
this.externalSources[index] = updatedSource;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
deleteSource(index: number, updatedSource: ExternalSource) {
|
||||
this.externalSources.splice(index, 1);
|
||||
this.resetFilter();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<ng-container *transloco="let t; read:'manage-smart-filters'">
|
||||
<form [formGroup]="listForm">
|
||||
<div class="mb-3" *ngIf="filters.length >= 3">
|
||||
<label for="filter" class="form-label">{{t('filter')}}</label>
|
||||
<div class="input-group">
|
||||
<input id="filter" autocomplete="off" class="form-control" formControlName="filterQuery" type="text" aria-describedby="reset-input">
|
||||
<button class="btn btn-outline-secondary" type="button" id="reset-input" (click)="resetFilter()">{{t('clear')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ul>
|
||||
<li class="list-group-item" *ngFor="let f of filters | filter: filterList">
|
||||
<a [href]="'all-series?' + f.filter" target="_blank">{{f.name}}</a>
|
||||
<button class="btn btn-danger float-end" (click)="deleteFilter(f)">
|
||||
<i class="fa-solid fa-trash" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('delete')}}</span>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<li class="list-group-item" *ngIf="filters.length === 0">
|
||||
{{t('no-data')}}
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
|
||||
ul {
|
||||
margin:0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
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);
|
||||
|
||||
span {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FilterService} from "../../../_services/filter.service";
|
||||
import {SmartFilter} from "../../../_models/metadata/v2/smart-filter";
|
||||
import {Router} from "@angular/router";
|
||||
import {ConfirmService} from "../../../shared/confirm.service";
|
||||
import {translate, TranslocoDirective} from "@ngneat/transloco";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms";
|
||||
import {FilterPipe} from "../../../pipe/filter.pipe";
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-smart-filters',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule, TranslocoDirective, FilterPipe],
|
||||
templateUrl: './manage-smart-filters.component.html',
|
||||
styleUrls: ['./manage-smart-filters.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ManageSmartFiltersComponent {
|
||||
|
||||
private readonly filterService = inject(FilterService);
|
||||
private readonly confirmService = inject(ConfirmService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
filters: Array<SmartFilter> = [];
|
||||
listForm: FormGroup = new FormGroup({
|
||||
'filterQuery': new FormControl('', [])
|
||||
});
|
||||
|
||||
filterList = (listItem: SmartFilter) => {
|
||||
const filterVal = (this.listForm.value.filterQuery || '').toLowerCase();
|
||||
return listItem.name.toLowerCase().indexOf(filterVal) >= 0;
|
||||
}
|
||||
resetFilter() {
|
||||
this.listForm.get('filterQuery')?.setValue('');
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
||||
constructor() {
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
loadData() {
|
||||
this.filterService.getAllFilters().subscribe(filters => {
|
||||
this.filters = filters;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
async loadFilter(f: SmartFilter) {
|
||||
await this.router.navigateByUrl('all-series?' + f.filter);
|
||||
}
|
||||
|
||||
async deleteFilter(f: SmartFilter) {
|
||||
if (!await this.confirmService.confirm(translate('toasts.confirm-delete-smart-filter'))) return;
|
||||
|
||||
this.filterService.deleteFilter(f.id).subscribe(() => {
|
||||
this.toastr.success(translate('toasts.smart-filter-deleted'));
|
||||
this.resetFilter();
|
||||
this.loadData();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -11,9 +11,16 @@
|
|||
</a>
|
||||
</ng-container>
|
||||
<ng-template #internal>
|
||||
<a class="side-nav-item" href="javascript:void(0);" [ngClass]="{'closed': (navService.sideNavCollapsed$ | async), 'active': highlighted}" [routerLink]="link">
|
||||
<ng-container *ngIf="queryParams && queryParams !== {}; else regInternalLink">
|
||||
<a class="side-nav-item" href="javascript:void(0);" [ngClass]="{'closed': (navService.sideNavCollapsed$ | async), 'active': highlighted}" (click)="openLink()">
|
||||
<ng-container [ngTemplateOutlet]="inner"></ng-container>
|
||||
</a>
|
||||
</ng-container>
|
||||
<ng-template #regInternalLink>
|
||||
<a class="side-nav-item" href="javascript:void(0);" [ngClass]="{'closed': (navService.sideNavCollapsed$ | async), 'active': highlighted}" [routerLink]="link" [queryParams]="queryParams">
|
||||
<ng-container [ngTemplateOutlet]="inner"></ng-container>
|
||||
</a>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
</ng-template>
|
||||
|
|
|
|||
|
|
@ -41,6 +41,11 @@ export class SideNavItemComponent implements OnInit {
|
|||
* If external, link will be used as full href and rel will be applied
|
||||
*/
|
||||
@Input() external: boolean = false;
|
||||
/**
|
||||
* If using a link, then you can pass optional queryParameters
|
||||
*/
|
||||
@Input() queryParams: any | undefined = undefined;
|
||||
|
||||
|
||||
@Input() comparisonMethod: 'startsWith' | 'equals' = 'equals';
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
|
@ -54,8 +59,9 @@ export class SideNavItemComponent implements OnInit {
|
|||
takeUntilDestroyed(this.destroyRef),
|
||||
map(evt => evt as NavigationEnd))
|
||||
.subscribe((evt: NavigationEnd) => {
|
||||
this.updateHighlight(evt.url.split('?')[0]);
|
||||
|
||||
const tokens = evt.url.split('?');
|
||||
const [token1, token2 = undefined] = tokens;
|
||||
this.updateHighlight(token1, token2);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -66,23 +72,31 @@ export class SideNavItemComponent implements OnInit {
|
|||
|
||||
}
|
||||
|
||||
updateHighlight(page: string) {
|
||||
updateHighlight(page: string, queryParams?: string) {
|
||||
if (this.link === undefined) {
|
||||
this.highlighted = false;
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!page.endsWith('/')) {
|
||||
if (!page.endsWith('/') && !queryParams) {
|
||||
page = page + '/';
|
||||
}
|
||||
|
||||
|
||||
if (this.comparisonMethod === 'equals' && page === this.link) {
|
||||
this.highlighted = true;
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
if (this.comparisonMethod === 'startsWith' && page.startsWith(this.link)) {
|
||||
|
||||
if (queryParams && queryParams === this.queryParams) {
|
||||
this.highlighted = true;
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
this.highlighted = true;
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
|
|
@ -92,4 +106,12 @@ export class SideNavItemComponent implements OnInit {
|
|||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
openLink() {
|
||||
if (Object.keys(this.queryParams).length === 0) {
|
||||
this.router.navigateByUrl(this.link!);
|
||||
return
|
||||
}
|
||||
this.router.navigateByUrl(this.link + '?' + this.queryParams);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +1,74 @@
|
|||
<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>-->
|
||||
<!-- <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/">
|
||||
<ng-container actions>
|
||||
<app-card-actionables [actions]="homeActions" [labelBy]="t('home')" iconClass="fa-ellipsis-v" (actionHandler)="handleHomeActions()"></app-card-actionables>
|
||||
</ng-container>
|
||||
</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>
|
||||
</ng-container>
|
||||
</app-side-nav-item>
|
||||
<app-side-nav-item icon="fa-bookmark" [title]="t('bookmarks')" link="/bookmarks/"></app-side-nav-item>
|
||||
<app-side-nav-item icon="fa-regular fa-rectangle-list" [title]="t('all-series')" link="/all-series/" *ngIf="libraries.length > 0"></app-side-nav-item>
|
||||
<div class="mb-2 mt-3 ms-2 me-2" *ngIf="libraries.length > 10 && (navService?.sideNavCollapsed$ | async) === false">
|
||||
<ng-container *ngIf="navStreams$ | async as streams">
|
||||
<ng-container *ngIf="showAll">
|
||||
<app-side-nav-item icon="fa fa-chevron-left" [title]="t('back')" (click)="showLess()"></app-side-nav-item>
|
||||
<div class="mb-2 mt-3 ms-2 me-2" *ngIf="streams.length > 10 && (navService?.sideNavCollapsed$ | async) === false">
|
||||
<label for="filter" class="form-label visually-hidden">{{t('filter-label')}}</label>
|
||||
<div class="form-group">
|
||||
<input id="filter" autocomplete="off" class="form-control" [(ngModel)]="filterQuery" type="text" aria-describedby="reset-input">
|
||||
<button type="button" [attr.aria-label]="t('clear')" class="btn-close" id="reset-input" (click)="filterQuery = '';"></button>
|
||||
<input id="filter" autocomplete="off" class="form-control" [(ngModel)]="filterQuery" type="text" aria-describedby="reset-input">
|
||||
<button type="button" [attr.aria-label]="t('clear')" class="btn-close" id="reset-input" (click)="filterQuery = '';"></button>
|
||||
</div>
|
||||
</div>
|
||||
<app-side-nav-item *ngFor="let library of libraries | filter: filterLibrary" [link]="'/library/' + library.id + '/'"
|
||||
[icon]="getLibraryTypeIcon(library.type)" [imageUrl]="getLibraryImage(library)" [title]="library.name" [comparisonMethod]="'startsWith'">
|
||||
<ng-container actions>
|
||||
<app-card-actionables [actions]="actions" [labelBy]="library.name" iconClass="fa-ellipsis-v" (actionHandler)="performAction($event, library)"></app-card-actionables>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngFor="let navStream of streams | filter: filterLibrary">
|
||||
<ng-container [ngSwitch]="navStream.streamType">
|
||||
<ng-container *ngSwitchCase="SideNavStreamType.Library">
|
||||
<app-side-nav-item [link]="'/library/' + navStream.libraryId + '/'"
|
||||
[icon]="getLibraryTypeIcon(navStream.library!.type)" [imageUrl]="getLibraryImage(navStream.library!)" [title]="navStream.name" [comparisonMethod]="'startsWith'">
|
||||
<ng-container actions>
|
||||
<app-card-actionables [actions]="actions" [labelBy]="navStream.name" iconClass="fa-ellipsis-v"
|
||||
(actionHandler)="performAction($event, navStream.library!)"></app-card-actionables>
|
||||
</ng-container>
|
||||
</app-side-nav-item>
|
||||
</ng-container>
|
||||
</app-side-nav-item>
|
||||
|
||||
<ng-container *ngSwitchCase="SideNavStreamType.AllSeries">
|
||||
<app-side-nav-item icon="fa-regular fa-rectangle-list" [title]="t('all-series')" link="/all-series/"></app-side-nav-item>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="SideNavStreamType.Bookmarks">
|
||||
<app-side-nav-item icon="fa-bookmark" [title]="t('bookmarks')" link="/bookmarks/"></app-side-nav-item>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="SideNavStreamType.ReadingLists">
|
||||
<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>
|
||||
</ng-container>
|
||||
</app-side-nav-item>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="SideNavStreamType.Collections">
|
||||
<app-side-nav-item icon="fa-list" [title]="t('collections')" link="/collections/"></app-side-nav-item>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="SideNavStreamType.WantToRead">
|
||||
<app-side-nav-item icon="fa-star" [title]="t('want-to-read')" link="/want-to-read/"></app-side-nav-item>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="SideNavStreamType.SmartFilter">
|
||||
<app-side-nav-item icon="fa-bars-staggered" [title]="navStream.name" link="/all-series" [queryParams]="navStream.smartFilterEncoded"></app-side-nav-item>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="SideNavStreamType.ExternalSource">
|
||||
<app-side-nav-item icon="fa-server" [title]="navStream.name" [link]="navStream.externalSource.host + 'login?apiKey=' + navStream.externalSource.apiKey" [external]="true"></app-side-nav-item>
|
||||
</ng-container>
|
||||
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="totalSize > 10 && !showAll">
|
||||
<app-side-nav-item icon="fa fa-chevron-right" [title]="t('more')" (click)="showMore()"></app-side-nav-item>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="side-nav-overlay" (click)="toggleNavBar()" [ngClass]="{'closed' : (navService.sideNavCollapsed$ | async)}"></div>
|
||||
<div class="bottom" [ngClass]="{'closed' : (navService.sideNavCollapsed$ | async),
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
} from '@angular/core';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import {filter, map, shareReplay, take} from 'rxjs/operators';
|
||||
import {distinctUntilChanged, filter, map, shareReplay, take, tap} from 'rxjs/operators';
|
||||
import { ImportCblModalComponent } from 'src/app/reading-list/_modals/import-cbl-modal/import-cbl-modal.component';
|
||||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
|
||||
|
|
@ -20,7 +20,7 @@ import { ActionService } from '../../../_services/action.service';
|
|||
import { LibraryService } from '../../../_services/library.service';
|
||||
import { NavService } from '../../../_services/nav.service';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {switchMap} from "rxjs";
|
||||
import {BehaviorSubject, merge, Observable, of, ReplaySubject, startWith, switchMap} from "rxjs";
|
||||
import {CommonModule} from "@angular/common";
|
||||
import {SideNavItemComponent} from "../side-nav-item/side-nav-item.component";
|
||||
import {FilterPipe} from "../../../pipe/filter.pipe";
|
||||
|
|
@ -29,6 +29,8 @@ 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";
|
||||
import {SideNavStream} from "../../../_models/sidenav/sidenav-stream";
|
||||
import {SideNavStreamType} from "../../../_models/sidenav/sidenav-stream-type.enum";
|
||||
|
||||
@Component({
|
||||
selector: 'app-side-nav',
|
||||
|
|
@ -41,19 +43,78 @@ import {CustomizeDashboardModalComponent} from "../customize-dashboard-modal/cus
|
|||
export class SideNavComponent implements OnInit {
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly actionFactoryService = inject(ActionFactoryService);
|
||||
|
||||
libraries: Library[] = [];
|
||||
actions: ActionItem<Library>[] = [];
|
||||
cachedData: SideNavStream[] | null = null;
|
||||
actions: ActionItem<Library>[] = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
|
||||
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;
|
||||
}
|
||||
|
||||
constructor(private libraryService: LibraryService,
|
||||
filterQuery: string = '';
|
||||
filterLibrary = (stream: SideNavStream) => {
|
||||
return stream.name.toLowerCase().indexOf((this.filterQuery || '').toLowerCase()) >= 0;
|
||||
}
|
||||
showAll: boolean = false;
|
||||
totalSize = 0;
|
||||
protected readonly SideNavStreamType = SideNavStreamType;
|
||||
|
||||
private showAllSubject = new BehaviorSubject<boolean>(false);
|
||||
showAll$ = this.showAllSubject.asObservable();
|
||||
|
||||
private loadDataSubject = new ReplaySubject<void>();
|
||||
loadData$ = this.loadDataSubject.asObservable();
|
||||
|
||||
loadDataOnInit$: Observable<SideNavStream[]> = this.loadData$.pipe(
|
||||
switchMap(() => {
|
||||
if (this.cachedData != null) {
|
||||
return of(this.cachedData);
|
||||
}
|
||||
return this.navService.getSideNavStreams().pipe(
|
||||
map(data => {
|
||||
this.cachedData = data; // Cache the data after initial load
|
||||
return data;
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
navStreams$ = merge(
|
||||
this.showAll$.pipe(
|
||||
startWith(false),
|
||||
distinctUntilChanged(),
|
||||
tap(showAll => this.showAll = showAll),
|
||||
switchMap(showAll =>
|
||||
showAll
|
||||
? this.loadDataOnInit$.pipe(
|
||||
tap(d => this.totalSize = d.length),
|
||||
)
|
||||
: this.loadDataOnInit$.pipe(
|
||||
tap(d => this.totalSize = d.length),
|
||||
map(d => d.slice(0, 10))
|
||||
)
|
||||
),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
), this.messageHub.messages$.pipe(
|
||||
filter(event => event.event === EVENTS.LibraryModified || event.event === EVENTS.SideNavUpdate),
|
||||
tap(() => {
|
||||
this.cachedData = null; // Reset cached data to null to get latest
|
||||
}),
|
||||
switchMap(() => {
|
||||
if (this.showAll) return this.loadDataOnInit$;
|
||||
else return this.loadDataOnInit$.pipe(map(d => d.slice(0, 10)))
|
||||
}), // Reload data when events occur
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
).pipe(
|
||||
startWith(null),
|
||||
filter(data => data !== null),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
);
|
||||
|
||||
|
||||
constructor(
|
||||
public utilityService: UtilityService, private messageHub: MessageHubService,
|
||||
private actionFactoryService: ActionFactoryService, private actionService: ActionService,
|
||||
private actionService: ActionService,
|
||||
public navService: NavService, private router: Router, private readonly cdRef: ChangeDetectorRef,
|
||||
private ngbModal: NgbModal, private imageService: ImageService, public readonly accountService: AccountService) {
|
||||
|
||||
|
|
@ -74,20 +135,7 @@ export class SideNavComponent implements OnInit {
|
|||
ngOnInit(): void {
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (!user) return;
|
||||
this.libraryService.getLibraries().pipe(take(1), shareReplay()).subscribe((libraries: Library[]) => {
|
||||
this.libraries = libraries;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
// TODO: Investigate this, as it might be expensive
|
||||
this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef), filter(event => event.event === EVENTS.LibraryModified)).subscribe(event => {
|
||||
this.libraryService.getLibraries().pipe(take(1), shareReplay()).subscribe((libraries: Library[]) => {
|
||||
this.libraries = [...libraries];
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
this.loadDataSubject.next();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -112,10 +160,8 @@ 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'});
|
||||
}
|
||||
|
|
@ -141,8 +187,17 @@ export class SideNavComponent implements OnInit {
|
|||
return null;
|
||||
}
|
||||
|
||||
|
||||
toggleNavBar() {
|
||||
this.navService.toggleSideNav();
|
||||
}
|
||||
|
||||
showMore() {
|
||||
this.showAllSubject.next(true);
|
||||
}
|
||||
|
||||
showLess() {
|
||||
this.showAllSubject.next(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
<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}}">
|
||||
<span *ngIf="item.isProvided; else nonProvidedTitle">
|
||||
{{item.name | streamName }}
|
||||
</span>
|
||||
<ng-template #nonProvidedTitle>
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
<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">
|
||||
<ng-container *ngIf="item.isProvided; else nonProvided">{{t('provided')}}</ng-container>
|
||||
<ng-template #nonProvided>
|
||||
<ng-container [ngSwitch]="item.streamType">
|
||||
<ng-container *ngSwitchCase="SideNavStreamType.Library">{{t('library')}}</ng-container>
|
||||
<ng-container *ngSwitchCase="SideNavStreamType.SmartFilter">{{t('smart-filter')}}</ng-container>
|
||||
<ng-container *ngSwitchCase="SideNavStreamType.ExternalSource">{{t('external-source')}}</ng-container>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="ps-1" *ngIf="!item.isProvided">
|
||||
<ng-container [ngSwitch]="item.streamType">
|
||||
<ng-container *ngSwitchCase="SideNavStreamType.Library">
|
||||
<a [href]="'/library/' + this.item.libraryId" target="_blank">{{item.library?.name}}</a>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="SideNavStreamType.SmartFilter">
|
||||
<a [href]="'/all-series?' + this.item.smartFilterEncoded" target="_blank">{{t('load-filter')}}</a>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="SideNavStreamType.ExternalSource">
|
||||
<a [href]="item.externalSource!.host! + 'login?apiKey=' + item.externalSource!.apiKey" target="_blank" rel="noopener noreferrer">{{item.externalSource!.host!}}</a>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
.list-item {
|
||||
height: 60px;
|
||||
max-height: 60px;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import {ChangeDetectionStrategy, Component, EventEmitter, Input, Output} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {SideNavStream} from "../../../_models/sidenav/sidenav-stream";
|
||||
import {StreamNamePipe} from "../../../pipe/stream-name.pipe";
|
||||
import {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {SideNavStreamType} from "../../../_models/sidenav/sidenav-stream-type.enum";
|
||||
|
||||
@Component({
|
||||
selector: 'app-sidenav-stream-list-item',
|
||||
standalone: true,
|
||||
imports: [CommonModule, StreamNamePipe, TranslocoDirective],
|
||||
templateUrl: './sidenav-stream-list-item.component.html',
|
||||
styleUrls: ['./sidenav-stream-list-item.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class SidenavStreamListItemComponent {
|
||||
@Input({required: true}) item!: SideNavStream;
|
||||
@Input({required: true}) position: number = 0;
|
||||
@Output() hide: EventEmitter<SideNavStream> = new EventEmitter<SideNavStream>();
|
||||
protected readonly SideNavStreamType = SideNavStreamType;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue