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); 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) private void AssertStatementSame(FilterStatementDto statement, FilterField field, FilterComparison combination, string value)

View File

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

View File

@ -1079,10 +1079,11 @@ public class OpdsController : BaseApiController
/// <param name="volumeId"></param> /// <param name="volumeId"></param>
/// <param name="chapterId"></param> /// <param name="chapterId"></param>
/// <param name="pageNumber"></param> /// <param name="pageNumber"></param>
/// <param name="saveProgress">Optional parameter. Can pass false and progress saving will be suppressed</param>
/// <returns></returns> /// <returns></returns>
[HttpGet("{apiKey}/image")] [HttpGet("{apiKey}/image")]
public async Task<ActionResult> GetPageStreamedImage(string apiKey, [FromQuery] int libraryId, [FromQuery] int seriesId, 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); var userId = await GetUser(apiKey);
if (pageNumber < 0) return BadRequest(await _localizationService.Translate(userId, "greater-0", "Page")); 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[] // Calculates SHA1 Hash for byte[]
Response.AddCacheHeader(content); Response.AddCacheHeader(content);
// Save progress for the user // Save progress for the user (except Panels, they will use a direct connection)
await _readerService.SaveReadingProgress(new ProgressDto() var userAgent = Request.Headers["User-Agent"].ToString();
if (!userAgent.StartsWith("Panels", StringComparison.InvariantCultureIgnoreCase) || !saveProgress)
{ {
ChapterId = chapterId, await _readerService.SaveReadingProgress(new ProgressDto()
PageNum = pageNumber, {
SeriesId = seriesId, ChapterId = chapterId,
VolumeId = volumeId, PageNum = pageNumber,
LibraryId =libraryId SeriesId = seriesId,
}, await GetUser(apiKey)); VolumeId = volumeId,
LibraryId =libraryId
}, await GetUser(apiKey));
}
return File(content, MimeTypeMap.GetMimeType(format)); 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) 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) 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 {NavigationStart, Router, RouterOutlet} from '@angular/router';
import {map, shareReplay, take} from 'rxjs/operators'; import {map, shareReplay, take} from 'rxjs/operators';
import { AccountService } from './_services/account.service'; import { AccountService } from './_services/account.service';
@ -26,8 +26,10 @@ export class AppComponent implements OnInit {
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
private readonly offcanvas = inject(NgbOffcanvas); 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 libraryService: LibraryService,
private router: Router, private ngbModal: NgbModal, ratingConfig: NgbRatingConfig, private router: Router, private ngbModal: NgbModal, ratingConfig: NgbRatingConfig,
@Inject(DOCUMENT) private document: Document, private themeService: ThemeService) { @Inject(DOCUMENT) private document: Document, private themeService: ThemeService) {
@ -88,7 +90,7 @@ export class AppComponent implements OnInit {
if (user) { if (user) {
// Bootstrap anything that's needed // Bootstrap anything that's needed
this.themeService.getThemes().subscribe(); 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(); this.loadFromPresetsAndSetup();
} }
loadSavedFilter(event: Select2UpdateEvent<any>) { // loadSavedFilter(event: Select2UpdateEvent<any>) {
// Load the filter from the backend and update the screen // // Load the filter from the backend and update the screen
if (event.value === undefined || typeof(event.value) === 'string') return; // if (event.value === undefined || typeof(event.value) === 'string') return;
const smartFilter = event.value as SmartFilter; // const smartFilter = event.value as SmartFilter;
this.filterV2 = this.filterUtilitiesService.decodeSeriesFilter(smartFilter.filter); // this.filterV2 = this.filterUtilitiesService.decodeSeriesFilter(smartFilter.filter);
this.cdRef.markForCheck(); // this.cdRef.markForCheck();
console.log('update event: ', event); // console.log('update event: ', event);
} // }
//
createFilterValue(event: Select2AutoCreateEvent<any>) { // createFilterValue(event: Select2AutoCreateEvent<any>) {
// Create a new name and filter // // Create a new name and filter
if (!this.filterV2) return; // if (!this.filterV2) return;
this.filterV2.name = event.value; // this.filterV2.name = event.value;
this.filterService.saveFilter(this.filterV2).subscribe(() => { // this.filterService.saveFilter(this.filterV2).subscribe(() => {
//
const item = { // const item = {
value: { // value: {
filter: this.filterUtilitiesService.encodeSeriesFilter(this.filterV2!), // filter: this.filterUtilitiesService.encodeSeriesFilter(this.filterV2!),
name: event.value, // name: event.value,
} as SmartFilter, // } as SmartFilter,
label: event.value // label: event.value
}; // };
this.smartFilters.push(item); // this.smartFilters.push(item);
this.sortGroup.get('name')?.setValue(item); // this.sortGroup.get('name')?.setValue(item);
this.cdRef.markForCheck(); // this.cdRef.markForCheck();
this.toastr.success(translate('toasts.smart-filter-updated')); // this.toastr.success(translate('toasts.smart-filter-updated'));
this.apply(); // this.apply();
}); // });
//
console.log('create event: ', event); // console.log('create event: ', event);
} // }
close() { close() {

View File

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

View File

@ -22,7 +22,7 @@
<ng-container [ngSwitch]="navStream.streamType"> <ng-container [ngSwitch]="navStream.streamType">
<ng-container *ngSwitchCase="SideNavStreamType.Library"> <ng-container *ngSwitchCase="SideNavStreamType.Library">
<app-side-nav-item [link]="'/library/' + navStream.libraryId + '/'" <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> <ng-container actions>
<app-card-actionables [actions]="actions" [labelBy]="navStream.name" iconClass="fa-ellipsis-v" <app-card-actionables [actions]="actions" [labelBy]="navStream.name" iconClass="fa-ellipsis-v"
(actionHandler)="performAction($event, navStream.library!)"></app-card-actionables> (actionHandler)="performAction($event, navStream.library!)"></app-card-actionables>

View File

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

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.7.9.4" "version": "0.7.10.0"
}, },
"servers": [ "servers": [
{ {
@ -4137,6 +4137,15 @@
"type": "integer", "type": "integer",
"format": "int32" "format": "int32"
} }
},
{
"name": "saveProgress",
"in": "query",
"description": "Optional parameter. Can pass false and progress saving will be suppressed",
"schema": {
"type": "boolean",
"default": true
}
} }
], ],
"responses": { "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": { "/api/Plugin/authenticate": {
"post": { "post": {
"tags": [ "tags": [
@ -19687,6 +19791,10 @@
"name": "Image", "name": "Image",
"description": "Responsible for servicing up images stored in Kavita for entities" "description": "Responsible for servicing up images stored in Kavita for entities"
}, },
{
"name": "Panels",
"description": "For the Panels app explicitly"
},
{ {
"name": "Rating", "name": "Rating",
"description": "Responsible for providing external ratings for Series" "description": "Responsible for providing external ratings for Series"