More Reader Fixes (#1696)

* Fixed resizing or layout changes causing page change on double reader

* Implemented the debug log pattern on double renderers. Fixed a case when navigation backwards and showing only one page. Updated so go to page or slider update will handle selecting the right page number for pair display.

* All Spread cases for double working

* Cleanup dead code

* Ensure we can jump to last page
This commit is contained in:
Joe Milazzo 2022-12-13 15:57:51 -06:00 committed by GitHub
parent 90d5834ffa
commit c640ae3637
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 146 additions and 104 deletions

View File

@ -310,6 +310,16 @@ public class StatisticService : IStatisticService
.ToListAsync(); .ToListAsync();
} }
public void ReadCountByDay()
{
// _context.AppUserProgresses
// .GroupBy(p => p.LastModified.Day)
// .Select(g =>
// {
// Day = g.Key,
// })
}
public Task<IEnumerable<ReadHistoryEvent>> GetHistory() public Task<IEnumerable<ReadHistoryEvent>> GetHistory()
{ {
// _context.AppUserProgresses // _context.AppUserProgresses

View File

@ -162,6 +162,7 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
return needsSplitting; return needsSplitting;
} }
isValid() { isValid() {
return this.renderWithCanvas; return this.renderWithCanvas;
} }

View File

@ -7,7 +7,7 @@ import { ReaderService } from 'src/app/_services/reader.service';
import { LayoutMode } from '../../_models/layout-mode'; import { LayoutMode } from '../../_models/layout-mode';
import { FITTING_OPTION, PAGING_DIRECTION } from '../../_models/reader-enums'; import { FITTING_OPTION, PAGING_DIRECTION } from '../../_models/reader-enums';
import { ReaderSetting } from '../../_models/reader-setting'; import { ReaderSetting } from '../../_models/reader-setting';
import { ImageRenderer } from '../../_models/renderer'; import { DEBUG_MODES, ImageRenderer } from '../../_models/renderer';
import { ManagaReaderService } from '../../_series/managa-reader.service'; import { ManagaReaderService } from '../../_series/managa-reader.service';
/** /**
@ -26,11 +26,10 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
@Input() bookmark$!: Observable<number>; @Input() bookmark$!: Observable<number>;
@Input() showClickOverlay$!: Observable<boolean>; @Input() showClickOverlay$!: Observable<boolean>;
@Input() pageNum$!: Observable<{pageNum: number, maxPages: number}>; @Input() pageNum$!: Observable<{pageNum: number, maxPages: number}>;
@Input() getPage!: (pageNum: number) => HTMLImageElement; @Input() getPage!: (pageNum: number) => HTMLImageElement;
@Output() imageHeight: EventEmitter<number> = new EventEmitter<number>(); @Output() imageHeight: EventEmitter<number> = new EventEmitter<number>();
debugMode: DEBUG_MODES = DEBUG_MODES.None;
imageFitClass$!: Observable<string>; imageFitClass$!: Observable<string>;
showClickOverlayClass$!: Observable<string>; showClickOverlayClass$!: Observable<string>;
readerModeClass$!: Observable<string>; readerModeClass$!: Observable<string>;
@ -61,6 +60,7 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
*/ */
shouldRenderDouble$!: Observable<boolean>; shouldRenderDouble$!: Observable<boolean>;
private readonly onDestroy = new Subject<void>(); private readonly onDestroy = new Subject<void>();
get ReaderMode() {return ReaderMode;} get ReaderMode() {return ReaderMode;}
@ -172,22 +172,27 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
if (!this.isValid()) return false; if (!this.isValid()) return false;
if (this.mangaReaderService.isCoverImage(this.pageNum)) { if (this.mangaReaderService.isCoverImage(this.pageNum)) {
console.log('Not rendering double as current page is cover image'); this.debugLog('Not rendering double as current page is cover image');
return false; return false;
} }
if (this.mangaReaderService.isWidePage(this.pageNum) ) { if (this.mangaReaderService.isWidePage(this.pageNum) ) {
console.log('Not rendering double as current page is wide image'); this.debugLog('Not rendering double as current page is wide image');
return false;
}
if (this.mangaReaderService.isSecondLastImage(this.pageNum, this.maxPages)) {
this.debugLog('Not rendering double as current page is last');
return false; return false;
} }
if (this.mangaReaderService.isLastImage(this.pageNum, this.maxPages)) { if (this.mangaReaderService.isLastImage(this.pageNum, this.maxPages)) {
console.log('Not rendering double as current page is last and there are an odd number of pages'); this.debugLog('Not rendering double as current page is last');
return false; return false;
} }
if (this.mangaReaderService.isWidePage(this.pageNum + 1) ) { if (this.mangaReaderService.isWidePage(this.pageNum + 1) ) {
console.log('Not rendering double as next page is wide image'); this.debugLog('Not rendering double as next page is wide image');
return false; return false;
} }
@ -225,49 +230,66 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
switch (direction) { switch (direction) {
case PAGING_DIRECTION.FORWARD: case PAGING_DIRECTION.FORWARD:
if (this.mangaReaderService.isCoverImage(this.pageNum)) { if (this.mangaReaderService.isCoverImage(this.pageNum)) {
console.log('Moving forward 1 page as on cover image'); this.debugLog('Moving forward 1 page as on cover image');
return 1; return 1;
} }
if (this.mangaReaderService.isWidePage(this.pageNum)) { if (this.mangaReaderService.isWidePage(this.pageNum)) {
console.log('Moving forward 1 page as current page is wide'); this.debugLog('Moving forward 1 page as current page is wide');
return 1; return 1;
} }
if (this.mangaReaderService.isWidePage(this.pageNum + 1)) { if (this.mangaReaderService.isWidePage(this.pageNum + 1)) {
console.log('Moving forward 1 page as next page is wide'); this.debugLog('Moving forward 1 page as next page is wide');
return 1; return 1;
} }
if (this.mangaReaderService.isSecondLastImage(this.pageNum, this.maxPages)) { if (this.mangaReaderService.isSecondLastImage(this.pageNum, this.maxPages)) {
console.log('Moving forward 1 page as 2 pages left'); this.debugLog('Moving forward 1 page as 2 pages left');
return 1; return 1;
} }
if (this.mangaReaderService.isLastImage(this.pageNum, this.maxPages)) { if (this.mangaReaderService.isLastImage(this.pageNum, this.maxPages)) {
console.log('Moving forward 1 page as 1 page left'); this.debugLog('Moving forward 1 page as 1 page left');
return 1; return 1;
} }
console.log('Moving forward 2 pages'); this.debugLog('Moving forward 2 pages');
return 2; return 2;
case PAGING_DIRECTION.BACKWARDS: case PAGING_DIRECTION.BACKWARDS:
if (this.mangaReaderService.isCoverImage(this.pageNum)) { if (this.mangaReaderService.isCoverImage(this.pageNum)) {
console.log('Moving back 1 page as on cover image'); this.debugLog('Moving back 1 page as on cover image');
return 1;
}
if (this.mangaReaderService.isWidePage(this.pageNum)) {
console.log('Moving back 1 page as current page is wide');
return 1;
}
if (this.mangaReaderService.isWidePage(this.pageNum - 1)) {
console.log('Moving back 1 page as prev page is wide');
return 1;
}
if (this.mangaReaderService.isWidePage(this.pageNum - 2)) {
console.log('Moving back 1 page as 2 pages back is wide');
return 1; return 1;
} }
console.log('Moving back 2 pages'); if (this.mangaReaderService.adjustForDoubleReader(this.pageNum - 1) != this.pageNum - 1 && !this.mangaReaderService.isWidePage(this.pageNum - 2)) {
this.debugLog('Moving back 2 pages as previous pair should be in a pair');
return 2;
}
if (this.mangaReaderService.isWidePage(this.pageNum)) {
this.debugLog('Moving back 1 page as current page is wide');
return 1;
}
if (this.mangaReaderService.isWidePage(this.pageNum - 1)) {
this.debugLog('Moving back 1 page as prev page is wide');
return 1;
}
if (this.mangaReaderService.isWidePage(this.pageNum - 2)) {
this.debugLog('Moving back 1 page as 2 pages back is wide');
return 1;
}
this.debugLog('Moving back 2 pages');
return 2; return 2;
} }
} }
reset(): void {} reset(): void {}
debugLog(message: string, extraData?: any) {
if (!(this.debugMode & DEBUG_MODES.Logs)) return;
if (extraData !== undefined) {
console.log(message, extraData);
} else {
console.log(message);
}
}
} }

