More Bugfixes (#2989)
This commit is contained in:
parent
1ae723b405
commit
a3e020fe17
49 changed files with 579 additions and 272 deletions
|
@ -10,6 +10,7 @@ import { Volume } from '../_models/volume';
|
|||
import { AccountService } from './account.service';
|
||||
import { DeviceService } from './device.service';
|
||||
import {SideNavStream} from "../_models/sidenav/sidenav-stream";
|
||||
import {SmartFilter} from "../_models/metadata/v2/smart-filter";
|
||||
|
||||
export enum Action {
|
||||
Submenu = -1,
|
||||
|
@ -150,6 +151,7 @@ export class ActionFactoryService {
|
|||
bookmarkActions: Array<ActionItem<Series>> = [];
|
||||
|
||||
sideNavStreamActions: Array<ActionItem<SideNavStream>> = [];
|
||||
smartFilterActions: Array<ActionItem<SmartFilter>> = [];
|
||||
|
||||
isAdmin = false;
|
||||
|
||||
|
@ -178,6 +180,10 @@ export class ActionFactoryService {
|
|||
return this.applyCallbackToList(this.sideNavStreamActions, callback);
|
||||
}
|
||||
|
||||
getSmartFilterActions(callback: ActionCallback<SmartFilter>) {
|
||||
return this.applyCallbackToList(this.smartFilterActions, callback);
|
||||
}
|
||||
|
||||
getVolumeActions(callback: ActionCallback<Volume>) {
|
||||
return this.applyCallbackToList(this.volumeActions, callback);
|
||||
}
|
||||
|
@ -620,6 +626,16 @@ export class ActionFactoryService {
|
|||
children: [],
|
||||
},
|
||||
];
|
||||
|
||||
this.smartFilterActions = [
|
||||
{
|
||||
action: Action.Delete,
|
||||
title: 'delete',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false,
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private applyCallback(action: ActionItem<any>, callback: (action: ActionItem<any>, data: any) => void) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Injectable, OnDestroy } from '@angular/core';
|
||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { Subject } from 'rxjs';
|
||||
import {Subject, tap} from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { BulkAddToCollectionComponent } from '../cards/_modals/bulk-add-to-collection/bulk-add-to-collection.component';
|
||||
import { AddToListModalComponent, ADD_FLOW } from '../reading-list/_modals/add-to-list-modal/add-to-list-modal.component';
|
||||
|
@ -19,9 +19,11 @@ import { LibraryService } from './library.service';
|
|||
import { MemberService } from './member.service';
|
||||
import { ReaderService } from './reader.service';
|
||||
import { SeriesService } from './series.service';
|
||||
import {translate, TranslocoService} from "@ngneat/transloco";
|
||||
import {translate} from "@ngneat/transloco";
|
||||
import {UserCollection} from "../_models/collection-tag";
|
||||
import {CollectionTagService} from "./collection-tag.service";
|
||||
import {SmartFilter} from "../_models/metadata/v2/smart-filter";
|
||||
import {FilterService} from "./filter.service";
|
||||
|
||||
export type LibraryActionCallback = (library: Partial<Library>) => void;
|
||||
export type SeriesActionCallback = (series: Series) => void;
|
||||
|
@ -46,7 +48,7 @@ export class ActionService implements OnDestroy {
|
|||
constructor(private libraryService: LibraryService, private seriesService: SeriesService,
|
||||
private readerService: ReaderService, private toastr: ToastrService, private modalService: NgbModal,
|
||||
private confirmService: ConfirmService, private memberService: MemberService, private deviceService: DeviceService,
|
||||
private readonly collectionTagService: CollectionTagService) { }
|
||||
private readonly collectionTagService: CollectionTagService, private filterService: FilterService) { }
|
||||
|
||||
ngOnDestroy() {
|
||||
this.onDestroy.next();
|
||||
|
@ -655,4 +657,21 @@ export class ActionService implements OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
async deleteFilter(filterId: number, callback?: BooleanActionCallback) {
|
||||
if (!await this.confirmService.confirm(translate('toasts.confirm-delete-smart-filter'))) {
|
||||
if (callback) {
|
||||
callback(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.filterService.deleteFilter(filterId).subscribe(_ => {
|
||||
this.toastr.success(translate('toasts.smart-filter-deleted'));
|
||||
|
||||
if (callback) {
|
||||
callback(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
export interface ServerInfoSlim {
|
||||
kavitaVersion: string;
|
||||
installId: string;
|
||||
isDocker: boolean;
|
||||
kavitaVersion: string;
|
||||
installId: string;
|
||||
isDocker: boolean;
|
||||
firstInstallVersion?: string;
|
||||
firstInstallDate?: string;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,12 @@
|
|||
|
||||
<dt>{{t('installId-title')}}</dt>
|
||||
<dd>{{serverInfo.installId}}</dd>
|
||||
|
||||
<dt>{{t('first-install-version-title')}}</dt>
|
||||
<dd>{{serverInfo.firstInstallVersion | defaultValue}}</dd>
|
||||
|
||||
<dt>{{t('first-install-date-title')}}</dt>
|
||||
<dd>{{serverInfo.firstInstallDate | date:'shortDate'}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core';
|
||||
import {ServerService} from 'src/app/_services/server.service';
|
||||
import {ServerInfoSlim} from '../_models/server-info';
|
||||
import {NgIf} from '@angular/common';
|
||||
import {DatePipe, NgIf} from '@angular/common';
|
||||
import {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {ChangelogComponent} from "../../announcements/_components/changelog/changelog.component";
|
||||
import {DefaultDatePipe} from "../../_pipes/default-date.pipe";
|
||||
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-system',
|
||||
|
@ -11,18 +13,16 @@ import {ChangelogComponent} from "../../announcements/_components/changelog/chan
|
|||
styleUrls: ['./manage-system.component.scss'],
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [NgIf, TranslocoDirective, ChangelogComponent]
|
||||
imports: [NgIf, TranslocoDirective, ChangelogComponent, DefaultDatePipe, DefaultValuePipe, DatePipe]
|
||||
})
|
||||
export class ManageSystemComponent implements OnInit {
|
||||
|
||||
serverInfo!: ServerInfoSlim;
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly serverService = inject(ServerService);
|
||||
|
||||
|
||||
constructor(public serverService: ServerService) { }
|
||||
serverInfo!: ServerInfoSlim;
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.serverService.getServerInfo().subscribe(info => {
|
||||
this.serverInfo = info;
|
||||
this.cdRef.markForCheck();
|
||||
|
|
|
@ -3,32 +3,18 @@
|
|||
<h2 title>
|
||||
{{t('title')}}
|
||||
</h2>
|
||||
<h6 subtitle >{{t('count', {count: filters.length | number})}}</h6>
|
||||
<div subtitle>
|
||||
<h6>
|
||||
<span>{{t('count', {count: filters.length | number})}}</span>
|
||||
<a class="ms-2" href="/all-series?name=New%20Filter">{{t('create')}}</a>
|
||||
</h6>
|
||||
|
||||
</div>
|
||||
|
||||
</app-side-nav-companion-bar>
|
||||
<app-card-detail-layout
|
||||
[isLoading]="isLoading"
|
||||
[items]="filters"
|
||||
[trackByIdentity]="trackByIdentity"
|
||||
[jumpBarKeys]="jumpbarKeys"
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
<!-- TODO: figure a way to get a hover effect -->
|
||||
<div class="card-item-container card clickable" (click)="loadSmartFilter(item)">
|
||||
<div class="overlay filter">
|
||||
<div class="card-overlay"></div>
|
||||
<div class="overlay-information overlay-information--centered">
|
||||
<div class="position-relative">
|
||||
<span class="card-title library mx-auto" style="width: auto;">
|
||||
<i class="fa-solid fa-filter" style="font-size: 26px;" [ngClass]="{'error': isErrored(item)}" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<span class="card-title">{{item.name}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-card-detail-layout>
|
||||
|
||||
<app-manage-smart-filters></app-manage-smart-filters>
|
||||
|
||||
|
||||
|
||||
</ng-container>
|
||||
|
|
|
@ -1,16 +1,2 @@
|
|||
.card-title {
|
||||
width: 146px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.card, .overlay {
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
.card-item-container .overlay-information.overlay-information--centered {
|
||||
top: 54px;
|
||||
left: 51px;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, injec
|
|||
import {CommonModule} from '@angular/common';
|
||||
import {JumpKey} from "../_models/jumpbar/jump-key";
|
||||
import {EVENTS, Message, MessageHubService} from "../_services/message-hub.service";
|
||||
import {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {translate, TranslocoDirective} from "@ngneat/transloco";
|
||||
import {CardItemComponent} from "../cards/card-item/card-item.component";
|
||||
import {
|
||||
SideNavCompanionBarComponent
|
||||
|
@ -11,14 +11,20 @@ import {SmartFilter} from "../_models/metadata/v2/smart-filter";
|
|||
import {FilterService} from "../_services/filter.service";
|
||||
import {CardDetailLayoutComponent} from "../cards/card-detail-layout/card-detail-layout.component";
|
||||
import {SafeHtmlPipe} from "../_pipes/safe-html.pipe";
|
||||
import {Router} from "@angular/router";
|
||||
import {Router, RouterLink} from "@angular/router";
|
||||
import {Series} from "../_models/series";
|
||||
import {JumpbarService} from "../_services/jumpbar.service";
|
||||
import {Action, ActionFactoryService, ActionItem} from "../_services/action-factory.service";
|
||||
import {CardActionablesComponent} from "../_single-module/card-actionables/card-actionables.component";
|
||||
import {ActionService} from "../_services/action.service";
|
||||
import {FilterPipe} from "../_pipes/filter.pipe";
|
||||
import {filter} from "rxjs";
|
||||
import {ManageSmartFiltersComponent} from "../sidenav/_components/manage-smart-filters/manage-smart-filters.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-all-filters',
|
||||
standalone: true,
|
||||
imports: [CommonModule, TranslocoDirective, CardItemComponent, SideNavCompanionBarComponent, CardDetailLayoutComponent, SafeHtmlPipe],
|
||||
imports: [CommonModule, TranslocoDirective, CardItemComponent, SideNavCompanionBarComponent, CardDetailLayoutComponent, SafeHtmlPipe, CardActionablesComponent, RouterLink, FilterPipe, ManageSmartFiltersComponent],
|
||||
templateUrl: './all-filters.component.html',
|
||||
styleUrl: './all-filters.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
@ -28,13 +34,20 @@ export class AllFiltersComponent implements OnInit {
|
|||
private readonly jumpbarService = inject(JumpbarService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly filterService = inject(FilterService);
|
||||
private readonly actionFactory = inject(ActionFactoryService);
|
||||
private readonly actionService = inject(ActionService);
|
||||
|
||||
filterActions = this.actionFactory.getSmartFilterActions(this.handleAction.bind(this));
|
||||
jumpbarKeys: Array<JumpKey> = [];
|
||||
filters: SmartFilter[] = [];
|
||||
isLoading = true;
|
||||
trackByIdentity = (index: number, item: SmartFilter) => item.name;
|
||||
|
||||
ngOnInit() {
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
loadData() {
|
||||
this.filterService.getAllFilters().subscribe(filters => {
|
||||
this.filters = filters;
|
||||
this.jumpbarKeys = this.jumpbarService.getJumpKeys(this.filters, (s: Series) => s.name);
|
||||
|
@ -51,4 +64,23 @@ export class AllFiltersComponent implements OnInit {
|
|||
return !decodeURIComponent(filter.filter).includes('¦');
|
||||
}
|
||||
|
||||
async deleteFilter(filter: SmartFilter) {
|
||||
await this.actionService.deleteFilter(filter.id, success => {
|
||||
this.filters = this.filters.filter(f => f.id != filter.id);
|
||||
this.jumpbarKeys = this.jumpbarService.getJumpKeys(this.filters, (s: Series) => s.name);
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
async handleAction(action: ActionItem<SmartFilter>, filter: SmartFilter) {
|
||||
switch (action.action) {
|
||||
case(Action.Delete):
|
||||
await this.deleteFilter(filter);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly filter = filter;
|
||||
}
|
||||
|
|
|
@ -57,15 +57,18 @@
|
|||
@if (title.length > 0 || actions.length > 0) {
|
||||
<div class="card-body">
|
||||
<div>
|
||||
<span class="card-title" placement="top" id="{{title}}_{{entity.id}}" [ngbTooltip]="tooltipTitle" (click)="handleClick($event)" tabindex="0">
|
||||
<app-promoted-icon [promoted]="isPromoted()"></app-promoted-icon>
|
||||
<app-series-format [format]="format"></app-series-format>
|
||||
{{title}}
|
||||
</span>
|
||||
<span class="card-actions float-end" *ngIf="actions && actions.length > 0">
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="title"></app-card-actionables>
|
||||
</span>
|
||||
<span class="card-title" placement="top" id="{{title}}_{{entity.id}}" [ngbTooltip]="tooltipTitle" (click)="handleClick($event)" tabindex="0">
|
||||
<app-promoted-icon [promoted]="isPromoted()"></app-promoted-icon>
|
||||
<app-series-format [format]="format"></app-series-format>
|
||||
{{title}}
|
||||
</span>
|
||||
@if (actions && actions.length > 0) {
|
||||
<span class="card-actions float-end">
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="title"></app-card-actionables>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (subtitleTemplate) {
|
||||
<div style="text-align: center">
|
||||
<ng-container [ngTemplateOutlet]="subtitleTemplate" [ngTemplateOutletContext]="{ $implicit: entity }"></ng-container>
|
||||
|
|
|
@ -37,7 +37,7 @@ import {FormsModule} from "@angular/forms";
|
|||
import {MangaFormatPipe} from "../../_pipes/manga-format.pipe";
|
||||
import {MangaFormatIconPipe} from "../../_pipes/manga-format-icon.pipe";
|
||||
import {SentenceCasePipe} from "../../_pipes/sentence-case.pipe";
|
||||
import {CommonModule} from "@angular/common";
|
||||
import {DecimalPipe, NgTemplateOutlet} from "@angular/common";
|
||||
import {RouterLink, RouterLinkActive} from "@angular/router";
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component";
|
||||
|
@ -51,7 +51,6 @@ import {SeriesFormatComponent} from "../../shared/series-format/series-format.co
|
|||
selector: 'app-card-item',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ImageComponent,
|
||||
NgbProgressbar,
|
||||
DownloadIndicatorComponent,
|
||||
|
@ -66,7 +65,9 @@ import {SeriesFormatComponent} from "../../shared/series-format/series-format.co
|
|||
SafeHtmlPipe,
|
||||
RouterLinkActive,
|
||||
PromotedIconComponent,
|
||||
SeriesFormatComponent
|
||||
SeriesFormatComponent,
|
||||
DecimalPipe,
|
||||
NgTemplateOutlet
|
||||
],
|
||||
templateUrl: './card-item.component.html',
|
||||
styleUrls: ['./card-item.component.scss'],
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
width="16px" height="16px" [styles]="{'vertical-align': 'text-top'}"
|
||||
[ngbTooltip]="collectionTag.source | providerName" tabindex="0"></app-image>
|
||||
<span class="ms-2 me-2">{{t('sync-progress', {title: series.length + ' / ' + collectionTag.totalSourceCount})}}</span>
|
||||
<i class="fa-solid fa-question-circle" aria-hidden="true" [ngbTooltip]="t('last-sync', {date: collectionTag.lastSyncUtc | date: 'shortDate' | defaultDate })"></i>
|
||||
<i class="fa-solid fa-question-circle" aria-hidden="true" [ngbTooltip]="t('last-sync', {date: collectionTag.lastSyncUtc | date: 'short' | defaultDate })"></i>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -154,12 +154,17 @@
|
|||
</ul>
|
||||
}
|
||||
|
||||
@if (hasData && !includeChapterAndFiles) {
|
||||
<li class="list-group-item" style="min-height: 34px">
|
||||
@if (hasData && (isAdmin$ | async)) {
|
||||
<li class="list-group-item" style="min-height: 34px" (click)="$event.stopPropagation()">
|
||||
<ng-container [ngTemplateOutlet]="extraTemplate"></ng-container>
|
||||
<a href="javascript:void(0)" (click)="toggleIncludeFiles()" class="float-end">
|
||||
{{t('include-extras')}}
|
||||
</a>
|
||||
<form [formGroup]="searchSettingsForm">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="search-include-extras" role="switch" formControlName="includeExtras" class="form-check-input"
|
||||
aria-labelledby="auto-close-label" aria-describedby="tag-promoted-help">
|
||||
<label class="form-check-label" for="search-include-extras">{{t('include-extras')}}</label>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
|
|
@ -13,14 +13,16 @@ import {
|
|||
TemplateRef,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
|
||||
import {debounceTime, distinctUntilChanged} from 'rxjs/operators';
|
||||
import { KEY_CODES } from 'src/app/shared/_services/utility.service';
|
||||
import { SearchResultGroup } from 'src/app/_models/search/search-result-group';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import { NgClass, NgTemplateOutlet } from '@angular/common';
|
||||
import {AsyncPipe, NgClass, NgTemplateOutlet} from '@angular/common';
|
||||
import {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {LoadingComponent} from "../../../shared/loading/loading.component";
|
||||
import {map, startWith, tap} from "rxjs";
|
||||
import {AccountService} from "../../../_services/account.service";
|
||||
|
||||
export interface SearchEvent {
|
||||
value: string;
|
||||
|
@ -33,11 +35,12 @@ export interface SearchEvent {
|
|||
styleUrls: ['./grouped-typeahead.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [ReactiveFormsModule, NgClass, NgTemplateOutlet, TranslocoDirective, LoadingComponent]
|
||||
imports: [ReactiveFormsModule, NgClass, NgTemplateOutlet, TranslocoDirective, LoadingComponent, AsyncPipe]
|
||||
})
|
||||
export class GroupedTypeaheadComponent implements OnInit {
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly accountService = inject(AccountService);
|
||||
|
||||
/**
|
||||
* Unique id to tie with a label element
|
||||
|
@ -97,12 +100,15 @@ export class GroupedTypeaheadComponent implements OnInit {
|
|||
@ContentChild('bookmarkTemplate') bookmarkTemplate!: TemplateRef<any>;
|
||||
|
||||
|
||||
|
||||
hasFocus: boolean = false;
|
||||
typeaheadForm: FormGroup = new FormGroup({});
|
||||
includeChapterAndFiles: boolean = false;
|
||||
|
||||
prevSearchTerm: string = '';
|
||||
searchSettingsForm = new FormGroup(({'includeExtras': new FormControl(false)}));
|
||||
isAdmin$ = this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef), map(u => {
|
||||
if (!u) return false;
|
||||
return this.accountService.hasAdminRole(u);
|
||||
}));
|
||||
|
||||
get searchTerm() {
|
||||
return this.typeaheadForm.get('typeahead')?.value || '';
|
||||
|
@ -139,6 +145,17 @@ export class GroupedTypeaheadComponent implements OnInit {
|
|||
this.typeaheadForm.addControl('typeahead', new FormControl(this.initialValue, []));
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.searchSettingsForm.get('includeExtras')!.valueChanges.pipe(
|
||||
startWith(false),
|
||||
map(val => {
|
||||
if (val === null) return false;
|
||||
return val;
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
tap((val: boolean) => this.toggleIncludeFiles(val)),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe();
|
||||
|
||||
this.typeaheadForm.valueChanges.pipe(
|
||||
debounceTime(this.debounceTime),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
|
@ -183,13 +200,22 @@ export class GroupedTypeaheadComponent implements OnInit {
|
|||
this.selected.emit(item);
|
||||
}
|
||||
|
||||
toggleIncludeFiles() {
|
||||
this.includeChapterAndFiles = true;
|
||||
toggleIncludeFiles(val: boolean) {
|
||||
const firstRun = val === false && val === this.includeChapterAndFiles;
|
||||
|
||||
this.includeChapterAndFiles = val;
|
||||
this.inputChanged.emit({value: this.searchTerm, includeFiles: this.includeChapterAndFiles});
|
||||
|
||||
this.hasFocus = true;
|
||||
this.inputElem.nativeElement.focus();
|
||||
this.openDropdown();
|
||||
if (!firstRun) {
|
||||
this.hasFocus = true;
|
||||
if (this.inputElem && this.inputElem.nativeElement) {
|
||||
this.inputElem.nativeElement.focus();
|
||||
}
|
||||
|
||||
this.openDropdown();
|
||||
}
|
||||
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<form [formGroup]="form" *transloco="let t">
|
||||
|
||||
@for(item of Items; let i = $index; track item) {
|
||||
@for(item of Items; let i = $index; track item; let isFirst = $first) {
|
||||
<div class="row g-0 mb-3">
|
||||
<div class="col-lg-10 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
|
@ -13,7 +13,7 @@
|
|||
<i class="fa-solid fa-plus" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('common.add')}}</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary" (click)="remove(i)">
|
||||
<button class="btn btn-secondary" (click)="remove(i)" [disabled]="isFirst">
|
||||
<i class="fa-solid fa-xmark" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('common.remove')}}</span>
|
||||
</button>
|
||||
|
|
|
@ -1,25 +1,35 @@
|
|||
<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>
|
||||
@if (filters.length >= 3) {
|
||||
<div class="mb-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>
|
||||
</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>
|
||||
@for(f of filters | filter: filterList; track f.name) {
|
||||
<li class="list-group-item">
|
||||
<span>
|
||||
@if (isErrored(f)) {
|
||||
<i class="fa-solid fa-triangle-exclamation red me-2" [ngbTooltip]="t('errored')"></i>
|
||||
<span class="visually-hidden">{{t('errored')}}</span>
|
||||
}
|
||||
<a [href]="'all-series?' + f.filter" target="_blank">{{f.name}}</a>
|
||||
</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">{{t('delete')}}</span>
|
||||
</button>
|
||||
</li>
|
||||
} @empty {
|
||||
<li class="list-group-item">
|
||||
{{t('no-data')}}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</ng-container>
|
||||
|
|
|
@ -19,3 +19,7 @@ ul {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.red {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FilterService} from "../../../_services/filter.service";
|
||||
import {SmartFilter} from "../../../_models/metadata/v2/smart-filter";
|
||||
import {Router} from "@angular/router";
|
||||
import {ConfirmService} from "../../../shared/confirm.service";
|
||||
import {translate, TranslocoDirective} from "@ngneat/transloco";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms";
|
||||
import {FilterPipe} from "../../../_pipes/filter.pipe";
|
||||
import {ActionService} from "../../../_services/action.service";
|
||||
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-smart-filters',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule, TranslocoDirective, FilterPipe],
|
||||
imports: [ReactiveFormsModule, TranslocoDirective, FilterPipe, NgbTooltip],
|
||||
templateUrl: './manage-smart-filters.component.html',
|
||||
styleUrls: ['./manage-smart-filters.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
@ -20,10 +18,9 @@ import {FilterPipe} from "../../../_pipes/filter.pipe";
|
|||
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);
|
||||
private readonly actionService = inject(ActionService);
|
||||
|
||||
filters: Array<SmartFilter> = [];
|
||||
listForm: FormGroup = new FormGroup({
|
||||
'filterQuery': new FormControl('', [])
|
||||
|
@ -33,11 +30,6 @@ export class ManageSmartFiltersComponent {
|
|||
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();
|
||||
|
@ -50,15 +42,18 @@ export class ManageSmartFiltersComponent {
|
|||
});
|
||||
}
|
||||
|
||||
async loadFilter(f: SmartFilter) {
|
||||
await this.router.navigateByUrl('all-series?' + f.filter);
|
||||
resetFilter() {
|
||||
this.listForm.get('filterQuery')?.setValue('');
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
isErrored(filter: SmartFilter) {
|
||||
return !decodeURIComponent(filter.filter).includes('¦');
|
||||
}
|
||||
|
||||
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'));
|
||||
await this.actionService.deleteFilter(f.id, success => {
|
||||
if (!success) return;
|
||||
this.resetFilter();
|
||||
this.loadData();
|
||||
});
|
||||
|
|
|
@ -147,8 +147,10 @@ export class SideNavComponent implements OnInit {
|
|||
this.accountService.hasValidLicense$.subscribe(res =>{
|
||||
if (!res) return;
|
||||
|
||||
this.homeActions.push({action: Action.Import, title: 'import-mal-stack', children: [], requiresAdmin: true, callback: this.importMalCollection.bind(this)});
|
||||
this.cdRef.markForCheck();
|
||||
if (this.homeActions.filter(f => f.title === 'import-mal-stack').length === 0) {
|
||||
this.homeActions.push({action: Action.Import, title: 'import-mal-stack', children: [], requiresAdmin: true, callback: this.importMalCollection.bind(this)});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -573,7 +573,8 @@
|
|||
|
||||
"all-filters": {
|
||||
"title": "All Smart Filters",
|
||||
"count": "{{count}} {{customize-dashboard-modal.title-smart-filters}}"
|
||||
"count": "{{count}} {{customize-dashboard-modal.title-smart-filters}}",
|
||||
"create": "{{common.create}}"
|
||||
},
|
||||
|
||||
"announcements": {
|
||||
|
@ -1273,6 +1274,8 @@
|
|||
"version-title": "Version",
|
||||
"installId-title": "Install ID",
|
||||
"more-info-title": "More Info",
|
||||
"first-install-version-title": "First Install Version",
|
||||
"first-install-date-title": "First Install Date",
|
||||
"home-page-title": "Home page:",
|
||||
"wiki-title": "Wiki:",
|
||||
"discord-title": "Discord:",
|
||||
|
@ -2022,7 +2025,8 @@
|
|||
"delete": "{{common.delete}}",
|
||||
"no-data": "No Smart Filters created",
|
||||
"filter": "{{common.filter}}",
|
||||
"clear": "{{common.clear}}"
|
||||
"clear": "{{common.clear}}",
|
||||
"errored": "There is an encoding error in the filter. You need to recreate it."
|
||||
},
|
||||
|
||||
"edit-external-source-item": {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue