Bugs from UX Overhaul (#3117)

Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
Joe Milazzo 2024-08-17 18:07:56 -05:00 committed by GitHub
parent 3b915a8289
commit d4bcd354dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 479 additions and 637 deletions

View File

@ -34,7 +34,7 @@ public class CblController : BaseApiController
/// <param name="comicVineMatching">Use comic vine matching or not. Defaults to false</param>
/// <returns></returns>
[HttpPost("validate")]
public async Task<ActionResult<CblImportSummaryDto>> ValidateCbl(IFormFile cbl, bool comicVineMatching = false)
public async Task<ActionResult<CblImportSummaryDto>> ValidateCbl(IFormFile cbl, [FromForm] bool comicVineMatching = false)
{
var userId = User.GetUserId();
try
@ -85,7 +85,7 @@ public class CblController : BaseApiController
/// <param name="comicVineMatching">Use comic vine matching or not. Defaults to false</param>
/// <returns></returns>
[HttpPost("import")]
public async Task<ActionResult<CblImportSummaryDto>> ImportCbl(IFormFile cbl, bool dryRun = false, bool comicVineMatching = false)
public async Task<ActionResult<CblImportSummaryDto>> ImportCbl(IFormFile cbl, [FromForm] bool dryRun = false, [FromForm] bool comicVineMatching = false)
{
try
{

View File

@ -881,6 +881,8 @@ public class OpdsController : BaseApiController
foreach (var chapter in chaptersForVolume)
{
var chapterId = chapter.Id;
if (chapterDict.ContainsKey(chapterId)) continue;
var chapterDto = _mapper.Map<ChapterDto>(chapter);
foreach (var mangaFile in chapter.Files)
{
@ -889,7 +891,6 @@ public class OpdsController : BaseApiController
chapterDto, apiKey, prefix, baseUrl));
}
}
}
var chapters = seriesDetail.StorylineChapters;

View File

@ -318,7 +318,7 @@ public class UploadController : BaseApiController
/// <summary>
/// Replaces volume cover image and locks it with a base64 encoded image.
/// </summary>
/// <remarks>This is a helper API for Komf - Kavita UI does not use. Volume will find first chapter to update.</remarks>
/// <remarks>This will not update the underlying chapter</remarks>
/// <param name="uploadFileDto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
@ -333,24 +333,15 @@ public class UploadController : BaseApiController
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(uploadFileDto.Id, VolumeIncludes.Chapters);
if (volume == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "volume-doesnt-exist"));
// Find the first chapter of the volume
var chapter = volume.Chapters[0];
var filePath = string.Empty;
var lockState = false;
if (!string.IsNullOrEmpty(uploadFileDto.Url))
{
filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetChapterFormat(chapter.Id, uploadFileDto.Id)}");
filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetVolumeFormat(uploadFileDto.Id)}");
lockState = uploadFileDto.LockCover;
}
chapter.CoverImage = filePath;
chapter.CoverImageLocked = lockState;
_imageService.UpdateColorScape(chapter);
_unitOfWork.ChapterRepository.Update(chapter);
volume.CoverImage = chapter.CoverImage;
volume.CoverImage = filePath;
volume.CoverImageLocked = lockState;
_imageService.UpdateColorScape(volume);
_unitOfWork.VolumeRepository.Update(volume);
@ -368,7 +359,7 @@ public class UploadController : BaseApiController
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(chapter.VolumeId, MessageFactoryEntityTypes.Volume), false);
MessageFactory.CoverUpdateEvent(uploadFileDto.Id, MessageFactoryEntityTypes.Volume), false);
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(volume.Id, MessageFactoryEntityTypes.Chapter), false);
return Ok();

View File

@ -59,6 +59,11 @@ public class SeriesDto : IHasReadTimeEstimate, IHasCoverImage
/// </summary>
public string FolderPath { get; set; } = default!;
/// <summary>
/// Lowest path (that is under library root) that contains all files for the series.
/// </summary>
/// <remarks><see cref="Services.Tasks.Scanner.Parser.Parser.NormalizePath"/> must be used before setting</remarks>
public string? LowestFolderPath { get; set; }
/// <summary>
/// The last time the folder for this series was scanned
/// </summary>
public DateTime LastFolderScanned { get; set; }

View File

@ -44,6 +44,7 @@ public interface IVolumeRepository
Task<IEnumerable<Volume>> GetVolumes(int seriesId);
Task<Volume?> GetVolumeByIdAsync(int volumeId);
Task<IList<Volume>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
Task<IEnumerable<string>> GetCoverImagesForLockedVolumesAsync();
}
public class VolumeRepository : IVolumeRepository
{
@ -252,4 +253,17 @@ public class VolumeRepository : IVolumeRepository
.Sum(p => p.PagesRead);
}
}
/// <summary>
/// Returns cover images for locked chapters
/// </summary>
/// <returns></returns>
public async Task<IEnumerable<string>> GetCoverImagesForLockedVolumesAsync()
{
return (await _context.Volume
.Where(c => c.CoverImageLocked)
.Select(c => c.CoverImage)
.Where(t => !string.IsNullOrEmpty(t))
.ToListAsync())!;
}
}

View File

@ -739,6 +739,16 @@ public class ImageService : IImageService
return $"v{volumeId}_c{chapterId}";
}
/// <summary>
/// Returns the name format for a volume cover image (custom)
/// </summary>
/// <param name="volumeId"></param>
/// <returns></returns>
public static string GetVolumeFormat(int volumeId)
{
return $"v{volumeId}";
}
/// <summary>
/// Returns the name format for a library cover image
/// </summary>

View File

@ -550,6 +550,7 @@ public class ReadingListService : IReadingListService
Results = new List<CblBookResult>(),
SuccessfulInserts = new List<CblBookResult>()
};
if (IsCblEmpty(cblReading, importSummary, out var readingListFromCbl)) return readingListFromCbl;
// Is there another reading list with the same name?

View File

@ -179,6 +179,10 @@ public class BackupService : IBackupService
_directoryService.CopyFilesToDirectory(
chapterImages.Select(s => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, s)), outputTempDir);
var volumeImages = await _unitOfWork.VolumeRepository.GetCoverImagesForLockedVolumesAsync();
_directoryService.CopyFilesToDirectory(
volumeImages.Select(s => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, s)), outputTempDir);
var libraryImages = await _unitOfWork.LibraryRepository.GetAllCoverImagesAsync();
_directoryService.CopyFilesToDirectory(
libraryImages.Select(s => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, s)), outputTempDir);

View File

@ -49,7 +49,7 @@ $image-width: 160px;
pointer-events: none;
position: absolute;
top: 0;
width: 158px;
width: 160px;
}
.not-read-badge {
@ -158,7 +158,6 @@ $image-width: 160px;
bottom: 0;
right: 0;
z-index: 115;
visibility: hidden;
}
.library {
@ -186,7 +185,7 @@ $image-width: 160px;
display: none;
}
.chapter,
.chapter,
.volume,
.series,
.expected {
@ -200,4 +199,4 @@ $image-width: 160px;
}
}
}
}
}

View File

@ -62,6 +62,7 @@ export interface Series extends IHasCover, IHasReadingTime {
* Highest level folder containing this series
*/
folderPath: string;
lowestFolderPath: string;
/**
* This is currently only used on Series detail page for recommendations
*/

View File

@ -23,7 +23,7 @@ export class CblConflictReasonPipe implements PipeTransform {
case CblImportReason.EmptyFile:
return failIcon + this.translocoService.translate('cbl-conflict-reason-pipe.empty-file');
case CblImportReason.NameConflict:
return failIcon + this.translocoService.translate('cbl-conflict-reason-pipe.chapter-missing', {readingListName: result.readingListName});
return failIcon + this.translocoService.translate('cbl-conflict-reason-pipe.name-conflict', {readingListName: result.readingListName});
case CblImportReason.SeriesCollision:
return failIcon + this.translocoService.translate('cbl-conflict-reason-pipe.series-collision', {seriesLink: `<a href="/library/${result.libraryId}/series/${result.seriesId}" target="_blank">${result.series}</a>`});
case CblImportReason.SeriesMissing:

View File

@ -99,6 +99,7 @@ export class NavService {
*/
showNavBar() {
this.renderer.setStyle(this.document.querySelector('body'), 'margin-top', 'var(--nav-offset)');
this.renderer.removeStyle(this.document.querySelector('body'), 'scrollbar-gutter');
this.renderer.setStyle(this.document.querySelector('body'), 'height', 'calc(var(--vh)*100 - var(--nav-offset))');
this.renderer.setStyle(this.document.querySelector('html'), 'height', 'calc(var(--vh)*100 - var(--nav-offset))');
this.navbarVisibleSource.next(true);
@ -109,6 +110,7 @@ export class NavService {
*/
hideNavBar() {
this.renderer.setStyle(this.document.querySelector('body'), 'margin-top', '0px');
this.renderer.setStyle(this.document.querySelector('body'), 'scrollbar-gutter', 'initial');
this.renderer.removeStyle(this.document.querySelector('body'), 'height');
this.renderer.removeStyle(this.document.querySelector('html'), 'height');
this.navbarVisibleSource.next(false);

View File

@ -16,7 +16,7 @@
{{t('cover-image-description')}}
</p>
<app-cover-image-chooser [(imageUrls)]="imageUrls" (imageSelected)="updateSelectedIndex($event)" (selectedBase64Url)="updateSelectedImage($event)"
[showReset]="volume.coverImageLocked" (resetClicked)="handleReset()"></app-cover-image-chooser>
[showReset]="true" (resetClicked)="handleReset()"></app-cover-image-chooser>
</ng-template>
</li>
@ -34,15 +34,15 @@
</app-setting-item>
</div>
</div>
<!-- <div class="col-lg-6 col-md-12 pe-2">-->
<!-- <div class="mb-3">-->
<!-- <app-setting-item [title]="t('words-label')" [toggleOnViewClick]="false" [showEdit]="false">-->
<!-- <ng-template #view>-->
<!-- {{t('words-count', {num: volume.wordCount | compactNumber})}}-->
<!-- </ng-template>-->
<!-- </app-setting-item>-->
<!-- </div>-->
<!-- </div>-->
<div class="col-lg-6 col-md-12 pe-2">
<div class="mb-3">
<app-setting-item [title]="t('words-label')" [toggleOnViewClick]="false" [showEdit]="false">
<ng-template #view>
{{t('words-count', {num: volume.wordCount | compactNumber})}}
</ng-template>
</app-setting-item>
</div>
</div>
</div>
<div class="row g-0">

View File

@ -1,5 +1,5 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, Input, OnInit} from '@angular/core';
import {FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms";
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core';
import {FormControl, FormGroup, FormsModule, ReactiveFormsModule} from "@angular/forms";
import {
NgbActiveModal,
NgbInputDatepicker,
@ -29,26 +29,15 @@ import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
import {ReadTimePipe} from "../../_pipes/read-time.pipe";
import {Action, ActionFactoryService, ActionItem} from "../../_services/action-factory.service";
import {Volume} from "../../_models/volume";
import {SeriesService} from "../../_services/series.service";
import {Breakpoint, UtilityService} from "../../shared/_services/utility.service";
import {ImageService} from "../../_services/image.service";
import {UploadService} from "../../_services/upload.service";
import {MetadataService} from "../../_services/metadata.service";
import {AccountService} from "../../_services/account.service";
import {ActionService} from "../../_services/action.service";
import {DownloadService} from "../../shared/_services/download.service";
import {Chapter} from "../../_models/chapter";
import {LibraryType} from "../../_models/library/library";
import {TypeaheadSettings} from "../../typeahead/_models/typeahead-settings";
import {Tag} from "../../_models/tag";
import {Language} from "../../_models/metadata/language";
import {Person, PersonRole} from "../../_models/metadata/person";
import {Genre} from "../../_models/metadata/genre";
import {AgeRatingDto} from "../../_models/metadata/age-rating-dto";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {forkJoin, Observable, of} from "rxjs";
import {map} from "rxjs/operators";
import {EditChapterModalCloseResult} from "../edit-chapter-modal/edit-chapter-modal.component";
import {PersonRole} from "../../_models/metadata/person";
import {forkJoin} from "rxjs";
import { MangaFormat } from 'src/app/_models/manga-format';
import {MangaFile} from "../../_models/manga-file";
import {VolumeService} from "../../_services/volume.service";
@ -167,18 +156,16 @@ export class EditVolumeModalComponent implements OnInit {
}
save() {
const model = this.editForm.value;
const selectedIndex = this.editForm.get('coverImageIndex')?.value || 0;
const apis = [];
if (selectedIndex > 0 || this.coverImageReset) {
apis.push(this.uploadService.updateVolumeCoverImage(model.id, this.selectedCover, !this.coverImageReset));
apis.push(this.uploadService.updateVolumeCoverImage(this.volume.id, this.selectedCover, !this.coverImageReset));
}
forkJoin(apis).subscribe(results => {
this.modal.close({success: true, volume: model, coverImageUpdate: selectedIndex > 0 || this.coverImageReset, needsReload: false, isDeleted: false} as EditVolumeModalCloseResult);
this.modal.close({success: true, volume: this.volume, coverImageUpdate: selectedIndex > 0 || this.coverImageReset, needsReload: false, isDeleted: false} as EditVolumeModalCloseResult);
});
}

View File

@ -54,6 +54,7 @@ $primary-color: #0062cc;
$action-bar-height: 38px;
// Drawer
.control-container {
padding-bottom: 5px;

View File

@ -482,10 +482,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
constructor(@Inject(DOCUMENT) private document: Document) {
this.navService.hideNavBar();
this.themeService.clearThemes();
this.navService.hideSideNav();
this.cdRef.markForCheck();
this.navService.hideNavBar();
this.navService.hideSideNav();
this.themeService.clearThemes();
this.cdRef.markForCheck();
}
/**

View File

@ -17,7 +17,7 @@
}
<ul class="list-group">
@for(collectionTag of lists | filter: filterList; let i = $index; track collectionTag.title) {
@for(collectionTag of lists | filter: filterList; let i = $index; track collectionTag.title + collectionTag.promoted) {
<li class="list-group-item clickable" tabindex="0" role="option" (click)="addToCollection(collectionTag)">
{{collectionTag.title}}
@if (collectionTag.promoted) {

View File

@ -10,15 +10,16 @@ import {
ViewChild,
ViewEncapsulation
} from '@angular/core';
import {FormGroup, FormControl, ReactiveFormsModule} from '@angular/forms';
import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
import {NgbActiveModal, NgbModalModule} from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import {ToastrService} from 'ngx-toastr';
import {UserCollection} from 'src/app/_models/collection-tag';
import { ReadingList } from 'src/app/_models/reading-list';
import { CollectionTagService } from 'src/app/_services/collection-tag.service';
import {ReadingList} from 'src/app/_models/reading-list';
import {CollectionTagService} from 'src/app/_services/collection-tag.service';
import {CommonModule} from "@angular/common";
import {FilterPipe} from "../../../_pipes/filter.pipe";
import {translate, TranslocoDirective, TranslocoService} from "@jsverse/transloco";
import {translate, TranslocoDirective} from "@jsverse/transloco";
import {ScrobbleProvider} from "../../../_services/scrobbling.service";
@Component({
selector: 'app-bulk-add-to-collection',
@ -60,7 +61,8 @@ export class BulkAddToCollectionComponent implements OnInit, AfterViewInit {
this.loading = true;
this.cdRef.markForCheck();
this.collectionService.allCollections(true).subscribe(tags => {
this.lists = tags;
// Don't allow Smart Collections in
this.lists = tags.filter(t => t.source === ScrobbleProvider.Kavita);
this.loading = false;
this.cdRef.markForCheck();
});

View File

@ -489,85 +489,215 @@
<a ngbNavLink>{{t(tabs[TabID.Info])}}</a>
<ng-template ngbNavContent>
<h5>{{t('info-title')}}</h5>
<div class="row g-0 mt-3 mb-3">
<div class="col-md-6" *ngIf="libraryName"><span class="fw-bold text-uppercase">{{t('library-title')}}</span> {{libraryName | sentenceCase}}</div>
<div class="col-md-6"><span class="fw-bold text-uppercase">{{t('format-title')}}</span> <app-tag-badge>{{series.format | mangaFormat}}</app-tag-badge></div>
</div>
<div class="row g-0 mt-3 mb-3">
<div class="col-md-6"><span class="fw-bold text-uppercase">{{t('created-title')}}</span> {{series.created | date:'shortDate'}}</div>
<div class="col-md-6"><span class="fw-bold text-uppercase">{{t('last-read-title')}}</span> {{series.latestReadDate | defaultDate | timeAgo}}</div>
<div class="col-md-6"><span class="fw-bold text-uppercase">{{t('last-added-title')}}</span> {{series.lastChapterAdded | defaultDate | timeAgo}}</div>
<div class="col-md-6"><span class="fw-bold text-uppercase">{{t('last-scanned-title')}}</span> {{series.lastFolderScanned | defaultDate | timeAgo}}</div>
<div class="row g-0">
<div class="col-lg-6 col-md-12 pe-2">
<div class="mb-3">
<app-setting-item [title]="t('library-title')" [toggleOnViewClick]="false" [showEdit]="false">
<ng-template #view>
{{libraryName! | sentenceCase}}
</ng-template>
</app-setting-item>
</div>
</div>
<div class="col-lg-6 col-md-12 pe-2">
<div class="mb-3">
<app-setting-item [title]="t('format-title')" [toggleOnViewClick]="false" [showEdit]="false">
<ng-template #view>
<app-tag-badge>{{series.format | mangaFormat}}</app-tag-badge>
</ng-template>
</app-setting-item>
</div>
</div>
</div>
<div class="row g-0 mt-3 mb-3">
<div class="col-auto"><span class="fw-bold text-uppercase">{{t('folder-path-title')}}</span> {{series.folderPath | defaultValue}}</div>
</div>
<div class="row g-0 mt-3 mb-3" *ngIf="metadata">
<div class="col-md-6">
<span class="fw-bold text-uppercase">{{t('max-items-title')}}</span> {{metadata.maxCount}}
<i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="t('highest-count-tooltip')" role="button" tabindex="0"></i>
<div class="row g-0">
<div class="col-lg-6 col-md-12 pe-2">
<div class="mb-3">
<app-setting-item [title]="t('folder-path-title')" [subtitle]="t('folder-path-tooltip')" [toggleOnViewClick]="false" [showEdit]="false">
<ng-template #view>
{{series.folderPath | defaultValue}}
</ng-template>
</app-setting-item>
</div>
</div>
<div class="col-md-6">
<span class="fw-bold text-uppercase">{{t('total-items-title')}}</span> {{metadata.totalCount}}
<i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="t('max-issue-tooltip')" role="button" tabindex="0"></i>
<div class="col-lg-6 col-md-12 pe-2">
<div class="mb-3">
<app-setting-item [title]="t('lowest-folder-path-title')" [subtitle]="t('lowest-folder-path-tooltip')" [toggleOnViewClick]="false" [showEdit]="false">
<ng-template #view>
{{series.lowestFolderPath | defaultValue}}
</ng-template>
</app-setting-item>
</div>
</div>
<div class="col-md-6"><span class="fw-bold text-uppercase">{{t('publication-status-title')}}</span> {{metadata.publicationStatus | publicationStatus}}</div>
<div class="col-md-6"><span class="fw-bold text-uppercase">{{t('total-pages-title')}}</span> {{series.pages}}</div>
<div class="col-md-6"><span class="fw-bold text-uppercase">{{t('size-title')}}</span> {{size | bytes}}</div>
</div>
<h4>Volumes</h4>
<div class="spinner-border text-secondary" role="status" *ngIf="isLoadingVolumes">
<span class="visually-hidden">{{t('loading')}}</span>
</div>
<ul class="list-unstyled" *ngIf="!isLoadingVolumes">
<li class="d-flex my-4" *ngFor="let volume of seriesVolumes">
<app-image class="me-3" style="width: 74px;" width="74px" [imageUrl]="imageService.getVolumeCoverImage(volume.id)"></app-image>
<div class="flex-grow-1">
<h5 class="mt-0 mb-1">{{formatVolumeName(volume)}}</h5>
<div>
<div class="row g-0">
<div class="col">
{{t('added-title')}} {{volume.createdUtc | utcToLocalTime | defaultDate}}
</div>
<div class="col">
{{t('last-modified-title')}} {{volume.lastModifiedUtc | utcToLocalTime | translocoDate: {dateStyle: 'short' } | defaultDate}}
</div>
</div>
<div class="row g-0">
<div class="col">
<button type="button" class="btn btn-outline-primary" (click)="collapse.toggle()"
[attr.aria-expanded]="!volumeCollapsed[volume.name]">
{{t('view-files')}}
</button>
</div>
<div class="col">
{{t('pages-title')}} {{volume.pages}}
</div>
</div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="volumeCollapsed[volume.name]">
<ul class="list-group mt-2">
<li *ngFor="let file of volume.volumeFiles" class="list-group-item">
<span>{{file.filePath}}</span>
<div class="row g-0">
<div class="col">
{{t('chapter-title')}} {{file.chapter}}
</div>
<div class="col">
{{t('pages-title')}} {{file.pages}}
</div>
<div class="col">
{{t('format-title')}} <span class="badge badge-secondary">{{utilityService.mangaFormatToText(file.format)}}</span>
</div>
</div>
</li>
</ul>
</div>
@if (metadata) {
<div class="row g-0">
<div class="col-lg-6 col-md-12 pe-2">
<div class="mb-3">
<app-setting-item [title]="t('max-items-title')" [subtitle]="t('highest-count-tooltip')" [toggleOnViewClick]="false" [showEdit]="false">
<ng-template #view>
{{metadata.maxCount}}
</ng-template>
</app-setting-item>
</div>
</div>
</li>
</ul>
<div class="col-lg-6 col-md-12 pe-2">
<div class="mb-3">
<app-setting-item [title]="t('total-items-title')" [subtitle]="t('max-issue-tooltip')" [toggleOnViewClick]="false" [showEdit]="false">
<ng-template #view>
{{metadata.totalCount}}
</ng-template>
</app-setting-item>
</div>
</div>
</div>
<div class="row g-0">
<div class="col-lg-6 col-md-12 pe-2">
<div class="mb-3">
<app-setting-item [title]="t('publication-status-title')" [toggleOnViewClick]="false" [showEdit]="false">
<ng-template #view>
{{metadata.publicationStatus | publicationStatus}}
</ng-template>
</app-setting-item>
</div>
</div>
<div class="col-lg-6 col-md-12 pe-2">
<div class="mb-3">
<app-setting-item [title]="t('size-title')" [toggleOnViewClick]="false" [showEdit]="false">
<ng-template #view>
{{size | bytes}}
</ng-template>
</app-setting-item>
</div>
</div>
</div>
}
<div class="row g-0">
<div class="col-lg-6 col-md-12 pe-2">
<div class="mb-3">
<app-setting-item [title]="t('created-title')" [toggleOnViewClick]="false" [showEdit]="false">
<ng-template #view>
{{series.created | date:'shortDate'}}
</ng-template>
</app-setting-item>
</div>
</div>
<div class="col-lg-6 col-md-12 pe-2">
<div class="mb-3">
<app-setting-item [title]="t('last-added-title')" [toggleOnViewClick]="false" [showEdit]="false">
<ng-template #view>
{{series.lastChapterAdded | defaultDate | timeAgo}}
</ng-template>
</app-setting-item>
</div>
</div>
</div>
<div class="row g-0">
<div class="col-lg-6 col-md-12 pe-2">
<div class="mb-3">
<app-setting-item [title]="t('last-scanned-title')" [toggleOnViewClick]="false" [showEdit]="false">
<ng-template #view>
{{series.lastFolderScanned | defaultDate | timeAgo}}
</ng-template>
</app-setting-item>
</div>
</div>
<div class="col-lg-6 col-md-12 pe-2">
<div class="mb-3">
<app-setting-item [title]="t('last-read-title')" [toggleOnViewClick]="false" [showEdit]="false">
<ng-template #view>
{{series.lastChapterAdded | defaultDate | timeAgo}}
</ng-template>
</app-setting-item>
</div>
</div>
</div>
<div class="row g-0">
<div class="col-lg-6 col-md-12 pe-2">
<div class="mb-3">
<app-setting-item [title]="t('total-pages-title')" [toggleOnViewClick]="false" [showEdit]="false">
<ng-template #view>
{{series.pages | number}}
</ng-template>
</app-setting-item>
</div>
</div>
<div class="col-lg-6 col-md-12 pe-2">
<div class="mb-3">
<app-setting-item [title]="t('total-words-title')" [toggleOnViewClick]="false" [showEdit]="false">
<ng-template #view>
{{series.wordCount | number}}
</ng-template>
</app-setting-item>
</div>
</div>
</div>
<h4>Volumes</h4>
@if (isLoadingVolumes) {
<div class="spinner-border text-secondary" role="status" *ngIf="isLoadingVolumes">
<span class="visually-hidden">{{t('loading')}}</span>
</div>
} @else {
<ul class="list-unstyled">
@for (volume of seriesVolumes; track volume.id) {
<li class="d-flex my-4">
<app-image class="me-3" style="width: 74px;" width="74px" [imageUrl]="imageService.getVolumeCoverImage(volume.id)"></app-image>
<div class="flex-grow-1">
<h5 class="mt-0 mb-1">{{formatVolumeName(volume)}}</h5>
<div>
<div class="row g-0">
<div class="col">
{{t('added-title')}} {{volume.createdUtc | utcToLocalTime | defaultDate}}
</div>
<div class="col">
{{t('last-modified-title')}} {{volume.lastModifiedUtc | utcToLocalTime | translocoDate: {dateStyle: 'short' } | defaultDate}}
</div>
</div>
<div class="row g-0">
<div class="col">
<button type="button" class="btn btn-outline-primary" (click)="collapse.toggle()"
[attr.aria-expanded]="!volumeCollapsed[volume.name]">
{{t('view-files')}}
</button>
</div>
<div class="col">
{{t('pages-title')}} {{volume.pages}}
</div>
</div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="volumeCollapsed[volume.name]">
<ul class="list-group mt-2">
@for(file of volume.volumeFiles; track file.id) {
<li class="list-group-item">
<span>{{file.filePath}}</span>
<div class="row g-0">
<div class="col">
{{t('chapter-title')}} {{file.chapter}}
</div>
<div class="col">
{{t('pages-title')}} {{file.pages}}
</div>
<div class="col">
{{t('format-title')}} <span class="badge badge-secondary">{{utilityService.mangaFormatToText(file.format)}}</span>
</div>
</div>
</li>
}
</ul>
</div>
</div>
</div>
</li>
}
</ul>
}
</ng-template>
</li>

View File

@ -54,7 +54,6 @@ import {TranslocoDatePipe} from "@jsverse/transloco-locale";
import {UtcToLocalTimePipe} from "../../../_pipes/utc-to-local-time.pipe";
import {EditListComponent} from "../../../shared/edit-list/edit-list.component";
import {AccountService} from "../../../_services/account.service";
import {LibraryType} from "../../../_models/library/library";
import {ToastrService} from "ngx-toastr";
import {Volume} from "../../../_models/volume";
import {Action, ActionFactoryService, ActionItem} from "../../../_services/action-factory.service";
@ -62,6 +61,7 @@ import {SettingButtonComponent} from "../../../settings/_components/setting-butt
import {ActionService} from "../../../_services/action.service";
import {DownloadService} from "../../../shared/_services/download.service";
import {SettingItemComponent} from "../../../settings/_components/setting-item/setting-item.component";
import {ReadTimePipe} from "../../../_pipes/read-time.pipe";
enum TabID {
General = 0,
@ -116,6 +116,7 @@ const blackList = [Action.Edit, Action.Info, Action.IncognitoRead, Action.Read,
EditListComponent,
SettingButtonComponent,
SettingItemComponent,
ReadTimePipe,
],
templateUrl: './edit-series-modal.component.html',
styleUrls: ['./edit-series-modal.component.scss'],

View File

@ -25,7 +25,7 @@
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, 158px);
grid-template-columns: repeat(auto-fill, 160px);
grid-gap: 0.5rem;
justify-content: space-between;
width: 100%;

View File

@ -2,9 +2,9 @@
<div class="card-item-container card {{selected ? 'selected-highlight' : ''}}">
<div class="overlay" (click)="handleClick($event)">
@if (total > 0 || suppressArchiveWarning) {
<app-image height="230px" width="158px" [styles]="{'border-radius': '.25rem .25rem 0 0'}" [imageUrl]="imageUrl"></app-image>
<app-image height="232.91px" width="160px" [styles]="{'border-radius': '.25rem .25rem 0 0'}" [imageUrl]="imageUrl"></app-image>
} @else if (total === 0 && !suppressArchiveWarning) {
<app-image height="230px" width="158px" [styles]="{'border-radius': '.25rem .25rem 0 0'}" [imageUrl]="imageService.errorImage"></app-image>
<app-image height="232.91px" width="160px" [styles]="{'border-radius': '.25rem .25rem 0 0'}" [imageUrl]="imageService.errorImage"></app-image>
}
<div class="progress-banner">

View File

@ -2,9 +2,9 @@
<div class="card-item-container card position-relative {{selected ? 'selected-highlight' : ''}}" >
<div class="overlay" (click)="handleClick($event)">
@if (chapter.pages > 0 || suppressArchiveWarning) {
<app-image height="230px" width="158px" [styles]="{'border-radius': '.25rem .25rem 0 0'}" [imageUrl]="imageService.getChapterCoverImage(chapter.id)"></app-image>
<app-image height="232.91px" width="160px" [styles]="{'border-radius': '.25rem .25rem 0 0'}" [imageUrl]="imageService.getChapterCoverImage(chapter.id)"></app-image>
} @else if (chapter.pages === 0 && !suppressArchiveWarning) {
<app-image height="230px" width="158px" [styles]="{'border-radius': '.25rem .25rem 0 0'}" [imageUrl]="imageService.errorImage"></app-image>
<app-image height="232.91px" width="160px" [styles]="{'border-radius': '.25rem .25rem 0 0'}" [imageUrl]="imageService.errorImage"></app-image>
}
<div class="progress-banner">

View File

@ -55,7 +55,7 @@
<div class="clickable col-auto"
*ngIf="showReset" tabindex="0" (click)="reset()"
[ngClass]="{'selected': !showApplyButton && selectedIndex === -1}">
<app-image class="card-img-top" [title]="t('reset-cover-tooltip')" height="230px" width="158px" [imageUrl]="imageService.resetCoverImage"></app-image>
<app-image class="card-img-top" [title]="t('reset-cover-tooltip')" height="232.91px" width="160px" [imageUrl]="imageService.resetCoverImage"></app-image>
<ng-container *ngIf="showApplyButton">
<br>
<button style="width: 100%;" class="btn btn-secondary" (click)="resetImage()">{{t('reset')}}</button>
@ -65,7 +65,7 @@
<div class="clickable col-auto"
*ngFor="let url of imageUrls; let idx = index;" tabindex="0" [attr.aria-label]="t('image-num', {num: idx + 1})" (click)="selectImage(idx)"
[ngClass]="{'selected': !showApplyButton && selectedIndex === idx}">
<app-image class="card-img-top" height="230px" width="158px" [imageUrl]="url" [processEvents]="idx > 0"></app-image>
<app-image class="card-img-top" height="232.91px" width="160px" [imageUrl]="url" [processEvents]="idx > 0"></app-image>
<ng-container *ngIf="showApplyButton">
<br>
<button class="btn btn-primary" style="width: 100%;"

View File

@ -16,7 +16,7 @@ $image-width: 160px;
.chooser {
display: grid;
grid-template-columns: repeat(auto-fill, 158px);
grid-template-columns: repeat(auto-fill, 160px);
grid-gap: 0.5rem;
justify-content: space-around;
}

View File

@ -20,12 +20,12 @@ export class ExternalListItemComponent {
@Input() imageUrl: string = '';
/**
* Size of the Image Height. Defaults to 230px.
* Size of the Image Height. Defaults to 232.91px.
*/
@Input() imageHeight: string = '230px';
@Input() imageHeight: string = '232.91px';
/**
* Size of the Image Width Defaults to 158px.
* Size of the Image Width Defaults to 160px.
*/
@Input() imageWidth: string = '158px';
@Input() imageWidth: string = '160px';
@Input() summary: string | null = '';
}

View File

@ -3,7 +3,7 @@
<div class="card-item-container card clickable position-relative">
<div class="overlay" (click)="handleClick()">
<ng-container>
<app-image [styles]="{'border-radius': '.25rem .25rem 0 0'}" height="230px" width="158px" [imageUrl]="data.coverUrl"></app-image>
<app-image [styles]="{'border-radius': '.25rem .25rem 0 0'}" height="232.91px" width="160px" [imageUrl]="data.coverUrl"></app-image>
</ng-container>

View File

@ -61,13 +61,13 @@ export class ListItemComponent implements OnInit {
@Input() seriesName: string = '';
/**
* Size of the Image Height. Defaults to 230px.
* Size of the Image Height. Defaults to 232.91px.
*/
@Input() imageHeight: string = '230px';
@Input() imageHeight: string = '232.91px';
/**
* Size of the Image Width Defaults to 158px.
* Size of the Image Width Defaults to 160px.
*/
@Input() imageWidth: string = '158px';
@Input() imageWidth: string = '160px';
@Input() seriesLink: string = '';
@Input() pagesRead: number = 0;

View File

@ -1,6 +1,6 @@
<div class="card-item-container card">
<div class="overlay">
<app-image [styles]="{'border-radius': '.25rem .25rem 0 0'}" height="230px" width="158px" classes="extreme-blur"
<app-image [styles]="{'border-radius': '.25rem .25rem 0 0'}" height="232.91px" width="160px" classes="extreme-blur"
[imageUrl]="imageUrl"></app-image>
<div class="card-overlay"></div>

View File

@ -2,9 +2,9 @@
<div class="card-item-container card position-relative {{selected ? 'selected-highlight' : ''}}">
<div class="overlay" (click)="handleClick()">
@if (series.pages > 0) {
<app-image height="230px" width="158px" [styles]="{'border-radius': '.25rem .25rem 0 0'}" [imageUrl]="imageService.getSeriesCoverImage(series.id)"></app-image>
<app-image height="232.91px" width="160px" [styles]="{'border-radius': '.25rem .25rem 0 0'}" [imageUrl]="imageService.getSeriesCoverImage(series.id)"></app-image>
} @else if (series.pages === 0) {
<app-image height="230px" width="158px" [styles]="{'border-radius': '.25rem .25rem 0 0'}" [imageUrl]="imageService.errorImage"></app-image>
<app-image height="232.91px" width="160px" [styles]="{'border-radius': '.25rem .25rem 0 0'}" [imageUrl]="imageService.errorImage"></app-image>
}
<div class="progress-banner">

View File

@ -2,9 +2,9 @@
<div class="card-item-container card {{selected ? 'selected-highlight' : ''}}" >
<div class="overlay position-relative" (click)="handleClick($event)">
@if (volume.pages > 0 || suppressArchiveWarning) {
<app-image height="230px" width="158px" [styles]="{'border-radius': '.25rem .25rem 0 0'}" [imageUrl]="imageService.getVolumeCoverImage(volume.id)"></app-image>
<app-image height="232.91px" width="160px" [styles]="{'border-radius': '.25rem .25rem 0 0'}" [imageUrl]="imageService.getVolumeCoverImage(volume.id)"></app-image>
} @else if (volume.pages === 0 && !suppressArchiveWarning) {
<app-image height="230px" width="158px" [styles]="{'border-radius': '.25rem .25rem 0 0'}" [imageUrl]="imageService.errorImage"></app-image>
<app-image height="232.91px" width="160px" [styles]="{'border-radius': '.25rem .25rem 0 0'}" [imageUrl]="imageService.errorImage"></app-image>
}
<div class="progress-banner">
@ -50,11 +50,13 @@
</div>
</div>
<div class="card-body meta-title">
<div class="card-content d-flex justify-content-center align-items-center text-center" style="width:100%;min-height:58px;">
{{volume.name}}
@if (libraryType === LibraryType.LightNovel || libraryType === LibraryType.Book) {
<div class="card-body meta-title">
<div class="card-content d-flex justify-content-center align-items-center text-center" style="width:100%;min-height:58px;">
{{volume.name}}
</div>
</div>
</div>
}
<div class="card-title-container">
<span class="card-title" id="{{volume.id}}" tabindex="0">

View File

@ -216,4 +216,5 @@ export class VolumeCardComponent implements OnInit {
this.readerService.readVolume(this.libraryId, this.seriesId, this.volume, false);
}
protected readonly LibraryType = LibraryType;
}

View File

@ -8,8 +8,7 @@ import {
ViewChild
} from '@angular/core';
import {BulkOperationsComponent} from "../cards/bulk-operations/bulk-operations.component";
import {TagBadgeComponent, TagBadgeCursor} from "../shared/tag-badge/tag-badge.component";
import {PageLayoutMode} from "../_models/page-layout-mode";
import {TagBadgeComponent} from "../shared/tag-badge/tag-badge.component";
import {AsyncPipe, DecimalPipe, DOCUMENT, NgStyle} from "@angular/common";
import {CardActionablesComponent} from "../_single-module/card-actionables/card-actionables.component";
import {CarouselReelComponent} from "../carousel/_components/carousel-reel/carousel-reel.component";
@ -31,15 +30,12 @@ import {
import {PersonBadgeComponent} from "../shared/person-badge/person-badge.component";
import {ReviewCardComponent} from "../_single-module/review-card/review-card.component";
import {SeriesCardComponent} from "../cards/series-card/series-card.component";
import {
SeriesMetadataDetailComponent
} from "../series-detail/_components/series-metadata-detail/series-metadata-detail.component";
import {VirtualScrollerModule} from "@iharbeck/ngx-virtual-scroller";
import {ActivatedRoute, Router, RouterLink} from "@angular/router";
import {ImageService} from "../_services/image.service";
import {ChapterService} from "../_services/chapter.service";
import {Chapter} from "../_models/chapter";
import {forkJoin, map, Observable, shareReplay, tap} from "rxjs";
import {forkJoin, map, Observable, tap} from "rxjs";
import {SeriesService} from "../_services/series.service";
import {Series} from "../_models/series";
import {AgeRating} from "../_models/metadata/age-rating";
@ -68,7 +64,6 @@ import {DefaultValuePipe} from "../_pipes/default-value.pipe";
import {ReadingList} from "../_models/reading-list";
import {ReadingListService} from "../_services/reading-list.service";
import {CardItemComponent} from "../cards/card-item/card-item.component";
import {PageBookmark} from "../_models/readers/page-bookmark";
import {RelatedTabComponent} from "../_single-modules/related-tab/related-tab.component";
import {AgeRatingImageComponent} from "../_single-modules/age-rating-image/age-rating-image.component";
import {CompactNumberPipe} from "../_pipes/compact-number.pipe";
@ -76,7 +71,6 @@ import {BadgeExpanderComponent} from "../shared/badge-expander/badge-expander.co
import {
MetadataDetailRowComponent
} from "../series-detail/_components/metadata-detail-row/metadata-detail-row.component";
import {HourEstimateRange} from "../_models/series-detail/hour-estimate-range";
import {DownloadButtonComponent} from "../series-detail/_components/download-button/download-button.component";
import {hasAnyCast} from "../_models/common/i-has-cast";
import {CarouselTabComponent} from "../carousel/_components/carousel-tab/carousel-tab.component";
@ -113,7 +107,6 @@ enum TabID {
PersonBadgeComponent,
ReviewCardComponent,
SeriesCardComponent,
SeriesMetadataDetailComponent,
TagBadgeComponent,
VirtualScrollerModule,
NgStyle,
@ -308,7 +301,7 @@ export class ChapterDetailComponent implements OnInit {
updateUrl(activeTab: TabID) {
const newUrl = `${this.router.url.split('#')[0]}#${activeTab}`;
this.router.navigateByUrl(newUrl, { onSameUrlNavigation: 'ignore' });
//this.router.navigateByUrl(newUrl, { onSameUrlNavigation: 'ignore' });
}
openPerson(field: FilterField, value: number) {

View File

@ -17,7 +17,7 @@
.card-container {
display: grid;
grid-template-columns: repeat(auto-fill, 158px);
grid-template-columns: repeat(auto-fill, 160px);
grid-gap: 0.5rem;
justify-content: space-around;
}

View File

@ -115,7 +115,7 @@
<div class="col-auto ms-2 d-none d-md-block">
<div class="card-actions" [ngbTooltip]="t('more-alt')">
<app-card-actionables (actionHandler)="performAction($event)" [actions]="seriesActions" [labelBy]="series.name" iconClass="fa-ellipsis-h" btnClass="btn-secondary-outline btn-sm"></app-card-actionables>
<app-card-actionables (actionHandler)="performAction($event)" [actions]="seriesActions" [labelBy]="series.name" iconClass="fa-ellipsis-h" btnClass="btn-secondary-outline btn"></app-card-actionables>
</div>
</div>
@ -161,37 +161,70 @@
</div>
</div>
@if (WebLinks.length > 0) {
<div class="mt-3 mb-2">
<div class="row g-0">
<div class="col-6">
<span class="fw-bold">{{t('weblinks-title')}}</span>
<div>
@for(link of WebLinks; track link) {
<a class="me-1" [href]="link | safeHtml" target="_blank" rel="noopener noreferrer" [title]="link">
<app-image height="24px" width="24px" aria-hidden="true" [imageUrl]="imageService.getWebLinkImage(link)"
[errorImage]="imageService.errorWebLinkImage"></app-image>
</a>
}
</div>
<div class="mt-3 mb-2">
<div class="row g-0">
<div class="col-6">
<span class="fw-bold">{{t('genres-title')}}</span>
<div>
<app-badge-expander [items]="seriesMetadata.genres" [itemsTillExpander]="5">
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="openFilter(FilterField.Genres, item.id)">{{item.title}}</a>
@if (!last) {
,
}
</ng-template>
</app-badge-expander>
</div>
</div>
<div class="col-6">
<span class="fw-bold">{{t('publication-status-title')}}</span>
<div>
@if (seriesMetadata.publicationStatus | publicationStatus; as pubStatus) {
<i class="fa-solid fa-hourglass-{{pubStatus === t('ongoing') ? 'empty' : 'end'}}" aria-hidden="true"></i>
<a class="dark-exempt btn-icon" (click)="openFilter(FilterField.PublicationStatus, seriesMetadata.publicationStatus)"
href="javascript:void(0);"
[ngbTooltip]="t('publication-status-tooltip') + (seriesMetadata.totalCount === 0 ? '' : ' (' + seriesMetadata.maxCount + ' / ' + seriesMetadata.totalCount + ')')">
{{pubStatus}}
</a>
}
</div>
<div class="col-6">
<span class="fw-bold">{{t('tags-title')}}</span>
<div>
<app-badge-expander [items]="seriesMetadata.tags" [itemsTillExpander]="5">
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="openFilter(FilterField.Tags, item.id)">{{item.title}}</a>
@if (!last) {
,
}
</ng-template>
</app-badge-expander>
</div>
</div>
</div>
}
</div>
<div class="mt-3 mb-2">
<div class="row g-0">
<div class="col-6">
<span class="fw-bold">{{t('weblinks-title')}}</span>
<div>
@for(link of WebLinks; track link) {
<a class="me-1" [href]="link | safeHtml" target="_blank" rel="noopener noreferrer" [title]="link">
<app-image height="24px" width="24px" aria-hidden="true" [imageUrl]="imageService.getWebLinkImage(link)"
[errorImage]="imageService.errorWebLinkImage"></app-image>
</a>
} @empty {
{{null | defaultValue}}
}
</div>
</div>
<div class="col-6">
<span class="fw-bold">{{t('publication-status-title')}}</span>
<div>
@if (seriesMetadata.publicationStatus | publicationStatus; as pubStatus) {
<a class="dark-exempt btn-icon" (click)="openFilter(FilterField.PublicationStatus, seriesMetadata.publicationStatus)"
href="javascript:void(0);"
[ngbTooltip]="t('publication-status-tooltip') + (seriesMetadata.totalCount === 0 ? '' : ' (' + seriesMetadata.maxCount + ' / ' + seriesMetadata.totalCount + ')')">
{{pubStatus}}
</a>
}
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -22,7 +22,7 @@
//
.card-container{
display: grid;
grid-template-columns: repeat(auto-fill, 158px);
grid-template-columns: repeat(auto-fill, 160px);
grid-gap: 0.5rem;
justify-content: space-around;
}

View File

@ -91,7 +91,6 @@ import {VirtualScrollerModule} from '@iharbeck/ngx-virtual-scroller';
import {BulkOperationsComponent} from '../../../cards/bulk-operations/bulk-operations.component';
import {ReviewCardComponent} from '../../../_single-module/review-card/review-card.component';
import {CarouselReelComponent} from '../../../carousel/_components/carousel-reel/carousel-reel.component';
import {SeriesMetadataDetailComponent} from '../series-metadata-detail/series-metadata-detail.component';
import {ImageComponent} from '../../../shared/image/image.component';
import {TagBadgeComponent} from '../../../shared/tag-badge/tag-badge.component';
import {
@ -175,7 +174,7 @@ interface StoryLineItem {
standalone: true,
imports: [SideNavCompanionBarComponent, CardActionablesComponent, ReactiveFormsModule, NgStyle,
TagBadgeComponent, ImageComponent, NgbTooltip, NgbProgressbar, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu,
NgbDropdownItem, SeriesMetadataDetailComponent, CarouselReelComponent, ReviewCardComponent, BulkOperationsComponent,
NgbDropdownItem, CarouselReelComponent, ReviewCardComponent, BulkOperationsComponent,
NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, VirtualScrollerModule, CardItemComponent,
EntityTitleComponent, SeriesCardComponent, ExternalSeriesCardComponent, NgbNavOutlet,
LoadingComponent, DecimalPipe, TranslocoDirective, NgTemplateOutlet, NextExpectedCardComponent,
@ -446,7 +445,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
constructor(@Inject(DOCUMENT) private document: Document) {
//this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.accountService.currentUser$.subscribe(user => {
@ -553,8 +552,15 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
}
updateUrl(activeTab: TabID) {
const newUrl = `${this.router.url.split('#')[0]}#${activeTab}`;
this.router.navigateByUrl(newUrl, { onSameUrlNavigation: 'ignore' });
var tokens = this.router.url.split('#');
const newUrl = `${tokens[0]}#${activeTab}`;
// if (tokens.length === 1 || tokens[1] === activeTab + '') {
// return;
// }
console.log('url:', newUrl);
//this.router.navigateByUrl(newUrl, { skipLocationChange: true, replaceUrl: true });
}
handleSeriesActionCallback(action: ActionItem<Series>, series: Series) {
@ -869,7 +875,6 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
* This assumes loadPage() has already primed all the calculations and state variables. Do not call directly.
*/
updateSelectedTab() {
console.log('updateSelectedTab')
// Book libraries only have Volumes or Specials enabled
if (this.libraryType === LibraryType.Book || this.libraryType === LibraryType.LightNovel) {
if (this.volumes.length === 0) {

View File

@ -1,168 +0,0 @@
<ng-container *transloco="let t; read: 'series-metadata-detail'">
<!-- <div class="row g-0 mt-2 mb-2">-->
<!-- <app-read-more [text]="seriesSummary" [maxLength]="utilityService.getActiveBreakpoint() >= Breakpoint.Desktop ? 1000 : 250"></app-read-more>-->
<!-- </div>-->
<!-- Ratings -->
<app-metadata-detail [tags]="['']" [libraryId]="series.libraryId" [heading]="t('rating-title')">
<ng-template #itemTemplate let-item>
<app-external-rating [seriesId]="series.id" [ratings]="ratings" [userRating]="series.userRating" [hasUserRated]="series.hasUserRated" [libraryType]="libraryType"></app-external-rating>
</ng-template>
</app-metadata-detail>
<!-- Weblinks -->
<ng-container *ngIf="WebLinks as links">
<app-metadata-detail [tags]="links" [libraryId]="series.libraryId" [heading]="t('links-title')">
<ng-template #itemTemplate let-item>
<a class="col me-1" [href]="item | safeHtml" target="_blank" rel="noopener noreferrer" [title]="item">
<app-image classes="favicon" width="24px" height="24px"
[imageUrl]="imageService.getWebLinkImage(item)"
[errorImage]="imageService.errorWebLinkImage"/>
</a>
</ng-template>
</app-metadata-detail>
</ng-container>
<!-- Genres -->
<app-metadata-detail [tags]="seriesMetadata.genres" [libraryId]="series.libraryId" [queryParam]="FilterField.Genres" [heading]="t('genres-title')">
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
</app-metadata-detail>
<!-- Tags -->
<app-metadata-detail [tags]="seriesMetadata.tags" [libraryId]="series.libraryId" [queryParam]="FilterField.Tags" [heading]="t('tags-title')">
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
</app-metadata-detail>
<!-- Collections -->
@if (collections$) {
<app-metadata-detail [tags]="(collections$ | async)!" [libraryId]="series.libraryId" [heading]="t('collections-title')">
<ng-template #itemTemplate let-item>
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="navigate('collections', item.id)" [selectionMode]="TagBadgeCursor.Clickable">
<app-promoted-icon [promoted]="item.promoted"></app-promoted-icon> {{item.title}}
</app-tag-badge>
</ng-template>
</app-metadata-detail>
}
<!-- Reading Lists -->
<!-- <app-metadata-detail [tags]="readingLists" [libraryId]="series.libraryId" [heading]="t('reading-lists-title')">-->
<!-- <ng-template #itemTemplate let-item>-->
<!-- <app-tag-badge a11y-click="13,32" class="col-auto" (click)="navigate('lists', item.id)" [selectionMode]="TagBadgeCursor.Clickable">-->
<!-- <span *ngIf="item.promoted">-->
<!-- <i class="fa fa-angle-double-up" aria-hidden="true"></i>&nbsp;-->
<!-- <span class="visually-hidden">({{t('promoted')}})</span>-->
<!-- </span>-->
<!-- {{item.title}}-->
<!-- </app-tag-badge>-->
<!-- </ng-template>-->
<!-- </app-metadata-detail>-->
<!-- Key Person Information -->
<!-- @if (libraryType === LibraryType.LightNovel || libraryType === LibraryType.Book) {-->
<!-- -->
<!-- <app-metadata-detail [tags]="seriesMetadata.coverArtists" [libraryId]="series.libraryId" [queryParam]="FilterField.CoverArtist" [heading]="t('cover-artists-title')">-->
<!-- <ng-template #itemTemplate let-item>-->
<!-- <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>-->
<!-- </ng-template>-->
<!-- </app-metadata-detail>-->
<!-- }-->
<!-- <app-metadata-detail [tags]="seriesMetadata.writers" [libraryId]="series.libraryId" [queryParam]="FilterField.Writers" [heading]="t('writers-title')">-->
<!-- <ng-template #itemTemplate let-item>-->
<!-- <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>-->
<!-- </ng-template>-->
<!-- </app-metadata-detail>-->
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" id="extended-series-metadata">
<!-- <app-metadata-detail [tags]="seriesMetadata.coverArtists" [libraryId]="series.libraryId" [queryParam]="FilterField.CoverArtist" [heading]="t('cover-artists-title')">-->
<!-- <ng-template #itemTemplate let-item>-->
<!-- <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>-->
<!-- </ng-template>-->
<!-- </app-metadata-detail>-->
<!-- <app-metadata-detail [tags]="seriesMetadata.colorists" [libraryId]="series.libraryId" [queryParam]="FilterField.Colorist" [heading]="t('colorists-title')">-->
<!-- <ng-template #itemTemplate let-item>-->
<!-- <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>-->
<!-- </ng-template>-->
<!-- </app-metadata-detail>-->
<!-- <app-metadata-detail [tags]="seriesMetadata.editors" [libraryId]="series.libraryId" [queryParam]="FilterField.Editor" [heading]="t('editors-title')">-->
<!-- <ng-template #itemTemplate let-item>-->
<!-- <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>-->
<!-- </ng-template>-->
<!-- </app-metadata-detail>-->
<!-- <app-metadata-detail [tags]="seriesMetadata.inkers" [libraryId]="series.libraryId" [queryParam]="FilterField.Inker" [heading]="t('inkers-title')">-->
<!-- <ng-template #itemTemplate let-item>-->
<!-- <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>-->
<!-- </ng-template>-->
<!-- </app-metadata-detail>-->
<!-- <app-metadata-detail [tags]="seriesMetadata.letterers" [libraryId]="series.libraryId" [queryParam]="FilterField.Letterer" [heading]="t('letterers-title')">-->
<!-- <ng-template #itemTemplate let-item>-->
<!-- <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>-->
<!-- </ng-template>-->
<!-- </app-metadata-detail>-->
<!-- <app-metadata-detail [tags]="seriesMetadata.pencillers" [libraryId]="series.libraryId" [queryParam]="FilterField.Penciller" [heading]="t('pencillers-title')">-->
<!-- <ng-template #itemTemplate let-item>-->
<!-- <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>-->
<!-- </ng-template>-->
<!-- </app-metadata-detail>-->
<!-- <app-metadata-detail [tags]="seriesMetadata.characters" [libraryId]="series.libraryId" [queryParam]="FilterField.Characters" [heading]="t('characters-title')">-->
<!-- <ng-template #itemTemplate let-item>-->
<!-- <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>-->
<!-- </ng-template>-->
<!-- </app-metadata-detail>-->
<!-- <app-metadata-detail [tags]="seriesMetadata.teams" [libraryId]="series.libraryId" [queryParam]="FilterField.Team" [heading]="t('teams-title')">-->
<!-- <ng-template #titleTemplate let-item>-->
<!-- {{item.name}}-->
<!-- </ng-template>-->
<!-- </app-metadata-detail>-->
<!-- <app-metadata-detail [tags]="seriesMetadata.locations" [libraryId]="series.libraryId" [queryParam]="FilterField.Location" [heading]="t('locations-title')">-->
<!-- <ng-template #titleTemplate let-item>-->
<!-- {{item.name}}-->
<!-- </ng-template>-->
<!-- </app-metadata-detail>-->
<!-- <app-metadata-detail [tags]="seriesMetadata.publishers" [libraryId]="series.libraryId" [queryParam]="FilterField.Publisher" [heading]="t('publishers-title')">-->
<!-- <ng-template #itemTemplate let-item>-->
<!-- <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>-->
<!-- </ng-template>-->
<!-- </app-metadata-detail>-->
<!-- <app-metadata-detail [tags]="seriesMetadata.imprints" [libraryId]="series.libraryId" [queryParam]="FilterField.Imprint" [heading]="t('imprints-title')">-->
<!-- <ng-template #titleTemplate let-item>-->
<!-- {{item.name}}-->
<!-- </ng-template>-->
<!-- </app-metadata-detail>-->
<!-- <app-metadata-detail [tags]="seriesMetadata.translators" [libraryId]="series.libraryId" [queryParam]="FilterField.Translators" [heading]="t('translators-title')">-->
<!-- <ng-template #itemTemplate let-item>-->
<!-- <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>-->
<!-- </ng-template>-->
<!-- </app-metadata-detail>-->
</div>
<div class="row g-0">
<hr class="col mt-3" *ngIf="hasExtendedProperties" >
<a [class.hidden]="hasExtendedProperties" *ngIf="hasExtendedProperties"
class="col col-md-auto align-self-end read-more-link" (click)="toggleView()">
<i aria-hidden="true" class="fa fa-caret-{{isCollapsed ? 'down' : 'up'}} me-1" aria-controls="extended-series-metadata"></i>
{{isCollapsed ? t('see-more') : t('see-less')}}
</a>
</div>
<app-series-info-cards [series]="series" [seriesMetadata]="seriesMetadata" (goTo)="handleGoTo($event)" [hasReadingProgress]="hasReadingProgress"></app-series-info-cards>
</ng-container>

View File

@ -1,153 +0,0 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
inject,
Input,
OnChanges, OnInit,
SimpleChanges,
ViewEncapsulation
} from '@angular/core';
import {Router} from '@angular/router';
import {TagBadgeComponent, TagBadgeCursor} from '../../../shared/tag-badge/tag-badge.component';
import {FilterUtilitiesService} from '../../../shared/_services/filter-utilities.service';
import {Breakpoint, UtilityService} from '../../../shared/_services/utility.service';
import {MangaFormat} from '../../../_models/manga-format';
import {ReadingList} from '../../../_models/reading-list';
import {Series} from '../../../_models/series';
import {SeriesMetadata} from '../../../_models/metadata/series-metadata';
import {ImageService} from 'src/app/_services/image.service';
import {CommonModule} from "@angular/common";
import {BadgeExpanderComponent} from "../../../shared/badge-expander/badge-expander.component";
import {SafeHtmlPipe} from "../../../_pipes/safe-html.pipe";
import {ExternalRatingComponent} from "../external-rating/external-rating.component";
import {ReadMoreComponent} from "../../../shared/read-more/read-more.component";
import {A11yClickDirective} from "../../../shared/a11y-click.directive";
import {PersonBadgeComponent} from "../../../shared/person-badge/person-badge.component";
import {NgbCollapse} from "@ng-bootstrap/ng-bootstrap";
import {SeriesInfoCardsComponent} from "../../../cards/series-info-cards/series-info-cards.component";
import {LibraryType} from "../../../_models/library/library";
import {MetadataDetailComponent} from "../metadata-detail/metadata-detail.component";
import {TranslocoDirective} from "@jsverse/transloco";
import {FilterField} from "../../../_models/metadata/v2/filter-field";
import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison";
import {ImageComponent} from "../../../shared/image/image.component";
import {Rating} from "../../../_models/rating";
import {CollectionTagService} from "../../../_services/collection-tag.service";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {shareReplay} from "rxjs/operators";
import {PromotedIconComponent} from "../../../shared/_components/promoted-icon/promoted-icon.component";
import {Observable} from "rxjs";
import {UserCollection} from "../../../_models/collection-tag";
@Component({
selector: 'app-series-metadata-detail',
standalone: true,
imports: [CommonModule, TagBadgeComponent, BadgeExpanderComponent, SafeHtmlPipe, ExternalRatingComponent,
ReadMoreComponent, A11yClickDirective, PersonBadgeComponent, NgbCollapse, SeriesInfoCardsComponent,
MetadataDetailComponent, TranslocoDirective, ImageComponent, PromotedIconComponent],
templateUrl: './series-metadata-detail.component.html',
styleUrls: ['./series-metadata-detail.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None
})
export class SeriesMetadataDetailComponent implements OnChanges, OnInit {
protected readonly imageService = inject(ImageService);
protected readonly utilityService = inject(UtilityService);
private readonly router = inject(Router);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly filterUtilityService = inject(FilterUtilitiesService);
private readonly collectionTagService = inject(CollectionTagService);
private readonly destroyRef = inject(DestroyRef);
protected readonly FilterField = FilterField;
protected readonly LibraryType = LibraryType;
protected readonly MangaFormat = MangaFormat;
protected readonly TagBadgeCursor = TagBadgeCursor;
protected readonly Breakpoint = Breakpoint;
@Input({required: true}) seriesMetadata!: SeriesMetadata;
@Input({required: true}) libraryType!: LibraryType;
@Input() hasReadingProgress: boolean = false;
/**
* Reading lists with a connection to the Series
*/
@Input() readingLists: Array<ReadingList> = [];
@Input({required: true}) series!: Series;
@Input({required: true}) ratings: Array<Rating> = [];
isCollapsed: boolean = true;
hasExtendedProperties: boolean = false;
/**
* Html representation of Series Summary
*/
seriesSummary: string = '';
collections$: Observable<UserCollection[]> | undefined;
get WebLinks() {
if (this.seriesMetadata?.webLinks === '') return [];
return this.seriesMetadata?.webLinks.split(',') || [];
}
ngOnInit() {
// If on desktop, we can just have all the data expanded by default:
this.isCollapsed = true; // this.utilityService.getActiveBreakpoint() < Breakpoint.Desktop;
// Check if there is a lot of extended data, if so, re-collapse
const sum = (this.seriesMetadata.colorists.length + this.seriesMetadata.editors.length
+ this.seriesMetadata.coverArtists.length + this.seriesMetadata.inkers.length
+ this.seriesMetadata.letterers.length + this.seriesMetadata.pencillers.length
+ this.seriesMetadata.publishers.length + this.seriesMetadata.characters.length
+ this.seriesMetadata.imprints.length + this.seriesMetadata.translators.length
+ this.seriesMetadata.writers.length + this.seriesMetadata.teams.length + this.seriesMetadata.locations.length) / 13;
if (sum > 10) {
this.isCollapsed = true;
}
this.collections$ = this.collectionTagService.allCollectionsForSeries(this.series.id).pipe(
takeUntilDestroyed(this.destroyRef), shareReplay({bufferSize: 1, refCount: true}));
this.cdRef.markForCheck();
}
ngOnChanges(changes: SimpleChanges): void {
this.hasExtendedProperties = this.seriesMetadata.colorists.length > 0 ||
this.seriesMetadata.editors.length > 0 ||
this.seriesMetadata.coverArtists.length > 0 ||
this.seriesMetadata.inkers.length > 0 ||
this.seriesMetadata.letterers.length > 0 ||
this.seriesMetadata.pencillers.length > 0 ||
this.seriesMetadata.publishers.length > 0 ||
this.seriesMetadata.characters.length > 0 ||
this.seriesMetadata.imprints.length > 0 ||
this.seriesMetadata.teams.length > 0 ||
this.seriesMetadata.locations.length > 0 ||
this.seriesMetadata.translators.length > 0
;
this.seriesSummary = (this.seriesMetadata?.summary === null ? '' : this.seriesMetadata.summary).replace(/\n/g, '<br>');
this.cdRef.markForCheck();
}
toggleView() {
this.isCollapsed = !this.isCollapsed;
this.cdRef.markForCheck();
}
handleGoTo(event: {queryParamName: FilterField, filter: any}) {
this.goTo(event.queryParamName, event.filter);
}
goTo(queryParamName: FilterField, filter: any) {
this.filterUtilityService.applyFilter(['library', this.series.libraryId], queryParamName,
FilterComparison.Equal, filter).subscribe();
}
navigate(basePage: string, id: number) {
this.router.navigate([basePage, id]);
}
}

View File

@ -88,6 +88,7 @@ export class SideNavItemComponent implements OnInit {
triggerHighlightCheck(routeUrl: string) {
const [url, queryParams] = routeUrl.split('?');
const [page, fragment = ''] = url.split('#');
this.updateHighlight(page, queryParams, url.includes('#') ? fragment : undefined);
}
@ -99,7 +100,7 @@ export class SideNavItemComponent implements OnInit {
return;
}
if (!page.endsWith('/') && !queryParams && this.fragment === undefined) {
if (!page.endsWith('/') && !queryParams && this.fragment === undefined && queryParams === undefined) {
page = page + '/';
}
@ -111,8 +112,9 @@ export class SideNavItemComponent implements OnInit {
fragmentEqual = true;
}
const queryParamsEqual = this.queryParams === queryParams;
if (this.comparisonMethod === 'equals' && page === this.link && fragmentEqual) {
if (this.comparisonMethod === 'equals' && page === this.link && fragmentEqual && queryParamsEqual) {
this.highlighted = true;
this.cdRef.markForCheck();
return;

View File

@ -7,22 +7,6 @@
<form [formGroup]="settingsForm">
<h4>{{t('global-settings-title')}}</h4>
<ng-container>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('page-layout-mode-label')" [subtitle]="t('page-layout-mode-tooltip')">
<ng-template #view>
{{settingsForm.get('globalPageLayoutMode')!.value | pageLayoutMode}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="manga-header"
formControlName="globalPageLayoutMode">
@for (opt of pageLayoutModes; track opt) {
<option [value]="opt.value">{{opt.value | pageLayoutMode}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('locale-label')" [subtitle]="t('locale-tooltip')">

View File

@ -1,6 +1,6 @@
.chooser {
display: grid;
grid-template-columns: repeat(auto-fill, 158px);
grid-template-columns: repeat(auto-fill, 160px);
grid-gap: 0.5rem;
justify-content: space-around;
}

View File

@ -14,7 +14,7 @@
}
.card-container{
display: grid;
grid-template-columns: repeat(auto-fill, 158px);
grid-template-columns: repeat(auto-fill, 160px);
grid-gap: 0.5rem;
justify-content: space-around;
}

View File

@ -437,7 +437,7 @@ export class VolumeDetailComponent implements OnInit {
updateUrl(activeTab: TabID) {
const newUrl = `${this.router.url.split('#')[0]}#${activeTab}`;
this.router.navigateByUrl(newUrl, { onSameUrlNavigation: 'ignore' });
//this.router.navigateByUrl(newUrl, { onSameUrlNavigation: 'ignore' });
}
openPerson(field: FilterField, value: number) {

View File

@ -801,19 +801,22 @@
"user-reviews-local": "Local Reviews",
"user-reviews-plus": "External Reviews",
"writers-title": "Writers",
"cover-artists-title": "Artists",
"characters-title": "Characters",
"colorists-title": "Colorists",
"editors-title": "Editors",
"inkers-title": "Inkers",
"letterers-title": "Letterers",
"translators-title": "Translators",
"pencillers-title": "Pencillers",
"publishers-title": "Publishers",
"imprints-title": "Imprints",
"teams-title": "Teams",
"locations-title": "Locations",
"writers-title": "{{metadata-fields.writers-title}}",
"cover-artists-title": "{{metadata-fields.cover-artists-title}}",
"characters-title": "{{metadata-fields.characters-title}}",
"colorists-title": "{{metadata-fields.colorists-title}}",
"editors-title": "{{metadata-fields.editors-title}}",
"inkers-title": "{{metadata-fields.inkers-title}}",
"letterers-title": "{{metadata-fields.letterers-title}}",
"translators-title": "{{metadata-fields.translators-title}}",
"pencillers-title": "{{metadata-fields.pencillers-title}}",
"publishers-title": "{{metadata-fields.publishers-title}}",
"imprints-title": "{{metadata-fields.imprints-title}}",
"teams-title": "{{metadata-fields.teams-title}}",
"locations-title": "{{metadata-fields.locations-title}}",
"genres-title": "{{metadata-fields.genres-title}}",
"tags-title": "{{metadata-fields.tags-title}}",
"ongoing": "{{publication-status-pipe.ongoing}}",
"volume-num": "{{common.volume-num}}",
"reading-lists-title": "{{side-nav.reading-lists}}",
@ -828,16 +831,13 @@
"publication-status-tooltip": "Publication Status"
},
"series-metadata-detail": {
"links-title": "Links",
"rating-title": "Ratings",
"genres-title": "Genres",
"tags-title": "Tags",
"metadata-fields": {
"collections-title": "{{side-nav.collections}}",
"reading-lists-title": "{{side-nav.reading-lists}}",
"genres-title": "Genres",
"tags-title": "Tags",
"writers-title": "Writers",
"cover-artists-title": "Cover Artists",
"cover-artists-title": "Artists",
"characters-title": "Characters",
"colorists-title": "Colorists",
"editors-title": "Editors",
@ -848,12 +848,7 @@
"publishers-title": "Publishers",
"imprints-title": "Imprints",
"teams-title": "Teams",
"locations-title": "Locations",
"promoted": "{{common.promoted}}",
"see-more": "See More",
"see-less": "See Less"
"locations-title": "Locations"
},
"download-button": {
@ -1422,7 +1417,7 @@
"bust-cache-task-success": "Kavita+ Cache busted",
"bust-locale-task": "Bust Locale Cache",
"bust-locale-task-desc": "Busts the Locale Cache. This can fix issues with strings not showing correctly after an update",
"bust-locale-task-desc": "Busts the Locale Cache. This can fix issues with strings not showing correctly after an update. A browser refresh is needed.",
"bust-locale-task-success": "Locale Cache busted",
"clear-reading-cache-task": "Clear Reading Cache",
@ -1641,7 +1636,7 @@
"chapters": "Chapters",
"people": "People",
"tags": "Tags",
"genres": "Genres",
"genres": "{{metadata-fields.genres-title}}",
"bookmarks": "{{side-nav.bookmarks}}",
"libraries": "Libraries",
"reading-lists": "Reading Lists",
@ -1842,8 +1837,8 @@
"format-label": "Format",
"libraries-label": "Libraries",
"collections-label": "Collections",
"genres-label": "Genres",
"tags-label": "Tags",
"genres-label": "{{metadata-fields.genres-title}}",
"tags-label": "{{metadata-fields.tags-title}}",
"cover-artist-label": "Cover Artist",
"writer-label": "Writer",
"publisher-label": "Publisher",
@ -1897,8 +1892,9 @@
"related-tab": "{{tabs.related-tab}}",
"info-tab": "{{tabs.info-tab}}",
"tasks-tab": "{{tabs.tasks-tab}}",
"genres-label": "Genres",
"tags-label": "Tags",
"genres-label": "{{metadata-fields.genres-title}}",
"tags-label": "{{metadata-fields.tags-title}}",
"cover-artist-label": "Cover Artist",
"writer-label": "Writer",
"publisher-label": "Publisher",
@ -1914,6 +1910,7 @@
"location-label": "{{filter-field-pipe.location}}",
"language-label": "Language",
"age-rating-label": "Age Rating",
"publication-status-label": "Publication Status",
"required-field": "{{validation.required-field}}",
"close": "{{common.close}}",
@ -1928,24 +1925,28 @@
"save": "{{common.save}}",
"field-locked-alt": "Field is locked",
"info-title": "Information",
"library-title": "Library:",
"format-title": "Format:",
"created-title": "Created:",
"last-read-title": "Last Read:",
"last-added-title": "Last Item Added:",
"last-scanned-title": "Last Scanned:",
"folder-path-title": "Folder Path:",
"publication-status-title": "Publication Status:",
"total-pages-title": "Total Pages:",
"total-items-title": "Total Items:",
"max-items-title": "Max Items:",
"size-title": "Size:",
"library-title": "Library",
"format-title": "Format",
"created-title": "Created",
"last-read-title": "Last Read",
"last-added-title": "Last Item Added",
"last-scanned-title": "Last Scanned",
"folder-path-title": "Folder Path",
"folder-path-tooltip": "Highest path from library root that contains all series files",
"lowest-folder-path-title": "Lowest Folder Path",
"lowest-folder-path-tooltip": "Lowest path from library root that contains all series files",
"publication-status-title": "Publication Status",
"total-pages-title": "Total Pages",
"total-items-title": "Total Items",
"max-items-title": "Max Items",
"size-title": "Size",
"loading": "{{common.loading}}",
"added-title": "Added:",
"last-modified-title": "Last Modified:",
"added-title": "Added",
"last-modified-title": "Last Modified",
"view-files": "View Files",
"pages-title": "Pages:",
"chapter-title": "Chapter:",
"pages-title": "Pages",
"words-title": "Words",
"chapter-title": "Chapter",
"volume-num": "{{common.volume-num}}",
"highest-count-tooltip": "Highest Count found across all ComicInfo in the Series",
"max-issue-tooltip": "Max Issue or Volume field from all ComicInfo in the series",
@ -2143,9 +2144,9 @@
"genre-count": "{{num}} Genres",
"tag-count": "{{num}} Tags",
"people-count": "{{num}} People",
"tags": "Tags",
"tags": "{{metadata-fields.tags-title}}",
"people": "People",
"genres": "Genres"
"genres": "{{metadata-fields.genres-title}}"
},
"errors": {
@ -2253,13 +2254,13 @@
"filter-field-pipe": {
"age-rating": "Age Rating",
"characters": "Characters",
"characters": "{{metadata-fields.characters-title}}",
"collection-tags": "Collection Tags",
"colorist": "Colorist",
"cover-artist": "Cover Artist",
"editor": "Editor",
"formats": "Formats",
"genres": "Genres",
"genres": "{{metadata-fields.genres-title}}",
"inker": "Inker",
"team": "Team",
"location": "Location",
@ -2275,10 +2276,10 @@
"release-year": "Release Year",
"series-name": "Series Name",
"summary": "Summary",
"tags": "Tags",
"tags": "{{metadata-fields.tags-title}}",
"translators": "Translators",
"user-rating": "User Rating",
"writers": "Writers",
"writers": "{{metadata-fields.writers-title}}",
"path": "Path",
"file-path": "File Path",
"want-to-read": "Want to Read",

View File

@ -1,3 +1,9 @@
@include media-breakpoint-down(md) {
.card-actions {
visibility: visible !important;
}
}
.card {
background-color: var(--card-bg-color);
color: var(--card-text-color);
@ -21,7 +27,7 @@
}
}
$image-height: 230px;
$image-height: 232.91px;
$image-filter-height: 160px;
$image-width: 160px;

View File

@ -2,7 +2,7 @@
"openapi": "3.0.1",
"info": {
"title": "Kavita",
"description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.2.3",
"description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.2.4",
"license": {
"name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
@ -1120,17 +1120,6 @@
"Cbl"
],
"summary": "The first step in a cbl import. This validates the cbl file that if an import occured, would it be successful.\r\nIf this returns errors, the cbl will always be rejected by Kavita.",
"parameters": [
{
"name": "comicVineMatching",
"in": "query",
"description": "Use comic vine matching or not. Defaults to false",
"schema": {
"type": "boolean",
"default": false
}
}
],
"requestBody": {
"content": {
"multipart/form-data": {
@ -1140,12 +1129,19 @@
"cbl": {
"type": "string",
"format": "binary"
},
"comicVineMatching": {
"type": "boolean",
"default": false
}
}
},
"encoding": {
"cbl": {
"style": "form"
},
"comicVineMatching": {
"style": "form"
}
}
}
@ -1181,26 +1177,6 @@
"Cbl"
],
"summary": "Performs the actual import (assuming dryRun = false)",
"parameters": [
{
"name": "dryRun",
"in": "query",
"description": "If true, will only emulate the import but not perform. This should be done to preview what will happen",
"schema": {
"type": "boolean",
"default": false
}
},
{
"name": "comicVineMatching",
"in": "query",
"description": "Use comic vine matching or not. Defaults to false",
"schema": {
"type": "boolean",
"default": false
}
}
],
"requestBody": {
"content": {
"multipart/form-data": {
@ -1210,12 +1186,26 @@
"cbl": {
"type": "string",
"format": "binary"
},
"dryRun": {
"type": "boolean",
"default": false
},
"comicVineMatching": {
"type": "boolean",
"default": false
}
}
},
"encoding": {
"cbl": {
"style": "form"
},
"dryRun": {
"style": "form"
},
"comicVineMatching": {
"style": "form"
}
}
}
@ -12778,7 +12768,7 @@
"Upload"
],
"summary": "Replaces volume cover image and locks it with a base64 encoded image.",
"description": "This is a helper API for Komf - Kavita UI does not use. Volume will find first chapter to update.",
"description": "This will not update the underlying chapter",
"requestBody": {
"description": "",
"content": {