View File

@ -7,7 +7,7 @@ import { ReaderService } from 'src/app/_services/reader.service';
import { LayoutMode } from '../../_models/layout-mode'; import { LayoutMode } from '../../_models/layout-mode';
import { FITTING_OPTION, PAGING_DIRECTION } from '../../_models/reader-enums'; import { FITTING_OPTION, PAGING_DIRECTION } from '../../_models/reader-enums';
import { ReaderSetting } from '../../_models/reader-setting'; import { ReaderSetting } from '../../_models/reader-setting';
import { ImageRenderer } from '../../_models/renderer'; import { DEBUG_MODES, ImageRenderer } from '../../_models/renderer';
import { ManagaReaderService } from '../../_series/managa-reader.service'; import { ManagaReaderService } from '../../_series/managa-reader.service';
/** /**
@ -28,11 +28,11 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
@Input() bookmark$!: Observable<number>; @Input() bookmark$!: Observable<number>;
@Input() showClickOverlay$!: Observable<boolean>; @Input() showClickOverlay$!: Observable<boolean>;
@Input() pageNum$!: Observable<{pageNum: number, maxPages: number}>; @Input() pageNum$!: Observable<{pageNum: number, maxPages: number}>;
@Input() getPage!: (pageNum: number) => HTMLImageElement; @Input() getPage!: (pageNum: number) => HTMLImageElement;
@Output() imageHeight: EventEmitter<number> = new EventEmitter<number>(); @Output() imageHeight: EventEmitter<number> = new EventEmitter<number>();
debugMode: DEBUG_MODES = DEBUG_MODES.None;
imageFitClass$!: Observable<string>; imageFitClass$!: Observable<string>;
showClickOverlayClass$!: Observable<string>; showClickOverlayClass$!: Observable<string>;
readerModeClass$!: Observable<string>; readerModeClass$!: Observable<string>;
@ -115,8 +115,8 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
this.shouldRenderDouble$ = this.pageNum$.pipe( this.shouldRenderDouble$ = this.pageNum$.pipe(
takeUntil(this.onDestroy), takeUntil(this.onDestroy),
map((_) => this.shouldRenderDouble()), map(() => this.shouldRenderDouble()),
filter(_ => this.isValid()), filter(() => this.isValid()),
shareReplay() shareReplay()
); );
@ -173,22 +173,27 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
if (!this.isValid()) return false; if (!this.isValid()) return false;
if (this.mangaReaderService.isCoverImage(this.pageNum)) { if (this.mangaReaderService.isCoverImage(this.pageNum)) {
console.log('Not rendering double as current page is cover image'); this.debugLog('Not rendering double as current page is cover image');
return false; return false;
} }
if (this.mangaReaderService.isWidePage(this.pageNum)) { if (this.mangaReaderService.isWidePage(this.pageNum)) {
console.log('Not rendering double as current page is wide image'); this.debugLog('Not rendering double as current page is wide image');
return false;
}
if (this.mangaReaderService.isSecondLastImage(this.pageNum, this.maxPages)) {
this.debugLog('Not rendering double as current page is last');
return false; return false;
} }
if (this.mangaReaderService.isWidePage(this.pageNum + 1) ) { if (this.mangaReaderService.isWidePage(this.pageNum + 1) ) {
console.log('Not rendering double as next page is wide image'); this.debugLog('Not rendering double as next page is wide image');
return false; return false;
} }
if (this.mangaReaderService.isLastImage(this.pageNum, this.maxPages)) { if (this.mangaReaderService.isLastImage(this.pageNum, this.maxPages)) {
console.log('Not rendering double as current page is last and there are an odd number of pages'); this.debugLog('Not rendering double as current page is last and there are an odd number of pages');
return false; return false;
} }
@ -219,92 +224,85 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
switch (direction) { switch (direction) {
case PAGING_DIRECTION.FORWARD: case PAGING_DIRECTION.FORWARD:
if (this.mangaReaderService.isCoverImage(this.pageNum)) { if (this.mangaReaderService.isCoverImage(this.pageNum)) {
console.log('Moving forward 1 page as on cover image'); this.debugLog('Moving forward 1 page as on cover image');
return 1;
}
if (this.mangaReaderService.isWidePage(this.pageNum)) {
this.debugLog('Moving forward 1 page as current page is wide');
return 1;
}
if (this.mangaReaderService.isWidePage(this.pageNum + 1)) {
this.debugLog('Moving forward 1 page as current page is wide');
return 1; return 1;
} }
if (this.mangaReaderService.isSecondLastImage(this.pageNum, this.maxPages)) { if (this.mangaReaderService.isSecondLastImage(this.pageNum, this.maxPages)) {
console.log('Moving forward 1 page as 2 pages left'); this.debugLog('Moving forward 1 page as 2 pages left');
return 1; return 1;
} }
if (this.mangaReaderService.isWidePage(this.pageNum)) {
console.log('Moving forward 1 page as current page is wide');
return 1;
}
if (this.mangaReaderService.isWidePage(this.pageNum + 1)) {
console.log('Moving forward 1 page as current page is wide');
return 1;
}
if (this.mangaReaderService.isLastImage(this.pageNum + 1, this.maxPages)) {
console.log('Moving forward 2 pages as right image is the last page and we just rendered double page');
return 2;
}
if (this.pageNum === this.maxPages - 1) {
console.log('Moving forward 0 page as on last page');
return 0;
}
if (this.mangaReaderService.isLastImage(this.pageNum, this.maxPages)) { if (this.mangaReaderService.isLastImage(this.pageNum, this.maxPages)) {
console.log('Moving forward 1 page as 1 page left'); this.debugLog('Moving forward 2 pages as right image is the last page and we just rendered double page');
return 1;
}
console.log('Moving forward 2 pages');
return 2;
case PAGING_DIRECTION.BACKWARDS:
if (this.mangaReaderService.isCoverImage(this.pageNum)) {
console.log('Moving back 1 page as on cover image');
return 1;
}
if (this.mangaReaderService.isWidePage(this.pageNum + 1)) {
console.log('Moving back 2 page as right page is wide');
return 2; return 2;
} }
if (this.mangaReaderService.isWidePage(this.pageNum + 2)) { this.debugLog('Moving forward 2 pages');
console.log('Moving back 1 page as coming from wide page'); return 2;
case PAGING_DIRECTION.BACKWARDS:
if (this.mangaReaderService.isCoverImage(this.pageNum)) {
this.debugLog('Moving back 1 page as on cover image');
return 1; return 1;
} }
if (this.mangaReaderService.adjustForDoubleReader(this.pageNum - 1) != this.pageNum - 1 && !this.mangaReaderService.isWidePage(this.pageNum - 2)) {
this.debugLog('Moving back 2 pages as previous pair should be in a pair');
return 2;
}
if (this.mangaReaderService.isWidePage(this.pageNum)) { if (this.mangaReaderService.isWidePage(this.pageNum)) {
console.log('Moving back 1 page as left page is wide'); this.debugLog('Moving back 1 page as left page is wide');
return 1; return 1;
} }
if (this.mangaReaderService.isWidePage(this.pageNum) && (!this.mangaReaderService.isWidePage(this.pageNum - 4))) { if (this.mangaReaderService.isWidePage(this.pageNum) && (!this.mangaReaderService.isWidePage(this.pageNum - 4))) {
console.log('Moving back 1 page as left page is wide'); this.debugLog('Moving back 1 page as left page is wide');
return 1; return 1;
} }
if (this.mangaReaderService.isWidePage(this.pageNum - 1)) { if (this.mangaReaderService.isWidePage(this.pageNum - 1)) {
console.log('Moving back 1 page as prev page is wide'); this.debugLog('Moving back 1 page as prev page is wide');
return 1; return 1;
} }
if (this.mangaReaderService.isWidePage(this.pageNum - 2)) { if (this.mangaReaderService.isWidePage(this.pageNum - 2)) {
console.log('Moving back 1 page as 2 pages back is wide'); this.debugLog('Moving back 1 page as 2 pages back is wide');
return 1; return 1;
} }
if (this.mangaReaderService.isWidePage(this.pageNum + 2)) { if (this.mangaReaderService.isWidePage(this.pageNum + 2)) {
console.log('Moving back 2 page as 2 pages back is wide'); this.debugLog('Moving back 2 page as 2 pages back is wide');
return 1; return 1;
} }
// Not sure about this condition on moving backwards // Not sure about this condition on moving backwards
if (this.mangaReaderService.isSecondLastImage(this.pageNum, this.maxPages)) { if (this.mangaReaderService.isSecondLastImage(this.pageNum, this.maxPages)) {
console.log('Moving back 1 page as 2 pages left'); this.debugLog('Moving back 1 page as 2 pages left');
return 1; return 1;
} }
console.log('Moving back 2 pages'); this.debugLog('Moving back 2 pages');
return 2; return 2;
} }
} }
reset(): void {} reset(): void {}
debugLog(message: string, extraData?: any) {
if (!(this.debugMode & DEBUG_MODES.Logs)) return;
if (extraData !== undefined) {
console.log(message, extraData);
} else {
console.log(message);
}
}
} }

View File

@ -32,8 +32,6 @@ const enum DEBUG_MODES {
* Turn on Page outline * Turn on Page outline
*/ */
Outline = 8 Outline = 8
} }
@Component({ @Component({

View File

@ -473,16 +473,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.generalSettingsForm.get('pageSplitOption')?.disable(); this.generalSettingsForm.get('pageSplitOption')?.disable();
this.generalSettingsForm.get('fittingOption')?.setValue(this.mangaReaderService.translateScalingOption(ScalingOption.FitToHeight)); this.generalSettingsForm.get('fittingOption')?.setValue(this.mangaReaderService.translateScalingOption(ScalingOption.FitToHeight));
this.generalSettingsForm.get('fittingOption')?.disable(); this.generalSettingsForm.get('fittingOption')?.disable();
// If we are in double mode, we need to check if our current page is on a right edge or not, if so adjust by decrementing by 1
if (this.readerMode !== ReaderMode.Webtoon) {
this.setPageNum(this.mangaReaderService.adjustForDoubleReader(this.pageNum));
}
} }
this.cdRef.markForCheck(); this.cdRef.markForCheck();
// Re-render the current page when we switch layouts // Re-render the current page when we switch layouts
if (changeOccurred) { if (changeOccurred) {
this.setPageNum(this.adjustPagesForDoubleRenderer(this.pageNum));
this.loadPage(); this.loadPage();
} }
}); });
@ -604,6 +600,15 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}; };
} }
// If we are in double mode, we need to check if our current page is on a right edge or not, if so adjust by decrementing by 1
adjustPagesForDoubleRenderer(pageNum: number) {
if (pageNum === this.maxPages - 1) return pageNum;
if (this.readerMode !== ReaderMode.Webtoon && this.layoutMode !== LayoutMode.Single) {
return this.mangaReaderService.adjustForDoubleReader(pageNum);
}
return pageNum;
}
disableDoubleRendererIfScreenTooSmall() { disableDoubleRendererIfScreenTooSmall() {
if (window.innerWidth > window.innerHeight) { if (window.innerWidth > window.innerHeight) {
this.generalSettingsForm.get('layoutMode')?.enable(); this.generalSettingsForm.get('layoutMode')?.enable();
@ -736,10 +741,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
page = this.maxPages - 1; page = this.maxPages - 1;
} }
// If we are in double mode, we need to check if our current page is on a right edge or not, if so adjust by decrementing by 1 page = this.adjustPagesForDoubleRenderer(page);
if (this.layoutMode !== LayoutMode.Single && this.readerMode !== ReaderMode.Webtoon) {
page = this.mangaReaderService.adjustForDoubleReader(page);
}
this.setPageNum(page); // first call this.setPageNum(page); // first call
this.goToPageEvent = new BehaviorSubject<number>(this.pageNum); this.goToPageEvent = new BehaviorSubject<number>(this.pageNum);
@ -1121,9 +1123,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.pagingDirectionSubject.next(PAGING_DIRECTION.BACKWARDS); this.pagingDirectionSubject.next(PAGING_DIRECTION.BACKWARDS);
} }
this.setPageNum(page); this.setPageNum(this.adjustPagesForDoubleRenderer(page));
this.refreshSlider.emit(); this.refreshSlider.emit();
this.goToPageEvent.next(page); this.goToPageEvent.next(this.pageNum);
this.render(); this.render();
} }
@ -1179,7 +1181,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.pagingDirectionSubject.next(PAGING_DIRECTION.BACKWARDS); this.pagingDirectionSubject.next(PAGING_DIRECTION.BACKWARDS);
} }
this.setPageNum(page); this.setPageNum(this.adjustPagesForDoubleRenderer(page));
this.goToPageEvent.next(page); this.goToPageEvent.next(page);
this.render(); this.render();
} }

