diff --git a/API/Controllers/RatingController.cs b/API/Controllers/RatingController.cs index 4b816bab7..48e609d6b 100644 --- a/API/Controllers/RatingController.cs +++ b/API/Controllers/RatingController.cs @@ -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 }); } diff --git a/API/DTOs/RatingDto.cs b/API/DTOs/RatingDto.cs index fd3299868..89f4aebe5 100644 --- a/API/DTOs/RatingDto.cs +++ b/API/DTOs/RatingDto.cs @@ -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; } } diff --git a/API/Data/Repositories/AppUserProgressRepository.cs b/API/Data/Repositories/AppUserProgressRepository.cs index 070cc1cf5..e93bcc753 100644 --- a/API/Data/Repositories/AppUserProgressRepository.cs +++ b/API/Data/Repositories/AppUserProgressRepository.cs @@ -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 GetHighestFullyReadVolumeForSeries(int seriesId, int userId) diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index cf47e8688..4322181ca 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -137,7 +137,7 @@ public interface ISeriesRepository Task> GetSeriesMetadataForIds(IEnumerable seriesIds); Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, bool customOnly = true); Task GetSeriesDtoByNamesAndMetadataIdsForUser(int userId, IEnumerable names, LibraryType libraryType, string aniListUrl, string malUrl); - Task GetAverageUserRating(int seriesId); + Task 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 /// /// - public async Task GetAverageUserRating(int seriesId) + public async Task 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) diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index 4e7ea9f65..a7a309844 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -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 { diff --git a/API/SignalR/Presence/PresenceTracker.cs b/API/SignalR/Presence/PresenceTracker.cs index 60d53bcac..57413442c 100644 --- a/API/SignalR/Presence/PresenceTracker.cs +++ b/API/SignalR/Presence/PresenceTracker.cs @@ -65,7 +65,7 @@ public class PresenceTracker : IPresenceTracker _unitOfWork.UserRepository.Update(user); await _unitOfWork.CommitAsync(); } - catch (Exception ex) + catch (Exception) { // Swallow the exception } diff --git a/Kavita.Common/Configuration.cs b/Kavita.Common/Configuration.cs index 56c3f0401..4d04eb94d 100644 --- a/Kavita.Common/Configuration.cs +++ b/Kavita.Common/Configuration.cs @@ -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) { diff --git a/UI/Web/src/app/_models/rating.ts b/UI/Web/src/app/_models/rating.ts index e501aa30a..a4c4b79ed 100644 --- a/UI/Web/src/app/_models/rating.ts +++ b/UI/Web/src/app/_models/rating.ts @@ -5,4 +5,5 @@ export interface Rating { meanScore: number; favoriteCount: number; provider: ScrobbleProvider; + providerUrl: string | undefined; } diff --git a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.html b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.html index 88ae883f3..1768f4c83 100644 --- a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.html +++ b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.html @@ -4,7 +4,7 @@

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 Can I Use WebP or Can I Use AVIF. 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.

- +
diff --git a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts index bfe408d4b..605ba4814 100644 --- a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts +++ b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts @@ -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; diff --git a/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.html b/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.html index 2e4aefafb..3f4d3efcd 100644 --- a/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.html +++ b/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.html @@ -5,10 +5,25 @@
- +
+ +
+
+ +
+ +
+ +
diff --git a/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.ts b/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.ts index a985f1bec..504af06cf 100644 --- a/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.ts +++ b/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.ts @@ -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(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(this.parent.nativeElement, 'mouseup'); + const touchEnd$ = fromEvent(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(); + } + + } diff --git a/UI/Web/src/app/registration/_components/register/register.component.ts b/UI/Web/src/app/registration/_components/register/register.component.ts index bf2acbe85..0356d6e84 100644 --- a/UI/Web/src/app/registration/_components/register/register.component.ts +++ b/UI/Web/src/app/registration/_components/register/register.component.ts @@ -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'); diff --git a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.html b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.html index 290fe4f44..c384113a6 100644 --- a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.html +++ b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.html @@ -30,5 +30,6 @@ - {{rating.favoriteCount}} +
{{rating.favoriteCount}}
+ Entry
diff --git a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts index 0035c8be5..9da47a716 100644 --- a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts @@ -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, '
'); } diff --git a/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.scss b/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.scss index 05bf23368..b08c8ae82 100644 --- a/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.scss +++ b/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.scss @@ -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 { diff --git a/openapi.json b/openapi.json index 9f7acc96d..e7b157700 100644 --- a/openapi.json +++ b/openapi.json @@ -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