Prep for Hotfix (#2362)

This commit is contained in:
Joe Milazzo 2023-10-29 07:20:19 -05:00 committed by GitHub
parent 0cf760ecd3
commit 30f1cd20a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 252 additions and 72 deletions

View File

@ -56,7 +56,7 @@ public class SmartFilterHelperTests
};
var encodedFilter = SmartFilterHelper.Encode(filter);
Assert.Equal("name=Test&stmts=comparison%253D0%252Cfield%253D4%252Cvalue%253D0&sortOptions=sortField%3D2%26isAscending%3DFalse&limitTo=10&combination=1", encodedFilter);
Assert.Equal("name=Test&stmts=comparison%253D0%252Cfield%253D4%252Cvalue%253D0&sortOptions=sortField%3D2%2CisAscending%3DFalse&limitTo=10&combination=1", encodedFilter);
}
private void AssertStatementSame(FilterStatementDto statement, FilterField field, FilterComparison combination, string value)

View File

@ -412,14 +412,15 @@ public class LibraryController : BaseApiController
[HttpPost("update")]
public async Task<ActionResult> UpdateLibrary(UpdateLibraryDto dto)
{
var userId = User.GetUserId();
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(dto.Id, LibraryIncludes.Folders);
if (library == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "library-doesnt-exist"));
if (library == null) return BadRequest(await _localizationService.Translate(userId, "library-doesnt-exist"));
var newName = dto.Name.Trim();
if (await _unitOfWork.LibraryRepository.LibraryExists(newName) && !library.Name.Equals(newName))
return BadRequest(await _localizationService.Translate(User.GetUserId(), "library-name-exists"));
return BadRequest(await _localizationService.Translate(userId, "library-name-exists"));
var originalFolders = library.Folders.Select(x => x.Path).ToList();
var originalFoldersCount = library.Folders.Count;
library.Name = newName;
library.Folders = dto.Folders.Select(s => new FolderPath() {Path = s}).Distinct().ToList();
@ -445,8 +446,8 @@ public class LibraryController : BaseApiController
_unitOfWork.LibraryRepository.Update(library);
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-library-update"));
if (originalFolders.Count != dto.Folders.Count() || typeUpdate)
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(userId, "generic-library-update"));
if (originalFoldersCount != dto.Folders.Count() || typeUpdate)
{
await _libraryWatcher.RestartWatching();
_taskScheduler.ScanLibrary(library.Id);
@ -458,8 +459,9 @@ public class LibraryController : BaseApiController
}
await _eventHub.SendMessageAsync(MessageFactory.LibraryModified,
MessageFactory.LibraryModifiedEvent(library.Id, "update"), false);
await _eventHub.SendMessageAsync(MessageFactory.SideNavUpdate,
MessageFactory.SideNavUpdateEvent(User.GetUserId()), false);
MessageFactory.SideNavUpdateEvent(userId), false);
await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey);

View File

@ -1079,10 +1079,11 @@ public class OpdsController : BaseApiController
/// <param name="volumeId"></param>
/// <param name="chapterId"></param>
/// <param name="pageNumber"></param>
/// <param name="saveProgress">Optional parameter. Can pass false and progress saving will be suppressed</param>
/// <returns></returns>
[HttpGet("{apiKey}/image")]
public async Task<ActionResult> GetPageStreamedImage(string apiKey, [FromQuery] int libraryId, [FromQuery] int seriesId,
[FromQuery] int volumeId,[FromQuery] int chapterId, [FromQuery] int pageNumber)
[FromQuery] int volumeId,[FromQuery] int chapterId, [FromQuery] int pageNumber, [FromQuery] bool saveProgress = true)
{
var userId = await GetUser(apiKey);
if (pageNumber < 0) return BadRequest(await _localizationService.Translate(userId, "greater-0", "Page"));
@ -1101,15 +1102,20 @@ public class OpdsController : BaseApiController
// Calculates SHA1 Hash for byte[]
Response.AddCacheHeader(content);
// Save progress for the user
await _readerService.SaveReadingProgress(new ProgressDto()
// Save progress for the user (except Panels, they will use a direct connection)
var userAgent = Request.Headers["User-Agent"].ToString();
if (!userAgent.StartsWith("Panels", StringComparison.InvariantCultureIgnoreCase) || !saveProgress)
{
ChapterId = chapterId,
PageNum = pageNumber,
SeriesId = seriesId,
VolumeId = volumeId,
LibraryId =libraryId
}, await GetUser(apiKey));
await _readerService.SaveReadingProgress(new ProgressDto()
{
ChapterId = chapterId,
PageNum = pageNumber,
SeriesId = seriesId,
VolumeId = volumeId,
LibraryId =libraryId
}, await GetUser(apiKey));
}
return File(content, MimeTypeMap.GetMimeType(format));
}

View File

@ -0,0 +1,55 @@
using System.Threading.Tasks;
using API.Data;
using API.DTOs;
using API.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
/// <summary>
/// For the Panels app explicitly
/// </summary>
[AllowAnonymous]
public class PanelsController : BaseApiController
{
private readonly IReaderService _readerService;
private readonly IUnitOfWork _unitOfWork;
public PanelsController(IReaderService readerService, IUnitOfWork unitOfWork)
{
_readerService = readerService;
_unitOfWork = unitOfWork;
}
/// <summary>
/// Saves the progress of a given chapter.
/// </summary>
/// <param name="dto"></param>
/// <param name="apiKey"></param>
/// <returns></returns>
[HttpPost("save-progress")]
public async Task<ActionResult> SaveProgress(ProgressDto dto, [FromQuery] string apiKey)
{
if (string.IsNullOrEmpty(apiKey)) return Unauthorized("ApiKey is required");
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
await _readerService.SaveReadingProgress(dto, userId);
return Ok();
}
/// <summary>
/// Gets the Progress of a given chapter
/// </summary>
/// <param name="chapterId"></param>
/// <param name="apiKey"></param>
/// <returns>The number of pages read, 0 if none read</returns>
[HttpGet("get-progress")]
public async Task<ActionResult<int>> GetProgress(int chapterId, [FromQuery] string apiKey)
{
if (string.IsNullOrEmpty(apiKey)) return Unauthorized("ApiKey is required");
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
var progress = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(chapterId, userId);
return Ok(progress?.PageNum ?? 0);
}
}

View File

@ -72,7 +72,7 @@ public static class SmartFilterHelper
private static string EncodeSortOptions(SortOptions sortOptions)
{
return Uri.EscapeDataString($"sortField={(int) sortOptions.SortField}&isAscending={sortOptions.IsAscending}");
return Uri.EscapeDataString($"sortField={(int) sortOptions.SortField},isAscending={sortOptions.IsAscending}");
}
private static string EncodeFilterStatementDtos(ICollection<FilterStatementDto> statements)

View File