View File

@ -58,6 +58,8 @@ export class SingleRendererComponent implements OnInit, OnDestroy, ImageRenderer
tap(pageInfo => { tap(pageInfo => {
this.pageNum = pageInfo.pageNum; this.pageNum = pageInfo.pageNum;
this.maxPages = pageInfo.maxPages; this.maxPages = pageInfo.maxPages;
// TODO: Put this here this.currentImage = this.getPagez
}), }),
).subscribe(() => {}); ).subscribe(() => {});
@ -125,11 +127,6 @@ export class SingleRendererComponent implements OnInit, OnDestroy, ImageRenderer
if (img === null || img.length === 0 || img[0] === null) return; if (img === null || img.length === 0 || img[0] === null) return;
if (!this.isValid()) return; if (!this.isValid()) return;
// This seems to cause a problem after rendering a split
//if (this.mangaReaderService.shouldSplit(this.currentImage, this.pageSplit)) return;
this.currentImage = img[0]; this.currentImage = img[0];
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.imageHeight.emit(this.currentImage.height); this.imageHeight.emit(this.currentImage.height);

View File

@ -2,6 +2,21 @@ import { Observable } from "rxjs";
import { PAGING_DIRECTION } from "./reader-enums"; import { PAGING_DIRECTION } from "./reader-enums";
import { ReaderSetting } from "./reader-setting"; import { ReaderSetting } from "./reader-setting";
/**
* Bitwise enums for configuring how much debug information we want
*/
export const enum DEBUG_MODES {
/**
* No Debug information
*/
None = 0,
/**
* Turn on debug logging
*/
Logs = 2,
}
/** /**
* A generic interface for an image renderer * A generic interface for an image renderer
*/ */
@ -40,6 +55,4 @@ export interface ImageRenderer {
* This should reset any needed state, but not unset the image. * This should reset any needed state, but not unset the image.
*/ */
reset(): void; reset(): void;
} }

View File

@ -39,6 +39,7 @@ export class ManagaReaderService {
i++; i++;
}); });
console.log('pairs: ', this.pairs);
} }
adjustForDoubleReader(page: number) { adjustForDoubleReader(page: number) {
@ -94,7 +95,7 @@ export class ManagaReaderService {
* If the current page is second to last image * If the current page is second to last image
*/ */
isSecondLastImage(pageNum: number, maxPages: number) { isSecondLastImage(pageNum: number, maxPages: number) {
return maxPages - 2 === pageNum; return (maxPages - 2) === pageNum;
} }
/** /**

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0", "name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
}, },
"version": "0.6.1.14" "version": "0.6.1.15"
}, },
"servers": [ "servers": [
{ {