mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-06-03 21:54:47 -04:00
Remove From On Deck (#2131)
* Allow admins to customize the amount of progress time or last item added time for on deck calculation * Implemented the ability to remove series from on deck. They will be removed until the user reads a new chapter. Quite a few db lookup reduction calls for reading based stuff, like continue point, bookmarks, etc.
This commit is contained in:
parent
90a6c89486
commit
348bc062ee
@ -314,6 +314,7 @@ public class ReaderController : BaseApiController
|
|||||||
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was an issue saving progress");
|
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was an issue saving progress");
|
||||||
|
|
||||||
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markReadDto.SeriesId));
|
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markReadDto.SeriesId));
|
||||||
|
BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(markReadDto.SeriesId, user.Id));
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -376,13 +377,11 @@ public class ReaderController : BaseApiController
|
|||||||
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName!, markVolumeReadDto.SeriesId,
|
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName!, markVolumeReadDto.SeriesId,
|
||||||
markVolumeReadDto.VolumeId, 0, chapters.Sum(c => c.Pages)));
|
markVolumeReadDto.VolumeId, 0, chapters.Sum(c => c.Pages)));
|
||||||
|
|
||||||
if (await _unitOfWork.CommitAsync())
|
if (!await _unitOfWork.CommitAsync()) return BadRequest("Could not save progress");
|
||||||
{
|
|
||||||
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markVolumeReadDto.SeriesId));
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
return BadRequest("Could not save progress");
|
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markVolumeReadDto.SeriesId));
|
||||||
|
BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(markVolumeReadDto.SeriesId, user.Id));
|
||||||
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -406,14 +405,12 @@ public class ReaderController : BaseApiController
|
|||||||
var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds);
|
var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds);
|
||||||
await _readerService.MarkChaptersAsRead(user, dto.SeriesId, chapters.ToList());
|
await _readerService.MarkChaptersAsRead(user, dto.SeriesId, chapters.ToList());
|
||||||
|
|
||||||
if (await _unitOfWork.CommitAsync())
|
if (!await _unitOfWork.CommitAsync()) return BadRequest("Could not save progress");
|
||||||
{
|
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, dto.SeriesId));
|
||||||
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, dto.SeriesId));
|
BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(dto.SeriesId, user.Id));
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return BadRequest("Could not save progress");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -463,16 +460,14 @@ public class ReaderController : BaseApiController
|
|||||||
await _readerService.MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters);
|
await _readerService.MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await _unitOfWork.CommitAsync())
|
if (!await _unitOfWork.CommitAsync()) return BadRequest("Could not save progress");
|
||||||
{
|
|
||||||
foreach (var sId in dto.SeriesIds)
|
|
||||||
{
|
|
||||||
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, sId));
|
|
||||||
}
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
return BadRequest("Could not save progress");
|
foreach (var sId in dto.SeriesIds)
|
||||||
|
{
|
||||||
|
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, sId));
|
||||||
|
BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(sId, user.Id));
|
||||||
|
}
|
||||||
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -530,11 +525,14 @@ public class ReaderController : BaseApiController
|
|||||||
/// <param name="progressDto"></param>
|
/// <param name="progressDto"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpPost("progress")]
|
[HttpPost("progress")]
|
||||||
public async Task<ActionResult> BookmarkProgress(ProgressDto progressDto)
|
public async Task<ActionResult> SaveProgress(ProgressDto progressDto)
|
||||||
{
|
{
|
||||||
if (await _readerService.SaveReadingProgress(progressDto, User.GetUserId())) return Ok(true);
|
var userId = User.GetUserId();
|
||||||
|
if (!await _readerService.SaveReadingProgress(progressDto, userId))
|
||||||
|
return BadRequest("Could not save progress");
|
||||||
|
|
||||||
return BadRequest("Could not save progress");
|
|
||||||
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -545,9 +543,7 @@ public class ReaderController : BaseApiController
|
|||||||
[HttpGet("continue-point")]
|
[HttpGet("continue-point")]
|
||||||
public async Task<ActionResult<ChapterDto>> GetContinuePoint(int seriesId)
|
public async Task<ActionResult<ChapterDto>> GetContinuePoint(int seriesId)
|
||||||
{
|
{
|
||||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
return Ok(await _readerService.GetContinuePoint(seriesId, User.GetUserId()));
|
||||||
|
|
||||||
return Ok(await _readerService.GetContinuePoint(seriesId, userId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -558,8 +554,7 @@ public class ReaderController : BaseApiController
|
|||||||
[HttpGet("has-progress")]
|
[HttpGet("has-progress")]
|
||||||
public async Task<ActionResult<bool>> HasProgress(int seriesId)
|
public async Task<ActionResult<bool>> HasProgress(int seriesId)
|
||||||
{
|
{
|
||||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
return Ok(await _unitOfWork.AppUserProgressRepository.HasAnyProgressOnSeriesAsync(seriesId, User.GetUserId()));
|
||||||
return Ok(await _unitOfWork.AppUserProgressRepository.HasAnyProgressOnSeriesAsync(seriesId, userId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -570,10 +565,7 @@ public class ReaderController : BaseApiController
|
|||||||
[HttpGet("chapter-bookmarks")]
|
[HttpGet("chapter-bookmarks")]
|
||||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarks(int chapterId)
|
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarks(int chapterId)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForChapter(User.GetUserId(), chapterId));
|
||||||
if (user == null) return Unauthorized();
|
|
||||||
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
|
||||||
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForChapter(user.Id, chapterId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -584,11 +576,7 @@ public class ReaderController : BaseApiController
|
|||||||
[HttpPost("all-bookmarks")]
|
[HttpPost("all-bookmarks")]
|
||||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetAllBookmarks(FilterDto filterDto)
|
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetAllBookmarks(FilterDto filterDto)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
return Ok(await _unitOfWork.UserRepository.GetAllBookmarkDtos(User.GetUserId(), filterDto));
|
||||||
if (user == null) return Unauthorized();
|
|
||||||
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
|
||||||
|
|
||||||
return Ok(await _unitOfWork.UserRepository.GetAllBookmarkDtos(user.Id, filterDto));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -676,10 +664,7 @@ public class ReaderController : BaseApiController
|
|||||||
[HttpGet("volume-bookmarks")]
|
[HttpGet("volume-bookmarks")]
|
||||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForVolume(int volumeId)
|
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForVolume(int volumeId)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForVolume(User.GetUserId(), volumeId));
|
||||||
if (user == null) return Unauthorized();
|
|
||||||
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
|
||||||
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForVolume(user.Id, volumeId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -690,11 +675,7 @@ public class ReaderController : BaseApiController
|
|||||||
[HttpGet("series-bookmarks")]
|
[HttpGet("series-bookmarks")]
|
||||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForSeries(int seriesId)
|
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForSeries(int seriesId)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForSeries(User.GetUserId(), seriesId));
|
||||||
if (user == null) return Unauthorized();
|
|
||||||
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
|
||||||
|
|
||||||
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForSeries(user.Id, seriesId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -760,8 +741,7 @@ public class ReaderController : BaseApiController
|
|||||||
[HttpGet("next-chapter")]
|
[HttpGet("next-chapter")]
|
||||||
public async Task<ActionResult<int>> GetNextChapter(int seriesId, int volumeId, int currentChapterId)
|
public async Task<ActionResult<int>> GetNextChapter(int seriesId, int volumeId, int currentChapterId)
|
||||||
{
|
{
|
||||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
return await _readerService.GetNextChapterIdAsync(seriesId, volumeId, currentChapterId, User.GetUserId());
|
||||||
return await _readerService.GetNextChapterIdAsync(seriesId, volumeId, currentChapterId, userId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -779,8 +759,7 @@ public class ReaderController : BaseApiController
|
|||||||
[HttpGet("prev-chapter")]
|
[HttpGet("prev-chapter")]
|
||||||
public async Task<ActionResult<int>> GetPreviousChapter(int seriesId, int volumeId, int currentChapterId)
|
public async Task<ActionResult<int>> GetPreviousChapter(int seriesId, int volumeId, int currentChapterId)
|
||||||
{
|
{
|
||||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
return await _readerService.GetPrevChapterIdAsync(seriesId, volumeId, currentChapterId, User.GetUserId());
|
||||||
return await _readerService.GetPrevChapterIdAsync(seriesId, volumeId, currentChapterId, userId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -793,7 +772,7 @@ public class ReaderController : BaseApiController
|
|||||||
[ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new [] { "seriesId"})]
|
[ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new [] { "seriesId"})]
|
||||||
public async Task<ActionResult<HourEstimateRangeDto>> GetEstimateToCompletion(int seriesId)
|
public async Task<ActionResult<HourEstimateRangeDto>> GetEstimateToCompletion(int seriesId)
|
||||||
{
|
{
|
||||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
var userId = User.GetUserId();
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
||||||
|
|
||||||
// Get all sum of all chapters with progress that is complete then subtract from series. Multiply by modifiers
|
// Get all sum of all chapters with progress that is complete then subtract from series. Multiply by modifiers
|
||||||
|
@ -284,6 +284,18 @@ public class SeriesController : BaseApiController
|
|||||||
return Ok(pagedList);
|
return Ok(pagedList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a series from displaying on deck until the next read event on that series
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="seriesId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("remove-from-on-deck")]
|
||||||
|
public async Task<ActionResult> RemoveFromOnDeck([FromQuery] int seriesId)
|
||||||
|
{
|
||||||
|
await _unitOfWork.SeriesRepository.RemoveFromOnDeck(seriesId, User.GetUserId());
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runs a Cover Image Generation task
|
/// Runs a Cover Image Generation task
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -182,6 +182,24 @@ public class SettingsController : BaseApiController
|
|||||||
_unitOfWork.SettingsRepository.Update(setting);
|
_unitOfWork.SettingsRepository.Update(setting);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (setting.Key == ServerSettingKey.OnDeckProgressDays && updateSettingsDto.OnDeckProgressDays + string.Empty != setting.Value)
|
||||||
|
{
|
||||||
|
setting.Value = updateSettingsDto.OnDeckProgressDays + string.Empty;
|
||||||
|
_unitOfWork.SettingsRepository.Update(setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setting.Key == ServerSettingKey.OnDeckUpdateDays && updateSettingsDto.OnDeckUpdateDays + string.Empty != setting.Value)
|
||||||
|
{
|
||||||
|
setting.Value = updateSettingsDto.OnDeckUpdateDays + string.Empty;
|
||||||
|
_unitOfWork.SettingsRepository.Update(setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setting.Key == ServerSettingKey.TaskScan && updateSettingsDto.TaskScan != setting.Value)
|
||||||
|
{
|
||||||
|
setting.Value = updateSettingsDto.TaskScan;
|
||||||
|
_unitOfWork.SettingsRepository.Update(setting);
|
||||||
|
}
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.Port && updateSettingsDto.Port + string.Empty != setting.Value)
|
if (setting.Key == ServerSettingKey.Port && updateSettingsDto.Port + string.Empty != setting.Value)
|
||||||
{
|
{
|
||||||
if (OsInfo.IsDocker) continue;
|
if (OsInfo.IsDocker) continue;
|
||||||
|
@ -76,4 +76,12 @@ public class ServerSettingDto
|
|||||||
/// The size in MB for Caching API data
|
/// The size in MB for Caching API data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long CacheSize { get; set; }
|
public long CacheSize { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// How many Days since today in the past for reading progress, should content be considered for On Deck, before it gets removed automatically
|
||||||
|
/// </summary>
|
||||||
|
public int OnDeckProgressDays { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// How many Days since today in the past for chapter updates, should content be considered for On Deck, before it gets removed automatically
|
||||||
|
/// </summary>
|
||||||
|
public int OnDeckUpdateDays { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -52,6 +52,7 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
|||||||
public DbSet<ScrobbleEvent> ScrobbleEvent { get; set; } = null!;
|
public DbSet<ScrobbleEvent> ScrobbleEvent { get; set; } = null!;
|
||||||
public DbSet<ScrobbleError> ScrobbleError { get; set; } = null!;
|
public DbSet<ScrobbleError> ScrobbleError { get; set; } = null!;
|
||||||
public DbSet<ScrobbleHold> ScrobbleHold { get; set; } = null!;
|
public DbSet<ScrobbleHold> ScrobbleHold { get; set; } = null!;
|
||||||
|
public DbSet<AppUserOnDeckRemoval> AppUserOnDeckRemoval { get; set; } = null!;
|
||||||
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
|
2184
API/Data/Migrations/20230715125951_OnDeckRemoval.Designer.cs
generated
Normal file
2184
API/Data/Migrations/20230715125951_OnDeckRemoval.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
93
API/Data/Migrations/20230715125951_OnDeckRemoval.cs
Normal file
93
API/Data/Migrations/20230715125951_OnDeckRemoval.cs
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class OnDeckRemoval : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Title",
|
||||||
|
table: "ReadingList",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "TEXT",
|
||||||
|
oldNullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "NormalizedTitle",
|
||||||
|
table: "ReadingList",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "TEXT",
|
||||||
|
oldNullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AppUserOnDeckRemoval",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
SeriesId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
AppUserId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AppUserOnDeckRemoval", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AppUserOnDeckRemoval_AspNetUsers_AppUserId",
|
||||||
|
column: x => x.AppUserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AppUserOnDeckRemoval_Series_SeriesId",
|
||||||
|
column: x => x.SeriesId,
|
||||||
|
principalTable: "Series",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AppUserOnDeckRemoval_AppUserId",
|
||||||
|
table: "AppUserOnDeckRemoval",
|
||||||
|
column: "AppUserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AppUserOnDeckRemoval_SeriesId",
|
||||||
|
table: "AppUserOnDeckRemoval",
|
||||||
|
column: "SeriesId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AppUserOnDeckRemoval");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Title",
|
||||||
|
table: "ReadingList",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "TEXT");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "NormalizedTitle",
|
||||||
|
table: "ReadingList",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "TEXT");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -15,7 +15,7 @@ namespace API.Data.Migrations
|
|||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.5");
|
modelBuilder.HasAnnotation("ProductVersion", "7.0.8");
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||||
{
|
{
|
||||||
@ -183,6 +183,27 @@ namespace API.Data.Migrations
|
|||||||
b.ToTable("AppUserBookmark");
|
b.ToTable("AppUserBookmark");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AppUserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppUserId");
|
||||||
|
|
||||||
|
b.HasIndex("SeriesId");
|
||||||
|
|
||||||
|
b.ToTable("AppUserOnDeckRemoval");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
|
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@ -943,6 +964,7 @@ namespace API.Data.Migrations
|
|||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("NormalizedTitle")
|
b.Property<string>("NormalizedTitle")
|
||||||
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<bool>("Promoted")
|
b.Property<bool>("Promoted")
|
||||||
@ -958,6 +980,7 @@ namespace API.Data.Migrations
|
|||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("Title")
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
@ -1082,7 +1105,7 @@ namespace API.Data.Migrations
|
|||||||
b.Property<int>("LibraryId")
|
b.Property<int>("LibraryId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int?>("MalId")
|
b.Property<long?>("MalId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<DateTime?>("ProcessDateUtc")
|
b.Property<DateTime?>("ProcessDateUtc")
|
||||||
@ -1626,6 +1649,25 @@ namespace API.Data.Migrations
|
|||||||
b.Navigation("AppUser");
|
b.Navigation("AppUser");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AppUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("API.Entities.Series", "Series")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SeriesId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AppUser");
|
||||||
|
|
||||||
|
b.Navigation("Series");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
|
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||||
|
@ -14,6 +14,7 @@ using API.DTOs.Metadata;
|
|||||||
using API.DTOs.ReadingLists;
|
using API.DTOs.ReadingLists;
|
||||||
using API.DTOs.Search;
|
using API.DTOs.Search;
|
||||||
using API.DTOs.SeriesDetail;
|
using API.DTOs.SeriesDetail;
|
||||||
|
using API.DTOs.Settings;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Entities.Metadata;
|
using API.Entities.Metadata;
|
||||||
@ -137,6 +138,8 @@ public interface ISeriesRepository
|
|||||||
Task<IList<Series>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, bool customOnly = true);
|
Task<IList<Series>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, bool customOnly = true);
|
||||||
Task<SeriesDto?> GetSeriesDtoByNamesAndMetadataIdsForUser(int userId, IEnumerable<string> names, LibraryType libraryType, string aniListUrl, string malUrl);
|
Task<SeriesDto?> GetSeriesDtoByNamesAndMetadataIdsForUser(int userId, IEnumerable<string> names, LibraryType libraryType, string aniListUrl, string malUrl);
|
||||||
Task<int> GetAverageUserRating(int seriesId);
|
Task<int> GetAverageUserRating(int seriesId);
|
||||||
|
Task RemoveFromOnDeck(int seriesId, int userId);
|
||||||
|
Task ClearOnDeckRemoval(int seriesId, int userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SeriesRepository : ISeriesRepository
|
public class SeriesRepository : ISeriesRepository
|
||||||
@ -757,16 +760,33 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<PagedList<SeriesDto>> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter)
|
public async Task<PagedList<SeriesDto>> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter)
|
||||||
{
|
{
|
||||||
var cutoffProgressPoint = DateTime.Now - TimeSpan.FromDays(30);
|
var settings = await _context.ServerSetting
|
||||||
var cutoffLastAddedPoint = DateTime.Now - TimeSpan.FromDays(7);
|
.Select(x => x)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToListAsync();
|
||||||
|
var serverSettings = _mapper.Map<ServerSettingDto>(settings);
|
||||||
|
|
||||||
|
var cutoffProgressPoint = DateTime.Now - TimeSpan.FromDays(serverSettings.OnDeckProgressDays);
|
||||||
|
var cutoffLastAddedPoint = DateTime.Now - TimeSpan.FromDays(serverSettings.OnDeckUpdateDays);
|
||||||
|
|
||||||
var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Dashboard)
|
var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Dashboard)
|
||||||
.Where(id => libraryId == 0 || id == libraryId);
|
.Where(id => libraryId == 0 || id == libraryId);
|
||||||
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||||
|
|
||||||
|
// Don't allow any series the user has explicitly removed
|
||||||
|
var onDeckRemovals = _context.AppUserOnDeckRemoval
|
||||||
|
.Where(d => d.AppUserId == userId)
|
||||||
|
.Select(d => d.SeriesId)
|
||||||
|
.AsEnumerable();
|
||||||
|
|
||||||
|
// var onDeckRemovals = _context.AppUser.Where(u => u.Id == userId)
|
||||||
|
// .SelectMany(u => u.OnDeckRemovals.Select(d => d.Id))
|
||||||
|
// .AsEnumerable();
|
||||||
|
|
||||||
|
|
||||||
var query = _context.Series
|
var query = _context.Series
|
||||||
.Where(s => usersSeriesIds.Contains(s.Id))
|
.Where(s => usersSeriesIds.Contains(s.Id))
|
||||||
|
.Where(s => !onDeckRemovals.Contains(s.Id))
|
||||||
.Select(s => new
|
.Select(s => new
|
||||||
{
|
{
|
||||||
Series = s,
|
Series = s,
|
||||||
@ -1670,6 +1690,30 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
return avg.HasValue ? (int) avg.Value : 0;
|
return avg.HasValue ? (int) avg.Value : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task RemoveFromOnDeck(int seriesId, int userId)
|
||||||
|
{
|
||||||
|
var existingEntry = await _context.AppUserOnDeckRemoval
|
||||||
|
.Where(u => u.Id == userId && u.SeriesId == seriesId)
|
||||||
|
.AnyAsync();
|
||||||
|
if (existingEntry) return;
|
||||||
|
_context.AppUserOnDeckRemoval.Add(new AppUserOnDeckRemoval()
|
||||||
|
{
|
||||||
|
SeriesId = seriesId,
|
||||||
|
AppUserId = userId
|
||||||
|
});
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ClearOnDeckRemoval(int seriesId, int userId)
|
||||||
|
{
|
||||||
|
var existingEntry = await _context.AppUserOnDeckRemoval
|
||||||
|
.Where(u => u.Id == userId && u.SeriesId == seriesId)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (existingEntry == null) return;
|
||||||
|
_context.AppUserOnDeckRemoval.Remove(existingEntry);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> IsSeriesInWantToRead(int userId, int seriesId)
|
public async Task<bool> IsSeriesInWantToRead(int userId, int seriesId)
|
||||||
{
|
{
|
||||||
var libraryIds = await _context.Library.GetUserLibraries(userId).ToListAsync();
|
var libraryIds = await _context.Library.GetUserLibraries(userId).ToListAsync();
|
||||||
|
@ -108,6 +108,8 @@ public static class Seed
|
|||||||
new() {Key = ServerSettingKey.HostName, Value = string.Empty},
|
new() {Key = ServerSettingKey.HostName, Value = string.Empty},
|
||||||
new() {Key = ServerSettingKey.EncodeMediaAs, Value = EncodeFormat.PNG.ToString()},
|
new() {Key = ServerSettingKey.EncodeMediaAs, Value = EncodeFormat.PNG.ToString()},
|
||||||
new() {Key = ServerSettingKey.LicenseKey, Value = string.Empty},
|
new() {Key = ServerSettingKey.LicenseKey, Value = string.Empty},
|
||||||
|
new() {Key = ServerSettingKey.OnDeckProgressDays, Value = $"{30}"},
|
||||||
|
new() {Key = ServerSettingKey.OnDeckUpdateDays, Value = $"{7}"},
|
||||||
new() {
|
new() {
|
||||||
Key = ServerSettingKey.CacheSize, Value = Configuration.DefaultCacheMemory + string.Empty
|
Key = ServerSettingKey.CacheSize, Value = Configuration.DefaultCacheMemory + string.Empty
|
||||||
}, // Not used from DB, but DB is sync with appSettings.json
|
}, // Not used from DB, but DB is sync with appSettings.json
|
||||||
|
@ -37,6 +37,10 @@ public class AppUser : IdentityUser<int>, IHasConcurrencyToken
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ICollection<Device> Devices { get; set; } = null!;
|
public ICollection<Device> Devices { get; set; } = null!;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// A list of Series the user doesn't want on deck
|
||||||
|
/// </summary>
|
||||||
|
//public ICollection<Series> OnDeckRemovals { get; set; } = null!;
|
||||||
|
/// <summary>
|
||||||
/// An API Key to interact with external services, like OPDS
|
/// An API Key to interact with external services, like OPDS
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ApiKey { get; set; }
|
public string? ApiKey { get; set; }
|
||||||
|
11
API/Entities/AppUserOnDeckRemoval.cs
Normal file
11
API/Entities/AppUserOnDeckRemoval.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
namespace API.Entities;
|
||||||
|
|
||||||
|
public class AppUserOnDeckRemoval
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int SeriesId { get; set; }
|
||||||
|
public Series Series { get; set; }
|
||||||
|
public int AppUserId { get; set; }
|
||||||
|
public AppUser AppUser { get; set; }
|
||||||
|
|
||||||
|
}
|
@ -133,5 +133,15 @@ public enum ServerSettingKey
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Description("Cache")]
|
[Description("Cache")]
|
||||||
CacheSize = 24,
|
CacheSize = 24,
|
||||||
|
/// <summary>
|
||||||
|
/// How many Days since today in the past for reading progress, should content be considered for On Deck, before it gets removed automatically
|
||||||
|
/// </summary>
|
||||||
|
[Description("OnDeckProgressDays")]
|
||||||
|
OnDeckProgressDays = 25,
|
||||||
|
/// <summary>
|
||||||
|
/// How many Days since today in the past for chapter updates, should content be considered for On Deck, before it gets removed automatically
|
||||||
|
/// </summary>
|
||||||
|
[Description("OnDeckUpdateDays")]
|
||||||
|
OnDeckUpdateDays = 26,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -73,6 +73,12 @@ public class ServerSettingConverter : ITypeConverter<IEnumerable<ServerSetting>,
|
|||||||
case ServerSettingKey.CacheSize:
|
case ServerSettingKey.CacheSize:
|
||||||
destination.CacheSize = long.Parse(row.Value);
|
destination.CacheSize = long.Parse(row.Value);
|
||||||
break;
|
break;
|
||||||
|
case ServerSettingKey.OnDeckProgressDays:
|
||||||
|
destination.OnDeckProgressDays = int.Parse(row.Value);
|
||||||
|
break;
|
||||||
|
case ServerSettingKey.OnDeckUpdateDays:
|
||||||
|
destination.OnDeckUpdateDays = int.Parse(row.Value);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -262,6 +262,7 @@ public class ReaderService : IReaderService
|
|||||||
BookScrollId = progressDto.BookScrollId
|
BookScrollId = progressDto.BookScrollId
|
||||||
});
|
});
|
||||||
_unitOfWork.UserRepository.Update(userWithProgress);
|
_unitOfWork.UserRepository.Update(userWithProgress);
|
||||||
|
BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(progressDto.SeriesId, userId));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -88,6 +88,10 @@ export enum Action {
|
|||||||
* Import some data into Kavita
|
* Import some data into Kavita
|
||||||
*/
|
*/
|
||||||
Import = 18,
|
Import = 18,
|
||||||
|
/**
|
||||||
|
* Removes the Series from On Deck inclusion
|
||||||
|
*/
|
||||||
|
RemoveFromOnDeck = 19,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActionItem<T> {
|
export interface ActionItem<T> {
|
||||||
@ -563,9 +567,7 @@ export class ActionFactoryService {
|
|||||||
|
|
||||||
// Checks the whole tree for the action and returns true if it exists
|
// Checks the whole tree for the action and returns true if it exists
|
||||||
public hasAction(actions: Array<ActionItem<any>>, action: Action) {
|
public hasAction(actions: Array<ActionItem<any>>, action: Action) {
|
||||||
var actionFound = false;
|
if (actions.length === 0) return false;
|
||||||
|
|
||||||
if (actions.length === 0) return actionFound;
|
|
||||||
|
|
||||||
for (let i = 0; i < actions.length; i++)
|
for (let i = 0; i < actions.length; i++)
|
||||||
{
|
{
|
||||||
@ -573,8 +575,7 @@ export class ActionFactoryService {
|
|||||||
if (this.hasAction(actions[i].children, action)) return true;
|
if (this.hasAction(actions[i].children, action)) return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
return actionFound;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -226,4 +226,8 @@ export class SeriesService {
|
|||||||
getOverallRating(seriesId: number) {
|
getOverallRating(seriesId: number) {
|
||||||
return this.httpClient.get<Rating>(this.baseUrl + 'rating/overall?seriesId=' + seriesId);
|
return this.httpClient.get<Rating>(this.baseUrl + 'rating/overall?seriesId=' + seriesId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeFromOnDeck(seriesId: number) {
|
||||||
|
return this.httpClient.post(this.baseUrl + 'series/remove-from-on-deck?seriesId=' + seriesId, {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,4 +18,6 @@ export interface ServerSettings {
|
|||||||
enableFolderWatching: boolean;
|
enableFolderWatching: boolean;
|
||||||
hostName: string;
|
hostName: string;
|
||||||
cacheSize: number;
|
cacheSize: number;
|
||||||
|
onDeckProgressDays: number;
|
||||||
|
onDeckUpdateDays: number;
|
||||||
}
|
}
|
||||||
|
@ -114,6 +114,38 @@
|
|||||||
</p>
|
</p>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-4 col-sm-12 pe-2">
|
||||||
|
<label for="on-deck-progress-days" class="form-label">On Deck Last Progress (days)</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="onDeckProgressDaysTooltip" role="button" tabindex="0"></i>
|
||||||
|
<ng-template #onDeckProgressDaysTooltip>The number of days since last progress before kicking something off On Deck.</ng-template>
|
||||||
|
<span class="visually-hidden" id="on-deck-progress-days-help">The number of days since last progress before kicking something off On Deck.</span>
|
||||||
|
<input id="on-deck-progress-days" aria-describedby="on-deck-progress-days-help" class="form-control" formControlName="onDeckProgressDays"
|
||||||
|
type="number" inputmode="numeric" step="1" min="1"
|
||||||
|
[class.is-invalid]="settingsForm.get('onDeckProgressDays')?.invalid && settingsForm.get('onDeckProgressDays')?.touched">
|
||||||
|
<ng-container *ngIf="settingsForm.get('onDeckProgressDays')?.errors as errors">
|
||||||
|
<p class="invalid-feedback" *ngIf="errors.min">
|
||||||
|
Must be at least 1 day
|
||||||
|
</p>
|
||||||
|
<p class="invalid-feedback" *ngIf="errors.required">
|
||||||
|
This field is required
|
||||||
|
</p>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-sm-12 pe-2">
|
||||||
|
<label for="on-deck-update-days" class="form-label">On Deck Last Chapter Add (days)</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="onDeckUpdateDaysTooltip" role="button" tabindex="0"></i>
|
||||||
|
<ng-template #onDeckUpdateDaysTooltip>The number of days since last chapter was added to include something On Deck.</ng-template>
|
||||||
|
<span class="visually-hidden" id="on-deck-update-days-help">The number of days since last chapter was added to include something On Deck.</span>
|
||||||
|
<input id="on-deck-update-days" aria-describedby="on-deck-update-days-help" class="form-control" formControlName="onDeckUpdateDays"
|
||||||
|
type="number" inputmode="numeric" step="1" min="1"
|
||||||
|
[class.is-invalid]="settingsForm.get('onDeckUpdateDays')?.invalid && settingsForm.get('onDeckUpdateDays')?.touched">
|
||||||
|
<ng-container *ngIf="settingsForm.get('onDeckUpdateDays')?.errors as errors">
|
||||||
|
<p class="invalid-feedback" *ngIf="errors.min">
|
||||||
|
Must be at least 1 day
|
||||||
|
</p>
|
||||||
|
<p class="invalid-feedback" *ngIf="errors.required">
|
||||||
|
This field is required
|
||||||
|
</p>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3 mt-3">
|
<div class="mb-3 mt-3">
|
||||||
@ -125,7 +157,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TODO: Move this to Plugins tab once we build out some basic tables -->
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="opds" aria-describedby="opds-info" class="form-label">OPDS</label>
|
<label for="opds" aria-describedby="opds-info" class="form-label">OPDS</label>
|
||||||
<p class="accent" id="opds-info">OPDS support will allow all users to use OPDS to read and download content from the server.</p>
|
<p class="accent" id="opds-info">OPDS support will allow all users to use OPDS to read and download content from the server.</p>
|
||||||
|
@ -25,10 +25,6 @@ export class ManageSettingsComponent implements OnInit {
|
|||||||
taskFrequencies: Array<string> = [];
|
taskFrequencies: Array<string> = [];
|
||||||
logLevels: Array<string> = [];
|
logLevels: Array<string> = [];
|
||||||
|
|
||||||
get TagBadgeCursor() {
|
|
||||||
return TagBadgeCursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(private settingsService: SettingsService, private toastr: ToastrService,
|
constructor(private settingsService: SettingsService, private toastr: ToastrService,
|
||||||
private serverService: ServerService) { }
|
private serverService: ServerService) { }
|
||||||
|
|
||||||
@ -57,6 +53,8 @@ export class ManageSettingsComponent implements OnInit {
|
|||||||
this.settingsForm.addControl('enableFolderWatching', new FormControl(this.serverSettings.enableFolderWatching, [Validators.required]));
|
this.settingsForm.addControl('enableFolderWatching', new FormControl(this.serverSettings.enableFolderWatching, [Validators.required]));
|
||||||
this.settingsForm.addControl('encodeMediaAs', new FormControl(this.serverSettings.encodeMediaAs, []));
|
this.settingsForm.addControl('encodeMediaAs', new FormControl(this.serverSettings.encodeMediaAs, []));
|
||||||
this.settingsForm.addControl('hostName', new FormControl(this.serverSettings.hostName, [Validators.pattern(/^(http:|https:)+[^\s]+[\w]$/)]));
|
this.settingsForm.addControl('hostName', new FormControl(this.serverSettings.hostName, [Validators.pattern(/^(http:|https:)+[^\s]+[\w]$/)]));
|
||||||
|
this.settingsForm.addControl('onDeckProgressDays', new FormControl(this.serverSettings.onDeckProgressDays, [Validators.required]));
|
||||||
|
this.settingsForm.addControl('onDeckUpdateDays', new FormControl(this.serverSettings.onDeckUpdateDays, [Validators.required]));
|
||||||
|
|
||||||
this.serverService.getServerInfo().subscribe(info => {
|
this.serverService.getServerInfo().subscribe(info => {
|
||||||
if (info.isDocker) {
|
if (info.isDocker) {
|
||||||
@ -84,6 +82,8 @@ export class ManageSettingsComponent implements OnInit {
|
|||||||
this.settingsForm.get('encodeMediaAs')?.setValue(this.serverSettings.encodeMediaAs);
|
this.settingsForm.get('encodeMediaAs')?.setValue(this.serverSettings.encodeMediaAs);
|
||||||
this.settingsForm.get('hostName')?.setValue(this.serverSettings.hostName);
|
this.settingsForm.get('hostName')?.setValue(this.serverSettings.hostName);
|
||||||
this.settingsForm.get('cacheSize')?.setValue(this.serverSettings.cacheSize);
|
this.settingsForm.get('cacheSize')?.setValue(this.serverSettings.cacheSize);
|
||||||
|
this.settingsForm.get('onDeckProgressDays')?.setValue(this.serverSettings.onDeckProgressDays);
|
||||||
|
this.settingsForm.get('onDeckUpdateDays')?.setValue(this.serverSettings.onDeckUpdateDays);
|
||||||
this.settingsForm.markAsPristine();
|
this.settingsForm.markAsPristine();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,16 +8,16 @@ import {
|
|||||||
OnInit,
|
OnInit,
|
||||||
Output
|
Output
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import {Router} from '@angular/router';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import {ToastrService} from 'ngx-toastr';
|
||||||
import { Series } from 'src/app/_models/series';
|
import {Series} from 'src/app/_models/series';
|
||||||
import { ImageService } from 'src/app/_services/image.service';
|
import {ImageService} from 'src/app/_services/image.service';
|
||||||
import { ActionFactoryService, Action, ActionItem } from 'src/app/_services/action-factory.service';
|
import {Action, ActionFactoryService, ActionItem} from 'src/app/_services/action-factory.service';
|
||||||
import { SeriesService } from 'src/app/_services/series.service';
|
import {SeriesService} from 'src/app/_services/series.service';
|
||||||
import { ActionService } from 'src/app/_services/action.service';
|
import {ActionService} from 'src/app/_services/action.service';
|
||||||
import { EditSeriesModalComponent } from '../_modals/edit-series-modal/edit-series-modal.component';
|
import {EditSeriesModalComponent} from '../_modals/edit-series-modal/edit-series-modal.component';
|
||||||
import { RelationKind } from 'src/app/_models/series-detail/relation-kind';
|
import {RelationKind} from 'src/app/_models/series-detail/relation-kind';
|
||||||
import {CommonModule} from "@angular/common";
|
import {CommonModule} from "@angular/common";
|
||||||
import {CardItemComponent} from "../card-item/card-item.component";
|
import {CardItemComponent} from "../card-item/card-item.component";
|
||||||
import {RelationshipPipe} from "../../pipe/relationship.pipe";
|
import {RelationshipPipe} from "../../pipe/relationship.pipe";
|
||||||
@ -47,6 +47,10 @@ export class SeriesCardComponent implements OnInit, OnChanges {
|
|||||||
* If the Series has a relationship to display
|
* If the Series has a relationship to display
|
||||||
*/
|
*/
|
||||||
@Input() relation: RelationKind | undefined = undefined;
|
@Input() relation: RelationKind | undefined = undefined;
|
||||||
|
/**
|
||||||
|
* When a series card is shown on deck, a special actionable is added to the list
|
||||||
|
*/
|
||||||
|
@Input() isOnDeck: boolean = false;
|
||||||
|
|
||||||
@Output() clicked = new EventEmitter<Series>();
|
@Output() clicked = new EventEmitter<Series>();
|
||||||
/**
|
/**
|
||||||
@ -79,6 +83,19 @@ export class SeriesCardComponent implements OnInit, OnChanges {
|
|||||||
ngOnChanges(changes: any) {
|
ngOnChanges(changes: any) {
|
||||||
if (this.data) {
|
if (this.data) {
|
||||||
this.actions = this.actionFactoryService.getSeriesActions((action: ActionItem<Series>, series: Series) => this.handleSeriesActionCallback(action, series));
|
this.actions = this.actionFactoryService.getSeriesActions((action: ActionItem<Series>, series: Series) => this.handleSeriesActionCallback(action, series));
|
||||||
|
if (this.isOnDeck) {
|
||||||
|
const othersIndex = this.actions.findIndex(obj => obj.title === 'Others');
|
||||||
|
if (this.actions[othersIndex].children.findIndex(o => o.action === Action.RemoveFromOnDeck) < 0) {
|
||||||
|
this.actions[othersIndex].children.push({
|
||||||
|
action: Action.RemoveFromOnDeck,
|
||||||
|
title: 'Remove From On Deck',
|
||||||
|
callback: (action: ActionItem<Series>, series: Series) => this.handleSeriesActionCallback(action, series),
|
||||||
|
class: 'danger',
|
||||||
|
requiresAdmin: false,
|
||||||
|
children: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -125,6 +142,9 @@ export class SeriesCardComponent implements OnInit, OnChanges {
|
|||||||
const device = (action._extra!.data as Device);
|
const device = (action._extra!.data as Device);
|
||||||
this.actionService.sendSeriesToDevice(series.id, device);
|
this.actionService.sendSeriesToDevice(series.id, device);
|
||||||
break;
|
break;
|
||||||
|
case Action.RemoveFromOnDeck:
|
||||||
|
this.seriesService.removeFromOnDeck(series.id).subscribe(() => this.reload.emit(series.id));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,8 @@
|
|||||||
|
|
||||||
<app-carousel-reel [items]="inProgress" title="On Deck" (sectionClick)="handleSectionClick($event)">
|
<app-carousel-reel [items]="inProgress" title="On Deck" (sectionClick)="handleSectionClick($event)">
|
||||||
<ng-template #carouselItem let-item let-position="idx">
|
<ng-template #carouselItem let-item let-position="idx">
|
||||||
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" (reload)="reloadInProgress(item)" (dataChanged)="reloadInProgress($event)"></app-series-card>
|
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" [isOnDeck]="true"
|
||||||
|
(reload)="reloadInProgress($event)" (dataChanged)="reloadInProgress($event)"></app-series-card>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</app-carousel-reel>
|
</app-carousel-reel>
|
||||||
|
|
||||||
|
@ -120,15 +120,17 @@ export class DashboardComponent implements OnInit {
|
|||||||
this.loadRecentlyAddedSeries();
|
this.loadRecentlyAddedSeries();
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadInProgress(series: Series | boolean) {
|
reloadInProgress(series: Series | number) {
|
||||||
if (series === true || series === false) {
|
// if (typeof series === 'number') {
|
||||||
if (!series) {return;}
|
// this.loadOnDeck();
|
||||||
}
|
// return;
|
||||||
// If the update to Series doesn't affect the requirement to be in this stream, then ignore update request
|
// }
|
||||||
const seriesObj = (series as Series);
|
//
|
||||||
if (seriesObj.pagesRead !== seriesObj.pages && seriesObj.pagesRead !== 0) {
|
// // If the update to Series doesn't affect the requirement to be in this stream, then ignore update request
|
||||||
return;
|
// const seriesObj = (series as Series);
|
||||||
}
|
// if (seriesObj.pagesRead !== seriesObj.pages && seriesObj.pagesRead !== 0) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
this.loadOnDeck();
|
this.loadOnDeck();
|
||||||
}
|
}
|
||||||
|
36
openapi.json
36
openapi.json
@ -7,7 +7,7 @@
|
|||||||
"name": "GPL-3.0",
|
"name": "GPL-3.0",
|
||||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||||
},
|
},
|
||||||
"version": "0.7.4.2"
|
"version": "0.7.4.3"
|
||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
@ -7749,6 +7749,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/Series/remove-from-on-deck": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Series"
|
||||||
|
],
|
||||||
|
"summary": "Removes a series from displaying on deck until the next read event on that series",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "seriesId",
|
||||||
|
"in": "query",
|
||||||
|
"description": "",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/Series/refresh-metadata": {
|
"/api/Series/refresh-metadata": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@ -15877,6 +15901,16 @@
|
|||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "The size in MB for Caching API data",
|
"description": "The size in MB for Caching API data",
|
||||||
"format": "int64"
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"onDeckProgressDays": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "How many Days since today in the past for reading progress, should content be considered for On Deck, before it gets removed automatically",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"onDeckUpdateDays": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "How many Days since today in the past for chapter updates, should content be considered for On Deck, before it gets removed automatically",
|
||||||
|
"format": "int32"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
Loading…
x
Reference in New Issue
Block a user