Merge branch 'develop' of https://github.com/Kareadita/Kavita into develop

This commit is contained in:
majora2007 2022-03-18 01:30:08 +00:00
commit ad71ef2358
16 changed files with 114 additions and 54 deletions

View File

@ -202,7 +202,7 @@ public class SeriesServiceTests
Assert.NotEmpty(detail.Volumes);
Assert.Equal(2, detail.Volumes.Count()); // Volume 0 shouldn't be sent in Volumes
Assert.All(detail.Volumes, dto => Assert.Contains(dto.Name, new[] {"2", "3"}));
Assert.All(detail.Volumes, dto => Assert.Contains(dto.Name, new[] {"Volume 2", "Volume 3"})); // Volumes get names mapped
}
[Fact]

View File

@ -28,6 +28,7 @@ public class OpdsController : BaseApiController
private readonly IDirectoryService _directoryService;
private readonly ICacheService _cacheService;
private readonly IReaderService _readerService;
private readonly ISeriesService _seriesService;
private readonly XmlSerializer _xmlSerializer;
@ -61,13 +62,14 @@ public class OpdsController : BaseApiController
public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService,
IDirectoryService directoryService, ICacheService cacheService,
IReaderService readerService)
IReaderService readerService, ISeriesService seriesService)
{
_unitOfWork = unitOfWork;
_downloadService = downloadService;
_directoryService = directoryService;
_cacheService = cacheService;
_readerService = readerService;
_seriesService = seriesService;
_xmlSerializer = new XmlSerializer(typeof(Feed));
_xmlOpenSearchSerializer = new XmlSerializer(typeof(OpenSearchDescription));
@ -314,16 +316,17 @@ public class OpdsController : BaseApiController
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId)).ToList();
foreach (var item in items)
{
feed.Entries.Add(new FeedEntry()
{
Id = item.ChapterId.ToString(),
Title = $"{item.SeriesName} Chapter {item.ChapterNumber}",
Links = new List<FeedLink>()
{
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/series/{item.SeriesId}/volume/{item.VolumeId}/chapter/{item.ChapterId}"),
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/chapter-cover?chapterId={item.ChapterId}")
}
});
feed.Entries.Add(CreateChapter(apiKey, $"{item.SeriesName} Chapter {item.ChapterNumber}", item.ChapterId, item.VolumeId, item.SeriesId));
// new FeedEntry()
// {
// Id = item.ChapterId.ToString(),
// Title = $"{item.SeriesName} Chapter {item.ChapterNumber}",
// Links = new List<FeedLink>()
// {
// CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/series/{item.SeriesId}/volume/{item.VolumeId}/chapter/{item.ChapterId}"),
// CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/chapter-cover?chapterId={item.ChapterId}")
// }
// }
}
return CreateXmlResult(SerializeXml(feed));
@ -521,15 +524,30 @@ public class OpdsController : BaseApiController
return BadRequest("OPDS is not enabled on this server");
var userId = await GetUser(apiKey);
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
var volumes = await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId);
var feed = CreateFeed(series.Name + " - Volumes", $"{apiKey}/series/{series.Id}", apiKey);
var feed = CreateFeed(series.Name + " - Storyline", $"{apiKey}/series/{series.Id}", apiKey);
SetFeedId(feed, $"series-{series.Id}");
feed.Links.Add(CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/series-cover?seriesId={seriesId}"));
foreach (var volumeDto in volumes)
// NOTE: I want to try and use ReaderService to get SeriesDetails.
var seriesDetail = await _seriesService.GetSeriesDetail(seriesId, userId);
foreach (var volume in seriesDetail.Volumes)
{
feed.Entries.Add(CreateVolume(volumeDto, seriesId, apiKey));
feed.Entries.Add(CreateVolume(volume, seriesId, apiKey)); // We might want to emulate a volume but make this a chapter
}
foreach (var storylineChapter in seriesDetail.StorylineChapters.Where(c => !c.IsSpecial))
{
feed.Entries.Add(CreateChapter(apiKey, storylineChapter.Title, storylineChapter.Id, storylineChapter.VolumeId, seriesId));
}
foreach (var special in seriesDetail.Specials)
{
feed.Entries.Add(CreateChapter(apiKey, special.Title, special.Id, special.VolumeId, seriesId));
}
return CreateXmlResult(SerializeXml(feed));
}
@ -698,6 +716,23 @@ public class OpdsController : BaseApiController
};
}
private static FeedEntry CreateChapter(string apiKey, string title, int chapterId, int volumeId, int seriesId)
{
return new FeedEntry()
{
Id = chapterId.ToString(),
Title = title,
Links = new List<FeedLink>()
{
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation,
Prefix + $"{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}"),
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image,
$"/api/image/chapter-cover?chapterId={chapterId}")
}
};
}
private FeedEntry CreateChapter(int seriesId, int volumeId, int chapterId, MangaFile mangaFile, SeriesDto series, Volume volume, ChapterDto chapter, string apiKey)
{
var fileSize =

View File

@ -1,5 +1,4 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -14,7 +13,6 @@ using API.Entities.Enums;
using API.Helpers;
using API.SignalR;
using Microsoft.Extensions.Logging;
using Microsoft.VisualBasic;
namespace API.Services;
@ -460,22 +458,40 @@ public class SeriesService : ISeriesService
{
volume.Name += $" - {firstChapter.TitleName}";
}
processedVolumes.Add(volume);
}
}
else
{
processedVolumes = volumes.Where(v => v.Number > 0).ToList();
processedVolumes.ForEach(v => v.Name = $"Volume {v.Name}");
}
var specials = new List<ChapterDto>();
foreach (var chapter in chapters.Where(c => c.IsSpecial))
foreach (var chapter in chapters)
{
chapter.Title = Parser.Parser.CleanSpecialTitle(chapter.Title);
specials.Add(chapter);
if (chapter.IsSpecial)
{
chapter.Title = Parser.Parser.CleanSpecialTitle(chapter.Title);
specials.Add(chapter);
}
else
{
var title = libraryType switch
{
LibraryType.Book => $"Book {chapter.Title}",
LibraryType.Comic => $"Issue #{chapter.Title}",
LibraryType.Manga => $"Chapter {chapter.Title}",
_ => "Chapter "
};
chapter.Title = title;
}
}
// Don't show chapter 0 (aka single volume chapters) in the Chapters tab or books that are just single numbers (they show as volumes)
IEnumerable<ChapterDto> retChapters;
if (libraryType == LibraryType.Book)
@ -497,7 +513,7 @@ public class SeriesService : ISeriesService
Volumes = processedVolumes,
StorylineChapters = volumes
.Where(v => v.Number == 0)
.SelectMany(v => v.Chapters)
.SelectMany(v => v.Chapters.Where(c => !c.IsSpecial))
.OrderBy(c => float.Parse(c.Number), new ChapterSortComparer())
};

View File

@ -1,4 +1,5 @@
import { Injectable } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core';
import { ReplaySubject, take } from 'rxjs';
@Injectable({
@ -25,7 +26,10 @@ export class NavService {
*/
sideNavVisibility$ = this.sideNavVisibilitySource.asObservable();
constructor() {
private renderer: Renderer2;
constructor(@Inject(DOCUMENT) private document: Document, rendererFactory: RendererFactory2) {
this.renderer = rendererFactory.createRenderer(null, null);
this.showNavBar();
const sideNavState = (localStorage.getItem(this.localStorageSideNavKey) === 'true') || false;
this.sideNavCollapseSource.next(sideNavState);
@ -36,6 +40,7 @@ export class NavService {
* Shows the top nav bar. This should be visible on all pages except the reader.
*/
showNavBar() {
this.renderer.setStyle(this.document.querySelector('body'), 'margin-top', '56px');
this.navbarVisibleSource.next(true);
}
@ -43,6 +48,7 @@ export class NavService {
* Hides the top nav bar.
*/
hideNavBar() {
this.renderer.setStyle(this.document.querySelector('body'), 'margin-top', '0px');
this.navbarVisibleSource.next(false);
}

View File

@ -1,7 +1,7 @@
<app-side-nav-companion-bar>
<h1 title>
<h2 title>
Admin Dashboard
</h1>
</h2>
</app-side-nav-companion-bar>
<div class="container-fluid">
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav nav-tabs">

View File

@ -1,8 +1,8 @@
<app-side-nav-companion-bar [hasFilter]="true" (filterOpen)="filterOpen.emit($event)">
<h1 title>
<h2 title>
<app-card-actionables [actions]="actions"></app-card-actionables>
All Series
</h1>
</h2>
<h6 subtitle style="margin-left:40px;">{{pagination?.totalItems}} Series</h6>
</app-side-nav-companion-bar>
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>

View File

@ -2,12 +2,15 @@
<div [ngClass]="{'closed' : !(navService?.sideNavCollapsed$ | async), 'content-wrapper': navService.sideNavVisibility$ | async}">
<a id="content"></a>
<app-side-nav *ngIf="navService.sideNavVisibility$ | async"></app-side-nav>
<div class="container-fluid" style="padding-top: 10px; padding-bottom: 10px;" *ngIf="navService.sideNavVisibility$ | async else noSideNav">
<div class="companion-bar" [ngClass]="{'companion-bar-content': (navService?.sideNavCollapsed$ | async)}">
<router-outlet></router-outlet>
<div class="container-fluid">
<div style="padding-top: 10px; padding-bottom: 65px;" *ngIf="navService.sideNavVisibility$ | async else noSideNav">
<div class="companion-bar" [ngClass]="{'companion-bar-content': (navService?.sideNavCollapsed$ | async)}">
<router-outlet></router-outlet>
</div>
</div>
<ng-template #noSideNav>
<router-outlet></router-outlet>
</ng-template>
</div>
<ng-template #noSideNav>
<router-outlet></router-outlet>
</ng-template>
</div>

View File

@ -1,8 +1,8 @@
<app-side-nav-companion-bar [hasFilter]="false" (filterOpen)="filterOpen.emit($event)">
<h1 title>
<h2 title>
<app-card-actionables [actions]="collectionTagActions"></app-card-actionables>
Collections
</h1>
</h2>
<h6 subtitle style="margin-left:40px;">{{collections.length}} Items</h6>
</app-side-nav-companion-bar>
<app-card-detail-layout

View File

@ -1,8 +1,8 @@
<app-side-nav-companion-bar [hasFilter]="true" (filterOpen)="filterOpen.emit($event)">
<h1 title>
<h2 title>
<app-card-actionables [actions]="actions"></app-card-actionables>
{{libraryName}}
</h1>
</h2>
<h6 subtitle style="margin-left:40px;">{{pagination?.totalItems}} Series</h6>
<div main>
<!-- TODO: Implement Tabs here for Recommended and Library view -->

View File

@ -1,7 +1,7 @@
<app-side-nav-companion-bar [hasFilter]="true" (filterOpen)="filterOpen.emit($event)">
<h1 title>
<h2 title>
On Deck
</h1>
</h2>
</app-side-nav-companion-bar>
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<app-card-detail-layout

View File

@ -1,11 +1,11 @@
<app-side-nav-companion-bar [showGoBack]="true">
<h1 title>
<h2 title>
<span *ngIf="actions.length > 0">
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="readingList.title"></app-card-actionables>
</span>
{{readingList.title}}&nbsp;<span *ngIf="readingList?.promoted">(<i class="fa fa-angle-double-up" aria-hidden="true"></i>)</span>&nbsp;
<span class="badge bg-primary rounded-pill" attr.aria-label="{{items.length}} total items">{{items.length}}</span>
</h1>
</h2>
</app-side-nav-companion-bar>
<div class="container-fluid mt-2" *ngIf="readingList">
<div class="mb-3">

View File

@ -1,8 +1,8 @@
<app-side-nav-companion-bar [showGoBack]="true" pageHeader="Home">
<h1 title>
<h2 title>
<app-card-actionables [actions]="actions"></app-card-actionables>
Reading Lists
</h1>
</h2>
<h6 subtitle>{{pagination?.totalItems}} Items</h6>
</app-side-nav-companion-bar>

View File

@ -1,7 +1,7 @@
<app-side-nav-companion-bar [hasFilter]="true" (filterOpen)="filterOpen.emit($event)">
<h1 title>
<h2 title>
Recently Added
</h1>
</h2>
</app-side-nav-companion-bar>
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<app-card-detail-layout

View File

@ -67,7 +67,7 @@
<ng-template ngbNavContent>
<div class="row g-0">
<ng-container *ngFor="let chapter of specials; let idx = index; trackBy: trackByChapterIdentity">
<app-card-item class="col-auto p-2" *ngIf="chapter.isSpecial" [entity]="chapter" [title]="chapter.title || chapter.range" (click)="openChapter(chapter)"
<app-card-item class="col-auto p-2" [entity]="chapter" [title]="chapter.title || chapter.range" (click)="openChapter(chapter)"
[imageUrl]="imageService.getChapterCoverImage(chapter.id)"
[read]="chapter.pagesRead" [total]="chapter.pages" [actions]="chapterActions" (selection)="bulkSelectionService.handleCardSelection('special', idx, chapters.length, $event)" [selected]="bulkSelectionService.isCardSelected('special', idx)" [allowSelection]="true"></app-card-item>
</ng-container>
@ -79,12 +79,12 @@
<ng-template ngbNavContent>
<div class="row g-0">
<ng-container *ngFor="let volume of volumes; let idx = index; trackBy: trackByVolumeIdentity">
<app-card-item class="col-auto p-2" *ngIf="volume.number != 0" [entity]="volume" [title]="formatVolumeTitle(volume)" (click)="openVolume(volume)"
<app-card-item class="col-auto p-2" *ngIf="volume.number != 0" [entity]="volume" [title]="volume.name" (click)="openVolume(volume)"
[imageUrl]="imageService.getVolumeCoverImage(volume.id) + '&offset=' + coverImageOffset"
[read]="volume.pagesRead" [total]="volume.pages" [actions]="volumeActions" (selection)="bulkSelectionService.handleCardSelection('volume', idx, volumes.length, $event)" [selected]="bulkSelectionService.isCardSelected('volume', idx)" [allowSelection]="true"></app-card-item>
</ng-container>
<ng-container *ngFor="let chapter of storyChapters; let idx = index; trackBy: trackByChapterIdentity">
<app-card-item class="col-auto p-2" *ngIf="!chapter.isSpecial" [entity]="chapter" [title]="formatChapterTitle(chapter)" (click)="openChapter(chapter)"
<app-card-item class="col-auto p-2" *ngIf="!chapter.isSpecial" [entity]="chapter" [title]="chapter.title" (click)="openChapter(chapter)"
[imageUrl]="imageService.getChapterCoverImage(chapter.id) + '&offset=' + coverImageOffset"
[read]="chapter.pagesRead" [total]="chapter.pages" [actions]="chapterActions" (selection)="bulkSelectionService.handleCardSelection('chapter', idx, storyChapters.length, $event)" [selected]="bulkSelectionService.isCardSelected('chapter', idx)" [allowSelection]="true"></app-card-item>
</ng-container>
@ -96,7 +96,7 @@
<ng-template ngbNavContent>
<div class="row g-0">
<ng-container *ngFor="let volume of volumes; let idx = index; trackBy: trackByVolumeIdentity">
<app-card-item class="col-auto p-2" [entity]="volume" [title]="formatVolumeTitle(volume)" (click)="openVolume(volume)"
<app-card-item class="col-auto p-2" [entity]="volume" [title]="volume.name" (click)="openVolume(volume)"
[imageUrl]="imageService.getVolumeCoverImage(volume.id) + '&offset=' + coverImageOffset"
[read]="volume.pagesRead" [total]="volume.pages" [actions]="volumeActions" (selection)="bulkSelectionService.handleCardSelection('volume', idx, volumes.length, $event)" [selected]="bulkSelectionService.isCardSelected('volume', idx)" [allowSelection]="true"></app-card-item>
</ng-container>
@ -108,7 +108,7 @@
<ng-template ngbNavContent>
<div class="row g-0">
<ng-container *ngFor="let chapter of chapters; let idx = index; trackBy: trackByChapterIdentity">
<app-card-item class="col-auto p-2" *ngIf="!chapter.isSpecial" [entity]="chapter" [title]="formatChapterTitle(chapter)" (click)="openChapter(chapter)"
<app-card-item class="col-auto p-2" *ngIf="!chapter.isSpecial" [entity]="chapter" [title]="chapter.title" (click)="openChapter(chapter)"
[imageUrl]="imageService.getChapterCoverImage(chapter.id) + '&offset=' + coverImageOffset"
[read]="chapter.pagesRead" [total]="chapter.pages" [actions]="chapterActions" (selection)="bulkSelectionService.handleCardSelection('chapter', idx, chapters.length, $event)" [selected]="bulkSelectionService.isCardSelected('chapter', idx)" [allowSelection]="true"></app-card-item>
</ng-container>

View File

@ -1,7 +1,7 @@
<app-side-nav-companion-bar>
<h1 title>
<h2 title>
User Dashboard
</h1>
</h2>
</app-side-nav-companion-bar>
<div class="container-fluid">
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav nav-tabs">

View File

@ -8,7 +8,7 @@ body {
color-scheme: var(--color-scheme);
max-height: 100%;
overflow-y: auto;
margin-top: 56px;
//margin-top: 56px; // Set by nav service
}