Side Nav Redesign (#2310)
This commit is contained in:
parent
5c2ebb87cc
commit
00dddaefae
88 changed files with 5971 additions and 572 deletions
8
UI/Web/src/app/_models/common-stream.ts
Normal file
8
UI/Web/src/app/_models/common-stream.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export interface CommonStream {
|
||||
id: number;
|
||||
name: string;
|
||||
isProvided: boolean;
|
||||
order: number;
|
||||
visible: boolean;
|
||||
smartFilterEncoded?: string;
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import {Observable} from "rxjs";
|
||||
import {StreamType} from "./stream-type.enum";
|
||||
import {CommonStream} from "../common-stream";
|
||||
|
||||
export interface DashboardStream {
|
||||
export interface DashboardStream extends CommonStream {
|
||||
id: number;
|
||||
name: string;
|
||||
isProvided: boolean;
|
||||
|
|
@ -12,3 +13,5 @@ export interface DashboardStream {
|
|||
order: number;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
3
UI/Web/src/app/_models/events/sidenav-update-event.ts
Normal file
3
UI/Web/src/app/_models/events/sidenav-update-event.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export interface SideNavUpdateEvent {
|
||||
userId: number;
|
||||
}
|
||||
6
UI/Web/src/app/_models/sidenav/external-source.ts
Normal file
6
UI/Web/src/app/_models/sidenav/external-source.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export interface ExternalSource {
|
||||
id: number;
|
||||
name: string;
|
||||
host: string;
|
||||
apiKey: string;
|
||||
}
|
||||
10
UI/Web/src/app/_models/sidenav/sidenav-stream-type.enum.ts
Normal file
10
UI/Web/src/app/_models/sidenav/sidenav-stream-type.enum.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export enum SideNavStreamType {
|
||||
Collections = 1,
|
||||
ReadingLists = 2,
|
||||
Bookmarks = 3,
|
||||
Library = 4,
|
||||
SmartFilter = 5,
|
||||
ExternalSource = 6,
|
||||
AllSeries = 7,
|
||||
WantToRead = 8,
|
||||
}
|
||||
18
UI/Web/src/app/_models/sidenav/sidenav-stream.ts
Normal file
18
UI/Web/src/app/_models/sidenav/sidenav-stream.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import {SideNavStreamType} from "./sidenav-stream-type.enum";
|
||||
import {Library, LibraryType} from "../library";
|
||||
import {CommonStream} from "../common-stream";
|
||||
import {ExternalSource} from "./external-source";
|
||||
|
||||
export interface SideNavStream extends CommonStream {
|
||||
name: string;
|
||||
order: number;
|
||||
libraryId?: number;
|
||||
isProvided: boolean;
|
||||
streamType: SideNavStreamType;
|
||||
library?: Library;
|
||||
visible: boolean;
|
||||
smartFilterId: number;
|
||||
smartFilterEncoded?: string;
|
||||
externalSource?: ExternalSource;
|
||||
|
||||
}
|
||||
|
|
@ -12,18 +12,18 @@ export class DashboardService {
|
|||
constructor(private httpClient: HttpClient) { }
|
||||
|
||||
getDashboardStreams(visibleOnly = true) {
|
||||
return this.httpClient.get<Array<DashboardStream>>(this.baseUrl + 'account/dashboard?visibleOnly=' + visibleOnly);
|
||||
return this.httpClient.get<Array<DashboardStream>>(this.baseUrl + 'stream/dashboard?visibleOnly=' + visibleOnly);
|
||||
}
|
||||
|
||||
updateDashboardStreamPosition(streamName: string, dashboardStreamId: number, fromPosition: number, toPosition: number) {
|
||||
return this.httpClient.post(this.baseUrl + 'account/update-dashboard-position', {streamName, dashboardStreamId, fromPosition, toPosition}, TextResonse);
|
||||
return this.httpClient.post(this.baseUrl + 'stream/update-dashboard-position', {streamName, id: dashboardStreamId, fromPosition, toPosition}, TextResonse);
|
||||
}
|
||||
|
||||
updateDashboardStream(stream: DashboardStream) {
|
||||
return this.httpClient.post(this.baseUrl + 'account/update-dashboard-stream', stream, TextResonse);
|
||||
return this.httpClient.post(this.baseUrl + 'stream/update-dashboard-stream', stream, TextResonse);
|
||||
}
|
||||
|
||||
createDashboardStream(smartFilterId: number) {
|
||||
return this.httpClient.post<DashboardStream>(this.baseUrl + 'account/add-dashboard-stream?smartFilterId=' + smartFilterId, {});
|
||||
return this.httpClient.post<DashboardStream>(this.baseUrl + 'stream/add-dashboard-stream?smartFilterId=' + smartFilterId, {});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { ThemeProgressEvent } from '../_models/events/theme-progress-event';
|
|||
import { UserUpdateEvent } from '../_models/events/user-update-event';
|
||||
import { User } from '../_models/user';
|
||||
import {DashboardUpdateEvent} from "../_models/events/dashboard-update-event";
|
||||
import {SideNavUpdateEvent} from "../_models/events/sidenav-update-event";
|
||||
|
||||
export enum EVENTS {
|
||||
UpdateAvailable = 'UpdateAvailable',
|
||||
|
|
@ -86,7 +87,11 @@ export enum EVENTS {
|
|||
/**
|
||||
* User's dashboard needs to be re-rendered
|
||||
*/
|
||||
DashboardUpdate = 'DashboardUpdate'
|
||||
DashboardUpdate = 'DashboardUpdate',
|
||||
/**
|
||||
* User's sidenav needs to be re-rendered
|
||||
*/
|
||||
SideNavUpdate = 'SideNavUpdate'
|
||||
}
|
||||
|
||||
export interface Message<T> {
|
||||
|
|
@ -187,6 +192,12 @@ export class MessageHubService {
|
|||
payload: resp.body as DashboardUpdateEvent
|
||||
});
|
||||
});
|
||||
this.hubConnection.on(EVENTS.SideNavUpdate, resp => {
|
||||
this.messagesSource.next({
|
||||
event: EVENTS.SideNavUpdate,
|
||||
payload: resp.body as SideNavUpdateEvent
|
||||
});
|
||||
});
|
||||
|
||||
this.hubConnection.on(EVENTS.NotificationProgress, (resp: NotificationProgressEvent) => {
|
||||
this.messagesSource.next({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import { DOCUMENT } from '@angular/common';
|
||||
import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core';
|
||||
import { ReplaySubject, take } from 'rxjs';
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {environment} from "../../environments/environment";
|
||||
import {SideNavStream} from "../_models/sidenav/sidenav-stream";
|
||||
import {TextResonse} from "../_types/text-response";
|
||||
import {DashboardStream} from "../_models/dashboard/dashboard-stream";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
|
@ -27,15 +32,36 @@ export class NavService {
|
|||
sideNavVisibility$ = this.sideNavVisibilitySource.asObservable();
|
||||
|
||||
private renderer: Renderer2;
|
||||
baseUrl = environment.apiUrl;
|
||||
|
||||
constructor(@Inject(DOCUMENT) private document: Document, rendererFactory: RendererFactory2) {
|
||||
constructor(@Inject(DOCUMENT) private document: Document, rendererFactory: RendererFactory2, private httpClient: HttpClient) {
|
||||
this.renderer = rendererFactory.createRenderer(null, null);
|
||||
this.showNavBar();
|
||||
const sideNavState = (localStorage.getItem(this.localStorageSideNavKey) === 'true') || false;
|
||||
this.sideNavCollapseSource.next(sideNavState);
|
||||
this.showSideNav();
|
||||
}
|
||||
|
||||
|
||||
getSideNavStreams(visibleOnly = true) {
|
||||
return this.httpClient.get<Array<SideNavStream>>(this.baseUrl + 'stream/sidenav?visibleOnly=' + visibleOnly);
|
||||
}
|
||||
|
||||
updateSideNavStreamPosition(streamName: string, sideNavStreamId: number, fromPosition: number, toPosition: number) {
|
||||
return this.httpClient.post(this.baseUrl + 'stream/update-sidenav-position', {streamName, id: sideNavStreamId, fromPosition, toPosition}, TextResonse);
|
||||
}
|
||||
|
||||
updateSideNavStream(stream: SideNavStream) {
|
||||
return this.httpClient.post(this.baseUrl + 'stream/update-sidenav-stream', stream, TextResonse);
|
||||
}
|
||||
|
||||
createSideNavStream(smartFilterId: number) {
|
||||
return this.httpClient.post<SideNavStream>(this.baseUrl + 'stream/add-sidenav-stream?smartFilterId=' + smartFilterId, {});
|
||||
}
|
||||
|
||||
createSideNavStreamFromExternalSource(externalSourceId: number) {
|
||||
return this.httpClient.post<SideNavStream>(this.baseUrl + 'stream/add-sidenav-stream-from-external-source?externalSourceId=' + externalSourceId, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the top nav bar. This should be visible on all pages except the reader.
|
||||
*/
|
||||
|
|
@ -47,7 +73,7 @@ export class NavService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Hides the top nav bar.
|
||||
* Hides the top nav bar.
|
||||
*/
|
||||
hideNavBar() {
|
||||
this.renderer.setStyle(this.document.querySelector('body'), 'margin-top', '0px');
|
||||
|
|
|
|||
|
|
@ -9,13 +9,17 @@
|
|||
::ng-deep .changelog {
|
||||
|
||||
h1 {
|
||||
font-size: 26px;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
p, ul {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
img {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,9 @@
|
|||
<label for="release-year" class="form-label">{{t('release-year-label')}}</label>
|
||||
<div class="input-group {{metadata.releaseYearLocked ? 'lock-active' : ''}}">
|
||||
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: metadata, field: 'releaseYearLocked' }"></ng-container>
|
||||
<input type="number" inputmode="numeric" class="form-control" id="release-year" formControlName="releaseYear" maxlength="4" minlength="4" [class.is-invalid]="editSeriesForm.get('releaseYear')?.invalid && editSeriesForm.get('releaseYear')?.touched">
|
||||
<input type="number" inputmode="numeric" class="form-control" id="release-year" formControlName="releaseYear"
|
||||
maxlength="4" minlength="4"
|
||||
[class.is-invalid]="editSeriesForm.get('releaseYear')?.invalid && editSeriesForm.get('releaseYear')?.touched">
|
||||
<ng-container *ngIf="editSeriesForm.get('releaseYear')?.errors as errors">
|
||||
<p class="invalid-feedback" *ngIf="errors.pattern">
|
||||
This must be a valid year greater than 1000 and 4 characters long
|
||||
|
|
|
|||
36
UI/Web/src/app/external-source.service.ts
Normal file
36
UI/Web/src/app/external-source.service.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import {environment} from "../environments/environment";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {ExternalSource} from "./_models/sidenav/external-source";
|
||||
import {TextResonse} from "./_types/text-response";
|
||||
import {map} from "rxjs/operators";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ExternalSourceService {
|
||||
|
||||
baseUrl = environment.apiUrl;
|
||||
constructor(private httpClient: HttpClient) { }
|
||||
|
||||
getExternalSources() {
|
||||
return this.httpClient.get<Array<ExternalSource>>(this.baseUrl + 'stream/external-sources');
|
||||
}
|
||||
|
||||
createSource(source: ExternalSource) {
|
||||
return this.httpClient.post<ExternalSource>(this.baseUrl + 'stream/create-external-source', source);
|
||||
}
|
||||
|
||||
updateSource(source: ExternalSource) {
|
||||
return this.httpClient.post<ExternalSource>(this.baseUrl + 'stream/update-external-source', source);
|
||||
}
|
||||
|
||||
deleteSource(externalSourceId: number) {
|
||||
return this.httpClient.delete(this.baseUrl + 'stream/delete-external-source?externalSourceId=' + externalSourceId);
|
||||
}
|
||||
|
||||
sourceExists(name: string, host: string, apiKey: string) {
|
||||
return this.httpClient.get<string>(this.baseUrl + `stream/external-source-exists?host=${encodeURIComponent(host)}&name=${name}&apiKey=${apiKey}`, TextResonse)
|
||||
.pipe(map(s => s == 'true'));
|
||||
}
|
||||
}
|
||||
|
|
@ -443,7 +443,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
get LayoutMode() { return LayoutMode; }
|
||||
get ReadingDirection() { return ReadingDirection; }
|
||||
get Breakpoint() { return Breakpoint; }
|
||||
get FittingOption() { return this.generalSettingsForm.get('fittingOption')?.value || FITTING_OPTION.HEIGHT; }
|
||||
get FittingOption() { return this.generalSettingsForm?.get('fittingOption')?.value || FITTING_OPTION.HEIGHT; }
|
||||
get ReadingAreaWidth() {
|
||||
return this.readingArea?.nativeElement.scrollWidth - this.readingArea?.nativeElement.clientWidth;
|
||||
}
|
||||
|
|
|
|||
15
UI/Web/src/app/pipe/stream-name.pipe.ts
Normal file
15
UI/Web/src/app/pipe/stream-name.pipe.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import {translate} from "@ngneat/transloco";
|
||||
|
||||
@Pipe({
|
||||
name: 'streamName',
|
||||
standalone: true,
|
||||
pure: true
|
||||
})
|
||||
export class StreamNamePipe implements PipeTransform {
|
||||
|
||||
transform(value: string): unknown {
|
||||
return translate('stream-pipe.' + value);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
||||
|
||||
<button class="btn btn-icon float-end" (click)="removeItem(item, i)" *ngIf="showRemoveButton">
|
||||
<button class="btn btn-icon float-end" (click)="removeItem(item, i)" *ngIf="showRemoveButton" [disabled]="disabled">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
<span class="visually-hidden" attr.aria-labelledby="item.id--{{i}}">{{t('remove-item-alt')}}</span>
|
||||
</button>
|
||||
|
|
@ -34,12 +34,12 @@
|
|||
<input id="reorder-{{i}}" class="form-control" type="number" inputmode="numeric" min="0" [max]="items.length - 1" [value]="i" style="width: 60px"
|
||||
(focusout)="updateIndex(i, item)" (keydown.enter)="updateIndex(i, item)" aria-describedby="instructions">
|
||||
</div>
|
||||
<i *ngIf="!accessibilityMode" class="fa fa-grip-vertical drag-handle" aria-hidden="true" cdkDragHandle></i>
|
||||
<i *ngIf="!accessibilityMode && !disabled" class="fa fa-grip-vertical drag-handle" aria-hidden="true" cdkDragHandle></i>
|
||||
</div>
|
||||
|
||||
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
||||
|
||||
<button class="btn btn-icon float-end" (click)="removeItem(item, i)" *ngIf="showRemoveButton">
|
||||
<button class="btn btn-icon float-end" (click)="removeItem(item, i)" *ngIf="showRemoveButton" [disabled]="disabled">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
<span class="visually-hidden" attr.aria-labelledby="item.id--{{i}}">{{t('remove-item-alt')}}</span>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export interface IndexUpdateEvent {
|
|||
fromPosition: number;
|
||||
toPosition: number;
|
||||
item: any;
|
||||
fromAccessibilityMode: boolean;
|
||||
}
|
||||
|
||||
export interface ItemRemoveEvent {
|
||||
|
|
@ -35,6 +36,10 @@ export class DraggableOrderedListComponent {
|
|||
* Parent scroll for virtualize pagination
|
||||
*/
|
||||
@Input() parentScroll!: Element | Window;
|
||||
/**
|
||||
* Disables drag and drop functionality. Useful if a filter is present which will skew actual index.
|
||||
*/
|
||||
@Input() disabled: boolean = false;
|
||||
@Input() trackByIdentity: TrackByFunction<any> = (index: number, item: any) => `${item.id}_${item.order}_${item.title}`;
|
||||
@Output() orderUpdated: EventEmitter<IndexUpdateEvent> = new EventEmitter<IndexUpdateEvent>();
|
||||
@Output() itemRemove: EventEmitter<ItemRemoveEvent> = new EventEmitter<ItemRemoveEvent>();
|
||||
|
|
@ -52,21 +57,23 @@ export class DraggableOrderedListComponent {
|
|||
this.orderUpdated.emit({
|
||||
fromPosition: event.previousIndex,
|
||||
toPosition: event.currentIndex,
|
||||
item: this.items[event.currentIndex]
|
||||
item: this.items[event.currentIndex],
|
||||
fromAccessibilityMode: false
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateIndex(previousIndex: number, item: any) {
|
||||
// get the new value of the input
|
||||
var inputElem = <HTMLInputElement>document.querySelector('#reorder-' + previousIndex);
|
||||
const inputElem = <HTMLInputElement>document.querySelector('#reorder-' + previousIndex);
|
||||
const newIndex = parseInt(inputElem.value, 10);
|
||||
if (previousIndex === newIndex) return;
|
||||
moveItemInArray(this.items, previousIndex, newIndex);
|
||||
this.orderUpdated.emit({
|
||||
fromPosition: previousIndex,
|
||||
toPosition: newIndex,
|
||||
item: this.items[newIndex]
|
||||
item: this.items[newIndex],
|
||||
fromAccessibilityMode: true
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,13 @@
|
|||
width: 100%;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
::ng-deep .update-body {
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -1,16 +1,18 @@
|
|||
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 {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} from "@ngneat/transloco";
|
||||
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],
|
||||
imports: [CommonModule, ReactiveFormsModule, TranslocoDirective, FilterPipe],
|
||||
templateUrl: './manage-smart-filters.component.html',
|
||||
styleUrls: ['./manage-smart-filters.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
@ -23,6 +25,19 @@ export class ManageSmartFiltersComponent {
|
|||
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();
|
||||
|
|
@ -44,6 +59,7 @@ export class ManageSmartFiltersComponent {
|
|||
|
||||
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;
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
<button class="btn btn-outline-secondary" type="button" id="reset-input" (click)="listForm.get('filterQuery')?.setValue('');">{{t('clear')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center" *ngFor="let item of items | filter: filterList; let i = index">
|
||||
{{item}}
|
||||
<button class="btn btn-primary" *ngIf="clicked !== undefined" (click)="handleClick(item)">
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
<span class="visually-hidden">{{t('open-filtered-search',{item: item})}}</span>
|
||||
</button>
|
||||
</li>
|
||||
</div>
|
||||
</ul>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
<ng-template #tooltip>{{tooltipText}}</ng-template>
|
||||
<div class="input-group">
|
||||
<input #apiKey [type]="InputType" readonly class="form-control" id="api-key--{{title}}" aria-describedby="button-addon4" [value]="key" (click)="selectAll()">
|
||||
<button class="btn btn-outline-secondary" type="button" (click)="show()" [title]="t('show')" *ngIf="hideData">
|
||||
<span class="visually-hidden">t('show')</span><i class="fa fa-eye" aria-hidden="true"></i>
|
||||
<button class="btn btn-outline-secondary" type="button" (click)="toggleVisibility()" [title]="isDataHidden ? t('show') : t('hide')" *ngIf="hideData">
|
||||
<span class="visually-hidden">{{isDataHidden ? t('show') : t('hide')}}</span><i class="fa {{isDataHidden ? 'fa-eye' : 'fa-eye-slash'}}" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" type="button" (click)="copy()" [title]="t('copy')">
|
||||
<span class="visually-hidden">Copy</span><i class="fa fa-copy" aria-hidden="true"></i>
|
||||
|
|
|
|||
|
|
@ -35,8 +35,10 @@ export class ApiKeyComponent implements OnInit {
|
|||
key: string = '';
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
isDataHidden: boolean = this.hideData;
|
||||
|
||||
get InputType() {
|
||||
return this.hideData ? 'password' : 'text';
|
||||
return (this.hideData && this.isDataHidden) ? 'password' : 'text';
|
||||
}
|
||||
|
||||
constructor(private confirmService: ConfirmService, private accountService: AccountService, private toastr: ToastrService, private clipboard: Clipboard,
|
||||
|
|
@ -83,8 +85,8 @@ export class ApiKeyComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
show() {
|
||||
this.inputElem.nativeElement.setAttribute('type', 'text');
|
||||
toggleVisibility() {
|
||||
this.isDataHidden = !this.isDataHidden;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
<ul>
|
||||
<li class="list-group-item" *ngFor="let f of filters">
|
||||
<span (click)="loadFilter(f)">{{f.name}}</span>
|
||||
<button class="btn btn-danger float-end" (click)="deleteFilter(f)">
|
||||
<i class="fa-solid fa-trash" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">Delete</span>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<li class="list-group-item" *ngIf="filters.length === 0">No Smart Filters created</li>
|
||||
</ul>
|
||||
|
|
@ -428,9 +428,6 @@
|
|||
<ng-container *ngIf="tab.fragment === FragmentID.Stats">
|
||||
<app-user-stats></app-user-stats>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="tab.fragment === FragmentID.SmartFilters">
|
||||
<app-manage-smart-filters></app-manage-smart-filters>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="tab.fragment === FragmentID.Scrobbling">
|
||||
<app-user-scrobble-history></app-user-scrobble-history>
|
||||
<app-user-holds></app-user-holds>
|
||||
|
|
|
|||
|
|
@ -48,8 +48,7 @@ import { NgbNav, NgbNavItem, NgbNavItemRole, NgbNavLink, NgbNavContent, NgbAccor
|
|||
import { SideNavCompanionBarComponent } from '../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
|
||||
import {LocalizationService} from "../../_services/localization.service";
|
||||
import {Language} from "../../_models/metadata/language";
|
||||
import {translate, TranslocoDirective, TranslocoService} from "@ngneat/transloco";
|
||||
import {ManageSmartFiltersComponent} from "../manage-smart-filters/manage-smart-filters.component";
|
||||
import {translate, TranslocoDirective} from "@ngneat/transloco";
|
||||
|
||||
enum AccordionPanelID {
|
||||
ImageReader = 'image-reader',
|
||||
|
|
@ -64,9 +63,7 @@ enum FragmentID {
|
|||
Theme = 'theme',
|
||||
Devices = 'devices',
|
||||
Stats = 'stats',
|
||||
SmartFilters = 'smart-filters',
|
||||
Scrobbling = 'scrobbling'
|
||||
|
||||
}
|
||||
|
||||
@Component({
|
||||
|
|
@ -79,7 +76,7 @@ enum FragmentID {
|
|||
ChangePasswordComponent, ChangeAgeRestrictionComponent, AnilistKeyComponent, ReactiveFormsModule, NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader,
|
||||
NgbAccordionToggle, NgbAccordionButton, NgbCollapse, NgbAccordionCollapse, NgbAccordionBody, NgbTooltip, NgTemplateOutlet, ColorPickerModule, ApiKeyComponent,
|
||||
ThemeManagerComponent, ManageDevicesComponent, UserStatsComponent, UserScrobbleHistoryComponent, UserHoldsComponent, NgbNavOutlet, TitleCasePipe, SentenceCasePipe,
|
||||
TranslocoDirective, ManageSmartFiltersComponent]
|
||||
TranslocoDirective]
|
||||
})
|
||||
export class UserPreferencesComponent implements OnInit, OnDestroy {
|
||||
|
||||
|
|
@ -110,7 +107,6 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||
{title: '3rd-party-clients-tab', fragment: FragmentID.Clients},
|
||||
{title: 'theme-tab', fragment: FragmentID.Theme},
|
||||
{title: 'devices-tab', fragment: FragmentID.Devices},
|
||||
{title: 'smart-filters-tab', fragment: FragmentID.SmartFilters},
|
||||
{title: 'stats-tab', fragment: FragmentID.Stats},
|
||||
];
|
||||
locales: Array<Language> = [{title: 'English', isoCode: 'en'}];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue