mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Merge branch 'develop' of https://github.com/Kareadita/Kavita into develop
This commit is contained in:
commit
ad71ef2358
@ -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]
|
||||
|
@ -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 =
|
||||
|
@ -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())
|
||||
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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 -->
|
||||
|
@ -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
|
||||
|
@ -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}} <span *ngIf="readingList?.promoted">(<i class="fa fa-angle-double-up" aria-hidden="true"></i>)</span>
|
||||
<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">
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user