Release Shakeout (#1266)

* Fixed an issue with fit to screen where spread images would fail to generate a paging area long enough.

* Fixed pagination placement on original scaling

* Fixed an issue with webtoon reader not reporting scroll events due to a fix from manga reader.

* Fixing select on black book-reader theme

* Fixing canvas split centering

* Fixed a bug with white mode in book reader not rendering correctly. When bookmarking new pages after previously have viewing bookmarks for a series, ensure we clear out the temp cache else your new files wont be visible till next day.

* Use grid on related tab

* Clear bookmarks was not hooked up. Bulk add to collection didn't have label hidden

* Fixed bug where filter might stay open between pages

* Fixed typo on relationship for Adaptation

* Contains was missing from series relation modal

* Tweaked some methods and wording on reading list page

* Cleaned up the phrasing when we abort a scan.

* Fixed issue where typeahead wasn't reopening and it wasn't filtering selected options

* Fixed some typeahead bugs and decreased interval for docker health check

* Cleaned up and fixed some logic with receiving cover image update events

Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
Joseph Milazzo 2022-05-20 17:50:17 -05:00 committed by GitHub
parent 49d8a7c6ca
commit a062341564
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 90 additions and 144 deletions

View File

@ -1,25 +0,0 @@
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/.idea
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

View File

@ -150,8 +150,7 @@ namespace API.Controllers
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
var totalPages = await _cacheService.CacheBookmarkForSeries(user.Id, seriesId);
// TODO: Change Includes to None from LinkedSeries branch
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.None);
return Ok(new BookmarkInfoDto()
{
@ -172,11 +171,6 @@ namespace API.Controllers
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was an issue saving progress");
// var series = new List<SeriesDto>()
// {await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(markReadDto.SeriesId, user.Id)};
// await _unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, series);
// await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
// MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, markReadDto.SeriesId, series[0], series[0].Pages));
return Ok();
}
@ -194,16 +188,6 @@ namespace API.Controllers
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was an issue saving progress");
// Should I do this for every chapter? Maybe in a background task?
// foreach (var chapterId in await
// _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new List<int>() {markReadDto.SeriesId}))
// {
// await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
// MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, chapterId, MessageFactoryEntityTypes.Chapter, 0));
// }
//
// await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
// MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, markReadDto.SeriesId, MessageFactoryEntityTypes.Series, 0));
return Ok();
}
@ -580,6 +564,7 @@ namespace API.Controllers
if (await _bookmarkService.BookmarkPage(user, bookmarkDto, path))
{
BackgroundJob.Enqueue(() => _cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId));
return Ok();
}
@ -599,6 +584,7 @@ namespace API.Controllers
if (await _bookmarkService.RemoveBookmarkPage(user, bookmarkDto))
{
BackgroundJob.Enqueue(() => _cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId));
return Ok();
}

View File

@ -102,6 +102,8 @@ namespace API.Controllers
if (_unitOfWork.HasChanges())
{
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false);
await _unitOfWork.CommitAsync();
return Ok();
}
@ -245,6 +247,10 @@ namespace API.Controllers
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(chapter.VolumeId, MessageFactoryEntityTypes.Volume), false);
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter), false);
return Ok();
}

View File

@ -1,40 +0,0 @@
#This Dockerfile pulls the latest git commit and builds Kavita from source
FROM mcr.microsoft.com/dotnet/sdk:6.0-focal AS builder
ENV DEBIAN_FRONTEND=noninteractive
ARG TARGETPLATFORM
#Installs nodejs and npm
RUN curl -fsSL https://deb.nodesource.com/setup_14.x | bash - \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*
#Builds app based on platform
COPY build_target.sh /build_target.sh
RUN /build_target.sh
#Production image
FROM ubuntu:focal
#Move the output files to where they need to be
COPY --from=builder /Projects/Kavita/_output/build/Kavita /kavita
#Installs program dependencies
RUN apt-get update \
&& apt-get install -y libicu-dev libssl1.1 pwgen \
&& rm -rf /var/lib/apt/lists/*
#Creates the manga storage directory
RUN mkdir /manga /kavita/data
RUN cp /kavita/appsettings.Development.json /kavita/appsettings.json \
&& sed -i 's/Data source=kavita.db/Data source=data\/kavita.db/g' /kavita/appsettings.json
COPY entrypoint.sh /entrypoint.sh
EXPOSE 5000
WORKDIR /kavita
ENTRYPOINT ["/bin/bash"]
CMD ["/entrypoint.sh"]

View File

@ -32,6 +32,7 @@ namespace API.Services
string GetCachedEpubFile(int chapterId, Chapter chapter);
public void ExtractChapterFiles(string extractPath, IReadOnlyList<MangaFile> files);
Task<int> CacheBookmarkForSeries(int userId, int seriesId);
void CleanupBookmarkCache(int bookmarkDtoSeriesId);
}
public class CacheService : ICacheService
{
@ -240,5 +241,17 @@ namespace API.Services
_directoryService.Flatten(destDirectory);
return files.Count;
}
/// <summary>
/// Clears a cached bookmarks for a series id folder
/// </summary>
/// <param name="seriesId"></param>
public void CleanupBookmarkCache(int seriesId)
{
var destDirectory = _directoryService.FileSystem.Path.Join(_directoryService.CacheDirectory, seriesId + "_bookmarks");
if (!_directoryService.Exists(destDirectory)) return;
_directoryService.ClearAndDeleteDirectory(destDirectory);
}
}
}

View File

@ -182,7 +182,7 @@ namespace API.Services.Tasks
/// </summary>
public Task CleanupBookmarks()
{
// This is disabled for now while we test and validate a new method of deleting bookmarks
// TODO: This is disabled for now while we test and validate a new method of deleting bookmarks
return Task.CompletedTask;
// Search all files in bookmarks/ except bookmark files and delete those
// var bookmarkDirectory =

View File

@ -215,12 +215,12 @@ public class ScannerService : IScannerService
// That way logging and UI informing is all in one place with full context
_logger.LogError("Some of the root folders for the library are empty. " +
"Either your mount has been disconnected or you are trying to delete all series in the library. " +
"Scan will be aborted. " +
"Scan has be aborted. " +
"Check that your mount is connected or change the library's root folder and rescan");
await _eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent( $"Some of the root folders for the library, {libraryName}, are empty.",
"Either your mount has been disconnected or you are trying to delete all series in the library. " +
"Scan will be aborted. " +
"Scan has be aborted. " +
"Check that your mount is connected or change the library's root folder and rescan"));
return false;

View File

@ -29,7 +29,7 @@ EXPOSE 5000
WORKDIR /kavita
HEALTHCHECK --interval=300s --timeout=15s --start-period=30s --retries=3 CMD curl --fail http://localhost:5000 || exit 1
HEALTHCHECK --interval=30s --timeout=15s --start-period=30s --retries=3 CMD curl --fail http://localhost:5000 || exit 1
ENTRYPOINT [ "/bin/bash" ]
CMD ["/entrypoint.sh"]

View File

@ -2,6 +2,7 @@ import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { map } from 'rxjs/operators';
import { environment } from 'src/environments/environment';
import { UtilityService } from '../shared/_services/utility.service';
import { PaginatedResult } from '../_models/pagination';
import { ReadingList, ReadingListItem } from '../_models/reading-list';
import { ActionItem } from './action-factory.service';
@ -13,7 +14,7 @@ export class ReadingListService {
baseUrl = environment.apiUrl;
constructor(private httpClient: HttpClient) { }
constructor(private httpClient: HttpClient, private utilityService: UtilityService) { }
getReadingList(readingListId: number) {
return this.httpClient.get<ReadingList>(this.baseUrl + 'readinglist?readingListId=' + readingListId);
@ -21,11 +22,11 @@ export class ReadingListService {
getReadingLists(includePromoted: boolean = true, pageNum?: number, itemsPerPage?: number) {
let params = new HttpParams();
params = this._addPaginationIfExists(params, pageNum, itemsPerPage);
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
return this.httpClient.post<PaginatedResult<ReadingList[]>>(this.baseUrl + 'readinglist/lists?includePromoted=' + includePromoted, {}, {observe: 'response', params}).pipe(
map((response: any) => {
return this._cachePaginatedResults(response, new PaginatedResult<ReadingList[]>());
return this.utilityService.createPaginatedResult(response, new PaginatedResult<ReadingList[]>());
})
);
}
@ -86,29 +87,4 @@ export class ReadingListService {
if (readingList?.promoted && !isAdmin) return false;
return true;
}
_addPaginationIfExists(params: HttpParams, pageNum?: number, itemsPerPage?: number) {
// TODO: Move to utility service
if (pageNum !== null && pageNum !== undefined && itemsPerPage !== null && itemsPerPage !== undefined) {
params = params.append('pageNumber', pageNum + '');
params = params.append('pageSize', itemsPerPage + '');
}
return params;
}
_cachePaginatedResults(response: any, paginatedVariable: PaginatedResult<any[]>) {
// TODO: Move to utility service
if (response.body === null) {
paginatedVariable.result = [];
} else {
paginatedVariable.result = response.body;
}
const pageHeader = response.headers.get('Pagination');
if (pageHeader !== null) {
paginatedVariable.pagination = JSON.parse(pageHeader);
}
return paginatedVariable;
}
}

View File

@ -18,6 +18,7 @@ export class ToggleService {
.pipe(filter(event => event instanceof NavigationStart))
.subscribe((event) => {
this.toggleState = false;
this.toggleStateSource.next(this.toggleState);
});
this.toggleStateSource.next(false);
}
@ -26,7 +27,6 @@ export class ToggleService {
this.toggleState = !this.toggleState;
this.toggleStateSource.pipe(take(1)).subscribe(state => {
this.toggleState = !state;
console.log('Toggle: ', this.toggleState)
this.toggleStateSource.next(this.toggleState);
});

View File

@ -47,6 +47,15 @@ export const BookBlackTheme = `
--btn-disabled-text-color: white;
--btn-disabled-border-color: #6c757d;
/* Inputs */
--input-bg-color: #343a40;
--input-bg-readonly-color: #434648;
--input-focused-border-color: #ccc;
--input-text-color: #fff;
--input-placeholder-color: #aeaeae;
--input-border-color: #ccc;
--input-focus-boxshadow-color: rgb(255 255 255 / 50%);
/* Nav (Tabs) */
--nav-tab-border-color: rgba(44, 118, 88, 0.7);
--nav-tab-text-color: var(--body-text-color);

