mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-06-02 21:24:18 -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.Constants;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
|
using API.Extensions;
|
||||||
using API.Services.Plus;
|
using API.Services.Plus;
|
||||||
using EasyCaching.Core;
|
using EasyCaching.Core;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -69,7 +70,7 @@ public class RatingController : BaseApiController
|
|||||||
return Ok(new RatingDto()
|
return Ok(new RatingDto()
|
||||||
{
|
{
|
||||||
Provider = ScrobbleProvider.Kavita,
|
Provider = ScrobbleProvider.Kavita,
|
||||||
AverageScore = await _unitOfWork.SeriesRepository.GetAverageUserRating(seriesId),
|
AverageScore = await _unitOfWork.SeriesRepository.GetAverageUserRating(seriesId, User.GetUserId()),
|
||||||
FavoriteCount = 0
|
FavoriteCount = 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -7,4 +7,5 @@ public class RatingDto
|
|||||||
public int AverageScore { get; set; }
|
public int AverageScore { get; set; }
|
||||||
public int FavoriteCount { get; set; }
|
public int FavoriteCount { get; set; }
|
||||||
public ScrobbleProvider Provider { 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.DTOs;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using AutoMapper.QueryableExtensions;
|
using AutoMapper.QueryableExtensions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@ -164,9 +165,9 @@ public class AppUserProgressRepository : IAppUserProgressRepository
|
|||||||
(appUserProgresses, chapter) => new {appUserProgresses, chapter})
|
(appUserProgresses, chapter) => new {appUserProgresses, chapter})
|
||||||
.Where(p => p.appUserProgresses.SeriesId == seriesId && p.appUserProgresses.AppUserId == userId &&
|
.Where(p => p.appUserProgresses.SeriesId == seriesId && p.appUserProgresses.AppUserId == userId &&
|
||||||
p.appUserProgresses.PagesRead >= p.chapter.Pages)
|
p.appUserProgresses.PagesRead >= p.chapter.Pages)
|
||||||
.Select(p => p.chapter.Number)
|
.Select(p => p.chapter.Range)
|
||||||
.ToListAsync();
|
.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)
|
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<SeriesMetadataDto>> GetSeriesMetadataForIds(IEnumerable<int> seriesIds);
|
||||||
Task<IList<Series>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, bool customOnly = true);
|
Task<IList<Series>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, bool customOnly = true);
|
||||||
Task<SeriesDto?> GetSeriesDtoByNamesAndMetadataIdsForUser(int userId, IEnumerable<string> names, LibraryType libraryType, string aniListUrl, string malUrl);
|
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 RemoveFromOnDeck(int seriesId, int userId);
|
||||||
Task ClearOnDeckRemoval(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
|
/// Returns the Average rating for all users within Kavita instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="seriesId"></param>
|
/// <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
|
var avg = (await _context.AppUserRating
|
||||||
.Where(r => r.SeriesId == seriesId)
|
.Where(r => r.SeriesId == seriesId)
|
||||||
.AverageAsync(r => (int?) r.Rating));
|
.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)
|
public async Task RemoveFromOnDeck(int seriesId, int userId)
|
||||||
|
@ -127,7 +127,13 @@ public class TaskScheduler : ITaskScheduler
|
|||||||
if (setting != null)
|
if (setting != null)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Scheduling Backup Task for {Setting}", setting);
|
_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
|
else
|
||||||
{
|
{
|
||||||
|
@ -65,7 +65,7 @@ public class PresenceTracker : IPresenceTracker
|
|||||||
_unitOfWork.UserRepository.Update(user);
|
_unitOfWork.UserRepository.Update(user);
|
||||||
await _unitOfWork.CommitAsync();
|
await _unitOfWork.CommitAsync();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
// Swallow the exception
|
// Swallow the exception
|
||||||
}
|
}
|
||||||
|
@ -100,7 +100,7 @@ public static class Configuration
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return GetJwtToken(GetAppSettingFilename()) != "super secret unguessable key";
|
return !GetJwtToken(GetAppSettingFilename()).StartsWith("super secret unguessable key");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -5,4 +5,5 @@ export interface Rating {
|
|||||||
meanScore: number;
|
meanScore: number;
|
||||||
favoriteCount: number;
|
favoriteCount: number;
|
||||||
provider: ScrobbleProvider;
|
provider: ScrobbleProvider;
|
||||||
|
providerUrl: string | undefined;
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="row g-0">
|
<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>.
|
<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>
|
<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">
|
<div class="col-md-6 col-sm-12 mb-3">
|
||||||
<label for="settings-media-encodeMediaAs" class="form-label me-1">Save Media As</label>
|
<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>
|
<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() {
|
saveSettings() {
|
||||||
const modelSettings = Object.assign({}, this.serverSettings);
|
const modelSettings = Object.assign({}, this.serverSettings);
|
||||||
modelSettings.encodeMediaAs = parseInt(this.settingsForm.get('encodeMediaAs')?.value, 10);
|
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.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe(async (settings: ServerSettings) => {
|
||||||
this.serverSettings = settings;
|
this.serverSettings = settings;
|
||||||
|
@ -5,10 +5,25 @@
|
|||||||
<ng-container [ngSwitch]="mode">
|
<ng-container [ngSwitch]="mode">
|
||||||
<ng-container *ngSwitchCase="BookLineOverlayMode.None">
|
<ng-container *ngSwitchCase="BookLineOverlayMode.None">
|
||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<button class="btn btn-icon" (click)="switchMode(BookLineOverlayMode.Bookmark)">
|
<div class="col-auto">
|
||||||
<i class="fa-solid fa-book-bookmark" aria-hidden="true"></i>
|
<button class="btn btn-icon btn-sm" (click)="copy()">
|
||||||
<span class="visually-hidden">Create Bookmark</span>
|
<i class="fa-solid fa-copy" aria-hidden="true"></i>
|
||||||
</button>
|
<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>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngSwitchCase="BookLineOverlayMode.Bookmark">
|
<ng-container *ngSwitchCase="BookLineOverlayMode.Bookmark">
|
||||||
|
@ -8,12 +8,13 @@ import {
|
|||||||
OnInit, Output,
|
OnInit, Output,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {CommonModule} from '@angular/common';
|
import {CommonModule} from '@angular/common';
|
||||||
import {fromEvent, of} from "rxjs";
|
import {fromEvent, merge, of} from "rxjs";
|
||||||
import {catchError, filter, tap} from "rxjs/operators";
|
import {catchError, filter, tap} from "rxjs/operators";
|
||||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||||
import getBoundingClientRect from "@popperjs/core/lib/dom-utils/getBoundingClientRect";
|
import getBoundingClientRect from "@popperjs/core/lib/dom-utils/getBoundingClientRect";
|
||||||
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||||
import {ReaderService} from "../../../_services/reader.service";
|
import {ReaderService} from "../../../_services/reader.service";
|
||||||
|
import {ToastrService} from "ngx-toastr";
|
||||||
|
|
||||||
enum BookLineOverlayMode {
|
enum BookLineOverlayMode {
|
||||||
None = 0,
|
None = 0,
|
||||||
@ -39,6 +40,7 @@ export class BookLineOverlayComponent implements OnInit {
|
|||||||
|
|
||||||
xPath: string = '';
|
xPath: string = '';
|
||||||
selectedText: string = '';
|
selectedText: string = '';
|
||||||
|
previousSelection: string = '';
|
||||||
overlayPosition: { top: number; left: number } = { top: 0, left: 0 };
|
overlayPosition: { top: number; left: number } = { top: 0, left: 0 };
|
||||||
mode: BookLineOverlayMode = BookLineOverlayMode.None;
|
mode: BookLineOverlayMode = BookLineOverlayMode.None;
|
||||||
bookmarkForm: FormGroup = new FormGroup({
|
bookmarkForm: FormGroup = new FormGroup({
|
||||||
@ -50,47 +52,56 @@ export class BookLineOverlayComponent implements OnInit {
|
|||||||
private readonly readerService = inject(ReaderService);
|
private readonly readerService = inject(ReaderService);
|
||||||
|
|
||||||
get BookLineOverlayMode() { return BookLineOverlayMode; }
|
get BookLineOverlayMode() { return BookLineOverlayMode; }
|
||||||
constructor(private elementRef: ElementRef) {}
|
constructor(private elementRef: ElementRef, private toastr: ToastrService) {}
|
||||||
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
if (this.parent) {
|
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() === '')) {
|
const mouseUp$ = fromEvent<MouseEvent>(this.parent.nativeElement, 'mouseup');
|
||||||
this.reset();
|
const touchEnd$ = fromEvent<TouchEvent>(this.parent.nativeElement, 'touchend');
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectedText = selection ? selection.toString().trim() : '';
|
merge(mouseUp$, touchEnd$)
|
||||||
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
if (this.selectedText.length > 0 && this.mode === BookLineOverlayMode.None) {
|
.subscribe((event: MouseEvent | TouchEvent) => {
|
||||||
// Get x,y coord so we can position overlay
|
this.handleEvent(event);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
switchMode(mode: BookLineOverlayMode) {
|
||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
@ -122,7 +133,21 @@ export class BookLineOverlayComponent implements OnInit {
|
|||||||
this.mode = BookLineOverlayMode.None;
|
this.mode = BookLineOverlayMode.None;
|
||||||
this.xPath = '';
|
this.xPath = '';
|
||||||
this.selectedText = '';
|
this.selectedText = '';
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (selection) {
|
||||||
|
selection.removeAllRanges();
|
||||||
|
}
|
||||||
this.cdRef.markForCheck();
|
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';
|
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({
|
@Component({
|
||||||
selector: 'app-register',
|
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}$")]),
|
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) {
|
private toastr: ToastrService, private memberService: MemberService) {
|
||||||
|
|
||||||
this.memberService.adminExists().pipe(take(1)).subscribe(adminExists => {
|
this.memberService.adminExists().pipe(take(1)).subscribe(adminExists => {
|
||||||
if (adminExists) {
|
if (adminExists) {
|
||||||
this.router.navigateByUrl('login');
|
this.router.navigateByUrl('login');
|
||||||
|
@ -30,5 +30,6 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #externalPopContent let-rating="rating">
|
<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>
|
</ng-template>
|
||||||
|
@ -74,8 +74,10 @@ export class SeriesMetadataDetailComponent implements OnChanges {
|
|||||||
this.seriesMetadata.letterers.length > 0 ||
|
this.seriesMetadata.letterers.length > 0 ||
|
||||||
this.seriesMetadata.pencillers.length > 0 ||
|
this.seriesMetadata.pencillers.length > 0 ||
|
||||||
this.seriesMetadata.publishers.length > 0 ||
|
this.seriesMetadata.publishers.length > 0 ||
|
||||||
|
this.seriesMetadata.characters.length > 0 ||
|
||||||
this.seriesMetadata.translators.length > 0;
|
this.seriesMetadata.translators.length > 0;
|
||||||
|
|
||||||
|
|
||||||
if (this.seriesMetadata !== null) {
|
if (this.seriesMetadata !== null) {
|
||||||
this.seriesSummary = (this.seriesMetadata.summary === null ? '' : this.seriesMetadata.summary).replace(/\n/g, '<br>');
|
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-left-radius: var(--side-nav-border-radius);
|
||||||
border-top-right-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 {
|
.side-nav-overlay {
|
||||||
|
@ -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.5.1"
|
"version": "0.7.5.2"
|
||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
@ -14095,6 +14095,10 @@
|
|||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Misleading name but is the source of data (like a review coming from AniList)",
|
"description": "Misleading name but is the source of data (like a review coming from AniList)",
|
||||||
"format": "int32"
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"providerUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
Loading…
x
Reference in New Issue
Block a user