Lots of Bugfixes (#2977)

This commit is contained in:
Joe Milazzo 2024-06-04 17:43:15 -05:00 committed by GitHub
parent 8c629695ef
commit 616ed7a75d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 427 additions and 244 deletions

View file

@ -14,11 +14,11 @@ export class SearchService {
constructor(private httpClient: HttpClient) { }
search(term: string) {
search(term: string, includeChapterAndFiles: boolean = false) {
if (term === '') {
return of(new SearchResultGroup());
}
return this.httpClient.get<SearchResultGroup>(this.baseUrl + 'search/search?queryString=' + encodeURIComponent(term));
return this.httpClient.get<SearchResultGroup>(this.baseUrl + `search/search?includeChapterAndFiles=${includeChapterAndFiles}&queryString=${encodeURIComponent(term)}`);
}
getSeriesForMangaFile(mangaFileId: number) {

View file

@ -11,7 +11,6 @@ import {forkJoin} from "rxjs";
import {ToastrService} from "ngx-toastr";
import {DecimalPipe} from "@angular/common";
import {LoadingComponent} from "../../../shared/loading/loading.component";
import {AccountService} from "../../../_services/account.service";
import {ConfirmService} from "../../../shared/confirm.service";
@Component({

View file

@ -5,126 +5,166 @@
<input #input [id]="id" type="text" inputmode="search" autocomplete="off" formControlName="typeahead" [placeholder]="placeholder"
aria-haspopup="listbox" aria-owns="dropdown"
[attr.aria-expanded]="hasFocus && hasData"
aria-autocomplete="list" (focusout)="close($event)" (focus)="open($event)" role="search"
aria-autocomplete="list" (focusout)="close($event)" (focus)="open($event)" role="searchbox"
>
<div class="spinner-border spinner-border-sm" role="status" *ngIf="isLoading">
<span class="visually-hidden">{{t('loading')}}</span>
</div>
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="resetField()" *ngIf="typeaheadForm.get('typeahead')?.value.length > 0"></button>
@if (searchTerm.length > 0) {
@if (isLoading) {
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">{{t('loading')}}</span>
</div>
} @else {
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="resetField()"></button>
}
}
</div>
</div>
<div class="dropdown" *ngIf="hasFocus">
<ul class="list-group" role="listbox" id="dropdown">
<ng-container *ngIf="seriesTemplate !== undefined && groupedData.series.length > 0">
<li class="list-group-item section-header"><h5 id="series-group">Series</h5></li>
<ul class="list-group results" role="group" aria-describedby="series-group">
<li *ngFor="let option of groupedData.series; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" aria-labelledby="series-group" role="option">
<ng-container [ngTemplateOutlet]="seriesTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</ng-container>
@if (hasFocus) {
<div class="dropdown">
<ul class="list-group" role="listbox" id="dropdown">
<ng-container *ngIf="collectionTemplate !== undefined && groupedData.collections.length > 0">
<li class="list-group-item section-header"><h5>{{t('collections')}}</h5></li>
<ul class="list-group results">
<li *ngFor="let option of groupedData.collections; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="collectionTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</ng-container>
@if (seriesTemplate !== undefined && groupedData.series.length > 0) {
<li class="list-group-item section-header"><h5 id="series-group">Series</h5></li>
<ul class="list-group results" role="group" aria-describedby="series-group">
@for(option of groupedData.series; track option; let index = $index) {
<li (click)="handleResultClick(option)" tabindex="0"
class="list-group-item" aria-labelledby="series-group" role="option">
<ng-container [ngTemplateOutlet]="seriesTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
}
</ul>
}
<ng-container *ngIf="readingListTemplate !== undefined && groupedData.readingLists.length > 0">
<li class="list-group-item section-header"><h5>{{t('reading-lists')}}</h5></li>
<ul class="list-group results">
<li *ngFor="let option of groupedData.readingLists; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="readingListTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</ng-container>
<ng-container *ngIf="bookmarkTemplate !== undefined && groupedData.bookmarks.length > 0">
<li class="list-group-item section-header"><h5>{{t('bookmarks')}}</h5></li>
<ul class="list-group results">
<li *ngFor="let option of groupedData.bookmarks; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="bookmarkTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</ng-container>
@if (collectionTemplate !== undefined && groupedData.collections.length > 0) {
<li class="list-group-item section-header"><h5>{{t('collections')}}</h5></li>
<ul class="list-group results">
@for(option of groupedData.collections; track option; let index = $index) {
<li (click)="handleResultClick(option)" tabindex="0"
class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="collectionTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
}
</ul>
}
<ng-container *ngIf="libraryTemplate !== undefined && groupedData.libraries.length > 0">
<li class="list-group-item section-header"><h5 id="libraries-group">{{t('libraries')}}</h5></li>
<ul class="list-group results" role="group" aria-describedby="libraries-group">
<li *ngFor="let option of groupedData.libraries; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" aria-labelledby="libraries-group" role="option">
<ng-container [ngTemplateOutlet]="libraryTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</ng-container>
<ng-container *ngIf="genreTemplate !== undefined && groupedData.genres.length > 0">
<li class="list-group-item section-header"><h5>{{t('genres')}}</h5></li>
<ul class="list-group results">
<li *ngFor="let option of groupedData.genres; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="genreTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</ng-container>
@if (readingListTemplate !== undefined && groupedData.readingLists.length > 0) {
<li class="list-group-item section-header"><h5>{{t('reading-lists')}}</h5></li>
<ul class="list-group results">
@for(option of groupedData.readingLists; track option; let index = $index) {
<li (click)="handleResultClick(option)" tabindex="0"
class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="readingListTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
}
</ul>
}
<ng-container *ngIf="tagTemplate !== undefined && groupedData.tags.length > 0">
<li class="list-group-item section-header"><h5>{{t('tags')}}</h5></li>
<ul class="list-group results">
<li *ngFor="let option of groupedData.tags; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="tagTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</ng-container>
@if (bookmarkTemplate !== undefined && groupedData.bookmarks.length > 0) {
<li class="list-group-item section-header"><h5>{{t('bookmarks')}}</h5></li>
<ul class="list-group results">
@for(option of groupedData.bookmarks; track option; let index = $index) {
<li (click)="handleResultClick(option)" tabindex="0"
class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="bookmarkTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
}
</ul>
}
<ng-container *ngIf="personTemplate !== undefined && groupedData.persons.length > 0">
<li class="list-group-item section-header"><h5>{{t('people')}}</h5></li>
<ul class="list-group results">
<li *ngFor="let option of groupedData.persons; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="personTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</ng-container>
<ng-container *ngIf="chapterTemplate !== undefined && groupedData.chapters.length > 0">
<li class="list-group-item section-header"><h5>{{t('chapters')}}</h5></li>
<ul class="list-group results">
<li *ngFor="let option of groupedData.chapters; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="chapterTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</ng-container>
@if (libraryTemplate !== undefined && groupedData.libraries.length > 0) {
<li class="list-group-item section-header"><h5 id="libraries-group">{{t('libraries')}}</h5></li>
<ul class="list-group results" role="group" aria-describedby="libraries-group">
@for(option of groupedData.libraries; track option; let index = $index) {
<li (click)="handleResultClick(option)" tabindex="0"
class="list-group-item" aria-labelledby="libraries-group" role="option">
<ng-container [ngTemplateOutlet]="libraryTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
}
</ul>
}
<ng-container *ngIf="fileTemplate !== undefined && groupedData.files.length > 0">
<li class="list-group-item section-header"><h5>{{t('files')}}</h5></li>
<ul class="list-group results">
<li *ngFor="let option of groupedData.files; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="fileTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</ng-container>
@if (genreTemplate !== undefined && groupedData.genres.length > 0) {
<li class="list-group-item section-header"><h5>{{t('genres')}}</h5></li>
<ul class="list-group results">
@for(option of groupedData.genres; track option; let index = $index) {
<li (click)="handleResultClick(option)" tabindex="0"
class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="genreTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
}
</ul>
}
<ng-container *ngIf="!hasData && searchTerm.length > 0">
<ul class="list-group results">
<li class="list-group-item">
<ng-container [ngTemplateOutlet]="noResultsTemplate"></ng-container>
</li>
</ul>
@if (tagTemplate !== undefined && groupedData.tags.length > 0) {
<li class="list-group-item section-header"><h5>{{t('tags')}}</h5></li>
<ul class="list-group results">
@for(option of groupedData.tags; track option; let index = $index) {
<li (click)="handleResultClick(option)" tabindex="0"
class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="tagTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
}
</ul>
}
</ng-container>
</ul>
</div>
@if (personTemplate !== undefined && groupedData.persons.length > 0) {
<li class="list-group-item section-header"><h5>{{t('people')}}</h5></li>
<ul class="list-group results">
@for(option of groupedData.persons; track option; let index = $index) {
<li (click)="handleResultClick(option)" tabindex="0"
class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="personTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
}
</ul>
}
@if (chapterTemplate !== undefined && groupedData.chapters.length > 0) {
<li class="list-group-item section-header"><h5>{{t('chapters')}}</h5></li>
<ul class="list-group results">
@for(option of groupedData.chapters; track option; let index = $index) {
<li (click)="handleResultClick(option)" tabindex="0"
class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="chapterTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
}
</ul>
}
@if (fileTemplate !== undefined && groupedData.files.length > 0) {
<li class="list-group-item section-header"><h5>{{t('files')}}</h5></li>
<ul class="list-group results">
@for(option of groupedData.files; track option; let index = $index) {
<li (click)="handleResultClick(option)" tabindex="0"
class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="fileTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
}
</ul>
}
@if (!hasData && searchTerm.length > 0 && !isLoading) {
<ul class="list-group results">
<li class="list-group-item">
<ng-container [ngTemplateOutlet]="noResultsTemplate"></ng-container>
</li>
</ul>
}
@if (hasData && !includeChapterAndFiles) {
<li class="list-group-item" style="min-height: 34px">
<ng-container [ngTemplateOutlet]="extraTemplate"></ng-container>
<a href="javascript:void(0)" (click)="toggleIncludeFiles()" class="float-end">
{{t('include-extras')}}
</a>
</li>
}
</ul>
</div>
}
</form>

View file

@ -18,8 +18,14 @@ import { debounceTime } 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, NgIf, NgFor, NgTemplateOutlet } from '@angular/common';
import { NgClass, NgTemplateOutlet } from '@angular/common';
import {TranslocoDirective} from "@ngneat/transloco";
import {LoadingComponent} from "../../../shared/loading/loading.component";
export interface SearchEvent {
value: string;
includeFiles: boolean;
}
@Component({
selector: 'app-grouped-typeahead',
@ -27,9 +33,12 @@ import {TranslocoDirective} from "@ngneat/transloco";
styleUrls: ['./grouped-typeahead.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [ReactiveFormsModule, NgClass, NgIf, NgFor, NgTemplateOutlet, TranslocoDirective]
imports: [ReactiveFormsModule, NgClass, NgTemplateOutlet, TranslocoDirective, LoadingComponent]
})
export class GroupedTypeaheadComponent implements OnInit {
private readonly destroyRef = inject(DestroyRef);
private readonly cdRef = inject(ChangeDetectorRef);
/**
* Unique id to tie with a label element
*/
@ -47,6 +56,10 @@ export class GroupedTypeaheadComponent implements OnInit {
* Placeholder for the input
*/
@Input() placeholder: string = '';
/**
* When the search is active
*/
@Input() isLoading: boolean = false;
/**
* Number of milliseconds after typing before triggering inputChanged for data fetching
*/
@ -54,7 +67,7 @@ export class GroupedTypeaheadComponent implements OnInit {
/**
* Emits when the input changes from user interaction
*/
@Output() inputChanged: EventEmitter<string> = new EventEmitter();
@Output() inputChanged: EventEmitter<SearchEvent> = new EventEmitter();
/**
* Emits when something is clicked/selected
*/
@ -76,17 +89,18 @@ export class GroupedTypeaheadComponent implements OnInit {
@ContentChild('personTemplate') personTemplate: TemplateRef<any> | undefined;
@ContentChild('genreTemplate') genreTemplate!: TemplateRef<any>;
@ContentChild('noResultsTemplate') noResultsTemplate!: TemplateRef<any>;
@ContentChild('extraTemplate') extraTemplate!: TemplateRef<any>;
@ContentChild('libraryTemplate') libraryTemplate!: TemplateRef<any>;
@ContentChild('readingListTemplate') readingListTemplate!: TemplateRef<any>;
@ContentChild('fileTemplate') fileTemplate!: TemplateRef<any>;
@ContentChild('chapterTemplate') chapterTemplate!: TemplateRef<any>;
@ContentChild('bookmarkTemplate') bookmarkTemplate!: TemplateRef<any>;
private readonly destroyRef = inject(DestroyRef);
hasFocus: boolean = false;
isLoading: boolean = false;
typeaheadForm: FormGroup = new FormGroup({});
includeChapterAndFiles: boolean = false;
prevSearchTerm: string = '';
@ -101,8 +115,6 @@ export class GroupedTypeaheadComponent implements OnInit {
}
constructor(private readonly cdRef: ChangeDetectorRef) { }
@HostListener('window:click', ['$event'])
handleDocumentClick(event: any) {
this.close();
@ -127,7 +139,10 @@ export class GroupedTypeaheadComponent implements OnInit {
this.typeaheadForm.addControl('typeahead', new FormControl(this.initialValue, []));
this.cdRef.markForCheck();
this.typeaheadForm.valueChanges.pipe(debounceTime(this.debounceTime), takeUntilDestroyed(this.destroyRef)).subscribe(change => {
this.typeaheadForm.valueChanges.pipe(
debounceTime(this.debounceTime),
takeUntilDestroyed(this.destroyRef)
).subscribe(change => {
const value = this.typeaheadForm.get('typeahead')?.value;
if (value != undefined && value != '' && !this.hasFocus) {
@ -138,7 +153,7 @@ export class GroupedTypeaheadComponent implements OnInit {
if (value != undefined && value.length >= this.minQueryLength) {
if (this.prevSearchTerm === value) return;
this.inputChanged.emit(value);
this.inputChanged.emit({value, includeFiles: this.includeChapterAndFiles});
this.prevSearchTerm = value;
this.cdRef.markForCheck();
}
@ -164,10 +179,20 @@ export class GroupedTypeaheadComponent implements OnInit {
});
}
handleResultlick(item: any) {
handleResultClick(item: any) {
this.selected.emit(item);
}
toggleIncludeFiles() {
this.includeChapterAndFiles = true;
this.inputChanged.emit({value: this.searchTerm, includeFiles: this.includeChapterAndFiles});
this.hasFocus = true;
this.inputElem.nativeElement.focus();
this.openDropdown();
this.cdRef.markForCheck();
}
resetField() {
this.prevSearchTerm = '';
this.typeaheadForm.get('typeahead')?.setValue(this.initialValue);

View file

@ -16,6 +16,7 @@
#search
id="nav-search"
[minQueryLength]="2"
[isLoading]="isLoading"
initialValue=""
[placeholder]="t('search-alt')"
[groupedData]="searchResults"
@ -147,7 +148,6 @@
<ng-template #noResultsTemplate let-notFound>
{{t('no-data')}}
</ng-template>
</app-grouped-typeahead>
</div>
</div>

View file

@ -33,7 +33,7 @@ import {NgbDropdown, NgbDropdownItem, NgbDropdownMenu, NgbDropdownToggle} from '
import {EventsWidgetComponent} from '../events-widget/events-widget.component';
import {SeriesFormatComponent} from '../../../shared/series-format/series-format.component';
import {ImageComponent} from '../../../shared/image/image.component';
import {GroupedTypeaheadComponent} from '../grouped-typeahead/grouped-typeahead.component';
import {GroupedTypeaheadComponent, SearchEvent} from '../grouped-typeahead/grouped-typeahead.component';
import {TranslocoDirective} from "@ngneat/transloco";
import {FilterUtilitiesService} from "../../../shared/_services/filter-utilities.service";
import {FilterStatement} from "../../../_models/metadata/v2/filter-statement";
@ -66,7 +66,6 @@ export class NavHeaderComponent implements OnInit {
searchResults: SearchResultGroup = new SearchResultGroup();
searchTerm = '';
backToTopNeeded = false;
searchFocused: boolean = false;
scrollElem: HTMLElement;
@ -121,12 +120,14 @@ export class NavHeaderComponent implements OnInit {
onChangeSearch(val: string) {
onChangeSearch(evt: SearchEvent) {
this.isLoading = true;
this.searchTerm = val.trim();
this.searchTerm = evt.value.trim();
this.cdRef.markForCheck();
this.searchService.search(val.trim()).pipe(takeUntilDestroyed(this.destroyRef)).subscribe(results => {
this.searchService.search(this.searchTerm, evt.includeFiles).pipe(takeUntilDestroyed(this.destroyRef)).subscribe(results => {
this.searchResults = results;
this.isLoading = false;
this.cdRef.markForCheck();

View file

@ -34,7 +34,6 @@ import {SpreadType} from "ngx-extended-pdf-viewer/lib/options/spread-type";
import {PdfLayoutModePipe} from "../../_pipe/pdf-layout-mode.pipe";
import {PdfScrollModePipe} from "../../_pipe/pdf-scroll-mode.pipe";
import {PdfSpreadModePipe} from "../../_pipe/pdf-spread-mode.pipe";
import {HandtoolChanged} from "ngx-extended-pdf-viewer/lib/events/handtool-changed";
@Component({
selector: 'app-pdf-reader',

View file

@ -94,7 +94,7 @@ export class SeriesMetadataDetailComponent implements OnChanges, OnInit {
ngOnInit() {
// If on desktop, we can just have all the data expanded by default:
this.isCollapsed = this.utilityService.getActiveBreakpoint() < Breakpoint.Desktop;
this.isCollapsed = true; // this.utilityService.getActiveBreakpoint() < Breakpoint.Desktop;
// Check if there is a lot of extended data, if so, re-collapse
const sum = (this.seriesMetadata.colorists.length + this.seriesMetadata.editors.length
+ this.seriesMetadata.coverArtists.length + this.seriesMetadata.inkers.length

View file

@ -133,7 +133,7 @@ export class UtilityService {
);
}
deepEqual(object1: any, object2: any) {
deepEqual(object1: any | undefined | null, object2: any | undefined | null) {
if ((object1 === null || object1 === undefined) && (object2 !== null || object2 !== undefined)) return false;
if ((object2 === null || object2 === undefined) && (object1 !== null || object1 !== undefined)) return false;
if (object1 === null && object2 === null) return true;

View file

@ -65,7 +65,6 @@ export class SideNavComponent implements OnInit {
homeActions = [
{action: Action.Edit, title: 'customize', children: [], requiresAdmin: false, callback: this.openCustomize.bind(this)},
{action: Action.Import, title: 'import-cbl', children: [], requiresAdmin: true, callback: this.importCbl.bind(this)},
{action: Action.Import, title: 'import-mal-stack', children: [], requiresAdmin: true, callback: this.importMalCollection.bind(this)}, // This requires the Collection Rework (https://github.com/Kareadita/Kavita/issues/2810)
];
filterQuery: string = '';
@ -144,6 +143,13 @@ export class SideNavComponent implements OnInit {
this.navService.toggleSideNav();
this.cdRef.markForCheck();
});
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();
})
}
ngOnInit(): void {

View file

@ -1537,8 +1537,8 @@
"reading-lists": "Reading Lists",
"collections": "Collections",
"close": "{{common.close}}",
"loading": "{{common.loading}}"
"loading": "{{common.loading}}",
"include-extras": "Include Chapters & Files"
},
"nav-header": {
@ -1605,7 +1605,7 @@
"description": "Import your MAL Interest Stacks and create Collections within Kavita",
"series-count": "{{common.series-count}}",
"restack-count": "{{num}} Restacks",
"nothing-found": ""
"nothing-found": "Nothing found"
},
"edit-chapter-progress": {