View File

@ -94,7 +94,8 @@
</div>
<!-- TODO: move this inline style into a class -->
<div class="controls" style="display:flex; justify-content:space-between; align-items:center;">
<label id="fullscreen" class="form-label">Fullscreen&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="top" [ngbTooltip]="fullscreenTooltip" role="button" tabindex="1" aria-describedby="fullscreen-help"></i></label>
<label id="fullscreen" class="form-label">Fullscreen&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="top"
[ngbTooltip]="fullscreenTooltip" role="button" tabindex="1" aria-describedby="fullscreen-help"></i></label>
<ng-template #fullscreenTooltip>Put reader in fullscreen mode</ng-template>
<span class="visually-hidden" id="fullscreen-help">
<ng-container [ngTemplateOutlet]="fullscreenTooltip"></ng-container>

View File

@ -1,6 +1,12 @@
.controls {
margin: 0.25rem 0 0.25rem;
.form-select {
option{
background-color: var(--input-bg-color);
}
}
.form-label {
margin: 0;
}

View File

@ -284,6 +284,7 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy {
}
toggleFullscreen() {
this.isFullscreen = !this.isFullscreen;
this.fullscreen.emit();
}
}

View File

@ -64,9 +64,7 @@ export class BookmarksComponent implements OnInit, OnDestroy {
async handleAction(action: Action, series: Series) {
switch (action) {
case(Action.Delete):
if (!await this.confirmService.confirm('Are you sure you want to clear all bookmarks for ' + series.name + '? This cannot be undone.')) {
break;
}
this.clearBookmarks(series);
break;
case(Action.DownloadBookmark):
this.downloadBookmarks(series);

View File

@ -30,7 +30,7 @@
<div style="width: 100%;">
<div class="d-flex">
<div class="col-9 col-lg-10">
<label class="visually-hidden" class="form-label" for="add-rlist">Collection</label>
<label class="form-label visually-hidden" for="add-rlist">Collection</label>
<input width="100%" #title ngbAutofocus type="text" class="form-control mb-2" id="add-rlist" formControlName="title">
</div>
<div class="col-2">

View File

@ -11,7 +11,3 @@
.highlight {
color: var(--bulk-selection-highlight-text-color) !important;
}
::ng-deep button i.fa {
color: var(--bulk-selection-text-color);
}

View File

@ -55,6 +55,8 @@ export class EditSeriesRelationComponent implements OnInit, OnDestroy {
this.setupRelationRows(relations.alternativeSettings, RelationKind.AlternativeSetting);
this.setupRelationRows(relations.alternativeVersions, RelationKind.AlternativeVersion);
this.setupRelationRows(relations.doujinshis, RelationKind.Doujinshi);
this.setupRelationRows(relations.contains, RelationKind.Contains);
this.setupRelationRows(relations.parent, RelationKind.Parent);
});
this.libraryService.getLibraryNames().subscribe(names => {

View File

@ -31,9 +31,9 @@
</div>
</ng-container>
<div (click)="toggleMenu()" class="reading-area" [ngStyle]="{'background-color': backgroundColor}" #readingArea>
<div (click)="toggleMenu()" class="reading-area" [ngStyle]="{'background-color': backgroundColor, 'height': readerMode === ReaderMode.Webtoon ? 'inherit' : 'calc(var(--vh)*100)'}" #readingArea>
<ng-container *ngIf="readerMode !== ReaderMode.Webtoon; else webtoon">
<div [ngClass]="{'d-none': !renderWithCanvas }">
<div class="image-container" [ngClass]="{'d-none': !renderWithCanvas }">
<canvas #content class="{{getFittingOptionClass()}}"
ondragstart="return false;" onselectstart="return false;">
</canvas>
@ -47,7 +47,7 @@
title="Previous Page" aria-hidden="true"></i>
</div>
</div>
<div class="{{readerMode === ReaderMode.LeftRight ? 'right' : 'bottom'}} {{clickOverlayClass('right')}}" (click)="handlePageChange($event, 'right')" [ngStyle]="{'height': (readerMode === ReaderMode.LeftRight ? ImageHeight: 25 + '%')}">
<div class="{{readerMode === ReaderMode.LeftRight ? 'right' : 'bottom'}} {{clickOverlayClass('right')}}" (click)="handlePageChange($event, 'right')" [ngStyle]="{'height': (readerMode === ReaderMode.LeftRight ? ImageHeight: 25 + '%'), 'left': (readerMode === ReaderMode.LeftRight && (this.generalSettingsForm.get('fittingOption')?.value === FITTING_OPTION.ORIGINAL) ? ImageWidth: 'inherit')}">
<div *ngIf="showClickOverlay">
<i class="fa fa-angle-{{readingDirection === ReadingDirection.LeftToRight ? 'double-' : ''}}{{readerMode === ReaderMode.LeftRight ? 'right' : 'down'}}"
title="Next Page" aria-hidden="true"></i>

View File

@ -20,7 +20,7 @@ img {
.reading-area {
position: relative;
overflow: auto;
height: calc(var(--vh)*100);
//height: calc(var(--vh)*100); // this needs to be applied on the DOM because it breaks infinite scroller
}
.image-container {
@ -248,6 +248,9 @@ img {
width: 100%;
}
//$pagination-bg: rgba(0, 0, 0, 0);
$pagination-bg: rgba(0, 0, 255, 0.4);
.pagination-area {
cursor: pointer;
z-index: 2;
@ -262,7 +265,7 @@ img {
right: 0px;
top: 0px;
width: $side-width;
background: rgba(0, 0, 0, 0);
background: $pagination-bg;
}
.top {
@ -270,7 +273,7 @@ img {
right: 0px;
top: 0px;
width: 100%;
background: rgba(0, 0, 0, 0);
background: $pagination-bg;
}
.left {
@ -278,7 +281,7 @@ img {
left: 0px;
top: 0px;
width: $side-width;
background: rgba(0, 0, 0, 0);
background: $pagination-bg;
}
.bottom {
@ -286,7 +289,7 @@ img {
left: 0px;
bottom: 0px;
width: 100%;
background: rgba(0, 0, 0, 0);
background: $pagination-bg;
}
}

View File

@ -283,12 +283,24 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return this.bookmarks.hasOwnProperty(this.pageNum);
}
get WindowWidth() {
return this.readingArea?.nativeElement.scrollWidth + 'px';
}
get WindowHeight() {
return this.readingArea?.nativeElement.scrollHeight + 'px';
}
get ImageWidth() {
return this.image?.nativeElement.width + 'px';
}
get ImageHeight() {
return this.image?.nativeElement.height + 'px';
// If we are a cover image and implied fit to screen, then we need to take screen height rather than image height
if (this.isCoverImage()) {
return this.WindowHeight;
}
return this.image?.nativeElement.height + 'px';
}
get splitIconClass() {
@ -1018,7 +1030,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
if (!this.ctx || !this.canvas) { return; }
this.canvasImage.onload = null;
console.log('canvasImage: ', this.canvasImage?.height);
this.setCanvasSize();
@ -1055,8 +1066,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|| document.documentElement.clientHeight
|| document.body.clientHeight;
console.log(windowHeight);
const needsSplitting = this.isCoverImage();
let newScale = this.generalSettingsForm.get('fittingOption')?.value;
const widthRatio = windowWidth / (this.canvasImage.width / (needsSplitting ? 2 : 1));

View File

@ -10,7 +10,7 @@ export class RelationshipPipe implements PipeTransform {
if (relationship === undefined) return '';
switch (relationship) {
case RelationKind.Adaptation:
return 'Adaptaion';
return 'Adaptation';
case RelationKind.AlternativeSetting:
return 'Alternative Setting';
case RelationKind.AlternativeVersion:

View File

@ -20,7 +20,7 @@
<button class="btn btn-primary" title="Read" (click)="read()">
<span>
<i class="fa fa-book-open" aria-hidden="true"></i>
<span class="read-btn--text">&nbsp;Read</span> <!-- IDEA: We can provide them the ability to read/continue like we do with a series -->
<span class="read-btn--text">&nbsp;Read</span>
</span>
</button>
</div>
@ -47,7 +47,7 @@
<div *ngIf="items.length === 0">
No chapters added
Nothing added
</div>
<app-dragable-ordered-list [items]="items" (orderUpdated)="orderUpdated($event)" (itemRemove)="itemRemoved($event)" [accessibilityMode]="accessibilityMode">

View File

@ -132,9 +132,9 @@
<li [ngbNavItem]="TabID.Related" *ngIf="hasRelations">
<a ngbNavLink>Related</a>
<ng-template ngbNavContent>
<div class="row g-0">
<div class="card-container row g-0">
<ng-container *ngFor="let item of relations; let idx = index; trackBy: trackByRelatedSeriesIdentiy">
<app-series-card class="col-auto" [data]="item.series" [libraryId]="item.series.libraryId" [relation]="item.relation"></app-series-card>
<app-series-card class="col-auto mt-2 mb-2" [data]="item.series" [libraryId]="item.series.libraryId" [relation]="item.relation"></app-series-card>
</ng-container>
</div>
</ng-template>

View File

@ -51,10 +51,13 @@ export class ImageComponent implements OnChanges, OnDestroy {
if (this.imageUrl === undefined || this.imageUrl === null || this.imageUrl === '') return;
const enityType = this.imageService.getEntityTypeFromUrl(this.imageUrl);
if (enityType === updateEvent.entityType) {
const tokens = this.imageUrl.split('?')[1].split('&random=');
const tokens = this.imageUrl.split('?')[1].split('&');
//...seriesId=123&random=
const id = tokens[0].replace(enityType + 'Id=', '');
let id = tokens[0].replace(enityType + 'Id=', '');
if (id.includes('&')) {
id = id.split('&')[0];
}
if (id === (updateEvent.id + '')) {
this.imageUrl = this.imageService.randomize(this.imageUrl);
}

View File

@ -217,7 +217,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
}),
map(val => val.trim()),
auditTime(this.settings.debounce),
distinctUntilChanged(), // ?!: BUG Doesn't trigger the search to run when filtered array changes
//distinctUntilChanged(), // ?!: BUG Doesn't trigger the search to run when filtered array changes
filter(val => {
// If minimum filter characters not met, do not filter
if (this.settings.minCharacters === 0) return true;
@ -405,6 +405,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
openDropdown() {
setTimeout(() => {
this.typeaheadControl.setValue(this.typeaheadControl.value);
this.hasFocus = true;
});
}
@ -454,6 +455,7 @@ 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