Smart Filters & Dashboard Customization (#2282)
Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
parent
3d501c9532
commit
84f85b4f24
92 changed files with 7149 additions and 555 deletions
14
UI/Web/src/app/_models/dashboard/dashboard-stream.ts
Normal file
14
UI/Web/src/app/_models/dashboard/dashboard-stream.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import {Observable} from "rxjs";
|
||||
import {StreamType} from "./stream-type.enum";
|
||||
|
||||
export interface DashboardStream {
|
||||
id: number;
|
||||
name: string;
|
||||
isProvided: boolean;
|
||||
api: Observable<any[]>;
|
||||
smartFilterId: number;
|
||||
smartFilterEncoded?: string;
|
||||
streamType: StreamType;
|
||||
order: number;
|
||||
visible: boolean;
|
||||
}
|
7
UI/Web/src/app/_models/dashboard/stream-type.enum.ts
Normal file
7
UI/Web/src/app/_models/dashboard/stream-type.enum.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export enum StreamType {
|
||||
OnDeck = 1,
|
||||
RecentlyUpdated = 2,
|
||||
NewlyAdded = 3,
|
||||
SmartFilter = 4,
|
||||
MoreInGenre = 5
|
||||
}
|
3
UI/Web/src/app/_models/events/dashboard-update-event.ts
Normal file
3
UI/Web/src/app/_models/events/dashboard-update-event.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export interface DashboardUpdateEvent {
|
||||
userId: number;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
export interface LibraryModifiedEvent {
|
||||
libraryId: number;
|
||||
action: 'create' | 'delelte';
|
||||
}
|
||||
action: 'create' | 'delete';
|
||||
}
|
||||
|
|
|
@ -26,7 +26,8 @@ export enum FilterField
|
|||
ReleaseYear = 22,
|
||||
ReadTime = 23,
|
||||
Path = 24,
|
||||
FilePath = 25
|
||||
FilePath = 25,
|
||||
WantToRead = 26
|
||||
}
|
||||
|
||||
export const allFields = Object.keys(FilterField)
|
||||
|
|
5
UI/Web/src/app/_models/metadata/v2/smart-filter.ts
Normal file
5
UI/Web/src/app/_models/metadata/v2/smart-filter.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export interface SmartFilter {
|
||||
id: number;
|
||||
name: string;
|
||||
filter: string;
|
||||
}
|
|
@ -58,7 +58,7 @@ export class AccountService {
|
|||
filter(userUpdateEvent => userUpdateEvent.userName === this.currentUser?.username),
|
||||
switchMap(() => this.refreshAccount()))
|
||||
.subscribe(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
hasAdminRole(user: User) {
|
||||
return user && user.roles.includes(Role.Admin);
|
||||
|
|
29
UI/Web/src/app/_services/dashboard.service.ts
Normal file
29
UI/Web/src/app/_services/dashboard.service.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import {TextResonse} from "../_types/text-response";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {environment} from "../../environments/environment";
|
||||
import {DashboardStream} from "../_models/dashboard/dashboard-stream";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DashboardService {
|
||||
baseUrl = environment.apiUrl;
|
||||
constructor(private httpClient: HttpClient) { }
|
||||
|
||||
getDashboardStreams(visibleOnly = true) {
|
||||
return this.httpClient.get<Array<DashboardStream>>(this.baseUrl + 'account/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);
|
||||
}
|
||||
|
||||
updateDashboardStream(stream: DashboardStream) {
|
||||
return this.httpClient.post(this.baseUrl + 'account/update-dashboard-stream', stream, TextResonse);
|
||||
}
|
||||
|
||||
createDashboardStream(smartFilterId: number) {
|
||||
return this.httpClient.post<DashboardStream>(this.baseUrl + 'account/add-dashboard-stream?smartFilterId=' + smartFilterId, {});
|
||||
}
|
||||
}
|
26
UI/Web/src/app/_services/filter.service.ts
Normal file
26
UI/Web/src/app/_services/filter.service.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2";
|
||||
import {environment} from "../../environments/environment";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {JumpKey} from "../_models/jumpbar/jump-key";
|
||||
import {SmartFilter} from "../_models/metadata/v2/smart-filter";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class FilterService {
|
||||
|
||||
baseUrl = environment.apiUrl;
|
||||
constructor(private httpClient: HttpClient) { }
|
||||
|
||||
saveFilter(filter: SeriesFilterV2) {
|
||||
return this.httpClient.post(this.baseUrl + 'filter/update', filter);
|
||||
}
|
||||
getAllFilters() {
|
||||
return this.httpClient.get<Array<SmartFilter>>(this.baseUrl + 'filter');
|
||||
}
|
||||
deleteFilter(filterId: number) {
|
||||
return this.httpClient.delete(this.baseUrl + 'filter?filterId=' + filterId);
|
||||
}
|
||||
|
||||
}
|
|
@ -7,6 +7,7 @@ import { NotificationProgressEvent } from '../_models/events/notification-progre
|
|||
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";
|
||||
|
||||
export enum EVENTS {
|
||||
UpdateAvailable = 'UpdateAvailable',
|
||||
|
@ -82,6 +83,10 @@ export enum EVENTS {
|
|||
* A scrobbling token has expired
|
||||
*/
|
||||
ScrobblingKeyExpired = 'ScrobblingKeyExpired',
|
||||
/**
|
||||
* User's dashboard needs to be re-rendered
|
||||
*/
|
||||
DashboardUpdate = 'DashboardUpdate'
|
||||
}
|
||||
|
||||
export interface Message<T> {
|
||||
|
@ -109,7 +114,6 @@ export class MessageHubService {
|
|||
*/
|
||||
public onlineUsers$ = this.onlineUsersSource.asObservable();
|
||||
|
||||
|
||||
isAdmin: boolean = false;
|
||||
|
||||
constructor() {}
|
||||
|
@ -181,6 +185,13 @@ export class MessageHubService {
|
|||
});
|
||||
});
|
||||
|
||||
this.hubConnection.on(EVENTS.DashboardUpdate, resp => {
|
||||
console.log('dashboard update event came in')
|
||||
this.messagesSource.next({
|
||||
event: EVENTS.DashboardUpdate,
|
||||
payload: resp.body as DashboardUpdateEvent
|
||||
});
|
||||
});
|
||||
|
||||
this.hubConnection.on(EVENTS.NotificationProgress, (resp: NotificationProgressEvent) => {
|
||||
this.messagesSource.next({
|
||||
|
|
|
@ -23,14 +23,16 @@
|
|||
</div>
|
||||
|
||||
<div class="card-footer bg-transparent text-muted">
|
||||
<ng-container *ngIf="isMyReview; else normalReview">
|
||||
<i class="d-md-none fa-solid fa-star me-1" aria-hidden="true" [title]="t('your-review')"></i>
|
||||
</ng-container>
|
||||
<ng-template #normalReview>
|
||||
<img class="me-1" [ngSrc]="review.provider | providerImage" width="20" height="20" alt="">
|
||||
</ng-template>
|
||||
{{(isMyReview ? '' : review.username | defaultValue:'')}}
|
||||
<span style="float: right" *ngIf="review.isExternal">{{t('rating-percentage', {r: review.score})}}</span>
|
||||
<div class="review-user">
|
||||
<ng-container *ngIf="isMyReview; else normalReview">
|
||||
<i class="d-md-none fa-solid fa-star me-1" aria-hidden="true" [title]="t('your-review')"></i>
|
||||
</ng-container>
|
||||
<ng-template #normalReview>
|
||||
<img class="me-1" [ngSrc]="review.provider | providerImage" width="20" height="20" alt="">
|
||||
</ng-template>
|
||||
{{(isMyReview ? '' : review.username | defaultValue:'')}}
|
||||
</div>
|
||||
<span class="review-score" *ngIf="review.isExternal">{{t('rating-percentage', {r: review.score})}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -41,5 +41,9 @@
|
|||
}
|
||||
|
||||
.card-footer {
|
||||
font-size: 13px
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
max-width: 305px;
|
||||
justify-content: space-between;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
|
|
@ -1,36 +1,86 @@
|
|||
<app-side-nav-companion-bar></app-side-nav-companion-bar>
|
||||
|
||||
<ng-container *transloco="let t; read: 'dashboard'">
|
||||
<ng-container *ngIf="libraries$ | async as libraries">
|
||||
<ng-container *ngIf="libraries.length === 0 && !isLoading">
|
||||
<div class="mt-3" *ngIf="isAdmin$ | async as isAdmin">
|
||||
<div *ngIf="isAdmin" class="d-flex justify-content-center">
|
||||
<p>{{t('no-libraries')}} <a routerLink="/admin/dashboard" fragment="libraries">{{t('server-settings-link')}}</a>.</p>
|
||||
</div>
|
||||
<div *ngIf="!isAdmin" class="d-flex justify-content-center">
|
||||
<p>{{t('not-granted')}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="libraries$ | async as libraries">
|
||||
<ng-container *ngIf="libraries.length === 0 && !isLoadingAdmin">
|
||||
<div class="mt-3" *ngIf="isAdmin$ | async as isAdmin">
|
||||
<div *ngIf="isAdmin" class="d-flex justify-content-center">
|
||||
<p>{{t('no-libraries')}} <a routerLink="/admin/dashboard" fragment="libraries">{{t('server-settings-link')}}</a>.</p>
|
||||
</div>
|
||||
<div *ngIf="!isAdmin" class="d-flex justify-content-center">
|
||||
<p>{{t('not-granted')}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngFor="let stream of streams">
|
||||
<ng-container [ngSwitch]="stream.streamType">
|
||||
<ng-container *ngSwitchCase="StreamType.OnDeck" [ngTemplateOutlet]="onDeck" [ngTemplateOutletContext]="{ stream: stream }"></ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="StreamType.RecentlyUpdated" [ngTemplateOutlet]="recentlyUpdated" [ngTemplateOutletContext]="{ stream: stream }"></ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="StreamType.NewlyAdded" [ngTemplateOutlet]="newlyUpdated" [ngTemplateOutletContext]="{ stream: stream }"></ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="StreamType.SmartFilter" [ngTemplateOutlet]="smartFilter" [ngTemplateOutletContext]="{ stream: stream }"></ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="StreamType.MoreInGenre" [ngTemplateOutlet]="moreInGenre" [ngTemplateOutletContext]="{ stream: stream }"></ng-container>
|
||||
</ng-container>
|
||||
|
||||
<app-carousel-reel [items]="inProgress" [title]="t('on-deck-title')" (sectionClick)="handleSectionClick('on deck')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<ng-template #smartFilter let-stream: DashboardStream>
|
||||
<ng-container *ngIf="(stream.api | async) as data">
|
||||
<app-carousel-reel [items]="data" [title]="stream.name" (sectionClick)="handleFilterSectionClick(stream)">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" [isOnDeck]="false"
|
||||
(reload)="reloadStream(item.id)" (dataChanged)="reloadStream(item.id)"></app-series-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #onDeck let-stream: DashboardStream>
|
||||
<ng-container *ngIf="(stream.api | async) as data">
|
||||
<app-carousel-reel [items]="data" [title]="t('on-deck-title')" (sectionClick)="handleSectionClick('on deck')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" [isOnDeck]="true"
|
||||
(reload)="reloadInProgress($event)" (dataChanged)="reloadInProgress($event)"></app-series-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
(reload)="reloadStream(stream.id)" (dataChanged)="reloadStream(stream.id)"></app-series-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
||||
<app-carousel-reel [items]="recentlyUpdatedSeries" [title]="t('recently-updated-title')" (sectionClick)="handleSectionClick('recently updated series')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<ng-template #recentlyUpdated let-stream: DashboardStream>
|
||||
<ng-container *ngIf="(stream.api | async) as data">
|
||||
<app-carousel-reel [items]="data" [title]="t('recently-updated-title')" (sectionClick)="handleSectionClick('recently updated series')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-card-item [entity]="item" [title]="item.seriesName" [suppressLibraryLink]="libraryId !== 0" [imageUrl]="imageService.getSeriesCoverImage(item.seriesId)"
|
||||
[suppressArchiveWarning]="true" (clicked)="handleRecentlyAddedChapterClick(item)" [count]="item.count"></app-card-item>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
[suppressArchiveWarning]="true" (clicked)="handleRecentlyAddedChapterClick(item)" [count]="item.count"></app-card-item>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #newlyUpdated let-stream: DashboardStream>
|
||||
<ng-container *ngIf="(stream.api | async) as data">
|
||||
<app-carousel-reel [items]="data" [title]="t('recently-added-title')" (sectionClick)="handleSectionClick('newly added series')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" (dataChanged)="reloadStream(stream.id)"></app-series-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #moreInGenre let-stream: DashboardStream>
|
||||
<ng-container *ngIf="(stream.api | async) as data">
|
||||
<app-carousel-reel [items]="data" [title]="t('more-in-genre-title', {genre: genre?.title})" (sectionClick)="handleSectionClick('more in genre')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" (dataChanged)="reloadStream(stream.id)"></app-series-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
||||
<app-carousel-reel [items]="recentlyAddedSeries" [title]="t('recently-added-title')" (sectionClick)="handleSectionClick('newly added series')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" (dataChanged)="loadRecentlyAddedSeries()"></app-series-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</ng-container>
|
||||
|
||||
<app-loading [loading]="isLoadingDashboard"></app-loading>
|
||||
</ng-container>
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, Input, OnInit} from '@angular/core';
|
||||
import {Title} from '@angular/platform-browser';
|
||||
import {Router, RouterLink} from '@angular/router';
|
||||
import {Observable, of, ReplaySubject} from 'rxjs';
|
||||
import {debounceTime, map, shareReplay, take, tap} from 'rxjs/operators';
|
||||
import {Observable, of, ReplaySubject, Subject, switchMap} from 'rxjs';
|
||||
import {map, shareReplay, take, tap, throttleTime} from 'rxjs/operators';
|
||||
import {FilterUtilitiesService} from 'src/app/shared/_services/filter-utilities.service';
|
||||
import {SeriesAddedEvent} from 'src/app/_models/events/series-added-event';
|
||||
import {SeriesRemovedEvent} from 'src/app/_models/events/series-removed-event';
|
||||
import {Library} from 'src/app/_models/library';
|
||||
import {RecentlyAddedItem} from 'src/app/_models/recently-added-item';
|
||||
import {Series} from 'src/app/_models/series';
|
||||
import {SortField} from 'src/app/_models/metadata/series-filter';
|
||||
import {SeriesGroup} from 'src/app/_models/series-group';
|
||||
import {AccountService} from 'src/app/_services/account.service';
|
||||
import {ImageService} from 'src/app/_services/image.service';
|
||||
import {LibraryService} from 'src/app/_services/library.service';
|
||||
|
@ -20,13 +17,21 @@ import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
|||
import {CardItemComponent} from '../../cards/card-item/card-item.component';
|
||||
import {SeriesCardComponent} from '../../cards/series-card/series-card.component';
|
||||
import {CarouselReelComponent} from '../../carousel/_components/carousel-reel/carousel-reel.component';
|
||||
import {AsyncPipe, NgIf} from '@angular/common';
|
||||
import {AsyncPipe, NgForOf, NgIf, NgSwitch, NgSwitchCase, NgTemplateOutlet} from '@angular/common';
|
||||
import {
|
||||
SideNavCompanionBarComponent
|
||||
} from '../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
|
||||
import {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {translate, TranslocoDirective} from "@ngneat/transloco";
|
||||
import {FilterField} from "../../_models/metadata/v2/filter-field";
|
||||
import {FilterComparison} from "../../_models/metadata/v2/filter-comparison";
|
||||
import {DashboardService} from "../../_services/dashboard.service";
|
||||
import {MetadataService} from "../../_services/metadata.service";
|
||||
import {RecommendationService} from "../../_services/recommendation.service";
|
||||
import {Genre} from "../../_models/metadata/genre";
|
||||
import {DashboardStream} from "../../_models/dashboard/dashboard-stream";
|
||||
import {StreamType} from "../../_models/dashboard/stream-type.enum";
|
||||
import {SeriesRemovedEvent} from "../../_models/events/series-removed-event";
|
||||
import {LoadingComponent} from "../../shared/loading/loading.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
|
@ -34,7 +39,8 @@ import {FilterComparison} from "../../_models/metadata/v2/filter-comparison";
|
|||
styleUrls: ['./dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [SideNavCompanionBarComponent, NgIf, RouterLink, CarouselReelComponent, SeriesCardComponent, CardItemComponent, AsyncPipe, TranslocoDirective]
|
||||
imports: [SideNavCompanionBarComponent, NgIf, RouterLink, CarouselReelComponent, SeriesCardComponent,
|
||||
CardItemComponent, AsyncPipe, TranslocoDirective, NgSwitchCase, NgSwitch, NgForOf, NgTemplateOutlet, LoadingComponent]
|
||||
})
|
||||
export class DashboardComponent implements OnInit {
|
||||
|
||||
|
@ -44,13 +50,14 @@ export class DashboardComponent implements OnInit {
|
|||
@Input() libraryId: number = 0;
|
||||
|
||||
libraries$: Observable<Library[]> = of([]);
|
||||
isLoading = true;
|
||||
|
||||
isLoadingAdmin = true;
|
||||
isLoadingDashboard = true;
|
||||
isAdmin$: Observable<boolean> = of(false);
|
||||
|
||||
recentlyUpdatedSeries: SeriesGroup[] = [];
|
||||
inProgress: Series[] = [];
|
||||
recentlyAddedSeries: Series[] = [];
|
||||
streams: Array<DashboardStream> = [];
|
||||
genre: Genre | undefined;
|
||||
refreshStreams$ = new Subject<void>();
|
||||
|
||||
|
||||
/**
|
||||
* We use this Replay subject to slow the amount of times we reload the UI
|
||||
|
@ -58,112 +65,133 @@ export class DashboardComponent implements OnInit {
|
|||
private loadRecentlyAdded$: ReplaySubject<void> = new ReplaySubject<void>();
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly filterUtilityService = inject(FilterUtilitiesService);
|
||||
private readonly metadataService = inject(MetadataService);
|
||||
private readonly recommendationService = inject(RecommendationService);
|
||||
protected readonly StreamType = StreamType;
|
||||
|
||||
|
||||
|
||||
constructor(public accountService: AccountService, private libraryService: LibraryService,
|
||||
private seriesService: SeriesService, private router: Router,
|
||||
private titleService: Title, public imageService: ImageService,
|
||||
private messageHub: MessageHubService, private readonly cdRef: ChangeDetectorRef) {
|
||||
|
||||
this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(res => {
|
||||
if (res.event === EVENTS.SeriesAdded) {
|
||||
const seriesAddedEvent = res.payload as SeriesAddedEvent;
|
||||
private messageHub: MessageHubService, private readonly cdRef: ChangeDetectorRef,
|
||||
private dashboardService: DashboardService) {
|
||||
|
||||
|
||||
this.seriesService.getSeries(seriesAddedEvent.seriesId).subscribe(series => {
|
||||
if (this.recentlyAddedSeries.filter(s => s.id === series.id).length > 0) return;
|
||||
this.recentlyAddedSeries = [series, ...this.recentlyAddedSeries];
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
} else if (res.event === EVENTS.SeriesRemoved) {
|
||||
const seriesRemovedEvent = res.payload as SeriesRemovedEvent;
|
||||
this.loadDashboard();
|
||||
|
||||
this.inProgress = this.inProgress.filter(item => item.id != seriesRemovedEvent.seriesId);
|
||||
this.recentlyAddedSeries = this.recentlyAddedSeries.filter(item => item.id != seriesRemovedEvent.seriesId);
|
||||
this.recentlyUpdatedSeries = this.recentlyUpdatedSeries.filter(item => item.seriesId != seriesRemovedEvent.seriesId);
|
||||
this.cdRef.markForCheck();
|
||||
} else if (res.event === EVENTS.ScanSeries) {
|
||||
// We don't have events for when series are updated, but we do get events when a scan update occurs. Refresh recentlyAdded at that time.
|
||||
this.loadRecentlyAdded$.next();
|
||||
}
|
||||
});
|
||||
this.refreshStreams$.pipe(takeUntilDestroyed(this.destroyRef), throttleTime(10_000),
|
||||
tap(() => {
|
||||
this.loadDashboard()
|
||||
}))
|
||||
.subscribe();
|
||||
|
||||
this.isAdmin$ = this.accountService.currentUser$.pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
map(user => (user && this.accountService.hasAdminRole(user)) || false),
|
||||
shareReplay()
|
||||
);
|
||||
|
||||
this.loadRecentlyAdded$.pipe(debounceTime(1000), takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
||||
this.loadRecentlyUpdated();
|
||||
this.loadRecentlyAddedSeries();
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
// TODO: Solve how Websockets will work with these dyanamic streams
|
||||
this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(res => {
|
||||
|
||||
if (res.event === EVENTS.DashboardUpdate) {
|
||||
console.log('dashboard update triggered')
|
||||
this.refreshStreams$.next();
|
||||
} else if (res.event === EVENTS.SeriesAdded) {
|
||||
// const seriesAddedEvent = res.payload as SeriesAddedEvent;
|
||||
|
||||
// this.seriesService.getSeries(seriesAddedEvent.seriesId).subscribe(series => {
|
||||
// if (this.recentlyAddedSeries.filter(s => s.id === series.id).length > 0) return;
|
||||
// this.recentlyAddedSeries = [series, ...this.recentlyAddedSeries];
|
||||
// this.cdRef.markForCheck();
|
||||
// });
|
||||
this.refreshStreams$.next();
|
||||
} else if (res.event === EVENTS.SeriesRemoved) {
|
||||
//const seriesRemovedEvent = res.payload as SeriesRemovedEvent;
|
||||
|
||||
//
|
||||
// this.inProgress = this.inProgress.filter(item => item.id != seriesRemovedEvent.seriesId);
|
||||
// this.recentlyAddedSeries = this.recentlyAddedSeries.filter(item => item.id != seriesRemovedEvent.seriesId);
|
||||
// this.recentlyUpdatedSeries = this.recentlyUpdatedSeries.filter(item => item.seriesId != seriesRemovedEvent.seriesId);
|
||||
// this.cdRef.markForCheck();
|
||||
this.refreshStreams$.next();
|
||||
} else if (res.event === EVENTS.ScanSeries) {
|
||||
// We don't have events for when series are updated, but we do get events when a scan update occurs. Refresh recentlyAdded at that time.
|
||||
this.loadRecentlyAdded$.next();
|
||||
this.refreshStreams$.next();
|
||||
}
|
||||
});
|
||||
|
||||
this.isAdmin$ = this.accountService.currentUser$.pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
map(user => (user && this.accountService.hasAdminRole(user)) || false),
|
||||
shareReplay({bufferSize: 1, refCount: true})
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.titleService.setTitle('Kavita - Dashboard');
|
||||
this.isLoading = true;
|
||||
this.titleService.setTitle('Kavita');
|
||||
this.isLoadingAdmin = true;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.libraries$ = this.libraryService.getLibraries().pipe(take(1), takeUntilDestroyed(this.destroyRef), tap((libs) => {
|
||||
this.isLoading = false;
|
||||
this.isLoadingAdmin = false;
|
||||
this.cdRef.markForCheck();
|
||||
}));
|
||||
|
||||
this.reloadSeries();
|
||||
}
|
||||
|
||||
reloadSeries() {
|
||||
this.loadOnDeck();
|
||||
this.loadRecentlyUpdated();
|
||||
this.loadRecentlyAddedSeries();
|
||||
}
|
||||
|
||||
reloadInProgress(series: Series | number) {
|
||||
this.loadOnDeck();
|
||||
}
|
||||
|
||||
loadOnDeck() {
|
||||
let api = this.seriesService.getOnDeck(0, 1, 30);
|
||||
if (this.libraryId > 0) {
|
||||
api = this.seriesService.getOnDeck(this.libraryId, 1, 30);
|
||||
}
|
||||
api.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((updatedSeries) => {
|
||||
this.inProgress = updatedSeries.result;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
loadRecentlyAddedSeries() {
|
||||
let api = this.seriesService.getRecentlyAdded(1, 30);
|
||||
if (this.libraryId > 0) {
|
||||
const filter = this.filterUtilityService.createSeriesV2Filter();
|
||||
filter.statements.push({field: FilterField.Libraries, value: this.libraryId + '', comparison: FilterComparison.Equal});
|
||||
api = this.seriesService.getRecentlyAdded(1, 30, filter);
|
||||
}
|
||||
api.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((updatedSeries) => {
|
||||
this.recentlyAddedSeries = updatedSeries.result;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
loadRecentlyUpdated() {
|
||||
let api = this.seriesService.getRecentlyUpdatedSeries();
|
||||
if (this.libraryId > 0) {
|
||||
api = this.seriesService.getRecentlyUpdatedSeries();
|
||||
}
|
||||
api.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(updatedSeries => {
|
||||
this.recentlyUpdatedSeries = updatedSeries.filter(group => {
|
||||
if (this.libraryId === 0) return true;
|
||||
return group.libraryId === this.libraryId;
|
||||
loadDashboard() {
|
||||
this.isLoadingDashboard = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.dashboardService.getDashboardStreams().subscribe(streams => {
|
||||
this.streams = streams;
|
||||
this.streams.forEach(s => {
|
||||
switch (s.streamType) {
|
||||
case StreamType.OnDeck:
|
||||
s.api = this.seriesService.getOnDeck(0, 1, 20)
|
||||
.pipe(map(d => d.result), takeUntilDestroyed(this.destroyRef), shareReplay({bufferSize: 1, refCount: true}));
|
||||
break;
|
||||
case StreamType.NewlyAdded:
|
||||
s.api = this.seriesService.getRecentlyAdded(1, 20)
|
||||
.pipe(map(d => d.result), takeUntilDestroyed(this.destroyRef), shareReplay({bufferSize: 1, refCount: true}));
|
||||
break;
|
||||
case StreamType.RecentlyUpdated:
|
||||
s.api = this.seriesService.getRecentlyUpdatedSeries();
|
||||
break;
|
||||
case StreamType.SmartFilter:
|
||||
s.api = this.seriesService.getAllSeriesV2(0, 20, this.filterUtilityService.decodeSeriesFilter(s.smartFilterEncoded!))
|
||||
.pipe(map(d => d.result), takeUntilDestroyed(this.destroyRef), shareReplay({bufferSize: 1, refCount: true}));
|
||||
break;
|
||||
case StreamType.MoreInGenre:
|
||||
s.api = this.metadataService.getAllGenres().pipe(
|
||||
map(genres => {
|
||||
this.genre = genres[Math.floor(Math.random() * genres.length)];
|
||||
return this.genre;
|
||||
}),
|
||||
switchMap(genre => this.recommendationService.getMoreIn(0, genre.id, 0, 30)),
|
||||
map(p => p.result),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
shareReplay({bufferSize: 1, refCount: true})
|
||||
);
|
||||
break;
|
||||
}
|
||||
});
|
||||
this.isLoadingDashboard = false;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
handleRecentlyAddedChapterClick(item: RecentlyAddedItem) {
|
||||
this.router.navigate(['library', item.libraryId, 'series', item.seriesId]);
|
||||
reloadStream(streamId: number) {
|
||||
const index = this.streams.findIndex(s => s.id === streamId);
|
||||
if (index < 0) return;
|
||||
this.streams[index] = {...this.streams[index]};
|
||||
console.log('swapped out stream: ', this.streams[index]);
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
|
||||
async handleRecentlyAddedChapterClick(item: RecentlyAddedItem) {
|
||||
await this.router.navigate(['library', item.libraryId, 'series', item.seriesId]);
|
||||
}
|
||||
|
||||
async handleFilterSectionClick(stream: DashboardStream) {
|
||||
await this.router.navigateByUrl('all-series?' + stream.smartFilterEncoded);
|
||||
}
|
||||
|
||||
handleSectionClick(sectionTitle: string) {
|
||||
|
@ -180,7 +208,7 @@ export class DashboardComponent implements OnInit {
|
|||
} else if (sectionTitle.toLowerCase() === 'on deck') {
|
||||
const params: any = {};
|
||||
params['page'] = 1;
|
||||
params['title'] = 'On Deck';
|
||||
params['title'] = translate('dashboard.on-deck-title');
|
||||
|
||||
const filter = this.filterUtilityService.createSeriesV2Filter();
|
||||
filter.statements.push({field: FilterField.ReadProgress, comparison: FilterComparison.GreaterThan, value: '0'});
|
||||
|
@ -190,16 +218,23 @@ export class DashboardComponent implements OnInit {
|
|||
filter.sortOptions.isAscending = false;
|
||||
}
|
||||
this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params)
|
||||
}else if (sectionTitle.toLowerCase() === 'newly added series') {
|
||||
} else if (sectionTitle.toLowerCase() === 'newly added series') {
|
||||
const params: any = {};
|
||||
params['page'] = 1;
|
||||
params['title'] = 'Newly Added';
|
||||
params['title'] = translate('dashboard.recently-added-title');
|
||||
const filter = this.filterUtilityService.createSeriesV2Filter();
|
||||
if (filter.sortOptions) {
|
||||
filter.sortOptions.sortField = SortField.Created;
|
||||
filter.sortOptions.isAscending = false;
|
||||
}
|
||||
this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params)
|
||||
} else if (sectionTitle.toLowerCase() === 'more in genre') {
|
||||
const params: any = {};
|
||||
params['page'] = 1;
|
||||
params['title'] = translate('more-in-genre-title', {genre: this.genre?.title});
|
||||
const filter = this.filterUtilityService.createSeriesV2Filter();
|
||||
filter.statements.push({field: FilterField.Genres, value: this.genre?.id + '', comparison: FilterComparison.MustContains});
|
||||
this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,45 +4,26 @@
|
|||
<app-card-actionables [actions]="actions" (actionHandler)="performAction($event)"></app-card-actionables>
|
||||
<span>{{libraryName}}</span>
|
||||
</h2>
|
||||
<div main>
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav nav-pills" style="flex-wrap: nowrap;">
|
||||
<li *ngFor="let tab of tabs" [ngbNavItem]="tab">
|
||||
<a ngbNavLink>
|
||||
<span class="d-none d-sm-flex align-items-center"><i class="fa {{tab.icon}} me-1" style="padding-right: 5px;" aria-hidden="true"></i> {{t('library-detail.' + tab.title) | sentenceCase}}</span>
|
||||
<span class="d-flex d-sm-none">
|
||||
<i class="fa {{tab.icon}}" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container *ngIf="tab.title === 'recommended-tab'">
|
||||
<app-library-recommended [libraryId]="libraryId"></app-library-recommended>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="tab.title === 'library-tab'">
|
||||
<app-card-detail-layout
|
||||
[isLoading]="loadingSeries"
|
||||
[items]="series"
|
||||
[pagination]="pagination"
|
||||
[filterSettings]="filterSettings"
|
||||
[trackByIdentity]="trackByIdentity"
|
||||
[filterOpen]="filterOpen"
|
||||
[jumpBarKeys]="jumpKeys"
|
||||
[refresh]="refresh"
|
||||
(applyFilter)="updateFilter($event)"
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
<app-series-card [data]="item" [libraryId]="libraryId" [suppressLibraryLink]="true" (reload)="loadPage()"
|
||||
(selection)="bulkSelectionService.handleCardSelection('series', position, series.length, $event)"
|
||||
[selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true"></app-series-card>
|
||||
</ng-template>
|
||||
</app-card-detail-layout>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h6 subtitle class="subtitle-with-actionables" *ngIf="active.fragment === ''">{{t('common.series-count', {num: pagination.totalItems | number})}} </h6>
|
||||
</app-side-nav-companion-bar>
|
||||
<h6 subtitle class="subtitle-with-actionables" *ngIf="active.fragment === ''">{{t('common.series-count', {num: pagination.totalItems | number})}} </h6>
|
||||
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
|
||||
<div [ngbNavOutlet]="nav"></div>
|
||||
<app-card-detail-layout
|
||||
[isLoading]="loadingSeries"
|
||||
[items]="series"
|
||||
[pagination]="pagination"
|
||||
[filterSettings]="filterSettings"
|
||||
[trackByIdentity]="trackByIdentity"
|
||||
[filterOpen]="filterOpen"
|
||||
[jumpBarKeys]="jumpKeys"
|
||||
[refresh]="refresh"
|
||||
(applyFilter)="updateFilter($event)"
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
<app-series-card [data]="item" [libraryId]="libraryId" [suppressLibraryLink]="true" (reload)="loadPage()"
|
||||
(selection)="bulkSelectionService.handleCardSelection('series', position, series.length, $event)"
|
||||
[selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true"></app-series-card>
|
||||
</ng-template>
|
||||
</app-card-detail-layout>
|
||||
|
||||
|
||||
</ng-container>
|
||||
|
|
|
@ -33,7 +33,6 @@ import {SentenceCasePipe} from '../pipe/sentence-case.pipe';
|
|||
import {BulkOperationsComponent} from '../cards/bulk-operations/bulk-operations.component';
|
||||
import {SeriesCardComponent} from '../cards/series-card/series-card.component';
|
||||
import {CardDetailLayoutComponent} from '../cards/card-detail-layout/card-detail-layout.component';
|
||||
import {LibraryRecommendedComponent} from './library-recommended/library-recommended.component';
|
||||
import {DecimalPipe, NgFor, NgIf} from '@angular/common';
|
||||
import {NgbNav, NgbNavContent, NgbNavItem, NgbNavItemRole, NgbNavLink, NgbNavOutlet} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {
|
||||
|
@ -52,7 +51,8 @@ import {CardActionablesComponent} from "../_single-module/card-actionables/card-
|
|||
styleUrls: ['./library-detail.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [SideNavCompanionBarComponent, CardActionablesComponent, NgbNav, NgFor, NgbNavItem, NgbNavItemRole, NgbNavLink, NgbNavContent, NgIf, LibraryRecommendedComponent, CardDetailLayoutComponent, SeriesCardComponent, BulkOperationsComponent, NgbNavOutlet, DecimalPipe, SentenceCasePipe, TranslocoDirective]
|
||||
imports: [SideNavCompanionBarComponent, CardActionablesComponent, NgbNav, NgFor, NgbNavItem, NgbNavItemRole, NgbNavLink, NgbNavContent, NgIf
|
||||
, CardDetailLayoutComponent, SeriesCardComponent, BulkOperationsComponent, NgbNavOutlet, DecimalPipe, SentenceCasePipe, TranslocoDirective]
|
||||
})
|
||||
export class LibraryDetailComponent implements OnInit {
|
||||
|
||||
|
@ -284,9 +284,5 @@ export class LibraryDetailComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
seriesClicked(series: Series) {
|
||||
this.router.navigate(['library', this.libraryId, 'series', series.id]);
|
||||
}
|
||||
|
||||
trackByIdentity = (index: number, item: Series) => `${item.id}_${item.name}_${item.localizedName}_${item.pagesRead}`;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common';
|
|||
import { LibraryDetailComponent } from './library-detail.component';
|
||||
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { LibraryDetailRoutingModule } from './library-detail-routing.module';
|
||||
import { LibraryRecommendedComponent } from './library-recommended/library-recommended.component';
|
||||
|
||||
import {SentenceCasePipe} from "../pipe/sentence-case.pipe";
|
||||
import {CardDetailLayoutComponent} from "../cards/card-detail-layout/card-detail-layout.component";
|
||||
|
@ -27,7 +26,7 @@ import {CardActionablesComponent} from "../_single-module/card-actionables/card-
|
|||
SeriesCardComponent,
|
||||
BulkOperationsComponent,
|
||||
SideNavCompanionBarComponent,
|
||||
LibraryDetailComponent, LibraryRecommendedComponent
|
||||
LibraryDetailComponent,
|
||||
]
|
||||
})
|
||||
export class LibraryDetailModule { }
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
<ng-container *transloco="let t; read: 'library-recommended'">
|
||||
|
||||
<ng-container *ngIf="all$ | async as all">
|
||||
<p *ngIf="all.length === 0">
|
||||
{{t('no-data')}}
|
||||
</p>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="onDeck$ | async as onDeck">
|
||||
<app-carousel-reel [items]="onDeck" [title]="t('on-deck')">
|
||||
<ng-template #carouselItem let-item let-position="idx">
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" (reload)="reloadInProgress($event)" (dataChanged)="reloadInProgress($event)"></app-series-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="quickReads$ | async as quickReads">
|
||||
<app-carousel-reel [items]="quickReads" [title]="t('quick-reads')">
|
||||
<ng-template #carouselItem let-item let-position="idx">
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" (reload)="reloadInProgress($event)" (dataChanged)="reloadInProgress($event)"></app-series-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="quickCatchups$ | async as quickCatchups">
|
||||
<app-carousel-reel [items]="quickCatchups" [title]="t('quick-catchups')">
|
||||
<ng-template #carouselItem let-item let-position="idx">
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" (reload)="reloadInProgress($event)" (dataChanged)="reloadInProgress($event)"></app-series-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="highlyRated$ | async as highlyRated">
|
||||
<app-carousel-reel [items]="highlyRated" [title]="t('highly-rated')">
|
||||
<ng-template #carouselItem let-item let-position="idx">
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" (reload)="reloadInProgress($event)" (dataChanged)="reloadInProgress($event)"></app-series-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="rediscover$ | async as rediscover">
|
||||
<app-carousel-reel [items]="rediscover" [title]="t('rediscover')">
|
||||
<ng-template #carouselItem let-item let-position="idx">
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" (reload)="reloadInProgress($event)" (dataChanged)="reloadInProgress($event)"></app-series-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="genre$ | async as genre">
|
||||
<ng-container *ngIf="moreIn$ | async as moreIn">
|
||||
<ng-container *ngIf="moreIn.length > 1">
|
||||
<app-carousel-reel [items]="moreIn" [title]="t('more-in-genre', {genre: genre.title})">
|
||||
<ng-template #carouselItem let-item let-position="idx">
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" (reload)="reloadInProgress($event)" (dataChanged)="reloadInProgress($event)"></app-series-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
|
@ -1,91 +0,0 @@
|
|||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
DestroyRef,
|
||||
inject,
|
||||
Input,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { filter, map, merge, Observable, shareReplay } from 'rxjs';
|
||||
import { Genre } from 'src/app/_models/metadata/genre';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { MetadataService } from 'src/app/_services/metadata.service';
|
||||
import { RecommendationService } from 'src/app/_services/recommendation.service';
|
||||
import { SeriesService } from 'src/app/_services/series.service';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import { SeriesCardComponent } from '../../cards/series-card/series-card.component';
|
||||
import { CarouselReelComponent } from '../../carousel/_components/carousel-reel/carousel-reel.component';
|
||||
import { NgIf, AsyncPipe } from '@angular/common';
|
||||
import {TranslocoDirective} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-library-recommended',
|
||||
templateUrl: './library-recommended.component.html',
|
||||
styleUrls: ['./library-recommended.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgIf, CarouselReelComponent, SeriesCardComponent, AsyncPipe, TranslocoDirective]
|
||||
})
|
||||
export class LibraryRecommendedComponent implements OnInit {
|
||||
|
||||
@Input() libraryId: number = 0;
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
quickReads$!: Observable<Series[]>;
|
||||
quickCatchups$!: Observable<Series[]>;
|
||||
highlyRated$!: Observable<Series[]>;
|
||||
onDeck$!: Observable<Series[]>;
|
||||
rediscover$!: Observable<Series[]>;
|
||||
moreIn$!: Observable<Series[]>;
|
||||
genre$!: Observable<Genre>;
|
||||
|
||||
all$!: Observable<any>;
|
||||
|
||||
constructor(private recommendationService: RecommendationService, private seriesService: SeriesService,
|
||||
private metadataService: MetadataService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.quickReads$ = this.recommendationService.getQuickReads(this.libraryId, 0, 30)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef), map(p => p.result), shareReplay());
|
||||
|
||||
this.quickCatchups$ = this.recommendationService.getQuickCatchupReads(this.libraryId, 0, 30)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef), map(p => p.result), shareReplay());
|
||||
|
||||
this.highlyRated$ = this.recommendationService.getHighlyRated(this.libraryId, 0, 30)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef), map(p => p.result), shareReplay());
|
||||
|
||||
this.rediscover$ = this.recommendationService.getRediscover(this.libraryId, 0, 30)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef), map(p => p.result), shareReplay());
|
||||
|
||||
this.onDeck$ = this.seriesService.getOnDeck(this.libraryId, 0, 30)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef), map(p => p.result), shareReplay());
|
||||
|
||||
this.genre$ = this.metadataService.getAllGenres([this.libraryId]).pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
map(genres => genres[Math.floor(Math.random() * genres.length)]),
|
||||
shareReplay()
|
||||
);
|
||||
this.genre$.subscribe(genre => {
|
||||
this.moreIn$ = this.recommendationService.getMoreIn(this.libraryId, genre.id, 0, 30).pipe(takeUntilDestroyed(this.destroyRef), map(p => p.result), shareReplay());
|
||||
});
|
||||
|
||||
this.all$ = merge(this.quickReads$, this.quickCatchups$, this.highlyRated$, this.rediscover$, this.onDeck$, this.genre$).pipe(takeUntilDestroyed(this.destroyRef));
|
||||
}
|
||||
|
||||
|
||||
reloadInProgress(series: Series | number) {
|
||||
if (Number.isInteger(series)) {
|
||||
if (!series) {return;}
|
||||
}
|
||||
// If the update to Series doesn't affect the requirement to be in this stream, then ignore update request
|
||||
const seriesObj = (series as Series);
|
||||
if (seriesObj.pagesRead !== seriesObj.pages && seriesObj.pagesRead !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.quickReads$ = this.quickReads$.pipe(filter(series => !series.includes(seriesObj)));
|
||||
this.quickCatchups$ = this.quickCatchups$.pipe(filter(series => !series.includes(seriesObj)));
|
||||
}
|
||||
|
||||
}
|
|
@ -22,6 +22,9 @@
|
|||
<ng-container *ngSwitchCase="PredicateType.Number">
|
||||
<input type="number" inputmode="numeric" class="form-control me-2" formControlName="filterValue" min="0">
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="PredicateType.Boolean">
|
||||
<input type="checkbox" class="form-check-input mt-2 me-2" style="font-size: 1.5rem" formControlName="filterValue">
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="PredicateType.Dropdown">
|
||||
<ng-container *ngIf="dropdownOptions$ | async as opts">
|
||||
<ng-container *ngTemplateOutlet="dropdown; context: { options: opts, multipleAllowed: MultipleDropdownAllowed }"></ng-container>
|
||||
|
|
|
@ -30,6 +30,7 @@ enum PredicateType {
|
|||
Text = 1,
|
||||
Number = 2,
|
||||
Dropdown = 3,
|
||||
Boolean = 4
|
||||
}
|
||||
|
||||
const StringFields = [FilterField.SeriesName, FilterField.Summary, FilterField.Path, FilterField.FilePath];
|
||||
|
@ -41,6 +42,7 @@ const DropdownFields = [FilterField.PublicationStatus, FilterField.Languages, Fi
|
|||
FilterField.Writers, FilterField.Genres, FilterField.Libraries,
|
||||
FilterField.Formats, FilterField.CollectionTags, FilterField.Tags
|
||||
];
|
||||
const BooleanFields = [FilterField.WantToRead]
|
||||
|
||||
const DropdownFieldsWithoutMustContains = [
|
||||
FilterField.Libraries, FilterField.Formats, FilterField.AgeRating, FilterField.PublicationStatus
|
||||
|
@ -69,6 +71,9 @@ const DropdownComparisons = [FilterComparison.Equal,
|
|||
FilterComparison.Contains,
|
||||
FilterComparison.NotContains,
|
||||
FilterComparison.MustContains];
|
||||
const BooleanComparisons = [
|
||||
FilterComparison.Equal
|
||||
]
|
||||
|
||||
@Component({
|
||||
selector: 'app-metadata-row-filter',
|
||||
|
@ -155,7 +160,11 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||
stmt.value = stmt.value + '';
|
||||
}
|
||||
|
||||
if (!stmt.value && stmt.field !== FilterField.SeriesName) return;
|
||||
if (typeof stmt.value === 'boolean') {
|
||||
stmt.value = stmt.value + '';
|
||||
}
|
||||
|
||||
if (!stmt.value && (stmt.field !== FilterField.SeriesName && !BooleanFields.includes(stmt.field))) return;
|
||||
this.filterStatement.emit(stmt);
|
||||
});
|
||||
|
||||
|
@ -172,6 +181,8 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||
|
||||
if (StringFields.includes(this.preset.field)) {
|
||||
this.formGroup.get('filterValue')?.patchValue(val);
|
||||
} else if (BooleanFields.includes(this.preset.field)) {
|
||||
this.formGroup.get('filterValue')?.patchValue(val);
|
||||
} else if (DropdownFields.includes(this.preset.field)) {
|
||||
if (this.MultipleDropdownAllowed || val.includes(',')) {
|
||||
this.formGroup.get('filterValue')?.patchValue(val.split(',').map(d => parseInt(d, 10)));
|
||||
|
@ -270,6 +281,16 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||
return;
|
||||
}
|
||||
|
||||
if (BooleanFields.includes(inputVal)) {
|
||||
this.validComparisons$.next(BooleanComparisons);
|
||||
this.predicateType$.next(PredicateType.Boolean);
|
||||
|
||||
if (this.loaded) {
|
||||
this.formGroup.get('filterValue')?.patchValue(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (DropdownFields.includes(inputVal)) {
|
||||
let comps = [...DropdownComparisons];
|
||||
if (DropdownFieldsThatIncludeNumberComparisons.includes(inputVal)) {
|
||||
|
|
|
@ -62,6 +62,8 @@ export class FilterFieldPipe implements PipeTransform {
|
|||
return translate('filter-field-pipe.path');
|
||||
case FilterField.FilePath:
|
||||
return translate('filter-field-pipe.file-path');
|
||||
case FilterField.WantToRead:
|
||||
return translate('filter-field-pipe.want-to-read');
|
||||
default:
|
||||
throw new Error(`Invalid FilterField value: ${value}`);
|
||||
}
|
||||
|
|
|
@ -7,4 +7,5 @@ export class FilterSettings {
|
|||
* The number of statements that can be on the filter. Set to 1 to disable adding more.
|
||||
*/
|
||||
statementLimit: number = 0;
|
||||
saveDisabled: boolean = false;
|
||||
}
|
||||
|
|
|
@ -48,6 +48,11 @@
|
|||
<option *ngFor="let field of allSortFields" [value]="field">{{field | sortField}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-10">
|
||||
<label for="filter-name" class="form-label">{{t('filter-name-label')}}</label>
|
||||
<input id="filter-name" type="text" class="form-control" formControlName="name">
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="utilityService.getActiveBreakpoint() > Breakpoint.Tablet" [ngTemplateOutlet]="buttons"></ng-container>
|
||||
</div>
|
||||
<div class="row mb-3" *ngIf="utilityService.getActiveBreakpoint() <= Breakpoint.Tablet">
|
||||
|
@ -58,12 +63,18 @@
|
|||
</ng-template>
|
||||
<ng-template #buttons>
|
||||
<!-- TODO: I might want to put a Clear button which blanks out the whole filter -->
|
||||
<div class="col-md-2 col-sm-6 mt-4">
|
||||
<div class="col-md-1 col-sm-6 mt-4 pt-1">
|
||||
<button class="btn btn-secondary col-12" (click)="clear()">{{t('reset')}}</button>
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-6 mt-4">
|
||||
<div class="col-md-1 col-sm-6 mt-4 pt-1">
|
||||
<button class="btn btn-primary col-12" (click)="apply()">{{t('apply')}}</button>
|
||||
</div>
|
||||
<div class="col-md-1 col-sm-6 mt-4 pt-1">
|
||||
<button class="btn btn-primary col-12" (click)="save()" [disabled]="filterSettings.saveDisabled || !this.sortGroup.get('name')?.value">
|
||||
<!-- TODO: Icon here -->
|
||||
{{t('save')}}
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
|
|
|
@ -11,8 +11,7 @@ import {
|
|||
Output
|
||||
} from '@angular/core';
|
||||
import {FormControl, FormGroup, FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {NgbCollapse, NgbRating, NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {FilterUtilitiesService} from '../shared/_services/filter-utilities.service';
|
||||
import {NgbCollapse, NgbModal, NgbRating, NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {Breakpoint, UtilityService} from '../shared/_services/utility.service';
|
||||
import {Library} from '../_models/library';
|
||||
import {allSortFields, FilterEvent, FilterItem, SortField} from '../_models/metadata/series-filter';
|
||||
|
@ -23,10 +22,14 @@ import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
|||
import {TypeaheadComponent} from '../typeahead/_components/typeahead.component';
|
||||
import {DrawerComponent} from '../shared/drawer/drawer.component';
|
||||
import {AsyncPipe, NgForOf, NgIf, NgTemplateOutlet} from '@angular/common';
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
import {translate, TranslocoModule} from "@ngneat/transloco";
|
||||
import {SortFieldPipe} from "../pipe/sort-field.pipe";
|
||||
import {MetadataBuilderComponent} from "./_components/metadata-builder/metadata-builder.component";
|
||||
import {allFields} from "../_models/metadata/v2/filter-field";
|
||||
import {MetadataService} from "../_services/metadata.service";
|
||||
import {FilterUtilitiesService} from "../shared/_services/filter-utilities.service";
|
||||
import {FilterService} from "../_services/filter.service";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
|
||||
@Component({
|
||||
selector: 'app-metadata-filter',
|
||||
|
@ -81,9 +84,10 @@ export class MetadataFilterComponent implements OnInit {
|
|||
|
||||
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
|
||||
|
||||
constructor(public toggleService: ToggleService) {}
|
||||
constructor(public toggleService: ToggleService, private filterService: FilterService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.filterSettings === undefined) {
|
||||
|
@ -141,7 +145,8 @@ export class MetadataFilterComponent implements OnInit {
|
|||
|
||||
this.sortGroup = new FormGroup({
|
||||
sortField: new FormControl({value: this.filterV2?.sortOptions?.sortField || SortField.SortName, disabled: this.filterSettings.sortDisabled}, []),
|
||||
limitTo: new FormControl(this.filterV2?.limitTo || 0, [])
|
||||
limitTo: new FormControl(this.filterV2?.limitTo || 0, []),
|
||||
name: new FormControl(this.filterV2?.name || '', [])
|
||||
});
|
||||
|
||||
this.sortGroup.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
||||
|
@ -153,6 +158,7 @@ export class MetadataFilterComponent implements OnInit {
|
|||
}
|
||||
this.filterV2!.sortOptions!.sortField = parseInt(this.sortGroup.get('sortField')?.value, 10);
|
||||
this.filterV2!.limitTo = Math.max(parseInt(this.sortGroup.get('limitTo')?.value || '0', 10), 0);
|
||||
this.filterV2!.name = this.sortGroup.get('name')?.value || '';
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
|
@ -190,6 +196,15 @@ export class MetadataFilterComponent implements OnInit {
|
|||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
save() {
|
||||
if (!this.filterV2) return;
|
||||
this.filterV2.name = this.sortGroup.get('name')?.value;
|
||||
this.filterService.saveFilter(this.filterV2).subscribe(() => {
|
||||
this.toastr.success(translate('toasts.smart-filter-updated'));
|
||||
this.apply();
|
||||
})
|
||||
}
|
||||
|
||||
toggleSelected() {
|
||||
this.toggleService.toggle();
|
||||
this.cdRef.markForCheck();
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
|
||||
<button class="btn btn-icon float-end" (click)="removeItem(item, i)" *ngIf="showRemoveButton">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
<span class="visually-hidden" attr.aria-labelledby="item.id--{{i}}">{{t('remove-item')}}</span>
|
||||
<span class="visually-hidden" attr.aria-labelledby="item.id--{{i}}">{{t('remove-item-alt')}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,28 +1,27 @@
|
|||
.example-list {
|
||||
min-width: 500px;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-height: 60px;
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
.example-box {
|
||||
margin: 5px 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
max-height: 140px;
|
||||
height: 140px;
|
||||
|
||||
.drag-handle {
|
||||
cursor: move;
|
||||
font-size: 24px;
|
||||
// TODO: This needs to be calculation based
|
||||
margin-top: 215%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.cdk-drag-preview {
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
|
@ -30,19 +29,20 @@
|
|||
0 8px 10px 1px rgba(0, 0, 0, 0.14),
|
||||
0 3px 14px 2px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
|
||||
.cdk-drag-placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
.cdk-drag-animating {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
|
||||
.example-box:last-child {
|
||||
border: none;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
.example-list.cdk-drop-list-dragging .example-box:not(.cdk-drag-placeholder) {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
@ -70,4 +70,4 @@
|
|||
|
||||
virtual-scroller.empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -178,7 +178,7 @@ export class ReadingListDetailComponent implements OnInit {
|
|||
|
||||
orderUpdated(event: IndexUpdateEvent) {
|
||||
if (!this.readingList) return;
|
||||
this.readingListService.updatePosition(this.readingList.id, event.item.id, event.fromPosition, event.toPosition).subscribe(() => { /* No Operation */ });
|
||||
this.readingListService.updatePosition(this.readingList.id, event.item.id, event.fromPosition, event.toPosition).subscribe();
|
||||
}
|
||||
|
||||
itemRemoved(item: ReadingListItem, position: number) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<ng-container *transloco="let t; read: 'reading-list-item'">
|
||||
<div class="d-flex flex-row g-0 mb-2">
|
||||
<div class="d-flex flex-row g-0 mb-2 reading-list-item">
|
||||
<div class="pe-2">
|
||||
<app-image width="106px" maxHeight="125px" class="img-top me-3" [imageUrl]="imageService.getChapterCoverImage(item.chapterId)"></app-image>
|
||||
<ng-container *ngIf="item.pagesRead === 0 && item.pagesTotal > 0">
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
$image-height: 125px;
|
||||
|
||||
.reading-list-item {
|
||||
max-height: 140px;
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.progress-banner {
|
||||
height: 5px;
|
||||
|
||||
|
@ -9,12 +14,6 @@ $image-height: 125px;
|
|||
}
|
||||
}
|
||||
|
||||
.list-item-container {
|
||||
background: var(--card-list-item-bg-color);
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.badge-container {
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
|
@ -34,4 +33,4 @@ $image-height: 125px;
|
|||
border-style: solid;
|
||||
border-width: 0 var(--card-progress-triangle-size) var(--card-progress-triangle-size) 0;
|
||||
border-color: transparent var(--primary-color) transparent transparent;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -107,6 +107,49 @@ export class FilterUtilitiesService {
|
|||
}).join(','));
|
||||
}
|
||||
|
||||
decodeSeriesFilter(encodedFilter: string) {
|
||||
const filter = this.metadataService.createDefaultFilterDto();
|
||||
|
||||
if (encodedFilter.includes('name=')) {
|
||||
filter.name = decodeURIComponent(encodedFilter).split('name=')[1].split('&')[0];
|
||||
}
|
||||
|
||||
const stmtsStartIndex = encodedFilter.indexOf(statementsKey);
|
||||
let endIndex = encodedFilter.indexOf('&' + sortOptionsKey);
|
||||
if (endIndex < 0) {
|
||||
endIndex = encodedFilter.indexOf('&' + limitToKey);
|
||||
}
|
||||
|
||||
if (stmtsStartIndex !== -1 || endIndex !== -1) {
|
||||
// +1 is for the =
|
||||
const stmtsEncoded = encodedFilter.substring(stmtsStartIndex + statementsKey.length, endIndex);
|
||||
filter.statements = this.decodeFilterStatements(stmtsEncoded);
|
||||
}
|
||||
|
||||
if (encodedFilter.includes(sortOptionsKey)) {
|
||||
const optionsStartIndex = encodedFilter.indexOf('&' + sortOptionsKey);
|
||||
const endIndex = encodedFilter.indexOf('&' + limitToKey);
|
||||
const sortOptionsEncoded = encodedFilter.substring(optionsStartIndex + sortOptionsKey.length + 1, endIndex);
|
||||
const sortOptions = this.decodeSortOptions(sortOptionsEncoded);
|
||||
if (sortOptions) {
|
||||
filter.sortOptions = sortOptions;
|
||||
}
|
||||
}
|
||||
|
||||
if (encodedFilter.includes(limitToKey)) {
|
||||
const limitTo = decodeURIComponent(encodedFilter).split(limitToKey)[1].split('&')[0];
|
||||
filter.limitTo = parseInt(limitTo, 10);
|
||||
}
|
||||
|
||||
if (encodedFilter.includes(combinationKey)) {
|
||||
const combo = decodeURIComponent(encodedFilter).split(combinationKey)[1].split('&')[0];;
|
||||
filter.combination = parseInt(combo, 10) as FilterCombination;
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
|
||||
filterPresetsFromUrlV2(snapshot: ActivatedRouteSnapshot): SeriesFilterV2 {
|
||||
const filter = this.metadataService.createDefaultFilterDto();
|
||||
if (!window.location.href.includes('?')) return filter;
|
||||
|
|
|
@ -2,4 +2,8 @@
|
|||
width: 100%;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
<ng-container *transloco="let t; read: 'customize-dashboard-modal'">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{t('title')}}</h4>
|
||||
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<app-draggable-ordered-list [items]="items" (orderUpdated)="orderUpdated($event)" [accessibilityMode]="accessibilityMode"
|
||||
[showRemoveButton]="false">
|
||||
<ng-template #draggableItem let-position="idx" let-item>
|
||||
<app-stream-list-item [item]="item" [position]="position" (hide)="updateVisibility($event, position)"></app-stream-list-item>
|
||||
</ng-template>
|
||||
</app-draggable-ordered-list>
|
||||
|
||||
<h5>Smart Filters</h5>
|
||||
<ul class="list-group filter-list">
|
||||
<li class="filter list-group-item" *ngFor="let filter of smartFilters">
|
||||
{{filter.name}}
|
||||
<button class="btn btn-icon" (click)="addFilterToStream(filter)">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
Add
|
||||
</button>
|
||||
</li>
|
||||
<li class="list-group-item" *ngIf="smartFilters.length === 0">
|
||||
All Smart filters added to Dashboard or none created yet.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" (click)="close()">{{t('close')}}</button>
|
||||
</div>
|
||||
</ng-container>
|
|
@ -0,0 +1,24 @@
|
|||
::ng-deep .drag-handle {
|
||||
margin-top: 100% !important;
|
||||
}
|
||||
|
||||
app-stream-list-item {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.filter-list {
|
||||
margin: 0;
|
||||
padding:0;
|
||||
|
||||
.filter {
|
||||
padding: 0.5rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
margin: 5px 0;
|
||||
color: var(--list-group-hover-text-color);
|
||||
background-color: var(--list-group-hover-bg-color);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {SafeHtmlPipe} from "../../../pipe/safe-html.pipe";
|
||||
import {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {
|
||||
DraggableOrderedListComponent,
|
||||
IndexUpdateEvent
|
||||
} from "../../../reading-list/_components/draggable-ordered-list/draggable-ordered-list.component";
|
||||
import {
|
||||
ReadingListItemComponent
|
||||
} from "../../../reading-list/_components/reading-list-item/reading-list-item.component";
|
||||
import {forkJoin} from "rxjs";
|
||||
import {FilterService} from "../../../_services/filter.service";
|
||||
import {StreamListItemComponent} from "../stream-list-item/stream-list-item.component";
|
||||
import {SmartFilter} from "../../../_models/metadata/v2/smart-filter";
|
||||
import {DashboardService} from "../../../_services/dashboard.service";
|
||||
import {DashboardStream} from "../../../_models/dashboard/dashboard-stream";
|
||||
|
||||
@Component({
|
||||
selector: 'app-customize-dashboard-modal',
|
||||
standalone: true,
|
||||
imports: [CommonModule, SafeHtmlPipe, TranslocoDirective, DraggableOrderedListComponent, ReadingListItemComponent, StreamListItemComponent],
|
||||
templateUrl: './customize-dashboard-modal.component.html',
|
||||
styleUrls: ['./customize-dashboard-modal.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CustomizeDashboardModalComponent {
|
||||
|
||||
items: DashboardStream[] = [];
|
||||
smartFilters: SmartFilter[] = [];
|
||||
accessibilityMode: boolean = false;
|
||||
|
||||
private readonly dashboardService = inject(DashboardService);
|
||||
private readonly filterService = inject(FilterService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
|
||||
constructor(public modal: NgbActiveModal) {
|
||||
|
||||
forkJoin([this.dashboardService.getDashboardStreams(false), this.filterService.getAllFilters()]).subscribe(results => {
|
||||
this.items = results[0];
|
||||
const smartFilterStreams = new Set(results[0].filter(d => !d.isProvided).map(d => d.name));
|
||||
this.smartFilters = results[1].filter(d => !smartFilterStreams.has(d.name));
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
addFilterToStream(filter: SmartFilter) {
|
||||
this.dashboardService.createDashboardStream(filter.id).subscribe(stream => {
|
||||
this.smartFilters = this.smartFilters.filter(d => d.name !== filter.name);
|
||||
this.items.push(stream);
|
||||
this.cdRef.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
orderUpdated(event: IndexUpdateEvent) {
|
||||
this.dashboardService.updateDashboardStreamPosition(event.item.name, event.item.id, event.fromPosition, event.toPosition).subscribe();
|
||||
}
|
||||
|
||||
updateVisibility(item: DashboardStream, position: number) {
|
||||
this.items[position].visible = !this.items[position].visible;
|
||||
this.dashboardService.updateDashboardStream(this.items[position]).subscribe();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
close() {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,18 +1,21 @@
|
|||
<ng-container *transloco="let t; read: 'side-nav'">
|
||||
<div class="side-nav" [ngClass]="{'closed' : (navService.sideNavCollapsed$ | async), 'hidden': (navService.sideNavVisibility$ | async) === false, 'no-donate': (accountService.hasValidLicense$ | async) === true}" *ngIf="accountService.currentUser$ | async as user">
|
||||
<!-- <app-side-nav-item icon="fa-user-circle align-self-center phone-hidden" [title]="user.username | sentenceCase" link="/preferences/">
|
||||
<ng-container actions>
|
||||
Todo: This will be customize dashboard/side nav controls
|
||||
<a href="/preferences/" title="User Settings"><span class="visually-hidden">User Settings</span></a>
|
||||
</ng-container>
|
||||
</app-side-nav-item> -->
|
||||
<!-- <app-side-nav-item icon="fa-user-circle align-self-center phone-hidden" [title]="user.username | sentenceCase" link="/preferences/">-->
|
||||
<!-- <ng-container actions>-->
|
||||
<!-- <a href="/preferences/" title="User Settings"><span class="visually-hidden">User Settings</span></a>-->
|
||||
<!-- </ng-container>-->
|
||||
<!-- </app-side-nav-item>-->
|
||||
|
||||
<app-side-nav-item icon="fa-home" [title]="t('home')" link="/libraries/"></app-side-nav-item>
|
||||
<app-side-nav-item icon="fa-home" [title]="t('home')" link="/libraries/">
|
||||
<ng-container actions>
|
||||
<app-card-actionables [actions]="homeActions" [labelBy]="t('reading-lists')" iconClass="fa-ellipsis-v" (actionHandler)="handleHomeActions()"></app-card-actionables>
|
||||
</ng-container>
|
||||
</app-side-nav-item>
|
||||
<app-side-nav-item icon="fa-star" [title]="t('want-to-read')" link="/want-to-read/"></app-side-nav-item>
|
||||
<app-side-nav-item icon="fa-list" [title]="t('collections')" link="/collections/"></app-side-nav-item>
|
||||
<app-side-nav-item icon="fa-list-ol" [title]="t('reading-lists')" link="/lists/">
|
||||
<ng-container actions>
|
||||
<app-card-actionables [actions]="readingListActions" labelBy="t('reading-lists')" iconClass="fa-ellipsis-v" (actionHandler)="importCbl()"></app-card-actionables>
|
||||
<app-card-actionables [actions]="readingListActions" [labelBy]="t('reading-lists')" iconClass="fa-ellipsis-v" (actionHandler)="importCbl()"></app-card-actionables>
|
||||
</ng-container>
|
||||
</app-side-nav-item>
|
||||
<app-side-nav-item icon="fa-bookmark" [title]="t('bookmarks')" link="/bookmarks/"></app-side-nav-item>
|
||||
|
|
|
@ -27,11 +27,13 @@ import {FilterPipe} from "../../../pipe/filter.pipe";
|
|||
import {FormsModule} from "@angular/forms";
|
||||
import {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {CardActionablesComponent} from "../../../_single-module/card-actionables/card-actionables.component";
|
||||
import {SentenceCasePipe} from "../../../pipe/sentence-case.pipe";
|
||||
import {CustomizeDashboardModalComponent} from "../customize-dashboard-modal/customize-dashboard-modal.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-side-nav',
|
||||
standalone: true,
|
||||
imports: [CommonModule, SideNavItemComponent, CardActionablesComponent, FilterPipe, FormsModule, TranslocoDirective],
|
||||
imports: [CommonModule, SideNavItemComponent, CardActionablesComponent, FilterPipe, FormsModule, TranslocoDirective, SentenceCasePipe],
|
||||
templateUrl: './side-nav.component.html',
|
||||
styleUrls: ['./side-nav.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
@ -43,6 +45,7 @@ export class SideNavComponent implements OnInit {
|
|||
libraries: Library[] = [];
|
||||
actions: ActionItem<Library>[] = [];
|
||||
readingListActions = [{action: Action.Import, title: 'import-cbl', children: [], requiresAdmin: true, callback: this.importCbl.bind(this)}];
|
||||
homeActions = [{action: Action.Edit, title: 'customize', children: [], requiresAdmin: false, callback: this.handleHomeActions.bind(this)}];
|
||||
filterQuery: string = '';
|
||||
filterLibrary = (library: Library) => {
|
||||
return library.name.toLowerCase().indexOf((this.filterQuery || '').toLowerCase()) >= 0;
|
||||
|
@ -107,6 +110,12 @@ export class SideNavComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
handleHomeActions() {
|
||||
this.ngbModal.open(CustomizeDashboardModalComponent, {size: 'xl'});
|
||||
// TODO: If on /, then refresh the page layout
|
||||
}
|
||||
|
||||
|
||||
importCbl() {
|
||||
this.ngbModal.open(ImportCblModalComponent, {size: 'xl'});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<ng-container *transloco="let t; read: 'stream-list-item'">
|
||||
<div class="row pt-2 g-0 list-item">
|
||||
<div class="g-0">
|
||||
<h5 class="mb-1 pb-0" id="item.id--{{position}}">
|
||||
{{item.name}}
|
||||
<span class="float-end">
|
||||
<button class="btn btn-icon p-0" (click)="hide.emit(item)">
|
||||
<i class="me-1" [ngClass]="{'fas fa-eye': item.visible, 'fa-solid fa-eye-slash': !item.visible}" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('remove')}}</span>
|
||||
</button>
|
||||
</span>
|
||||
</h5>
|
||||
<div class="meta">
|
||||
<div class="ps-1">
|
||||
{{t(item.isProvided ? 'provided' : 'smart-filter')}}
|
||||
</div>
|
||||
<div class="ps-1" *ngIf="!item.isProvided">
|
||||
<a [href]="'/all-series?' + this.item.smartFilterEncoded" target="_blank">{{t('load-filter')}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
|
@ -0,0 +1,8 @@
|
|||
.list-item {
|
||||
height: 60px;
|
||||
max-height: 60px;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {ImageComponent} from "../../../shared/image/image.component";
|
||||
import {MangaFormatIconPipe} from "../../../pipe/manga-format-icon.pipe";
|
||||
import {MangaFormatPipe} from "../../../pipe/manga-format.pipe";
|
||||
import {NgbProgressbar} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {DashboardStream} from "../../../_models/dashboard/dashboard-stream";
|
||||
|
||||
@Component({
|
||||
selector: 'app-stream-list-item',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ImageComponent, MangaFormatIconPipe, MangaFormatPipe, NgbProgressbar, TranslocoDirective],
|
||||
templateUrl: './stream-list-item.component.html',
|
||||
styleUrls: ['./stream-list-item.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class StreamListItemComponent {
|
||||
@Input({required: true}) item!: DashboardStream;
|
||||
@Input({required: true}) position: number = 0;
|
||||
@Output() hide: EventEmitter<DashboardStream> = new EventEmitter<DashboardStream>();
|
||||
|
||||
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
|
||||
|
||||
}
|
|
@ -23,7 +23,7 @@ import {TopReadersComponent} from '../top-readers/top-readers.component';
|
|||
import {StatListComponent} from '../stat-list/stat-list.component';
|
||||
import {IconAndTitleComponent} from '../../../shared/icon-and-title/icon-and-title.component';
|
||||
import {AsyncPipe, DecimalPipe, NgIf} from '@angular/common';
|
||||
import {TranslocoDirective, TranslocoService} from "@ngneat/transloco";
|
||||
import {translate, TranslocoDirective, TranslocoService} from "@ngneat/transloco";
|
||||
import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison";
|
||||
import {FilterField} from "../../../_models/metadata/v2/filter-field";
|
||||
|
||||
|
@ -62,8 +62,6 @@ export class ServerStatsComponent {
|
|||
this.breakpointSubject.next(this.utilityService.getActiveBreakpoint());
|
||||
}
|
||||
|
||||
|
||||
translocoService = inject(TranslocoService);
|
||||
get Breakpoint() { return Breakpoint; }
|
||||
|
||||
constructor(private statService: StatisticsService, private router: Router, private imageService: ImageService,
|
||||
|
@ -115,7 +113,7 @@ export class ServerStatsComponent {
|
|||
this.metadataService.getAllGenres().subscribe(genres => {
|
||||
const ref = this.modalService.open(GenericListModalComponent, { scrollable: true });
|
||||
ref.componentInstance.items = genres.map(t => t.title);
|
||||
ref.componentInstance.title = this.translocoService.translate('server-stats.genres');
|
||||
ref.componentInstance.title = translate('server-stats.genres');
|
||||
ref.componentInstance.clicked = (item: string) => {
|
||||
this.filterUtilityService.applyFilter(['all-series'], FilterField.Genres, FilterComparison.Contains, genres.filter(g => g.title === item)[0].id + '');
|
||||
};
|
||||
|
@ -126,7 +124,7 @@ export class ServerStatsComponent {
|
|||
this.metadataService.getAllTags().subscribe(tags => {
|
||||
const ref = this.modalService.open(GenericListModalComponent, { scrollable: true });
|
||||
ref.componentInstance.items = tags.map(t => t.title);
|
||||
ref.componentInstance.title = this.translocoService.translate('server-stats.tags');
|
||||
ref.componentInstance.title = translate('server-stats.tags');
|
||||
ref.componentInstance.clicked = (item: string) => {
|
||||
this.filterUtilityService.applyFilter(['all-series'], FilterField.Tags, FilterComparison.Contains, tags.filter(g => g.title === item)[0].id + '');
|
||||
};
|
||||
|
@ -137,7 +135,7 @@ export class ServerStatsComponent {
|
|||
this.metadataService.getAllPeople().subscribe(people => {
|
||||
const ref = this.modalService.open(GenericListModalComponent, { scrollable: true });
|
||||
ref.componentInstance.items = [...new Set(people.map(person => person.name))];
|
||||
ref.componentInstance.title = this.translocoService.translate('server-stats.people');
|
||||
ref.componentInstance.title = translate('server-stats.people');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {inject, Pipe, PipeTransform} from '@angular/core';
|
||||
import { DayOfWeek } from 'src/app/_services/statistics.service';
|
||||
import {TranslocoService} from "@ngneat/transloco";
|
||||
import {translate, TranslocoService} from "@ngneat/transloco";
|
||||
|
||||
@Pipe({
|
||||
name: 'dayOfWeek',
|
||||
|
@ -8,24 +8,22 @@ import {TranslocoService} from "@ngneat/transloco";
|
|||
})
|
||||
export class DayOfWeekPipe implements PipeTransform {
|
||||
|
||||
translocoService = inject(TranslocoService);
|
||||
|
||||
transform(value: DayOfWeek): string {
|
||||
switch(value) {
|
||||
case DayOfWeek.Monday:
|
||||
return this.translocoService.translate('day-of-week-pipe.monday');
|
||||
return translate('day-of-week-pipe.monday');
|
||||
case DayOfWeek.Tuesday:
|
||||
return this.translocoService.translate('day-of-week-pipe.tuesday');
|
||||
return translate('day-of-week-pipe.tuesday');
|
||||
case DayOfWeek.Wednesday:
|
||||
return this.translocoService.translate('day-of-week-pipe.wednesday');
|
||||
return translate('day-of-week-pipe.wednesday');
|
||||
case DayOfWeek.Thursday:
|
||||
return this.translocoService.translate('day-of-week-pipe.thursday');
|
||||
return translate('day-of-week-pipe.thursday');
|
||||
case DayOfWeek.Friday:
|
||||
return this.translocoService.translate('day-of-week-pipe.friday');
|
||||
return translate('day-of-week-pipe.friday');
|
||||
case DayOfWeek.Saturday:
|
||||
return this.translocoService.translate('day-of-week-pipe.saturday');
|
||||
return translate('day-of-week-pipe.saturday');
|
||||
case DayOfWeek.Sunday:
|
||||
return this.translocoService.translate('day-of-week-pipe.sunday');
|
||||
return translate('day-of-week-pipe.sunday');
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<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>
|
|
@ -0,0 +1,21 @@
|
|||
|
||||
ul {
|
||||
margin:0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
padding: 0.5rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
margin: 5px 0;
|
||||
color: var(--list-group-hover-text-color);
|
||||
background-color: var(--list-group-hover-bg-color);
|
||||
|
||||
span {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FilterService} from "../../_services/filter.service";
|
||||
import {SmartFilter} from "../../_models/metadata/v2/smart-filter";
|
||||
import {Router} from "@angular/router";
|
||||
import {ConfirmService} from "../../shared/confirm.service";
|
||||
import {translate} from "@ngneat/transloco";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-smart-filters',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './manage-smart-filters.component.html',
|
||||
styleUrls: ['./manage-smart-filters.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ManageSmartFiltersComponent {
|
||||
|
||||
private readonly filterService = inject(FilterService);
|
||||
private readonly confirmService = inject(ConfirmService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
filters: Array<SmartFilter> = [];
|
||||
|
||||
constructor() {
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
loadData() {
|
||||
this.filterService.getAllFilters().subscribe(filters => {
|
||||
this.filters = filters;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
async loadFilter(f: SmartFilter) {
|
||||
await this.router.navigateByUrl('all-series?' + f.filter);
|
||||
}
|
||||
|
||||
async deleteFilter(f: SmartFilter) {
|
||||
if (!await this.confirmService.confirm(translate('toasts.confirm-delete-smart-filter'))) return;
|
||||
|
||||
this.filterService.deleteFilter(f.id).subscribe(() => {
|
||||
this.toastr.success(translate('toasts.smart-filter-deleted'));
|
||||
this.loadData();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -428,6 +428,9 @@
|
|||
<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>
|
||||
|
|
|
@ -49,6 +49,7 @@ import { SideNavCompanionBarComponent } from '../../sidenav/_components/side-nav
|
|||
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";
|
||||
|
||||
enum AccordionPanelID {
|
||||
ImageReader = 'image-reader',
|
||||
|
@ -63,6 +64,7 @@ enum FragmentID {
|
|||
Theme = 'theme',
|
||||
Devices = 'devices',
|
||||
Stats = 'stats',
|
||||
SmartFilters = 'smart-filters',
|
||||
Scrobbling = 'scrobbling'
|
||||
|
||||
}
|
||||
|
@ -76,7 +78,8 @@ enum FragmentID {
|
|||
imports: [SideNavCompanionBarComponent, NgbNav, NgFor, NgbNavItem, NgbNavItemRole, NgbNavLink, RouterLink, NgbNavContent, NgIf, ChangeEmailComponent,
|
||||
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]
|
||||
ThemeManagerComponent, ManageDevicesComponent, UserStatsComponent, UserScrobbleHistoryComponent, UserHoldsComponent, NgbNavOutlet, TitleCasePipe, SentenceCasePipe,
|
||||
TranslocoDirective, ManageSmartFiltersComponent]
|
||||
})
|
||||
export class UserPreferencesComponent implements OnInit, OnDestroy {
|
||||
|
||||
|
@ -107,6 +110,7 @@ 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'}];
|
||||
|
@ -115,7 +119,6 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||
opdsUrl: string = '';
|
||||
makeUrl: (val: string) => string = (val: string) => { return this.opdsUrl; };
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly trasnlocoService = inject(TranslocoService);
|
||||
|
||||
get AccordionPanelID() {
|
||||
return AccordionPanelID;
|
||||
|
@ -304,7 +307,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||
};
|
||||
|
||||
this.observableHandles.push(this.accountService.updatePreferences(data).subscribe((updatedPrefs) => {
|
||||
this.toastr.success(this.trasnlocoService.translate('user-preferences.success-toast'));
|
||||
this.toastr.success(translate('user-preferences.success-toast'));
|
||||
if (this.user) {
|
||||
this.user.preferences = updatedPrefs;
|
||||
this.cdRef.markForCheck();
|
||||
|
|
|
@ -14,7 +14,8 @@
|
|||
"not-granted": "You haven't been granted access to any libraries.",
|
||||
"on-deck-title": "On Deck",
|
||||
"recently-updated-title": "Recently Updated Series",
|
||||
"recently-added-title": "Newly Added Series"
|
||||
"recently-added-title": "Newly Added Series",
|
||||
"more-in-genre-title": "More In {{genre}}"
|
||||
},
|
||||
|
||||
"edit-user": {
|
||||
|
@ -98,6 +99,7 @@
|
|||
"devices-tab": "Devices",
|
||||
"stats-tab": "Stats",
|
||||
"scrobbling-tab": "Scrobbling",
|
||||
"smart-filters-tab": "Smart Filters",
|
||||
"success-toast": "User preferences updated",
|
||||
|
||||
"global-settings-title": "Global Settings",
|
||||
|
@ -1324,6 +1326,13 @@
|
|||
"read": "{{common.read}}"
|
||||
},
|
||||
|
||||
"stream-list-item": {
|
||||
"remove": "{{common.remove}}",
|
||||
"load-filter": "Load Filter",
|
||||
"provided": "Provided",
|
||||
"smart-filter": "Smart Filter"
|
||||
},
|
||||
|
||||
"reading-list-detail": {
|
||||
"item-count": "{{common.item-count}}",
|
||||
"page-settings-title": "Page Settings",
|
||||
|
@ -1494,10 +1503,12 @@
|
|||
"metadata-filter": {
|
||||
"filter-title": "Filter",
|
||||
"sort-by-label": "Sort By",
|
||||
"filter-name-label": "Filter Name",
|
||||
"ascending-alt": "Ascending",
|
||||
"descending-alt": "Descending",
|
||||
"reset": "{{common.reset}}",
|
||||
"apply": "{{common.apply}}",
|
||||
"save": "{{common.save}}",
|
||||
"limit-label": "Limit To",
|
||||
|
||||
"format-label": "Format",
|
||||
|
@ -1707,6 +1718,12 @@
|
|||
"remove-rule": "Remove Row"
|
||||
},
|
||||
|
||||
"customize-dashboard-modal": {
|
||||
"title": "Customize Dashboard",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}"
|
||||
},
|
||||
|
||||
"filter-field-pipe": {
|
||||
"age-rating": "Age Rating",
|
||||
"characters": "Characters",
|
||||
|
@ -1733,7 +1750,8 @@
|
|||
"user-rating": "User Rating",
|
||||
"writers": "Writers",
|
||||
"path": "Path",
|
||||
"file-path": "File Path"
|
||||
"file-path": "File Path",
|
||||
"want-to-read": "Want to Read"
|
||||
},
|
||||
|
||||
"filter-comparison-pipe": {
|
||||
|
@ -1755,6 +1773,8 @@
|
|||
"must-contains": "Must Contains"
|
||||
},
|
||||
|
||||
|
||||
|
||||
"toasts": {
|
||||
"regen-cover": "A job has been enqueued to regenerate the cover image",
|
||||
"no-pages": "There are no pages. Kavita was not able to read this archive.",
|
||||
|
@ -1831,7 +1851,10 @@
|
|||
"confirm-library-delete": "Are you sure you want to delete the {{name}} library? You cannot undo this action.",
|
||||
"confirm-library-type-change": "Changing library type will trigger a new scan with different parsing rules and may lead to series being re-created and hence you may loose progress and bookmarks. You should backup before you do this. Are you sure you want to continue?",
|
||||
"confirm-download-size": "The {{entityType}} is {{size}}. Are you sure you want to continue?",
|
||||
"list-doesnt-exist": "This list doesn't exist"
|
||||
"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"
|
||||
},
|
||||
|
||||
"actionable": {
|
||||
|
@ -1861,8 +1884,8 @@
|
|||
"read": "Read",
|
||||
"add-rule-group-and": "Add Rule Group (AND)",
|
||||
"add-rule-group-or": "Add Rule Group (OR)",
|
||||
"remove-rule-group": "Remove Rule Group"
|
||||
|
||||
"remove-rule-group": "Remove Rule Group",
|
||||
"customize": "Customize"
|
||||
},
|
||||
|
||||
"preferences": {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue