mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
Misc Fixes (#2155)
* Fixed default token key not being long enough and Kavita auto-generating * When scheduling nightly backup job, make it run at 2am to ensure everything else has ran. * Made the overlay system work better on mobile. In order to do this, had to implement my own copy button. * Tweaked the code to ensure we clear the selection doing anything and clicking off the overlay clears more reliably. * Cleaned up the overlay code * Added the ability to view the series that a rating is representing. Requires Kavita+ deployment. * When calculating overall average rating of server, if only review is yours, don't include it. When calculating overall average rating of server, scale to percentage (* 20) to match all other rating scales. * Fixed side nav on mobile without donate link not fully covering the height of the screen * Only trigger the task conversion warning on Media screen if you've touched the appropriate control. * Fixed a bug where bookmark directory wasn't able to be changed. * Fixed a bug where see More wouldn't show if there were just characters due to missing that check. * Fixed a typo in documentation * If a chapter has a range 1-6 and is fully read, when calculating highest chapter for Scrobbling, use the 6.
This commit is contained in:
parent
9e04276dfd
commit
52d19642f9
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
using API.DTOs;
|
||||
using API.Extensions;
|
||||
using API.Services.Plus;
|
||||
using EasyCaching.Core;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -69,7 +70,7 @@ public class RatingController : BaseApiController
|
||||
return Ok(new RatingDto()
|
||||
{
|
||||
Provider = ScrobbleProvider.Kavita,
|
||||
AverageScore = await _unitOfWork.SeriesRepository.GetAverageUserRating(seriesId),
|
||||
AverageScore = await _unitOfWork.SeriesRepository.GetAverageUserRating(seriesId, User.GetUserId()),
|
||||
FavoriteCount = 0
|
||||
});
|
||||
}
|
||||
|
@ -7,4 +7,5 @@ public class RatingDto
|
||||
public int AverageScore { get; set; }
|
||||
public int FavoriteCount { get; set; }
|
||||
public ScrobbleProvider Provider { get; set; }
|
||||
public string? ProviderUrl { get; set; }
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using API.Data.ManualMigrations;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -164,9 +165,9 @@ public class AppUserProgressRepository : IAppUserProgressRepository
|
||||
(appUserProgresses, chapter) => new {appUserProgresses, chapter})
|
||||
.Where(p => p.appUserProgresses.SeriesId == seriesId && p.appUserProgresses.AppUserId == userId &&
|
||||
p.appUserProgresses.PagesRead >= p.chapter.Pages)
|
||||
.Select(p => p.chapter.Number)
|
||||
.Select(p => p.chapter.Range)
|
||||
.ToListAsync();
|
||||
return list.Count == 0 ? 0 : list.DefaultIfEmpty().Where(d => d != null).Max(d => (int) Math.Floor(float.Parse(d)));
|
||||
return list.Count == 0 ? 0 : list.DefaultIfEmpty().Where(d => d != null).Max(d => (int) Math.Floor(Parser.MaxNumberFromRange(d)));
|
||||
}
|
||||
|
||||
public async Task<int> GetHighestFullyReadVolumeForSeries(int seriesId, int userId)
|
||||
|
@ -137,7 +137,7 @@ public interface ISeriesRepository
|
||||
Task<IList<SeriesMetadataDto>> GetSeriesMetadataForIds(IEnumerable<int> seriesIds);
|
||||
Task<IList<Series>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, bool customOnly = true);
|
||||
Task<SeriesDto?> GetSeriesDtoByNamesAndMetadataIdsForUser(int userId, IEnumerable<string> names, LibraryType libraryType, string aniListUrl, string malUrl);
|
||||
Task<int> GetAverageUserRating(int seriesId);
|
||||
Task<int> GetAverageUserRating(int seriesId, int userId);
|
||||
Task RemoveFromOnDeck(int seriesId, int userId);
|
||||
Task ClearOnDeckRemoval(int seriesId, int userId);
|
||||
}
|
||||
@ -1682,12 +1682,19 @@ public class SeriesRepository : ISeriesRepository
|
||||
/// Returns the Average rating for all users within Kavita instance
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
public async Task<int> GetAverageUserRating(int seriesId)
|
||||
public async Task<int> GetAverageUserRating(int seriesId, int userId)
|
||||
{
|
||||
// If there is 0 or 1 rating and that rating is you, return 0 back
|
||||
var countOfRatingsThatAreUser = await _context.AppUserRating
|
||||
.Where(r => r.SeriesId == seriesId).CountAsync(u => u.AppUserId == userId);
|
||||
if (countOfRatingsThatAreUser == 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
var avg = (await _context.AppUserRating
|
||||
.Where(r => r.SeriesId == seriesId)
|
||||
.AverageAsync(r => (int?) r.Rating));
|
||||
return avg.HasValue ? (int) avg.Value : 0;
|
||||
return avg.HasValue ? (int) (avg.Value * 20) : 0;
|
||||
}
|
||||
|
||||
public async Task RemoveFromOnDeck(int seriesId, int userId)
|
||||
|
@ -127,7 +127,13 @@ public class TaskScheduler : ITaskScheduler
|
||||
if (setting != null)
|
||||
{
|
||||
_logger.LogDebug("Scheduling Backup Task for {Setting}", setting);
|
||||
RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), () => CronConverter.ConvertToCronNotation(setting), RecurringJobOptions);
|
||||
var schedule = CronConverter.ConvertToCronNotation(setting);
|
||||
if (schedule == Cron.Daily())
|
||||
{
|
||||
// Override daily and make 2am so that everything on system has cleaned up and no blocking
|
||||
schedule = Cron.Daily(2);
|
||||
}
|
||||
RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), () => schedule, RecurringJobOptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -65,7 +65,7 @@ public class PresenceTracker : IPresenceTracker
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
// Swallow the exception
|
||||
}
|
||||
|
@ -100,7 +100,7 @@ public static class Configuration
|
||||
{
|
||||
try
|
||||
{
|
||||
return GetJwtToken(GetAppSettingFilename()) != "super secret unguessable key";
|
||||
return !GetJwtToken(GetAppSettingFilename()).StartsWith("super secret unguessable key");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -5,4 +5,5 @@ export interface Rating {
|
||||
meanScore: number;
|
||||
favoriteCount: number;
|
||||
provider: ScrobbleProvider;
|
||||
providerUrl: string | undefined;
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="row g-0">
|
||||
<p>WebP/AVIF can drastically reduce space requirements for files. WebP/AVIF is not supported on all browsers or versions. To learn if these settings are appropriate for your setup, visit <a href="https://caniuse.com/?search=webp" target="_blank" rel="noopener noreferrer">Can I Use WebP</a> or <a href="https://caniuse.com/?search=avif" target="_blank" rel="noopener noreferrer">Can I Use AVIF</a>.
|
||||
<b>You cannot convert back to PNG once you've gone to WebP/AVIF. You would need to refresh covers on your libraries to regenerate all covers. Bookmarks and favicons cannot be converted.</b></p>
|
||||
<div *ngIf="settingsForm.dirty" class="alert alert-danger" role="alert">You must trigger the media conversion task in Tasks Tab.</div>
|
||||
<div *ngIf="settingsForm.get('encodeMediaAs')?.dirty" class="alert alert-danger" role="alert">You must trigger the media conversion task in Tasks Tab.</div>
|
||||
<div class="col-md-6 col-sm-12 mb-3">
|
||||
<label for="settings-media-encodeMediaAs" class="form-label me-1">Save Media As</label>
|
||||
<i class="fa fa-info-circle" placement="right" [ngbTooltip]="encodeMediaAsTooltip" role="button" tabindex="0"></i>
|
||||
|
@ -47,6 +47,7 @@ export class ManageMediaSettingsComponent implements OnInit {
|
||||
saveSettings() {
|
||||
const modelSettings = Object.assign({}, this.serverSettings);
|
||||
modelSettings.encodeMediaAs = parseInt(this.settingsForm.get('encodeMediaAs')?.value, 10);
|
||||
modelSettings.bookmarksDirectory = this.settingsForm.get('bookmarksDirectory')?.value;
|
||||
|
||||
this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe(async (settings: ServerSettings) => {
|
||||
this.serverSettings = settings;
|
||||
|
@ -5,10 +5,25 @@
|
||||
<ng-container [ngSwitch]="mode">
|
||||
<ng-container *ngSwitchCase="BookLineOverlayMode.None">
|
||||
<div class="row g-0">
|
||||
<button class="btn btn-icon" (click)="switchMode(BookLineOverlayMode.Bookmark)">
|
||||
<i class="fa-solid fa-book-bookmark" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">Create Bookmark</span>
|
||||
</button>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-icon btn-sm" (click)="copy()">
|
||||
<i class="fa-solid fa-copy" aria-hidden="true"></i>
|
||||
<div>Copy</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-icon btn-sm" (click)="switchMode(BookLineOverlayMode.Bookmark)">
|
||||
<i class="fa-solid fa-book-bookmark" aria-hidden="true"></i>
|
||||
<div>Bookmark</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-icon btn-sm" (click)="reset()">
|
||||
<i class="fa-solid fa-times-circle" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">Close</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="BookLineOverlayMode.Bookmark">
|
||||
|
@ -8,12 +8,13 @@ import {
|
||||
OnInit, Output,
|
||||
} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {fromEvent, of} from "rxjs";
|
||||
import {fromEvent, merge, of} from "rxjs";
|
||||
import {catchError, filter, tap} from "rxjs/operators";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import getBoundingClientRect from "@popperjs/core/lib/dom-utils/getBoundingClientRect";
|
||||
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||
import {ReaderService} from "../../../_services/reader.service";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
|
||||
enum BookLineOverlayMode {
|
||||
None = 0,
|
||||
@ -39,6 +40,7 @@ export class BookLineOverlayComponent implements OnInit {
|
||||
|
||||
xPath: string = '';
|
||||
selectedText: string = '';
|
||||
previousSelection: string = '';
|
||||
overlayPosition: { top: number; left: number } = { top: 0, left: 0 };
|
||||
mode: BookLineOverlayMode = BookLineOverlayMode.None;
|
||||
bookmarkForm: FormGroup = new FormGroup({
|
||||
@ -50,47 +52,56 @@ export class BookLineOverlayComponent implements OnInit {
|
||||
private readonly readerService = inject(ReaderService);
|
||||
|
||||
get BookLineOverlayMode() { return BookLineOverlayMode; }
|
||||
constructor(private elementRef: ElementRef) {}
|
||||
constructor(private elementRef: ElementRef, private toastr: ToastrService) {}
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
if (this.parent) {
|
||||
fromEvent<MouseEvent>(this.parent.nativeElement, 'mouseup')
|
||||
.pipe(takeUntilDestroyed(this.destroyRef),
|
||||
tap((event: MouseEvent) => {
|
||||
const selection = window.getSelection();
|
||||
if (!event.target) return;
|
||||
|
||||
if (this.mode !== BookLineOverlayMode.None && (!selection || selection.toString().trim() === '')) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
const mouseUp$ = fromEvent<MouseEvent>(this.parent.nativeElement, 'mouseup');
|
||||
const touchEnd$ = fromEvent<TouchEvent>(this.parent.nativeElement, 'touchend');
|
||||
|
||||
this.selectedText = selection ? selection.toString().trim() : '';
|
||||
|
||||
if (this.selectedText.length > 0 && this.mode === BookLineOverlayMode.None) {
|
||||
// Get x,y coord so we can position overlay
|
||||
if (event.target) {
|
||||
const range = selection!.getRangeAt(0)
|
||||
const rect = range.getBoundingClientRect();
|
||||
const box = getBoundingClientRect(event.target as Element);
|
||||
this.xPath = this.readerService.getXPathTo(event.target);
|
||||
if (this.xPath !== '') {
|
||||
this.xPath = '//' + this.xPath;
|
||||
}
|
||||
|
||||
this.overlayPosition = {
|
||||
top: rect.top + window.scrollY - 64 - rect.height, // 64px is the top menu area
|
||||
left: rect.left + window.scrollX + 30 // Adjust 10 to center the overlay box horizontally
|
||||
};
|
||||
}
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}))
|
||||
.subscribe();
|
||||
merge(mouseUp$, touchEnd$)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((event: MouseEvent | TouchEvent) => {
|
||||
this.handleEvent(event);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleEvent(event: MouseEvent | TouchEvent) {
|
||||
const selection = window.getSelection();
|
||||
if (!event.target) return;
|
||||
|
||||
if ((!selection || selection.toString().trim() === '' || selection.toString().trim() === this.selectedText)) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedText = selection ? selection.toString().trim() : '';
|
||||
|
||||
if (this.selectedText.length > 0 && this.mode === BookLineOverlayMode.None) {
|
||||
// Get x,y coord so we can position overlay
|
||||
if (event.target) {
|
||||
const range = selection!.getRangeAt(0)
|
||||
const rect = range.getBoundingClientRect();
|
||||
const box = getBoundingClientRect(event.target as Element);
|
||||
this.xPath = this.readerService.getXPathTo(event.target);
|
||||
if (this.xPath !== '') {
|
||||
this.xPath = '//' + this.xPath;
|
||||
}
|
||||
|
||||
this.overlayPosition = {
|
||||
top: rect.top + window.scrollY - 64 - rect.height, // 64px is the top menu area
|
||||
left: rect.left + window.scrollX + 30 // Adjust 10 to center the overlay box horizontally
|
||||
};
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
switchMode(mode: BookLineOverlayMode) {
|
||||
this.mode = mode;
|
||||
this.cdRef.markForCheck();
|
||||
@ -122,7 +133,21 @@ export class BookLineOverlayComponent implements OnInit {
|
||||
this.mode = BookLineOverlayMode.None;
|
||||
this.xPath = '';
|
||||
this.selectedText = '';
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
selection.removeAllRanges();
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
async copy() {
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
await navigator.clipboard.writeText(selection.toString());
|
||||
this.toastr.info('Copied to clipboard');
|
||||
}
|
||||
this.reset();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import { NgIf, NgTemplateOutlet } from '@angular/common';
|
||||
import { SplashContainerComponent } from '../splash-container/splash-container.component';
|
||||
|
||||
/**
|
||||
* This is exclusivly used to register the first user on the server and nothing else
|
||||
* This is exclusively used to register the first user on the server and nothing else
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-register',
|
||||
@ -28,9 +28,9 @@ export class RegisterComponent {
|
||||
password: new FormControl('', [Validators.required, Validators.maxLength(32), Validators.minLength(6), Validators.pattern("^.{6,32}$")]),
|
||||
});
|
||||
|
||||
constructor(private router: Router, private accountService: AccountService,
|
||||
constructor(private router: Router, private accountService: AccountService,
|
||||
private toastr: ToastrService, private memberService: MemberService) {
|
||||
|
||||
|
||||
this.memberService.adminExists().pipe(take(1)).subscribe(adminExists => {
|
||||
if (adminExists) {
|
||||
this.router.navigateByUrl('login');
|
||||
|
@ -30,5 +30,6 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #externalPopContent let-rating="rating">
|
||||
<i class="fa-solid fa-heart" aria-hidden="true"></i> {{rating.favoriteCount}}
|
||||
<div><i class="fa-solid fa-heart" aria-hidden="true"></i> {{rating.favoriteCount}}</div>
|
||||
<a *ngIf="rating.providerUrl" [href]="rating.providerUrl" target="_blank" rel="noreferrer nofollow">Entry</a>
|
||||
</ng-template>
|
||||
|
@ -74,8 +74,10 @@ export class SeriesMetadataDetailComponent implements OnChanges {
|
||||
this.seriesMetadata.letterers.length > 0 ||
|
||||
this.seriesMetadata.pencillers.length > 0 ||
|
||||
this.seriesMetadata.publishers.length > 0 ||
|
||||
this.seriesMetadata.characters.length > 0 ||
|
||||
this.seriesMetadata.translators.length > 0;
|
||||
|
||||
|
||||
if (this.seriesMetadata !== null) {
|
||||
this.seriesSummary = (this.seriesMetadata.summary === null ? '' : this.seriesMetadata.summary).replace(/\n/g, '<br>');
|
||||
}
|
||||
|
@ -120,6 +120,10 @@
|
||||
border-top-left-radius: var(--side-nav-border-radius);
|
||||
border-top-right-radius: var(--side-nav-border-radius);
|
||||
}
|
||||
|
||||
&.no-donate {
|
||||
height: calc((var(--vh)*100) - 56px);
|
||||
}
|
||||
}
|
||||
|
||||
.side-nav-overlay {
|
||||
|
@ -7,7 +7,7 @@
|
||||
"name": "GPL-3.0",
|
||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||
},
|
||||
"version": "0.7.5.1"
|
||||
"version": "0.7.5.2"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
@ -14095,6 +14095,10 @@
|
||||
"type": "integer",
|
||||
"description": "Misleading name but is the source of data (like a review coming from AniList)",
|
||||
"format": "int32"
|
||||
},
|
||||
"providerUrl": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
Loading…
x
Reference in New Issue
Block a user