@ -1,4 +1,4 @@
import {Component, DestroyRef, HostListener, inject, Inject, OnInit} from '@angular/core';
import {ChangeDetectorRef, Component, DestroyRef, HostListener, inject, Inject, OnInit} from '@angular/core';
import {NavigationStart, Router, RouterOutlet} from '@angular/router';
import {map, shareReplay, take} from 'rxjs/operators';
import { AccountService } from './_services/account.service';
@ -26,8 +26,10 @@ export class AppComponent implements OnInit {
private readonly destroyRef = inject(DestroyRef);
private readonly offcanvas = inject(NgbOffcanvas);
public readonly navService = inject(NavService);
public readonly cdRef = inject(ChangeDetectorRef);
constructor(private accountService: AccountService, public navService: NavService,
constructor(private accountService: AccountService,
private libraryService: LibraryService,
private router: Router, private ngbModal: NgbModal, ratingConfig: NgbRatingConfig,
@Inject(DOCUMENT) private document: Document, private themeService: ThemeService) {
@ -88,7 +90,7 @@ export class AppComponent implements OnInit {
if (user) {
// Bootstrap anything that's needed
this.themeService.getThemes().subscribe();
this.libraryService.getLibraryNames().pipe(take(1), shareReplay()).subscribe();
this.libraryService.getLibraryNames().pipe(take(1), shareReplay({refCount: true, bufferSize: 1})).subscribe();
}
}
}

View File

@ -121,37 +121,37 @@ export class MetadataFilterComponent implements OnInit {
this.loadFromPresetsAndSetup();
}
loadSavedFilter(event: Select2UpdateEvent<any>) {
// Load the filter from the backend and update the screen
if (event.value === undefined || typeof(event.value) === 'string') return;
const smartFilter = event.value as SmartFilter;
this.filterV2 = this.filterUtilitiesService.decodeSeriesFilter(smartFilter.filter);
this.cdRef.markForCheck();
console.log('update event: ', event);
}
createFilterValue(event: Select2AutoCreateEvent<any>) {
// Create a new name and filter
if (!this.filterV2) return;
this.filterV2.name = event.value;
this.filterService.saveFilter(this.filterV2).subscribe(() => {
const item = {
value: {
filter: this.filterUtilitiesService.encodeSeriesFilter(this.filterV2!),
name: event.value,
} as SmartFilter,
label: event.value
};
this.smartFilters.push(item);
this.sortGroup.get('name')?.setValue(item);
this.cdRef.markForCheck();
this.toastr.success(translate('toasts.smart-filter-updated'));
this.apply();
});
console.log('create event: ', event);
}
// loadSavedFilter(event: Select2UpdateEvent<any>) {
// // Load the filter from the backend and update the screen
// if (event.value === undefined || typeof(event.value) === 'string') return;
// const smartFilter = event.value as SmartFilter;
// this.filterV2 = this.filterUtilitiesService.decodeSeriesFilter(smartFilter.filter);
// this.cdRef.markForCheck();
// console.log('update event: ', event);
// }
//
// createFilterValue(event: Select2AutoCreateEvent<any>) {
// // Create a new name and filter
// if (!this.filterV2) return;
// this.filterV2.name = event.value;
// this.filterService.saveFilter(this.filterV2).subscribe(() => {
//
// const item = {
// value: {
// filter: this.filterUtilitiesService.encodeSeriesFilter(this.filterV2!),
// name: event.value,
// } as SmartFilter,
// label: event.value
// };
// this.smartFilters.push(item);
// this.sortGroup.get('name')?.setValue(item);
// this.cdRef.markForCheck();
// this.toastr.success(translate('toasts.smart-filter-updated'));
// this.apply();
// });
//
// console.log('create event: ', event);
// }
close() {

View File

@ -209,9 +209,9 @@ export class FilterUtilitiesService {
}
decodeFilterStatements(encodedStatements: string): FilterStatement[] {
const statementStrings = decodeURIComponent(encodedStatements).split(',');
const statementStrings = decodeURIComponent(encodedStatements).split(',').map(s => decodeURIComponent(s));
return statementStrings.map(statementString => {
const parts = statementString.split('&');
const parts = statementString.split(',');
if (parts === null || parts.length < 3) return null;
const comparisonStartToken = parts.find(part => part.startsWith('comparison='));

View File

@ -22,7 +22,7 @@
<ng-container [ngSwitch]="navStream.streamType">
<ng-container *ngSwitchCase="SideNavStreamType.Library">
<app-side-nav-item [link]="'/library/' + navStream.libraryId + '/'"
[icon]="getLibraryTypeIcon(navStream.library!.type)" [imageUrl]="getLibraryImage(navStream.library!)" [title]="navStream.name" [comparisonMethod]="'startsWith'">
[icon]="getLibraryTypeIcon(navStream.library!.type)" [imageUrl]="getLibraryImage(navStream.library!)" [title]="navStream.library!.name" [comparisonMethod]="'startsWith'">
<ng-container actions>
<app-card-actionables [actions]="actions" [labelBy]="navStream.name" iconClass="fa-ellipsis-v"
(actionHandler)="performAction($event, navStream.library!)"></app-card-actionables>

View File

@ -8,7 +8,7 @@ import {
} from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import {distinctUntilChanged, filter, map, shareReplay, take, tap} from 'rxjs/operators';
import {distinctUntilChanged, filter, map, take, tap} from 'rxjs/operators';
import { ImportCblModalComponent } from 'src/app/reading-list/_modals/import-cbl-modal/import-cbl-modal.component';
import { ImageService } from 'src/app/_services/image.service';
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
@ -17,7 +17,6 @@ import { Library, LibraryType } from '../../../_models/library';
import { AccountService } from '../../../_services/account.service';
import { Action, ActionFactoryService, ActionItem } from '../../../_services/action-factory.service';
import { ActionService } from '../../../_services/action.service';
import { LibraryService } from '../../../_services/library.service';
import { NavService } from '../../../_services/nav.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {BehaviorSubject, merge, Observable, of, ReplaySubject, startWith, switchMap} from "rxjs";
@ -56,7 +55,18 @@ export class SideNavComponent implements OnInit {
}
showAll: boolean = false;
totalSize = 0;
protected readonly SideNavStreamType = SideNavStreamType;
private readonly router = inject(Router);
private readonly utilityService = inject(UtilityService);
private readonly messageHub = inject(MessageHubService);
private readonly actionService = inject(ActionService);
public readonly navService = inject(NavService);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly ngbModal = inject(NgbModal);
private readonly imageService = inject(ImageService);
public readonly accountService = inject(AccountService);
private showAllSubject = new BehaviorSubject<boolean>(false);
showAll$ = this.showAllSubject.asObservable();
@ -111,25 +121,22 @@ export class SideNavComponent implements OnInit {
takeUntilDestroyed(this.destroyRef),
);
collapseSideNavOnMobileNav$ = this.router.events.pipe(
filter(event => event instanceof NavigationEnd),
takeUntilDestroyed(this.destroyRef),
map(evt => evt as NavigationEnd),
filter(() => this.utilityService.getActiveBreakpoint() < Breakpoint.Tablet),
switchMap(() => this.navService.sideNavCollapsed$),
take(1),
filter(collapsed => !collapsed)
);
constructor(
public utilityService: UtilityService, private messageHub: MessageHubService,
private actionService: ActionService,
public navService: NavService, private router: Router, private readonly cdRef: ChangeDetectorRef,
private ngbModal: NgbModal, private imageService: ImageService, public readonly accountService: AccountService) {
this.router.events.pipe(
filter(event => event instanceof NavigationEnd),
takeUntilDestroyed(this.destroyRef),
map(evt => evt as NavigationEnd),
filter(() => this.utilityService.getActiveBreakpoint() < Breakpoint.Tablet),
switchMap(() => this.navService.sideNavCollapsed$),
take(1),
filter(collapsed => !collapsed)
).subscribe(() => {
constructor() {
this.collapseSideNavOnMobileNav$.subscribe(() => {
this.navService.toggleSideNav();
this.cdRef.markForCheck();
});
});
}
ngOnInit(): void {

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
},
"version": "0.7.9.4"
"version": "0.7.10.0"
},
"servers": [
{
@ -4137,6 +4137,15 @@
"type": "integer",
"format": "int32"
}
},
{
"name": "saveProgress",
"in": "query",
"description": "Optional parameter. Can pass false and progress saving will be suppressed",
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
@ -4168,6 +4177,101 @@
}
}
},
"/api/Panels/save-progress": {
"post": {
"tags": [
"Panels"
],
"summary": "Saves the progress of a given chapter.",
"parameters": [
{
"name": "apiKey",
"in": "query",
"description": "",
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProgressDto"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ProgressDto"
}
},
"application/*+json": {
"schema": {
"$ref": "#/components/schemas/ProgressDto"
}
}
}
},
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/api/Panels/get-progress": {
"get": {
"tags": [
"Panels"
],
"summary": "Gets the Progress of a given chapter",
"parameters": [
{
"name": "chapterId",
"in": "query",
"description": "",
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "apiKey",
"in": "query",
"description": "",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"text/plain": {
"schema": {
"type": "integer",
"format": "int32"
}
},
"application/json": {
"schema": {
"type": "integer",
"format": "int32"
}
},
"text/json": {
"schema": {
"type": "integer",
"format": "int32"
}
}
}
}
}
}
},
"/api/Plugin/authenticate": {
"post": {
"tags": [
@ -19687,6 +19791,10 @@
"name": "Image",
"description": "Responsible for servicing up images stored in Kavita for entities"
},
{
"name": "Panels",
"description": "For the Panels app explicitly"
},
{
"name": "Rating",
"description": "Responsible for providing external ratings for Series"