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'}];
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
"user-scrobble-history": {
|
||||
"title": "Scrobble History",
|
||||
"description": "Here you will find any scrobble events linked with your account. In order for events to exist, you must have an active scrobble provider configured. All events that have been processed will clear after a month. If there are non-processed events, it is likely these cannot form matches upstream. Please reach out to your admin to get them corrected.",
|
||||
"filter-label": "Filter",
|
||||
"filter-label": "{{common.filter}}",
|
||||
"created-header": "Created",
|
||||
"last-modified-header": "Last Modified",
|
||||
"type-header": "Type",
|
||||
|
@ -270,6 +270,7 @@
|
|||
"api-key": {
|
||||
"copy": "Copy",
|
||||
"show": "Show",
|
||||
"hide": "Hide",
|
||||
"regen-warning": "Regenerating your API key will invalidate any existing clients.",
|
||||
"no-key": "ERROR - KEY NOT SET",
|
||||
"confirm-reset": "This will invalidate any OPDS configurations you have setup. Are you sure you want to continue?",
|
||||
|
@ -301,8 +302,8 @@
|
|||
|
||||
"generic-list-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"clear": "Clear",
|
||||
"filter": "Filter",
|
||||
"clear": "{{common.clear}}",
|
||||
"filter": "{{common.filter}}",
|
||||
"open-filtered-search": "Open a filtered search for {{item}}"
|
||||
},
|
||||
|
||||
|
@ -756,10 +757,12 @@
|
|||
"collections": "Collections",
|
||||
"reading-lists": "Reading Lists",
|
||||
"bookmarks": "Bookmarks",
|
||||
"filter-label": "Filter",
|
||||
"filter-label": "{{common.filter}}",
|
||||
"all-series": "All Series",
|
||||
"clear": "Clear",
|
||||
"donate": "Donate"
|
||||
"clear": "{{common.clear}}",
|
||||
"donate": "Donate",
|
||||
"back": "Back",
|
||||
"more": "More"
|
||||
},
|
||||
|
||||
"library-settings-modal": {
|
||||
|
@ -994,7 +997,7 @@
|
|||
"title": "Add to Collection",
|
||||
"promoted": "{{common.promoted}}",
|
||||
"close": "{{common.close}}",
|
||||
"filter-label": "Filter",
|
||||
"filter-label": "{{common.filter}}",
|
||||
"clear": "{{common.clear}}",
|
||||
"no-data": "No collections created yet",
|
||||
"loading": "{{common.loading}}",
|
||||
|
@ -1019,7 +1022,7 @@
|
|||
"manage-alerts": {
|
||||
"description-part-1": "This table contains issues found during scan or reading of your media. This list is non-managed. You can clear it at any time and use Library (Force) Scan to perform analysis. A list of some common errors and what they mean can be found on the ",
|
||||
"description-part-2": "wiki.",
|
||||
"filter-label": "Filter",
|
||||
"filter-label": "{{common.filter}}",
|
||||
"clear-alerts": "Clear Alerts",
|
||||
"extension-header": "Extension",
|
||||
"file-header": "File",
|
||||
|
@ -1087,7 +1090,7 @@
|
|||
|
||||
"manage-scrobble-errors": {
|
||||
"description": "This table contains issues found during scrobbling. This list is non-managed. You can clear it at any time and wait for the next scrobble upload to see. If there is an unknown series, you are best correcting the series name or localized series name or adding a weblink for the providers.",
|
||||
"filter-label": "Filter",
|
||||
"filter-label": "{{common.filter}}",
|
||||
"clear-errors": "Clear Errors",
|
||||
"series-header": "Series",
|
||||
"created-header": "Created",
|
||||
|
@ -1332,7 +1335,9 @@
|
|||
"remove": "{{common.remove}}",
|
||||
"load-filter": "Load Filter",
|
||||
"provided": "Provided",
|
||||
"smart-filter": "Smart Filter"
|
||||
"smart-filter": "Smart Filter",
|
||||
"library": "Library",
|
||||
"external-source": "External Source"
|
||||
},
|
||||
|
||||
"reading-list-detail": {
|
||||
|
@ -1404,7 +1409,7 @@
|
|||
"add-to-list-modal": {
|
||||
"title": "Add to Reading List",
|
||||
"close": "{{common.close}}",
|
||||
"filter-label": "Filter",
|
||||
"filter-label": "{{common.filter}}",
|
||||
"promoted-alt": "Promoted",
|
||||
"no-data": "No lists created yet",
|
||||
"loading": "{{common.loading}}",
|
||||
|
@ -1505,7 +1510,7 @@
|
|||
},
|
||||
|
||||
"metadata-filter": {
|
||||
"filter-title": "Filter",
|
||||
"filter-title": "{{common.filter}}",
|
||||
"sort-by-label": "Sort By",
|
||||
"filter-name-label": "Filter Name",
|
||||
"ascending-alt": "Ascending",
|
||||
|
@ -1737,11 +1742,77 @@
|
|||
},
|
||||
|
||||
"customize-dashboard-modal": {
|
||||
"title": "Customize Dashboard",
|
||||
"no-data": "All Smart filters added to Dashboard or none created yet.",
|
||||
"title-dashboard": "Customize Dashboard",
|
||||
"title-sidenav": "Customize Side Nav",
|
||||
"title-external-sources": "External Sources",
|
||||
"title-smart-filters": "Smart Filters",
|
||||
"close": "{{common.close}}",
|
||||
"dashboard": "Dashboard",
|
||||
"sidenav": "Side Nav",
|
||||
"external-sources": "External Sources",
|
||||
"smart-filters": "Smart Filters"
|
||||
},
|
||||
|
||||
"customize-dashboard-streams": {
|
||||
"no-data": "All Smart filters added to Dashboard or none created yet.",
|
||||
"save": "{{common.save}}",
|
||||
"add": "{{common.add}}"
|
||||
"add": "{{common.add}}",
|
||||
"filter": "{{common.filter}}",
|
||||
"clear": "{{common.clear}}"
|
||||
},
|
||||
|
||||
"customize-sidenav-streams": {
|
||||
"no-data": "All Smart filters added to Side Nav or none created yet.",
|
||||
"no-data-external-source": "All External Sources added to Side Nav or none created yet.",
|
||||
"save": "{{common.save}}",
|
||||
"add": "{{common.add}}",
|
||||
"filter": "{{common.filter}}",
|
||||
"clear": "{{common.clear}}",
|
||||
"smart-filters-title": "Smart Filters",
|
||||
"external-sources-title": "{{customize-dashboard-modal.external-sources}}",
|
||||
"reorder-when-filter-present": "You cannot reorder items via drag & drop while a filter is present. Use {{customize-sidenav-streams.order-numbers-label}}",
|
||||
"order-numbers-label": "{{reading-list-detail.order-numbers-label}}"
|
||||
},
|
||||
|
||||
"manage-external-sources": {
|
||||
"add-source": "Add",
|
||||
"help-link": "More information",
|
||||
"description": "Add External Servers to your account and then add them to your Side Nav for a quick way to switch between your and your friend's server.",
|
||||
"clear": "{{common.clear}}",
|
||||
"filter": "{{common.filter}}"
|
||||
},
|
||||
|
||||
"manage-smart-filters": {
|
||||
"delete": "{{common.delete}}",
|
||||
"no-data": "No Smart Filters created",
|
||||
"filter": "{{common.filter}}",
|
||||
"clear": "{{common.clear}}"
|
||||
},
|
||||
|
||||
"edit-external-source-item": {
|
||||
"not-unique": "External source exists with this host. Ensure you don't have duplicates",
|
||||
"title": "New External Source",
|
||||
"host-label": "Host",
|
||||
"name-label": "Name",
|
||||
"api-key-label": "API Key",
|
||||
"save": "{{common.save}}",
|
||||
"edit": "{{common.edit}}",
|
||||
"cancel": "{{common.cancel}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"pattern": "Host must be a valid http(s):// url",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
|
||||
"stream-pipe": {
|
||||
"on-deck": "{{dashboard.on-deck-title}}",
|
||||
"recently-updated": "{{dashboard.recently-updated-title}}",
|
||||
"newly-added": "{{dashboard.recently-added-title}}",
|
||||
"more-in-genre": "{{dashboard.more-in-genre-title}}",
|
||||
"want-to-read": "{{side-nav.want-to-read}}",
|
||||
"collections": "{{side-nav.collections}}",
|
||||
"reading-lists": "{{side-nav.reading-lists}}",
|
||||
"bookmarks": "{{side-nav.bookmarks}}",
|
||||
"all-series": "{{side-nav.all-series}}"
|
||||
},
|
||||
|
||||
"filter-field-pipe": {
|
||||
|
@ -1875,7 +1946,8 @@
|
|||
"list-doesnt-exist": "This list doesn't exist",
|
||||
"confirm-delete-smart-filter": "Are you sure you want to delete this Smart Filter?",
|
||||
"smart-filter-deleted": "Smart Filter Deleted",
|
||||
"smart-filter-updated": "Created/Updated smart filter"
|
||||
"smart-filter-updated": "Created/Updated smart filter",
|
||||
"external-source-already-exists": "An External Source already exists with the same Name/Host/API Key"
|
||||
},
|
||||
|
||||
"actionable": {
|
||||
|
@ -1900,7 +1972,7 @@
|
|||
"read-incognito": "Read Incognito",
|
||||
"details": "Details",
|
||||
"view-series": "View Series",
|
||||
"clear": "Clear",
|
||||
"clear": "{{common.clear}}",
|
||||
"import-cbl": "Import CBL",
|
||||
"read": "Read",
|
||||
"add-rule-group-and": "Add Rule Group (AND)",
|
||||
|
@ -1950,6 +2022,8 @@
|
|||
"common": {
|
||||
"reset-to-default": "Reset to Default",
|
||||
"close": "Close",
|
||||
"clear": "Clear",
|
||||
"filter": "Filter",
|
||||
"cancel": "Cancel",
|
||||
"create": "Create",
|
||||
"save": "Save",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue