Merge branch 'develop' of https://github.com/Kareadita/Kavita into feature/comic-metadata-dl
This commit is contained in:
commit
d78240cd03
26 changed files with 253 additions and 55 deletions
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -28,7 +28,7 @@ body:
|
||||||
label: Kavita Version Number - If you don't see your version number listed, please update Kavita and see if your issue still persists.
|
label: Kavita Version Number - If you don't see your version number listed, please update Kavita and see if your issue still persists.
|
||||||
multiple: false
|
multiple: false
|
||||||
options:
|
options:
|
||||||
- 0.8.5.11 - Stable
|
- 0.8.6.2 - Stable
|
||||||
- Nightly Testing Branch
|
- Nightly Testing Branch
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
|
||||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
|
|
@ -13,7 +13,7 @@ name: "CodeQL"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "develop", "main" ]
|
branches: [ "develop"]
|
||||||
pull_request:
|
pull_request:
|
||||||
# The branches below must be a subset of the branches above
|
# The branches below must be a subset of the branches above
|
||||||
branches: [ "develop" ]
|
branches: [ "develop" ]
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,7 @@
|
||||||
<None Remove="Hangfire-log.db" />
|
<None Remove="Hangfire-log.db" />
|
||||||
<None Remove="obj\**" />
|
<None Remove="obj\**" />
|
||||||
<None Remove="cache\**" />
|
<None Remove="cache\**" />
|
||||||
|
<None Remove="cache-long\**" />
|
||||||
<None Remove="backups\**" />
|
<None Remove="backups\**" />
|
||||||
<None Remove="logs\**" />
|
<None Remove="logs\**" />
|
||||||
<None Remove="temp\**" />
|
<None Remove="temp\**" />
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
using System.Threading.Tasks;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Constants;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.Data.Repositories;
|
using API.Data.Repositories;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
|
|
@ -54,4 +56,29 @@ public class VolumeController : BaseApiController
|
||||||
|
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
|
[HttpPost("multiple")]
|
||||||
|
public async Task<ActionResult<bool>> DeleteMultipleVolumes(int[] volumesIds)
|
||||||
|
{
|
||||||
|
var volumes = await _unitOfWork.VolumeRepository.GetVolumesById(volumesIds);
|
||||||
|
if (volumes.Count != volumesIds.Length)
|
||||||
|
{
|
||||||
|
return BadRequest(_localizationService.Translate(User.GetUserId(), "volume-doesnt-exist"));
|
||||||
|
}
|
||||||
|
|
||||||
|
_unitOfWork.VolumeRepository.Remove(volumes);
|
||||||
|
|
||||||
|
if (!await _unitOfWork.CommitAsync())
|
||||||
|
{
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var volume in volumes)
|
||||||
|
{
|
||||||
|
await _eventHub.SendMessageAsync(MessageFactory.VolumeRemoved, MessageFactory.VolumeRemovedEvent(volume.Id, volume.SeriesId), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ public interface IVolumeRepository
|
||||||
void Add(Volume volume);
|
void Add(Volume volume);
|
||||||
void Update(Volume volume);
|
void Update(Volume volume);
|
||||||
void Remove(Volume volume);
|
void Remove(Volume volume);
|
||||||
|
void Remove(IList<Volume> volumes);
|
||||||
Task<IList<MangaFile>> GetFilesForVolume(int volumeId);
|
Task<IList<MangaFile>> GetFilesForVolume(int volumeId);
|
||||||
Task<string?> GetVolumeCoverImageAsync(int volumeId);
|
Task<string?> GetVolumeCoverImageAsync(int volumeId);
|
||||||
Task<IList<int>> GetChapterIdsByVolumeIds(IReadOnlyList<int> volumeIds);
|
Task<IList<int>> GetChapterIdsByVolumeIds(IReadOnlyList<int> volumeIds);
|
||||||
|
|
@ -43,6 +44,7 @@ public interface IVolumeRepository
|
||||||
Task<VolumeDto?> GetVolumeDtoAsync(int volumeId, int userId);
|
Task<VolumeDto?> GetVolumeDtoAsync(int volumeId, int userId);
|
||||||
Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(IList<int> seriesIds, bool includeChapters = false);
|
Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(IList<int> seriesIds, bool includeChapters = false);
|
||||||
Task<IEnumerable<Volume>> GetVolumes(int seriesId);
|
Task<IEnumerable<Volume>> GetVolumes(int seriesId);
|
||||||
|
Task<IList<Volume>> GetVolumesById(IList<int> volumeIds, VolumeIncludes includes = VolumeIncludes.None);
|
||||||
Task<Volume?> GetVolumeByIdAsync(int volumeId);
|
Task<Volume?> GetVolumeByIdAsync(int volumeId);
|
||||||
Task<IList<Volume>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
|
Task<IList<Volume>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
|
||||||
Task<IEnumerable<string>> GetCoverImagesForLockedVolumesAsync();
|
Task<IEnumerable<string>> GetCoverImagesForLockedVolumesAsync();
|
||||||
|
|
@ -72,6 +74,10 @@ public class VolumeRepository : IVolumeRepository
|
||||||
{
|
{
|
||||||
_context.Volume.Remove(volume);
|
_context.Volume.Remove(volume);
|
||||||
}
|
}
|
||||||
|
public void Remove(IList<Volume> volumes)
|
||||||
|
{
|
||||||
|
_context.Volume.RemoveRange(volumes);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a list of non-tracked files for a given volume.
|
/// Returns a list of non-tracked files for a given volume.
|
||||||
|
|
@ -180,6 +186,15 @@ public class VolumeRepository : IVolumeRepository
|
||||||
.OrderBy(vol => vol.MinNumber)
|
.OrderBy(vol => vol.MinNumber)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
public async Task<IList<Volume>> GetVolumesById(IList<int> volumeIds, VolumeIncludes includes = VolumeIncludes.None)
|
||||||
|
{
|
||||||
|
return await _context.Volume
|
||||||
|
.Where(vol => volumeIds.Contains(vol.Id))
|
||||||
|
.Includes(includes)
|
||||||
|
.AsSplitQuery()
|
||||||
|
.OrderBy(vol => vol.MinNumber)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a single volume with Chapter and Files
|
/// Returns a single volume with Chapter and Files
|
||||||
|
|
|
||||||
|
|
@ -204,8 +204,8 @@
|
||||||
"person-image-doesnt-exist": "Osoba nie istnieje w CoversDB",
|
"person-image-doesnt-exist": "Osoba nie istnieje w CoversDB",
|
||||||
"email-taken": "Adres e-mail jest już używany",
|
"email-taken": "Adres e-mail jest już używany",
|
||||||
"kavitaplus-restricted": "Jest to dostępne tylko dla Kavita+",
|
"kavitaplus-restricted": "Jest to dostępne tylko dla Kavita+",
|
||||||
"smart-filter-name-required": "Strona internetowa",
|
"smart-filter-name-required": "Inteligentny filtr wymaga nazwy",
|
||||||
"sidenav-stream-only-delete-smart-filter": "Jedynie filtry filtrowe mogą zostać usunięte z SideNav",
|
"sidenav-stream-only-delete-smart-filter": "Tylko inteligentne filtry mogą zostać usunięte z panelu bocznego",
|
||||||
"dashboard-stream-only-delete-smart-filter": "Tylko inteligentne strumienie filtrów mogą zostać usunięte z rozdzielczości",
|
"dashboard-stream-only-delete-smart-filter": "Tylko inteligentne strumienie filtrów może zostać usunięte z głównego panelu",
|
||||||
"smart-filter-system-name": "Nie można użyć nazwy systemu dostarczanego strumieniem"
|
"smart-filter-system-name": "Nie można użyć nazwy systemu dostarczanego strumieniem"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,7 @@ public class CacheService : ICacheService
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// Potential BUG: If the folder is left here and there are no files within, this could theoretically return without proper cache
|
||||||
return chapter;
|
return chapter;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,6 @@ public class StatsService : IStatsService
|
||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
var response = await (Configuration.StatsApiUrl + "/api/health/")
|
var response = await (Configuration.StatsApiUrl + "/api/health/")
|
||||||
.WithBasicHeaders(ApiKey)
|
.WithBasicHeaders(ApiKey)
|
||||||
.WithTimeout(TimeSpan.FromSeconds(30))
|
|
||||||
.GetAsync();
|
.GetAsync();
|
||||||
|
|
||||||
if (response.StatusCode == StatusCodes.Status200OK)
|
if (response.StatusCode == StatusCodes.Status200OK)
|
||||||
|
|
@ -197,7 +196,7 @@ public class StatsService : IStatsService
|
||||||
|
|
||||||
private async Task<int> MaxSeriesInAnyLibrary()
|
private async Task<int> MaxSeriesInAnyLibrary()
|
||||||
{
|
{
|
||||||
// If first time flow, just return 0
|
// If first time flow, return 0
|
||||||
if (!await _context.Series.AnyAsync()) return 0;
|
if (!await _context.Series.AnyAsync()) return 0;
|
||||||
return await _context.Series
|
return await _context.Series
|
||||||
.Select(s => _context.Library.Where(l => l.Id == s.LibraryId).SelectMany(l => l.Series!).Count())
|
.Select(s => _context.Library.Where(l => l.Id == s.LibraryId).SelectMany(l => l.Series!).Count())
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"TokenKey": "super secret unguessable key that is longer because we require it",
|
"TokenKey": "super secret unguessable key that is longer because we require it",
|
||||||
"Port": 5000,
|
"Port": 5000,
|
||||||
"IpAddresses": "0.0.0.0,::",
|
"IpAddresses": "",
|
||||||
"BaseUrl": "/",
|
"BaseUrl": "/",
|
||||||
"Cache": 75,
|
"Cache": 75,
|
||||||
"AllowIFraming": false
|
"AllowIFraming": false
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ Setup guides, FAQ, the more information we have on the [wiki](https://wiki.kavit
|
||||||
- HTML/Javascript editor of choice (VS Code/Sublime Text/Webstorm/Atom/etc)
|
- HTML/Javascript editor of choice (VS Code/Sublime Text/Webstorm/Atom/etc)
|
||||||
- [Git](https://git-scm.com/downloads)
|
- [Git](https://git-scm.com/downloads)
|
||||||
- [NodeJS](https://nodejs.org/en/download/) (Node 18.13.X or higher)
|
- [NodeJS](https://nodejs.org/en/download/) (Node 18.13.X or higher)
|
||||||
- .NET 8.0+
|
- .NET 9.0+
|
||||||
- dotnet tool install -g Swashbuckle.AspNetCore.Cli
|
- dotnet tool install -g Swashbuckle.AspNetCore.Cli
|
||||||
|
|
||||||
### Getting started ###
|
### Getting started ###
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Company>kavitareader.com</Company>
|
<Company>kavitareader.com</Company>
|
||||||
<Product>Kavita</Product>
|
<Product>Kavita</Product>
|
||||||
<AssemblyVersion>0.8.6.0</AssemblyVersion>
|
<AssemblyVersion>0.8.6.2</AssemblyVersion>
|
||||||
<NeutralLanguage>en</NeutralLanguage>
|
<NeutralLanguage>en</NeutralLanguage>
|
||||||
<TieredPGO>true</TieredPGO>
|
<TieredPGO>true</TieredPGO>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
@ -20,4 +20,4 @@
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="xunit.assert" Version="2.9.3" />
|
<PackageReference Include="xunit.assert" Version="2.9.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -41,7 +41,7 @@ your reading collection with your friends and family!
|
||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
If you want to try out Kavita, a demo is available:
|
If you want to try out Kavita, a demo is available:
|
||||||
[https://demo.kavitareader.com/](https://demo.kavitareader.com/)
|
[https://demo.kavitareader.com/](https://demo.kavitareader.com/login?apiKey=9003cf99-9213-4206-a787-af2fe4cc5f1f)
|
||||||
```
|
```
|
||||||
Username: demouser
|
Username: demouser
|
||||||
Password: Demouser64
|
Password: Demouser64
|
||||||
|
|
|
||||||
|
|
@ -472,8 +472,19 @@ export class ActionService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteMultipleVolumes(volumes: Array<Volume>, callback?: BooleanActionCallback) {
|
||||||
|
// TODO: Change translation key back to "toasts.confirm-delete-multiple-volumes"
|
||||||
|
if (!await this.confirmService.confirm(translate('toasts.confirm-delete-multiple-chapters', {count: volumes.length}))) return;
|
||||||
|
|
||||||
|
this.volumeService.deleteMultipleVolumes(volumes.map(v => v.id)).subscribe((success) => {
|
||||||
|
if (callback) {
|
||||||
|
callback(success);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async deleteMultipleChapters(seriesId: number, chapterIds: Array<Chapter>, callback?: BooleanActionCallback) {
|
async deleteMultipleChapters(seriesId: number, chapterIds: Array<Chapter>, callback?: BooleanActionCallback) {
|
||||||
if (!await this.confirmService.confirm(translate('toasts.confirm-delete-multiple-chapters'))) return;
|
if (!await this.confirmService.confirm(translate('toasts.confirm-delete-multiple-chapters', {count: chapterIds.length}))) return;
|
||||||
|
|
||||||
this.chapterService.deleteMultipleChapters(seriesId, chapterIds.map(c => c.id)).subscribe(() => {
|
this.chapterService.deleteMultipleChapters(seriesId, chapterIds.map(c => c.id)).subscribe(() => {
|
||||||
if (callback) {
|
if (callback) {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ export class VolumeService {
|
||||||
return this.httpClient.delete<boolean>(this.baseUrl + 'volume?volumeId=' + volumeId);
|
return this.httpClient.delete<boolean>(this.baseUrl + 'volume?volumeId=' + volumeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteMultipleVolumes(volumeIds: number[]) {
|
||||||
|
return this.httpClient.post<boolean>(this.baseUrl + "volume/multiple", volumeIds)
|
||||||
|
}
|
||||||
|
|
||||||
updateVolume(volume: any) {
|
updateVolume(volume: any) {
|
||||||
return this.httpClient.post(this.baseUrl + 'volume/update', volume, TextResonse);
|
return this.httpClient.post(this.baseUrl + 'volume/update', volume, TextResonse);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core';
|
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core';
|
||||||
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
|
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||||
import {ToastrService} from 'ngx-toastr';
|
import {ToastrService} from 'ngx-toastr';
|
||||||
import {debounceTime, distinctUntilChanged, filter, switchMap, tap} from 'rxjs';
|
import {catchError, debounceTime, distinctUntilChanged, filter, of, switchMap, tap} from 'rxjs';
|
||||||
import {SettingsService} from '../settings.service';
|
import {SettingsService} from '../settings.service';
|
||||||
import {ServerSettings} from '../_models/server-settings';
|
import {ServerSettings} from '../_models/server-settings';
|
||||||
import {translate, TranslocoModule} from "@jsverse/transloco";
|
import {translate, TranslocoModule} from "@jsverse/transloco";
|
||||||
|
|
@ -46,15 +46,21 @@ export class ManageEmailSettingsComponent implements OnInit {
|
||||||
|
|
||||||
// Automatically save settings as we edit them
|
// Automatically save settings as we edit them
|
||||||
this.settingsForm.valueChanges.pipe(
|
this.settingsForm.valueChanges.pipe(
|
||||||
debounceTime(300),
|
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
|
debounceTime(300),
|
||||||
filter(_ => this.settingsForm.valid),
|
filter(_ => this.settingsForm.valid),
|
||||||
takeUntilDestroyed(this.destroyRef),
|
takeUntilDestroyed(this.destroyRef),
|
||||||
switchMap(_ => {
|
switchMap(_ => {
|
||||||
const data = this.packData();
|
const data = this.packData();
|
||||||
return this.settingsService.updateServerSettings(data);
|
return this.settingsService.updateServerSettings(data).pipe(catchError(err => {
|
||||||
|
console.error(err);
|
||||||
|
return of(null);
|
||||||
|
}));
|
||||||
}),
|
}),
|
||||||
tap(settings => {
|
tap(settings => {
|
||||||
|
if (!settings) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.serverSettings = settings;
|
this.serverSettings = settings;
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core';
|
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core';
|
||||||
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
|
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||||
import {ToastrService} from 'ngx-toastr';
|
import {ToastrService} from 'ngx-toastr';
|
||||||
import {debounceTime, distinctUntilChanged, filter, switchMap, take, tap} from 'rxjs';
|
import {catchError, debounceTime, distinctUntilChanged, filter, of, switchMap, take, tap} from 'rxjs';
|
||||||
import {SettingsService} from '../settings.service';
|
import {SettingsService} from '../settings.service';
|
||||||
import {ServerSettings} from '../_models/server-settings';
|
import {ServerSettings} from '../_models/server-settings';
|
||||||
import {DirectoryPickerComponent, DirectoryPickerResult} from '../_modals/directory-picker/directory-picker.component';
|
import {DirectoryPickerComponent, DirectoryPickerResult} from '../_modals/directory-picker/directory-picker.component';
|
||||||
|
|
@ -55,9 +55,15 @@ export class ManageMediaSettingsComponent implements OnInit {
|
||||||
takeUntilDestroyed(this.destroyRef),
|
takeUntilDestroyed(this.destroyRef),
|
||||||
switchMap(_ => {
|
switchMap(_ => {
|
||||||
const data = this.packData();
|
const data = this.packData();
|
||||||
return this.settingsService.updateServerSettings(data);
|
return this.settingsService.updateServerSettings(data).pipe(catchError(err => {
|
||||||
|
console.error(err);
|
||||||
|
return of(null);
|
||||||
|
}));
|
||||||
}),
|
}),
|
||||||
tap(settings => {
|
tap(settings => {
|
||||||
|
if (!settings) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const encodingChanged = this.serverSettings.encodeMediaAs !== settings.encodeMediaAs;
|
const encodingChanged = this.serverSettings.encodeMediaAs !== settings.encodeMediaAs;
|
||||||
if (encodingChanged) {
|
if (encodingChanged) {
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@
|
||||||
@if (settingsForm.get('ipAddresses'); as formControl) {
|
@if (settingsForm.get('ipAddresses'); as formControl) {
|
||||||
<app-setting-item [title]="t('ip-address-label')" [subtitle]="t('ip-address-tooltip')">
|
<app-setting-item [title]="t('ip-address-label')" [subtitle]="t('ip-address-tooltip')">
|
||||||
<ng-template #view>
|
<ng-template #view>
|
||||||
{{formControl.value}}
|
{{formControl.value | defaultValue}}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #edit>
|
<ng-template #edit>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
|
|
@ -75,7 +75,7 @@
|
||||||
|
|
||||||
@if(settingsForm.dirty || !settingsForm.untouched) {
|
@if(settingsForm.dirty || !settingsForm.untouched) {
|
||||||
<div id="ipaddresses-validations" class="invalid-feedback">
|
<div id="ipaddresses-validations" class="invalid-feedback">
|
||||||
@if (formControl.errors?.pattern) {
|
@if (formControl.errors?.emptyOrPattern) {
|
||||||
<div>{{t('ip-address-validation')}}</div>
|
<div>{{t('ip-address-validation')}}</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core';
|
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core';
|
||||||
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
|
import {FormControl, FormGroup, ReactiveFormsModule, ValidatorFn, Validators} from '@angular/forms';
|
||||||
import {ToastrService} from 'ngx-toastr';
|
import {ToastrService} from 'ngx-toastr';
|
||||||
import {take} from 'rxjs/operators';
|
import {take} from 'rxjs/operators';
|
||||||
import {ServerService} from 'src/app/_services/server.service';
|
import {ServerService} from 'src/app/_services/server.service';
|
||||||
|
|
@ -10,7 +10,7 @@ import {WikiLink} from "../../_models/wiki";
|
||||||
import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component";
|
import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component";
|
||||||
import {SettingSwitchComponent} from "../../settings/_components/setting-switch/setting-switch.component";
|
import {SettingSwitchComponent} from "../../settings/_components/setting-switch/setting-switch.component";
|
||||||
import {ConfirmService} from "../../shared/confirm.service";
|
import {ConfirmService} from "../../shared/confirm.service";
|
||||||
import {debounceTime, distinctUntilChanged, filter, switchMap, tap} from "rxjs";
|
import {catchError, debounceTime, distinctUntilChanged, filter, of, switchMap, tap} from "rxjs";
|
||||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||||
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
|
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
|
||||||
import {EnterBlurDirective} from "../../_directives/enter-blur.directive";
|
import {EnterBlurDirective} from "../../_directives/enter-blur.directive";
|
||||||
|
|
@ -40,6 +40,7 @@ export class ManageSettingsComponent implements OnInit {
|
||||||
settingsForm: FormGroup = new FormGroup({});
|
settingsForm: FormGroup = new FormGroup({});
|
||||||
taskFrequencies: Array<string> = [];
|
taskFrequencies: Array<string> = [];
|
||||||
logLevels: Array<string> = [];
|
logLevels: Array<string> = [];
|
||||||
|
isDocker: boolean = false;
|
||||||
|
|
||||||
allowStatsTooltip = translate('manage-settings.allow-stats-tooltip-part-1') + ' <a href="' +
|
allowStatsTooltip = translate('manage-settings.allow-stats-tooltip-part-1') + ' <a href="' +
|
||||||
WikiLink.DataCollection +
|
WikiLink.DataCollection +
|
||||||
|
|
@ -61,7 +62,7 @@ export class ManageSettingsComponent implements OnInit {
|
||||||
this.settingsForm.addControl('taskScan', new FormControl(this.serverSettings.taskScan, [Validators.required]));
|
this.settingsForm.addControl('taskScan', new FormControl(this.serverSettings.taskScan, [Validators.required]));
|
||||||
this.settingsForm.addControl('taskBackup', new FormControl(this.serverSettings.taskBackup, [Validators.required]));
|
this.settingsForm.addControl('taskBackup', new FormControl(this.serverSettings.taskBackup, [Validators.required]));
|
||||||
this.settingsForm.addControl('taskCleanup', new FormControl(this.serverSettings.taskCleanup, [Validators.required]));
|
this.settingsForm.addControl('taskCleanup', new FormControl(this.serverSettings.taskCleanup, [Validators.required]));
|
||||||
this.settingsForm.addControl('ipAddresses', new FormControl(this.serverSettings.ipAddresses, [Validators.required, Validators.pattern(ValidIpAddress)]));
|
this.settingsForm.addControl('ipAddresses', new FormControl(this.serverSettings.ipAddresses, [this.emptyOrPattern(ValidIpAddress)]));
|
||||||
this.settingsForm.addControl('port', new FormControl(this.serverSettings.port, [Validators.required]));
|
this.settingsForm.addControl('port', new FormControl(this.serverSettings.port, [Validators.required]));
|
||||||
this.settingsForm.addControl('loggingLevel', new FormControl(this.serverSettings.loggingLevel, [Validators.required]));
|
this.settingsForm.addControl('loggingLevel', new FormControl(this.serverSettings.loggingLevel, [Validators.required]));
|
||||||
this.settingsForm.addControl('allowStatCollection', new FormControl(this.serverSettings.allowStatCollection, [Validators.required]));
|
this.settingsForm.addControl('allowStatCollection', new FormControl(this.serverSettings.allowStatCollection, [Validators.required]));
|
||||||
|
|
@ -76,6 +77,7 @@ export class ManageSettingsComponent implements OnInit {
|
||||||
this.settingsForm.addControl('onDeckProgressDays', new FormControl(this.serverSettings.onDeckProgressDays, [Validators.required]));
|
this.settingsForm.addControl('onDeckProgressDays', new FormControl(this.serverSettings.onDeckProgressDays, [Validators.required]));
|
||||||
this.settingsForm.addControl('onDeckUpdateDays', new FormControl(this.serverSettings.onDeckUpdateDays, [Validators.required]));
|
this.settingsForm.addControl('onDeckUpdateDays', new FormControl(this.serverSettings.onDeckUpdateDays, [Validators.required]));
|
||||||
|
|
||||||
|
|
||||||
// Automatically save settings as we edit them
|
// Automatically save settings as we edit them
|
||||||
this.settingsForm.valueChanges.pipe(
|
this.settingsForm.valueChanges.pipe(
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
|
|
@ -84,9 +86,16 @@ export class ManageSettingsComponent implements OnInit {
|
||||||
takeUntilDestroyed(this.destroyRef),
|
takeUntilDestroyed(this.destroyRef),
|
||||||
switchMap(_ => {
|
switchMap(_ => {
|
||||||
const data = this.packData();
|
const data = this.packData();
|
||||||
return this.settingsService.updateServerSettings(data);
|
return this.settingsService.updateServerSettings(data).pipe(catchError(err => {
|
||||||
|
console.error(err);
|
||||||
|
return of(null);
|
||||||
|
}));
|
||||||
}),
|
}),
|
||||||
tap(settings => {
|
tap(settings => {
|
||||||
|
if (!settings) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.serverSettings = settings;
|
this.serverSettings = settings;
|
||||||
this.resetForm();
|
this.resetForm();
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
|
|
@ -94,6 +103,7 @@ export class ManageSettingsComponent implements OnInit {
|
||||||
).subscribe();
|
).subscribe();
|
||||||
|
|
||||||
this.serverService.getServerInfo().subscribe(info => {
|
this.serverService.getServerInfo().subscribe(info => {
|
||||||
|
this.isDocker = info.isDocker;
|
||||||
if (info.isDocker) {
|
if (info.isDocker) {
|
||||||
this.settingsForm.get('ipAddresses')?.disable();
|
this.settingsForm.get('ipAddresses')?.disable();
|
||||||
this.settingsForm.get('port')?.disable();
|
this.settingsForm.get('port')?.disable();
|
||||||
|
|
@ -130,12 +140,18 @@ export class ManageSettingsComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
packData() {
|
packData() {
|
||||||
const modelSettings = this.settingsForm.value;
|
const modelSettings: ServerSettings = this.settingsForm.value;
|
||||||
modelSettings.bookmarksDirectory = this.serverSettings.bookmarksDirectory;
|
modelSettings.bookmarksDirectory = this.serverSettings.bookmarksDirectory;
|
||||||
modelSettings.smtpConfig = this.serverSettings.smtpConfig;
|
modelSettings.smtpConfig = this.serverSettings.smtpConfig;
|
||||||
modelSettings.installId = this.serverSettings.installId;
|
modelSettings.installId = this.serverSettings.installId;
|
||||||
modelSettings.installVersion = this.serverSettings.installVersion;
|
modelSettings.installVersion = this.serverSettings.installVersion;
|
||||||
|
|
||||||
|
// Disabled FormControls are not added to the value
|
||||||
|
if (this.isDocker) {
|
||||||
|
modelSettings.ipAddresses = this.serverSettings.ipAddresses;
|
||||||
|
modelSettings.port = this.serverSettings.port;
|
||||||
|
}
|
||||||
|
|
||||||
return modelSettings;
|
return modelSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,4 +187,19 @@ export class ManageSettingsComponent implements OnInit {
|
||||||
console.error('error: ', err);
|
console.error('error: ', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emptyOrPattern(pattern: RegExp): ValidatorFn {
|
||||||
|
return (control) => {
|
||||||
|
if (!control.value || control.value.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pattern.test(control.value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { 'emptyOrPattern': { 'requiredPattern': pattern.toString(), 'actualValue': control.value } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,18 @@ import {ToastrService} from 'ngx-toastr';
|
||||||
import {SettingsService} from '../settings.service';
|
import {SettingsService} from '../settings.service';
|
||||||
import {ServerSettings} from '../_models/server-settings';
|
import {ServerSettings} from '../_models/server-settings';
|
||||||
import {shareReplay} from 'rxjs/operators';
|
import {shareReplay} from 'rxjs/operators';
|
||||||
import {debounceTime, defer, distinctUntilChanged, filter, forkJoin, Observable, of, switchMap, tap} from 'rxjs';
|
import {
|
||||||
|
catchError,
|
||||||
|
debounceTime,
|
||||||
|
defer,
|
||||||
|
distinctUntilChanged,
|
||||||
|
filter,
|
||||||
|
forkJoin,
|
||||||
|
Observable,
|
||||||
|
of,
|
||||||
|
switchMap,
|
||||||
|
tap
|
||||||
|
} from 'rxjs';
|
||||||
import {ServerService} from 'src/app/_services/server.service';
|
import {ServerService} from 'src/app/_services/server.service';
|
||||||
import {Job} from 'src/app/_models/job/job';
|
import {Job} from 'src/app/_models/job/job';
|
||||||
import {UpdateNotificationModalComponent} from 'src/app/announcements/_components/update-notification/update-notification-modal.component';
|
import {UpdateNotificationModalComponent} from 'src/app/announcements/_components/update-notification/update-notification-modal.component';
|
||||||
|
|
@ -173,9 +184,15 @@ export class ManageTasksSettingsComponent implements OnInit {
|
||||||
// }),
|
// }),
|
||||||
switchMap(_ => {
|
switchMap(_ => {
|
||||||
const data = this.packData();
|
const data = this.packData();
|
||||||
return this.settingsService.updateServerSettings(data);
|
return this.settingsService.updateServerSettings(data).pipe(catchError(err => {
|
||||||
|
console.error(err);
|
||||||
|
return of(null);
|
||||||
|
}));
|
||||||
}),
|
}),
|
||||||
tap(settings => {
|
tap(settings => {
|
||||||
|
if (!settings) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.serverSettings = settings;
|
this.serverSettings = settings;
|
||||||
|
|
||||||
this.recurringTasks$ = this.serverService.getRecurringJobs().pipe(shareReplay());
|
this.recurringTasks$ = this.serverService.getRecurringJobs().pipe(shareReplay());
|
||||||
|
|
|
||||||
|
|
@ -53,22 +53,21 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (libraryType !== LibraryType.Images) {
|
@if (libraryType !== LibraryType.Images) {
|
||||||
<div class="card-body meta-title">
|
@if ((libraryType === LibraryType.LightNovel || libraryType === LibraryType.Book)) {
|
||||||
<span class="card-format"></span>
|
@if (volume.name) {
|
||||||
<div class="card-content d-flex justify-content-center align-items-center text-center" style="width:100%;min-height:58px;">
|
<div class="card-body meta-title">
|
||||||
@if (libraryType === LibraryType.LightNovel || libraryType === LibraryType.Book) {
|
<div class="card-content d-flex justify-content-center align-items-center text-center" style="width:100%;min-height:58px;">
|
||||||
{{volume.name}}
|
{{volume.name}}
|
||||||
} @else {
|
</div>
|
||||||
{{volume.chapters[0].titleName}}
|
</div>
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (actions && actions.length > 0) {
|
|
||||||
<span class="card-actions">
|
|
||||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="volume.name"></app-card-actionables>
|
|
||||||
</span>
|
|
||||||
}
|
}
|
||||||
</div>
|
} @else if (volume.chapters[0].titleName) {
|
||||||
|
<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.chapters[0].titleName}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="card-title-container">
|
<div class="card-title-container">
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ export class DraggableOrderedListComponent {
|
||||||
|
|
||||||
|
|
||||||
get BufferAmount() {
|
get BufferAmount() {
|
||||||
return Math.min(this.items.length / 20, 20);
|
return Math.floor(Math.min(this.items.length / 20, 20));
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,7 @@ import {CoverImageComponent} from "../../../_single-module/cover-image/cover-ima
|
||||||
import {DefaultModalOptions} from "../../../_models/default-modal-options";
|
import {DefaultModalOptions} from "../../../_models/default-modal-options";
|
||||||
import {LicenseService} from "../../../_services/license.service";
|
import {LicenseService} from "../../../_services/license.service";
|
||||||
import {PageBookmark} from "../../../_models/readers/page-bookmark";
|
import {PageBookmark} from "../../../_models/readers/page-bookmark";
|
||||||
|
import {VolumeRemovedEvent} from "../../../_models/events/volume-removed-event";
|
||||||
|
|
||||||
|
|
||||||
enum TabID {
|
enum TabID {
|
||||||
|
|
@ -353,11 +354,25 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
break;
|
break;
|
||||||
case Action.Delete:
|
case Action.Delete:
|
||||||
await this.actionService.deleteMultipleChapters(seriesId, chapters, () => {
|
if (chapters.length > 0) {
|
||||||
// No need to update the page as the backend will spam volume/chapter deletions
|
await this.actionService.deleteMultipleChapters(seriesId, chapters, () => {
|
||||||
this.bulkSelectionService.deselectAll();
|
// No need to update the page as the backend will spam volume/chapter deletions
|
||||||
this.cdRef.markForCheck();
|
this.bulkSelectionService.deselectAll();
|
||||||
});
|
this.cdRef.markForCheck();
|
||||||
|
});
|
||||||
|
|
||||||
|
// It's not possible to select both chapters and volumes
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedVolumeIds.length > 0) {
|
||||||
|
await this.actionService.deleteMultipleVolumes(selectedVolumeIds, () => {
|
||||||
|
// No need to update the page as the backend will spam volume deletions
|
||||||
|
this.bulkSelectionService.deselectAll();
|
||||||
|
this.cdRef.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -486,6 +501,11 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||||
const removedEvent = event.payload as ChapterRemovedEvent;
|
const removedEvent = event.payload as ChapterRemovedEvent;
|
||||||
if (removedEvent.seriesId !== this.seriesId) return;
|
if (removedEvent.seriesId !== this.seriesId) return;
|
||||||
this.loadPageSource.next(false);
|
this.loadPageSource.next(false);
|
||||||
|
} else if (event.event === EVENTS.VolumeRemoved) {
|
||||||
|
const volumeRemoveEvent = event.payload as VolumeRemovedEvent;
|
||||||
|
if (volumeRemoveEvent.seriesId === this.seriesId) {
|
||||||
|
this.loadPageSource.next(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -660,7 +680,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||||
case (Action.Delete):
|
case (Action.Delete):
|
||||||
await this.actionService.deleteChapter(chapter.id, (success) => {
|
await this.actionService.deleteChapter(chapter.id, (success) => {
|
||||||
if (!success) return;
|
if (!success) return;
|
||||||
|
|
||||||
this.chapters = this.chapters.filter(c => c.id != chapter.id);
|
this.chapters = this.chapters.filter(c => c.id != chapter.id);
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2640,6 +2640,7 @@
|
||||||
"alert-long-running": "This is a long running process. Please give it the time to complete before invoking again.",
|
"alert-long-running": "This is a long running process. Please give it the time to complete before invoking again.",
|
||||||
"confirm-delete-multiple-series": "Are you sure you want to delete {{count}} series? It will not modify files on disk.",
|
"confirm-delete-multiple-series": "Are you sure you want to delete {{count}} series? It will not modify files on disk.",
|
||||||
"confirm-delete-multiple-chapters": "Are you sure you want to delete {{count}} chapter/volumes? It will not modify files on disk.",
|
"confirm-delete-multiple-chapters": "Are you sure you want to delete {{count}} chapter/volumes? It will not modify files on disk.",
|
||||||
|
"confirm-delete-multiple-volumes": "Are you sure you want to delete {{count}} volumes? It will not modify files on disk.",
|
||||||
"confirm-delete-series": "Are you sure you want to delete this series? It will not modify files on disk.",
|
"confirm-delete-series": "Are you sure you want to delete this series? It will not modify files on disk.",
|
||||||
"confirm-delete-chapter": "Are you sure you want to delete this chapter? It will not modify files on disk.",
|
"confirm-delete-chapter": "Are you sure you want to delete this chapter? It will not modify files on disk.",
|
||||||
"confirm-delete-volume": "Are you sure you want to delete this volume? It will not modify files on disk.",
|
"confirm-delete-volume": "Are you sure you want to delete this volume? It will not modify files on disk.",
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
"devices-tab": "{{tabs.devices-tab}}",
|
"devices-tab": "{{tabs.devices-tab}}",
|
||||||
"smart-filters-tab": "{{tabs.smart-filters-tab}}",
|
"smart-filters-tab": "{{tabs.smart-filters-tab}}",
|
||||||
"success-toast": "Käyttäjäasetukset päivitetty",
|
"success-toast": "Käyttäjäasetukset päivitetty",
|
||||||
"global-settings-title": "Laajamittaiset asetukset",
|
"global-settings-title": "Yleisesti pätevät asetukset",
|
||||||
"page-layout-mode-label": "Sivun asettelutila",
|
"page-layout-mode-label": "Sivun asettelutila",
|
||||||
"page-layout-mode-tooltip": "Näytä kohteet kortteina tai luettelonäkymänä Sarjojen yksityiskohdat -sivulla.",
|
"page-layout-mode-tooltip": "Näytä kohteet kortteina tai luettelonäkymänä Sarjojen yksityiskohdat -sivulla.",
|
||||||
"locale-label": "Kieliasetus",
|
"locale-label": "Kieliasetus",
|
||||||
|
|
|
||||||
|
|
@ -2219,12 +2219,12 @@
|
||||||
"delete-device": "您确定要删除该设备吗?",
|
"delete-device": "您确定要删除该设备吗?",
|
||||||
"confirm-regen-covers": "刷新封面将强制重新生成所有封面图片。这是一项繁重的运算。您确定执行,而不想使用扫描操作代替吗?",
|
"confirm-regen-covers": "刷新封面将强制重新生成所有封面图片。这是一项繁重的运算。您确定执行,而不想使用扫描操作代替吗?",
|
||||||
"alert-long-running": "这是一个长时间运行的任务。请等待其完成后再次进行操作。",
|
"alert-long-running": "这是一个长时间运行的任务。请等待其完成后再次进行操作。",
|
||||||
"confirm-delete-multiple-series": "您确定要删除{{count}}个系列吗?这不会修改磁盘上的文件。",
|
"confirm-delete-multiple-series": "您确定要删除这 {{count}} 个系列吗?这不会修改磁盘上的文件。",
|
||||||
"confirm-delete-series": "您确定要删除此系列吗?这不会修改磁盘上的文件。",
|
"confirm-delete-series": "您确定要删除此系列吗?这不会修改磁盘上的文件。",
|
||||||
"confirm-delete-chapter": "您确定要删除此章节吗?它不会修改磁盘上的文件。",
|
"confirm-delete-chapter": "您确定要删除此章节吗?它不会修改磁盘上的文件。",
|
||||||
"confirm-delete-volume": "您确定要删除此卷吗?它不会修改磁盘上的文件。",
|
"confirm-delete-volume": "您确定要删除此卷吗?它不会修改磁盘上的文件。",
|
||||||
"alert-bad-theme": "主题中存在无效或不安全的CSS。请联系管理员进行修正。将默认为暗色主题。",
|
"alert-bad-theme": "主题中存在无效或不安全的CSS。请联系管理员进行修正。将默认为暗色主题。",
|
||||||
"confirm-library-delete": "您确定要删除{{name}}资料库吗?此操作无法撤销。",
|
"confirm-library-delete": "您确定要删除 {{name}} 资料库吗?此操作无法撤销。",
|
||||||
"confirm-library-type-change": "更改资料库类型将触发具有不同解析规则的新扫描,并可能导致重新创建系列,因此您可能会丢失进度和书签。您应该在执行此操作之前进行备份。您确定要继续吗?",
|
"confirm-library-type-change": "更改资料库类型将触发具有不同解析规则的新扫描,并可能导致重新创建系列,因此您可能会丢失进度和书签。您应该在执行此操作之前进行备份。您确定要继续吗?",
|
||||||
"confirm-download-size": "{{entityType}}的大小为{{size}}。确定要继续吗?",
|
"confirm-download-size": "{{entityType}}的大小为{{size}}。确定要继续吗?",
|
||||||
"confirm-download-size-ios": "iOS 在下载大于 200MB 的文件时出现问题,此下载可能无法完成。",
|
"confirm-download-size-ios": "iOS 在下载大于 200MB 的文件时出现问题,此下载可能无法完成。",
|
||||||
|
|
|
||||||
64
openapi.json
64
openapi.json
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"openapi": "3.0.4",
|
"openapi": "3.0.4",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "Kavita (v0.8.6.0)",
|
"title": "Kavita (v0.8.6.1)",
|
||||||
"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.6.0",
|
"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.6.1",
|
||||||
"license": {
|
"license": {
|
||||||
"name": "GPL-3.0",
|
"name": "GPL-3.0",
|
||||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||||
|
|
@ -14508,6 +14508,66 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/Volume/multiple": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Volume"
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"text/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"application/*+json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"content": {
|
||||||
|
"text/plain": {
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"text/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/want-to-read": {
|
"/api/want-to-read": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue