Security Event Logging & Bugfixes (#1882)
* Fixed bookmarking failing to convert to webp * Brought the ag-swipe/ng-swipe code into Kavita due to being abandoned by developer and angular requirements. * Fixed average reading time per week finally * Cleaned up some extra decimals on time duration pipe * Don't try to update index.html for base url on local. Fixed ag-swipe on prod mode. * Updated a link on theme manager to point to the new github * Range knobs should be primary color on firefox too * Implemented the ability to get thumbnails of pages inside an archive or pdf. * Updated packages and fixed opds-ps 1.2 issue * Fixed lock file * Allow Kavita's Swagger to hit instances with CORS * Added IP/Request logging for Security Audits * Linked up Summary tag from CBL into Kavita. * Redid the migration so SecurityEvent now has UTC date as well. * Split security logging to a separate file * Update to new versions of checkout and setup * Added a PR check on PR body to ensure that it doesn't contain any characters that break our discord hook. * Updating action * optimize regex in action * Fixed an issue where fit to width would cause the actual height of the image to be shown for pagination bars, instead of rendered. * Added some new code in GetPageFromFiles to ensure pages that exceed array map down to last file. * Added comment about robots * Fixed up unit tests for new ReaderService signature * Kavita now cleans up empty reading lists at night * Don't allow nightly cleanup to run if we are running media conversion tasks * Fixed some bugs in typeahead, it should behave much more reliably. * Fix an issue where emulate comic book wasn't extending to the bottom properly * Added support for Series Chapter 001 Volume 001 * Refactor XFrameOptions="SameOrigins" out to allow users to override in appsettings.json. * Added a rate limiter for some endpoints, but it doesn't seem to be triggering --------- Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
parent
21203414f0
commit
c10acb1279
60 changed files with 2890 additions and 302 deletions
|
@ -101,6 +101,10 @@ export class ReaderService {
|
|||
return this.baseUrl + 'reader/image?chapterId=' + chapterId + '&page=' + page;
|
||||
}
|
||||
|
||||
getThumbnailUrl(chapterId: number, page: number) {
|
||||
return this.baseUrl + 'reader/thumbnail?chapterId=' + chapterId + '&page=' + page;
|
||||
}
|
||||
|
||||
getBookmarkPageUrl(seriesId: number, apiKey: string, page: number) {
|
||||
return this.baseUrl + 'reader/bookmark-image?seriesId=' + seriesId + '&page=' + page + '&apiKey=' + encodeURIComponent(apiKey);
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
</div>
|
||||
<div class="d-flex align-items-center justify-content-between text-center row g-0">
|
||||
<button class="btn btn-small btn-icon col-1" [disabled]="prevChapterDisabled" (click)="loadPrevChapter()" title="Prev Chapter/Volume"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
|
||||
<div class="col-1">{{pageNum}}</div>
|
||||
<div class="col-1" (click)="goToPage(0)">{{pageNum}}</div>
|
||||
<div class="col-8">
|
||||
<ngb-progressbar style="cursor: pointer" title="Go to page" (click)="goToPage()" type="primary" height="5px" [value]="pageNum" [max]="maxPages - 1"></ngb-progressbar>
|
||||
</div>
|
||||
|
|
|
@ -379,10 +379,12 @@
|
|||
</div>
|
||||
<div class="row g-0 mb-2" *ngIf="metadata">
|
||||
<div class="col-md-6">
|
||||
Max Items: {{metadata.maxCount}} <i class="fa fa-info-circle ms-1" placement="right" ngbTooltip="Max of Volume/Issue field in ComicInfo. Used in conjunction with total items to determine publication status." role="button" tabindex="0"></i>
|
||||
Max Items: {{metadata.maxCount}}
|
||||
<i class="fa fa-info-circle ms-1" placement="right" ngbTooltip="Max of Volume/Issue field in ComicInfo. Used in conjunction with total items to determine publication status." role="button" tabindex="0"></i>
|
||||
</div>
|
||||
<div class="col-md-6" title="">
|
||||
Total Items: {{metadata.totalCount}} <i class="fa fa-info-circle ms-1" placement="right" ngbTooltip="Total number of issues/volumes in the series" role="button" tabindex="0"></i>
|
||||
<div class="col-md-6">
|
||||
Total Items: {{metadata.totalCount}}
|
||||
<i class="fa fa-info-circle ms-1" placement="right" ngbTooltip="Total number of issues/volumes in the series" role="button" tabindex="0"></i>
|
||||
</div>
|
||||
<div class="col-md-6">Publication Status: {{metadata.publicationStatus | publicationStatus}}</div>
|
||||
<div class="col-md-6">Total Pages: {{series.pages}}</div>
|
||||
|
|
|
@ -278,6 +278,9 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
this.collectionTagSettings.compareFn = (options: CollectionTag[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
this.collectionTagSettings.compareFnForAdd = (options: CollectionTag[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filterMatches(m.title, filter));
|
||||
}
|
||||
this.collectionTagSettings.selectionCompareFn = (a: CollectionTag, b: CollectionTag) => {
|
||||
return a.title === b.title;
|
||||
}
|
||||
|
@ -310,6 +313,9 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
this.tagsSettings.selectionCompareFn = (a: Tag, b: Tag) => {
|
||||
return a.id == b.id;
|
||||
}
|
||||
this.tagsSettings.compareFnForAdd = (options: Tag[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filterMatches(m.title, filter));
|
||||
}
|
||||
|
||||
if (this.metadata.tags) {
|
||||
this.tagsSettings.savedData = this.metadata.tags;
|
||||
|
@ -331,6 +337,9 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
this.genreSettings.compareFn = (options: Genre[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
this.genreSettings.compareFnForAdd = (options: Genre[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filterMatches(m.title, filter));
|
||||
}
|
||||
this.genreSettings.selectionCompareFn = (a: Genre, b: Genre) => {
|
||||
return a.title == b.title;
|
||||
}
|
||||
|
@ -372,6 +381,9 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
this.languageSettings.compareFn = (options: Language[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
this.languageSettings.compareFnForAdd = (options: Language[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filterMatches(m.title, filter));
|
||||
}
|
||||
this.languageSettings.fetchFn = (filter: string) => of(this.validLanguages)
|
||||
.pipe(map(items => this.languageSettings.compareFn(items, filter)));
|
||||
|
||||
|
@ -426,6 +438,9 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
personSettings.compareFn = (options: Person[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.name, filter));
|
||||
}
|
||||
personSettings.compareFnForAdd = (options: Person[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filterMatches(m.name, filter));
|
||||
}
|
||||
|
||||
personSettings.selectionCompareFn = (a: Person, b: Person) => {
|
||||
return a.name == b.name && a.role == b.role;
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
@use '../../../../manga-reader-common';
|
||||
|
||||
.image-container {
|
||||
height: calc(100vh); // override as on single, we -34px for the potential scrollbar
|
||||
|
||||
#image-1 {
|
||||
&.double {
|
||||
margin: 0 0 0 auto;
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
// Overrides for reverse
|
||||
.image-container {
|
||||
height: calc(100vh); // override as on single, we -34px for the potential scrollbar
|
||||
|
||||
&.reverse {
|
||||
overflow: unset;
|
||||
display: flex;
|
||||
|
|
|
@ -31,8 +31,8 @@ import { DoubleRendererComponent } from '../double-renderer/double-renderer.comp
|
|||
import { DoubleReverseRendererComponent } from '../double-reverse-renderer/double-reverse-renderer.component';
|
||||
import { SingleRendererComponent } from '../single-renderer/single-renderer.component';
|
||||
import { ChapterInfo } from '../../_models/chapter-info';
|
||||
import { SwipeEvent } from 'ng-swipe';
|
||||
import { DoubleNoCoverRendererComponent } from '../double-renderer-no-cover/double-no-cover-renderer.component';
|
||||
import { SwipeEvent } from 'src/app/ng-swipe/ag-swipe.core';
|
||||
|
||||
|
||||
const PREFETCH_PAGES = 10;
|
||||
|
@ -379,7 +379,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
// This is for the pagination area
|
||||
get MaxHeight() {
|
||||
if (this.FittingOption !== FITTING_OPTION.HEIGHT) {
|
||||
return this.mangaReaderService.getPageDimensions(this.pageNum)?.height + 'px';
|
||||
return Math.min(this.readingArea?.nativeElement?.clientHeight, this.mangaReaderService.getPageDimensions(this.pageNum)?.height!) + 'px';
|
||||
}
|
||||
return 'calc(var(--vh) * 100)';
|
||||
}
|
||||
|
|
|
@ -17,8 +17,8 @@ import { DoubleRendererComponent } from './_components/double-renderer/double-re
|
|||
import { DoubleReverseRendererComponent } from './_components/double-reverse-renderer/double-reverse-renderer.component';
|
||||
import { MangaReaderComponent } from './_components/manga-reader/manga-reader.component';
|
||||
import { FittingIconPipe } from './_pipes/fitting-icon.pipe';
|
||||
import { SwipeModule } from 'ng-swipe';
|
||||
import { DoubleNoCoverRendererComponent } from './_components/double-renderer-no-cover/double-no-cover-renderer.component';
|
||||
import { NgSwipeModule } from '../ng-swipe/ng-swipe.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -45,7 +45,7 @@ import { DoubleNoCoverRendererComponent } from './_components/double-renderer-no
|
|||
SharedModule,
|
||||
ReaderSharedModule,
|
||||
|
||||
SwipeModule
|
||||
NgSwipeModule
|
||||
],
|
||||
exports: [
|
||||
MangaReaderComponent
|
||||
|
|
|
@ -329,6 +329,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||
this.ageRatingSettings.compareFn = (options: AgeRatingDto[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
|
||||
|
||||
this.ageRatingSettings.selectionCompareFn = (a: AgeRatingDto, b: AgeRatingDto) => {
|
||||
return a.title == b.title;
|
||||
|
|
103
UI/Web/src/app/ng-swipe/ag-swipe.core.ts
Normal file
103
UI/Web/src/app/ng-swipe/ag-swipe.core.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
import { fromEvent, Observable, race, Subscription } from 'rxjs';
|
||||
import { elementAt, map, switchMap, takeUntil, tap } from 'rxjs/operators';
|
||||
|
||||
export interface SwipeCoordinates {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export enum SwipeDirection {
|
||||
X = 'x',
|
||||
Y = 'y'
|
||||
}
|
||||
|
||||
export interface SwipeStartEvent {
|
||||
x: number;
|
||||
y: number;
|
||||
direction: SwipeDirection;
|
||||
}
|
||||
|
||||
export interface SwipeEvent {
|
||||
direction: SwipeDirection;
|
||||
distance: number;
|
||||
}
|
||||
|
||||
export interface SwipeSubscriptionConfig {
|
||||
domElement: HTMLElement;
|
||||
onSwipeMove?: (event: SwipeEvent) => void;
|
||||
onSwipeEnd?: (event: SwipeEvent) => void;
|
||||
}
|
||||
|
||||
|
||||
export function createSwipeSubscription({ domElement, onSwipeMove, onSwipeEnd }: SwipeSubscriptionConfig): Subscription {
|
||||
if (!(domElement instanceof HTMLElement)) {
|
||||
throw new Error('Provided domElement should be an instance of HTMLElement');
|
||||
}
|
||||
|
||||
if ((typeof onSwipeMove !== 'function') && (typeof onSwipeEnd !== 'function')) {
|
||||
throw new Error('At least one of the following swipe event handler functions should be provided: onSwipeMove and/or onSwipeEnd');
|
||||
}
|
||||
|
||||
const touchStarts$ = fromEvent<TouchEvent>(domElement, 'touchstart').pipe(map(getTouchCoordinates));
|
||||
const touchMoves$ = fromEvent<TouchEvent>(domElement, 'touchmove').pipe(map(getTouchCoordinates));
|
||||
const touchEnds$ = fromEvent<TouchEvent>(domElement, 'touchend').pipe(map(getTouchCoordinates));
|
||||
const touchCancels$ = fromEvent<TouchEvent>(domElement, 'touchcancel');
|
||||
|
||||
const touchStartsWithDirection$: Observable<SwipeStartEvent> = touchStarts$.pipe(
|
||||
switchMap((touchStartEvent: SwipeCoordinates) => touchMoves$.pipe(
|
||||
elementAt(3),
|
||||
map((touchMoveEvent: SwipeCoordinates) => ({
|
||||
x: touchStartEvent.x,
|
||||
y: touchStartEvent.y,
|
||||
direction: getTouchDirection(touchStartEvent, touchMoveEvent)
|
||||
})
|
||||
))
|
||||
)
|
||||
);
|
||||
|
||||
return touchStartsWithDirection$.pipe(
|
||||
switchMap(touchStartEvent => touchMoves$.pipe(
|
||||
map(touchMoveEvent => getTouchDistance(touchStartEvent, touchMoveEvent)),
|
||||
tap((coordinates: SwipeCoordinates) => {
|
||||
if (typeof onSwipeMove !== 'function') { return; }
|
||||
onSwipeMove(getSwipeEvent(touchStartEvent, coordinates));
|
||||
}),
|
||||
takeUntil(race(
|
||||
touchEnds$.pipe(
|
||||
map(touchEndEvent => getTouchDistance(touchStartEvent, touchEndEvent)),
|
||||
tap((coordinates: SwipeCoordinates) => {
|
||||
if (typeof onSwipeEnd !== 'function') { return; }
|
||||
onSwipeEnd(getSwipeEvent(touchStartEvent, coordinates));
|
||||
})
|
||||
),
|
||||
touchCancels$
|
||||
))
|
||||
))
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
function getTouchCoordinates(touchEvent: TouchEvent): SwipeCoordinates {
|
||||
return {
|
||||
x: touchEvent.changedTouches[0].clientX,
|
||||
y: touchEvent.changedTouches[0].clientY
|
||||
};
|
||||
}
|
||||
|
||||
function getTouchDistance(startCoordinates: SwipeCoordinates, moveCoordinates: SwipeCoordinates): SwipeCoordinates {
|
||||
return {
|
||||
x: moveCoordinates.x - startCoordinates.x,
|
||||
y: moveCoordinates.y - startCoordinates.y
|
||||
};
|
||||
}
|
||||
|
||||
function getTouchDirection(startCoordinates: SwipeCoordinates, moveCoordinates: SwipeCoordinates): SwipeDirection {
|
||||
const { x, y } = getTouchDistance(startCoordinates, moveCoordinates);
|
||||
return Math.abs(x) < Math.abs(y) ? SwipeDirection.Y : SwipeDirection.X;
|
||||
}
|
||||
|
||||
function getSwipeEvent(touchStartEvent: SwipeStartEvent, coordinates: SwipeCoordinates): SwipeEvent {
|
||||
return {
|
||||
direction: touchStartEvent.direction,
|
||||
distance: coordinates[touchStartEvent.direction]
|
||||
};
|
||||
}
|
32
UI/Web/src/app/ng-swipe/ng-swipe.directive.ts
Normal file
32
UI/Web/src/app/ng-swipe/ng-swipe.directive.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { Directive, ElementRef, EventEmitter, NgZone, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { createSwipeSubscription, SwipeEvent } from './ag-swipe.core';
|
||||
|
||||
@Directive({
|
||||
selector: '[ngSwipe]'
|
||||
})
|
||||
export class SwipeDirective implements OnInit, OnDestroy {
|
||||
private swipeSubscription: Subscription | undefined;
|
||||
|
||||
@Output() swipeMove: EventEmitter<SwipeEvent> = new EventEmitter<SwipeEvent>();
|
||||
@Output() swipeEnd: EventEmitter<SwipeEvent> = new EventEmitter<SwipeEvent>();
|
||||
|
||||
constructor(
|
||||
private elementRef: ElementRef,
|
||||
private zone: NgZone
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.zone.runOutsideAngular(() => {
|
||||
this.swipeSubscription = createSwipeSubscription({
|
||||
domElement: this.elementRef.nativeElement,
|
||||
onSwipeMove: (swipeMoveEvent: SwipeEvent) => this.swipeMove.emit(swipeMoveEvent),
|
||||
onSwipeEnd: (swipeEndEvent: SwipeEvent) => this.swipeEnd.emit(swipeEndEvent)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.swipeSubscription?.unsubscribe?.();
|
||||
}
|
||||
}
|
18
UI/Web/src/app/ng-swipe/ng-swipe.module.ts
Normal file
18
UI/Web/src/app/ng-swipe/ng-swipe.module.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SwipeDirective } from './ng-swipe.directive';
|
||||
|
||||
// All code in this module is based on https://github.com/aGoncharuks/ag-swipe and may contain further enhancements or bugfixes.
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
SwipeDirective
|
||||
],
|
||||
imports: [
|
||||
CommonModule
|
||||
],
|
||||
exports: [
|
||||
SwipeDirective
|
||||
]
|
||||
})
|
||||
export class NgSwipeModule { }
|
|
@ -13,7 +13,7 @@ export class TimeDurationPipe implements PipeTransform {
|
|||
if (hours < 1) {
|
||||
return `${(hours * 60).toFixed(1)} minutes`;
|
||||
} else if (hours < 24) {
|
||||
return `${hours} hours`;
|
||||
return `${hours.toFixed(1)} hours`;
|
||||
} else if (hours < 720) {
|
||||
return `${(hours / 24).toFixed(1)} days`;
|
||||
} else if (hours < 8760) {
|
||||
|
|
|
@ -92,11 +92,17 @@ export class UtilityService {
|
|||
}
|
||||
|
||||
filter(input: string, filter: string): boolean {
|
||||
if (input === null || filter === null) return false;
|
||||
if (input === null || filter === null || input === undefined || filter === undefined) return false;
|
||||
const reg = /[_\.\-]/gi;
|
||||
return input.toUpperCase().replace(reg, '').includes(filter.toUpperCase().replace(reg, ''));
|
||||
}
|
||||
|
||||
filterMatches(input: string, filter: string): boolean {
|
||||
if (input === null || filter === null || input === undefined || filter === undefined) return false;
|
||||
const reg = /[_\.\-]/gi;
|
||||
return input.toUpperCase().replace(reg, '') === filter.toUpperCase().replace(reg, '');
|
||||
}
|
||||
|
||||
isVolume(d: any) {
|
||||
return d != null && d.hasOwnProperty('chapters');
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Average Reading / Week" [clickable]="false" fontClasses="fas fa-eye">
|
||||
{{avgHoursPerWeekSpentReading | timeDuration}}
|
||||
{{avgHoursPerWeekSpentReading | timeDuration}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
|
|
|
@ -236,16 +236,16 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
|||
this.filteredOptions = this.typeaheadForm.get('typeahead')!.valueChanges
|
||||
.pipe(
|
||||
// Adjust input box to grow
|
||||
tap(val => {
|
||||
tap((val: string) => {
|
||||
if (this.inputElem != null && this.inputElem.nativeElement != null) {
|
||||
this.renderer2.setStyle(this.inputElem.nativeElement, 'width', 15 * (val.trim().length + 1) + 'px');
|
||||
this.focusedIndex = 0;
|
||||
}
|
||||
}),
|
||||
map(val => val.trim()),
|
||||
map((val: string) => val.trim()),
|
||||
auditTime(this.settings.debounce),
|
||||
//distinctUntilChanged(), // ?!: BUG Doesn't trigger the search to run when filtered array changes
|
||||
filter(val => {
|
||||
filter((val: string) => {
|
||||
// If minimum filter characters not met, do not filter
|
||||
if (this.settings.minCharacters === 0) return true;
|
||||
|
||||
|
@ -256,11 +256,11 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
|||
return true;
|
||||
}),
|
||||
|
||||
switchMap(val => {
|
||||
switchMap((val: string) => {
|
||||
this.isLoadingOptions = true;
|
||||
return this.settings.fetchFn(val.trim()).pipe(takeUntil(this.onDestroy), map((items: any[]) => items.filter(item => this.filterSelected(item))));
|
||||
}),
|
||||
tap((filteredOptions) => {
|
||||
tap((filteredOptions: any[]) => {
|
||||
this.isLoadingOptions = false;
|
||||
this.focusedIndex = 0;
|
||||
this.cdRef.markForCheck();
|
||||
|
@ -398,6 +398,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
this.toggleSelection(opt);
|
||||
console.log('Selected ', opt);
|
||||
|
||||
this.resetField();
|
||||
this.onInputFocus();
|
||||
|
@ -410,6 +411,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
|||
const newItem = this.settings.addTransformFn(title);
|
||||
this.newItemAdded.emit(newItem);
|
||||
this.toggleSelection(newItem);
|
||||
console.log('Selected ', newItem);
|
||||
|
||||
this.resetField();
|
||||
this.onInputFocus();
|
||||
|
@ -482,14 +484,43 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
|||
|
||||
updateShowAddItem(options: any[]) {
|
||||
// ?! BUG This will still technicially allow you to add the same thing as a previously added item. (Code will just toggle it though)
|
||||
this.showAddItem = this.settings.addIfNonExisting && this.typeaheadControl.value.trim()
|
||||
&& this.typeaheadControl.value.trim().length >= Math.max(this.settings.minCharacters, 1)
|
||||
&& this.typeaheadControl.dirty
|
||||
&& (typeof this.settings.compareFn == 'function' && this.settings.compareFn(options, this.typeaheadControl.value.trim()).length === 0);
|
||||
this.showAddItem = false;
|
||||
this.cdRef.markForCheck();
|
||||
if (!this.settings.addIfNonExisting) return;
|
||||
|
||||
const inputText = this.typeaheadControl.value.trim();
|
||||
if (inputText.length < Math.max(this.settings.minCharacters, 1)) return;
|
||||
if (!this.typeaheadControl.dirty) return; // Do we need this?
|
||||
|
||||
// Check if this new option will interfere with any existing ones not shown
|
||||
|
||||
if (typeof this.settings.compareFnForAdd == 'function') {
|
||||
console.log('filtered options: ', this.optionSelection.selected());
|
||||
const willDuplicateExist = this.settings.compareFnForAdd(this.optionSelection.selected(), inputText);
|
||||
console.log('duplicate check: ', willDuplicateExist);
|
||||
if (willDuplicateExist.length > 0) {
|
||||
console.log("can't show add, duplicates will exist");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof this.settings.compareFn == 'function') {
|
||||
// The problem here is that compareFn can report that duplicate will exist as it does contains not match
|
||||
const matches = this.settings.compareFn(options, inputText);
|
||||
console.log('matches for ', inputText, ': ', matches);
|
||||
console.log('matches include input string: ', matches.includes(this.settings.addTransformFn(inputText)));
|
||||
if (matches.length > 0 && matches.includes(this.settings.addTransformFn(inputText))) {
|
||||
console.log("can't show add, there are still ");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.showAddItem = true;
|
||||
|
||||
if (this.showAddItem) {
|
||||
this.hasFocus = true;
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
toggleLock(event: any) {
|
||||
|
|
|
@ -29,9 +29,13 @@ export class TypeaheadSettings<T> {
|
|||
savedData!: T[] | T;
|
||||
/**
|
||||
* Function to compare the elements. Should return all elements that fit the matching criteria.
|
||||
* This is only used with non-Observable based fetchFn, but must be defined for all uses of typeahead (TODO)
|
||||
* This is only used with non-Observable based fetchFn, but must be defined for all uses of typeahead.
|
||||
*/
|
||||
compareFn!: ((optionList: T[], filter: string) => T[]);
|
||||
/**
|
||||
* Must be defined when addIfNonExisting is true. Used to ensure no duplicates exist when adding.
|
||||
*/
|
||||
compareFnForAdd!: ((optionList: T[], filter: string) => T[]);
|
||||
/**
|
||||
* Function which is used for comparing objects when keeping track of state.
|
||||
* Useful over shallow equal when you have image urls that have random numbers on them.
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</div>
|
||||
|
||||
<p *ngIf="isAdmin">
|
||||
Looking for a light or e-ink theme? We have some custom themes you can use on our <a href="https://wiki.kavitareader.com/en/guides/settings/themes" target="_blank" rel="noopener noreferrer">wiki</a>.
|
||||
Looking for a light or e-ink theme? We have some custom themes you can use on our <a href="https://github.com/Kareadita/Themes" target="_blank" rel="noopener noreferrer">theme github</a>.
|
||||
</p>
|
||||
|
||||
<div class="row g-0">
|
||||
|
|
|
@ -14,8 +14,8 @@
|
|||
<meta name="msapplication-config" content="assets/icons/browserconfig.xml">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="#000000">
|
||||
<meta name='robots' content='noindex,follow' />
|
||||
|
||||
<!-- Don't allow indexing from Bots -->
|
||||
<meta name='robots' content='none' />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
|
||||
|
|
|
@ -24,10 +24,10 @@ input:not([type="range"]), .form-control {
|
|||
}
|
||||
}
|
||||
|
||||
.form-range::-webkit-slider-thumb:active {
|
||||
.form-range::-webkit-slider-thumb:active, .form-range::-moz-range-thumb:active {
|
||||
background-color: var(--input-range-active-color);
|
||||
}
|
||||
|
||||
.form-range::-webkit-slider-thumb {
|
||||
.form-range::-webkit-slider-thumb, .form-range::-moz-range-thumb {
|
||||
background-color: var(--input-range-color);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue