Bookmarking Pages within the Reader (#469)
# Added - Added: Added the ability to bookmark certain pages within the manga (image) reader and later download them from the series context menu. # Fixed - Fixed: Fixed an issue where after adding a new folder to an existing library, a scan wouldn't be kicked off - Fixed: In some cases, after clicking the background of a modal, the modal would close, but state wouldn't be handled as if cancel was pushed # Changed - Changed: Admin contextual actions on cards will now be separated by a line to help differentiate. - Changed: Performance enhancement on an API used before reading # Dev - Bumped dependencies to latest versions ============================================= * Bumped versions of dependencies and refactored bookmark to progress. * Refactored method names in UI from bookmark to progress to prepare for new bookmark entity * Basic code is done, user can now bookmark a page (currently image reader only). * Comments and pipes * Some accessibility for new bookmark button * Fixed up the APIs to work correctly, added a new modal to quickly explore bookmarks (not implemented, not final). * Cleanup on the UI side to get the modal to look decent * Added dismissed handlers for modals where appropriate * Refactored UI to only show number of bookmarks across files to simplify delivery. Admin actionables are now separated by hr vs non-admin actions. * Basic API implemented, now to implement the ability to actually extract files. * Implemented the ability to download bookmarks. * Fixed a bug where adding a new folder to an existing library would not trigger a scan library task. * Fixed an issue that could cause bookmarked pages to get copied out of order. * Added handler from series-card component
This commit is contained in:
parent
d1d7df9291
commit
e9ec6671d5
49 changed files with 1860 additions and 241 deletions
|
|
@ -35,35 +35,35 @@
|
|||
<PackageReference Include="ExCSS" Version="4.1.0" />
|
||||
<PackageReference Include="Flurl" Version="3.0.2" />
|
||||
<PackageReference Include="Flurl.Http" Version="3.2.0" />
|
||||
<PackageReference Include="Hangfire" Version="1.7.20" />
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.20" />
|
||||
<PackageReference Include="Hangfire" Version="1.7.24" />
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.24" />
|
||||
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.32" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.35" />
|
||||
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.4">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.8">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.1.3" />
|
||||
<PackageReference Include="NetVips" Version="2.0.1" />
|
||||
<PackageReference Include="NetVips.Native" Version="8.11.0" />
|
||||
<PackageReference Include="NReco.Logging.File" Version="1.1.1" />
|
||||
<PackageReference Include="Sentry.AspNetCore" Version="3.8.2" />
|
||||
<PackageReference Include="NReco.Logging.File" Version="1.1.2" />
|
||||
<PackageReference Include="Sentry.AspNetCore" Version="3.8.3" />
|
||||
<PackageReference Include="SharpCompress" Version="0.28.3" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.26.0.34506">
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.27.0.35380">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.1.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.1.5" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="5.0.2" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.10.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.12.0" />
|
||||
<PackageReference Include="VersOne.Epub" Version="3.0.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
@ -77,22 +77,38 @@
|
|||
<None Remove="Hangfire-log.db" />
|
||||
<None Remove="obj\**" />
|
||||
<None Remove="wwwroot\**" />
|
||||
<None Remove="cache\**" />
|
||||
<None Remove="backups\**" />
|
||||
<None Remove="logs\**" />
|
||||
<None Remove="temp\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Interfaces\IMetadataService.cs" />
|
||||
<Compile Remove="obj\**" />
|
||||
<Compile Remove="wwwroot\**" />
|
||||
<Compile Remove="cache\**" />
|
||||
<Compile Remove="backups\**" />
|
||||
<Compile Remove="logs\**" />
|
||||
<Compile Remove="temp\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Remove="obj\**" />
|
||||
<EmbeddedResource Remove="wwwroot\**" />
|
||||
<EmbeddedResource Remove="cache\**" />
|
||||
<EmbeddedResource Remove="backups\**" />
|
||||
<EmbeddedResource Remove="logs\**" />
|
||||
<EmbeddedResource Remove="temp\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Remove="obj\**" />
|
||||
<Content Remove="wwwroot\**" />
|
||||
<Content Remove="cache\**" />
|
||||
<Content Remove="backups\**" />
|
||||
<Content Remove="logs\**" />
|
||||
<Content Remove="temp\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Comparators;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Downloads;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Interfaces;
|
||||
using API.Interfaces.Services;
|
||||
|
|
@ -21,12 +25,16 @@ namespace API.Controllers
|
|||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IArchiveService _archiveService;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly ICacheService _cacheService;
|
||||
private readonly NumericComparer _numericComparer;
|
||||
|
||||
public DownloadController(IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService)
|
||||
public DownloadController(IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService, ICacheService cacheService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_archiveService = archiveService;
|
||||
_directoryService = directoryService;
|
||||
_cacheService = cacheService;
|
||||
_numericComparer = new NumericComparer();
|
||||
}
|
||||
|
||||
[HttpGet("volume-size")]
|
||||
|
|
@ -39,7 +47,7 @@ namespace API.Controllers
|
|||
[HttpGet("chapter-size")]
|
||||
public async Task<ActionResult<long>> GetChapterSize(int chapterId)
|
||||
{
|
||||
var files = await _unitOfWork.VolumeRepository.GetFilesForChapter(chapterId);
|
||||
var files = await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapterId);
|
||||
return Ok(DirectoryService.GetTotalSize(files.Select(c => c.FilePath)));
|
||||
}
|
||||
|
||||
|
|
@ -96,7 +104,7 @@ namespace API.Controllers
|
|||
[HttpGet("chapter")]
|
||||
public async Task<ActionResult> DownloadChapter(int chapterId)
|
||||
{
|
||||
var files = await _unitOfWork.VolumeRepository.GetFilesForChapter(chapterId);
|
||||
var files = await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapterId);
|
||||
try
|
||||
{
|
||||
if (files.Count == 1)
|
||||
|
|
@ -132,5 +140,62 @@ namespace API.Controllers
|
|||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("bookmarks")]
|
||||
public async Task<ActionResult> DownloadBookmarkPages(DownloadBookmarkDto downloadBookmarkDto)
|
||||
{
|
||||
// We know that all bookmarks will be for one single seriesId
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(downloadBookmarkDto.Bookmarks.First().SeriesId);
|
||||
var totalFilePaths = new List<string>();
|
||||
|
||||
var tempFolder = $"download_{series.Id}_bookmarks";
|
||||
var fullExtractPath = Path.Join(DirectoryService.TempDirectory, tempFolder);
|
||||
if (new DirectoryInfo(fullExtractPath).Exists)
|
||||
{
|
||||
return BadRequest(
|
||||
"Server is currently processing this exact download. Please try again in a few minutes.");
|
||||
}
|
||||
DirectoryService.ExistOrCreate(fullExtractPath);
|
||||
|
||||
var uniqueChapterIds = downloadBookmarkDto.Bookmarks.Select(b => b.ChapterId).Distinct().ToList();
|
||||
|
||||
foreach (var chapterId in uniqueChapterIds)
|
||||
{
|
||||
var chapterExtractPath = Path.Join(fullExtractPath, $"{series.Id}_bookmark_{chapterId}");
|
||||
var chapterPages = downloadBookmarkDto.Bookmarks.Where(b => b.ChapterId == chapterId)
|
||||
.Select(b => b.Page).ToList();
|
||||
var mangaFiles = await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapterId);
|
||||
switch (series.Format)
|
||||
{
|
||||
case MangaFormat.Image:
|
||||
DirectoryService.ExistOrCreate(chapterExtractPath);
|
||||
_directoryService.CopyFilesToDirectory(mangaFiles.Select(f => f.FilePath), chapterExtractPath, $"{chapterId}_");
|
||||
break;
|
||||
case MangaFormat.Archive:
|
||||
case MangaFormat.Pdf:
|
||||
_cacheService.ExtractChapterFiles(chapterExtractPath, mangaFiles.ToList());
|
||||
var originalFiles = _directoryService.GetFilesWithExtension(chapterExtractPath,
|
||||
Parser.Parser.ImageFileExtensions);
|
||||
_directoryService.CopyFilesToDirectory(originalFiles, chapterExtractPath, $"{chapterId}_");
|
||||
DirectoryService.DeleteFiles(originalFiles);
|
||||
break;
|
||||
case MangaFormat.Epub:
|
||||
return BadRequest("Series is not in a valid format.");
|
||||
default:
|
||||
return BadRequest("Series is not in a valid format. Please rescan series and try again.");
|
||||
}
|
||||
|
||||
var files = _directoryService.GetFilesWithExtension(chapterExtractPath, Parser.Parser.ImageFileExtensions);
|
||||
// Filter out images that aren't in bookmarks
|
||||
Array.Sort(files, _numericComparer);
|
||||
totalFilePaths.AddRange(files.Where((t, i) => chapterPages.Contains(i)));
|
||||
}
|
||||
|
||||
|
||||
var (fileBytes, zipPath) = await _archiveService.CreateZipForDownload(totalFilePaths,
|
||||
tempFolder);
|
||||
DirectoryService.ClearAndDeleteDirectory(fullExtractPath);
|
||||
return File(fileBytes, "application/zip", $"{series.Name} - Bookmarks.zip");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -203,8 +203,7 @@ namespace API.Controllers
|
|||
{
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryForUserDto.Id);
|
||||
|
||||
var originalFolders = library.Folders.Select(x => x.Path);
|
||||
var differenceBetweenFolders = originalFolders.Except(libraryForUserDto.Folders);
|
||||
var originalFolders = library.Folders.Select(x => x.Path).ToList();
|
||||
|
||||
library.Name = libraryForUserDto.Name;
|
||||
library.Folders = libraryForUserDto.Folders.Select(s => new FolderPath() {Path = s}).ToList();
|
||||
|
|
@ -212,9 +211,9 @@ namespace API.Controllers
|
|||
_unitOfWork.LibraryRepository.Update(library);
|
||||
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was a critical issue updating the library.");
|
||||
if (differenceBetweenFolders.Any())
|
||||
if (originalFolders.Count != libraryForUserDto.Folders.Count())
|
||||
{
|
||||
_taskScheduler.ScanLibrary(library.Id, true);
|
||||
_taskScheduler.ScanLibrary(library.Id);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
|
|
|
|||
|
|
@ -50,15 +50,16 @@ namespace API.Controllers
|
|||
}
|
||||
|
||||
[HttpGet("chapter-info")]
|
||||
public async Task<ActionResult<ChapterInfoDto>> GetChapterInfo(int chapterId)
|
||||
public async Task<ActionResult<ChapterInfoDto>> GetChapterInfo(int seriesId, int chapterId)
|
||||
{
|
||||
// PERF: Write this in one DB call
|
||||
var chapter = await _cacheService.Ensure(chapterId);
|
||||
if (chapter == null) return BadRequest("Could not find Chapter");
|
||||
var volume = await _unitOfWork.SeriesRepository.GetVolumeAsync(chapter.VolumeId);
|
||||
|
||||
var volume = await _unitOfWork.SeriesRepository.GetVolumeDtoAsync(chapter.VolumeId);
|
||||
if (volume == null) return BadRequest("Could not find Volume");
|
||||
var (_, mangaFile) = await _cacheService.GetCachedPagePath(chapter, 0);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
||||
var mangaFile = (await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapterId)).First();
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
||||
|
||||
return Ok(new ChapterInfoDto()
|
||||
{
|
||||
|
|
@ -72,29 +73,6 @@ namespace API.Controllers
|
|||
});
|
||||
}
|
||||
|
||||
[HttpGet("get-bookmark")]
|
||||
public async Task<ActionResult<BookmarkDto>> GetBookmark(int chapterId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var bookmark = new BookmarkDto()
|
||||
{
|
||||
PageNum = 0,
|
||||
ChapterId = chapterId,
|
||||
VolumeId = 0,
|
||||
SeriesId = 0
|
||||
};
|
||||
if (user.Progresses == null) return Ok(bookmark);
|
||||
var progress = user.Progresses.SingleOrDefault(x => x.AppUserId == user.Id && x.ChapterId == chapterId);
|
||||
|
||||
if (progress != null)
|
||||
{
|
||||
bookmark.SeriesId = progress.SeriesId;
|
||||
bookmark.VolumeId = progress.VolumeId;
|
||||
bookmark.PageNum = progress.PagesRead;
|
||||
bookmark.BookScrollId = progress.BookScrollId;
|
||||
}
|
||||
return Ok(bookmark);
|
||||
}
|
||||
|
||||
[HttpPost("mark-read")]
|
||||
public async Task<ActionResult> MarkRead(MarkReadDto markReadDto)
|
||||
|
|
@ -232,21 +210,45 @@ namespace API.Controllers
|
|||
return BadRequest("Could not save progress");
|
||||
}
|
||||
|
||||
[HttpPost("bookmark")]
|
||||
public async Task<ActionResult> Bookmark(BookmarkDto bookmarkDto)
|
||||
[HttpGet("get-progress")]
|
||||
public async Task<ActionResult<ProgressDto>> GetProgress(int chapterId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var progressBookmark = new ProgressDto()
|
||||
{
|
||||
PageNum = 0,
|
||||
ChapterId = chapterId,
|
||||
VolumeId = 0,
|
||||
SeriesId = 0
|
||||
};
|
||||
if (user.Progresses == null) return Ok(progressBookmark);
|
||||
var progress = user.Progresses.SingleOrDefault(x => x.AppUserId == user.Id && x.ChapterId == chapterId);
|
||||
|
||||
if (progress != null)
|
||||
{
|
||||
progressBookmark.SeriesId = progress.SeriesId;
|
||||
progressBookmark.VolumeId = progress.VolumeId;
|
||||
progressBookmark.PageNum = progress.PagesRead;
|
||||
progressBookmark.BookScrollId = progress.BookScrollId;
|
||||
}
|
||||
return Ok(progressBookmark);
|
||||
}
|
||||
|
||||
[HttpPost("progress")]
|
||||
public async Task<ActionResult> BookmarkProgress(ProgressDto progressDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
|
||||
// Don't let user bookmark past total pages.
|
||||
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(bookmarkDto.ChapterId);
|
||||
if (bookmarkDto.PageNum > chapter.Pages)
|
||||
// Don't let user save past total pages.
|
||||
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(progressDto.ChapterId);
|
||||
if (progressDto.PageNum > chapter.Pages)
|
||||
{
|
||||
bookmarkDto.PageNum = chapter.Pages;
|
||||
progressDto.PageNum = chapter.Pages;
|
||||
}
|
||||
|
||||
if (bookmarkDto.PageNum < 0)
|
||||
if (progressDto.PageNum < 0)
|
||||
{
|
||||
bookmarkDto.PageNum = 0;
|
||||
progressDto.PageNum = 0;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -255,26 +257,26 @@ namespace API.Controllers
|
|||
// TODO: Look into creating a progress entry when a new item is added to the DB so we can just look it up and modify it
|
||||
user.Progresses ??= new List<AppUserProgress>();
|
||||
var userProgress =
|
||||
user.Progresses.SingleOrDefault(x => x.ChapterId == bookmarkDto.ChapterId && x.AppUserId == user.Id);
|
||||
user.Progresses.SingleOrDefault(x => x.ChapterId == progressDto.ChapterId && x.AppUserId == user.Id);
|
||||
|
||||
if (userProgress == null)
|
||||
{
|
||||
user.Progresses.Add(new AppUserProgress
|
||||
{
|
||||
PagesRead = bookmarkDto.PageNum,
|
||||
VolumeId = bookmarkDto.VolumeId,
|
||||
SeriesId = bookmarkDto.SeriesId,
|
||||
ChapterId = bookmarkDto.ChapterId,
|
||||
BookScrollId = bookmarkDto.BookScrollId,
|
||||
PagesRead = progressDto.PageNum,
|
||||
VolumeId = progressDto.VolumeId,
|
||||
SeriesId = progressDto.SeriesId,
|
||||
ChapterId = progressDto.ChapterId,
|
||||
BookScrollId = progressDto.BookScrollId,
|
||||
LastModified = DateTime.Now
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
userProgress.PagesRead = bookmarkDto.PageNum;
|
||||
userProgress.SeriesId = bookmarkDto.SeriesId;
|
||||
userProgress.VolumeId = bookmarkDto.VolumeId;
|
||||
userProgress.BookScrollId = bookmarkDto.BookScrollId;
|
||||
userProgress.PagesRead = progressDto.PageNum;
|
||||
userProgress.SeriesId = progressDto.SeriesId;
|
||||
userProgress.VolumeId = progressDto.VolumeId;
|
||||
userProgress.BookScrollId = progressDto.BookScrollId;
|
||||
userProgress.LastModified = DateTime.Now;
|
||||
}
|
||||
|
||||
|
|
@ -293,6 +295,139 @@ namespace API.Controllers
|
|||
return BadRequest("Could not save progress");
|
||||
}
|
||||
|
||||
[HttpGet("get-bookmarks")]
|
||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarks(int chapterId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
||||
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForChapter(user.Id, chapterId));
|
||||
}
|
||||
|
||||
[HttpPost("remove-bookmarks")]
|
||||
public async Task<ActionResult> RemoveBookmarks(int seriesId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user.Bookmarks == null) return Ok("Nothing to remove");
|
||||
try
|
||||
{
|
||||
user.Bookmarks = user.Bookmarks.Where(bmk => bmk.SeriesId == seriesId).ToList();
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Could not clear bookmarks");
|
||||
|
||||
}
|
||||
|
||||
[HttpGet("get-volume-bookmarks")]
|
||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForVolume(int volumeId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
||||
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForVolume(user.Id, volumeId));
|
||||
}
|
||||
|
||||
[HttpGet("get-series-bookmarks")]
|
||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForSeries(int seriesId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
||||
|
||||
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForSeries(user.Id, seriesId));
|
||||
}
|
||||
|
||||
[HttpPost("bookmark")]
|
||||
public async Task<ActionResult> BookmarkPage(BookmarkDto bookmarkDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
|
||||
// Don't let user save past total pages.
|
||||
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(bookmarkDto.ChapterId);
|
||||
if (bookmarkDto.Page > chapter.Pages)
|
||||
{
|
||||
bookmarkDto.Page = chapter.Pages;
|
||||
}
|
||||
|
||||
if (bookmarkDto.Page < 0)
|
||||
{
|
||||
bookmarkDto.Page = 0;
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
user.Bookmarks ??= new List<AppUserBookmark>();
|
||||
var userBookmark =
|
||||
user.Bookmarks.SingleOrDefault(x => x.ChapterId == bookmarkDto.ChapterId && x.AppUserId == user.Id && x.Page == bookmarkDto.Page);
|
||||
|
||||
if (userBookmark == null)
|
||||
{
|
||||
user.Bookmarks.Add(new AppUserBookmark()
|
||||
{
|
||||
Page = bookmarkDto.Page,
|
||||
VolumeId = bookmarkDto.VolumeId,
|
||||
SeriesId = bookmarkDto.SeriesId,
|
||||
ChapterId = bookmarkDto.ChapterId,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
userBookmark.Page = bookmarkDto.Page;
|
||||
userBookmark.SeriesId = bookmarkDto.SeriesId;
|
||||
userBookmark.VolumeId = bookmarkDto.VolumeId;
|
||||
}
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Could not save bookmark");
|
||||
}
|
||||
|
||||
[HttpPost("unbookmark")]
|
||||
public async Task<ActionResult> UnBookmarkPage(BookmarkDto bookmarkDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
|
||||
if (user.Bookmarks == null) return Ok();
|
||||
try {
|
||||
user.Bookmarks = user.Bookmarks.Where(x =>
|
||||
x.ChapterId == bookmarkDto.ChapterId
|
||||
&& x.AppUserId == user.Id
|
||||
&& x.Page != bookmarkDto.Page).ToList();
|
||||
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Could not remove bookmark");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the next logical chapter from the series.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -2,14 +2,10 @@
|
|||
{
|
||||
public class BookmarkDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int Page { get; set; }
|
||||
public int VolumeId { get; set; }
|
||||
public int ChapterId { get; set; }
|
||||
public int PageNum { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
/// <summary>
|
||||
/// For Book reader, this can be an optional string of the id of a part marker, to help resume reading position
|
||||
/// on pages that combine multiple "chapters".
|
||||
/// </summary>
|
||||
public string BookScrollId { get; set; }
|
||||
public int ChapterId { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
9
API/DTOs/Downloads/DownloadBookmarkDto.cs
Normal file
9
API/DTOs/Downloads/DownloadBookmarkDto.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace API.DTOs.Downloads
|
||||
{
|
||||
public class DownloadBookmarkDto
|
||||
{
|
||||
public IEnumerable<BookmarkDto> Bookmarks { get; set; }
|
||||
}
|
||||
}
|
||||
15
API/DTOs/ProgressDto.cs
Normal file
15
API/DTOs/ProgressDto.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
namespace API.DTOs
|
||||
{
|
||||
public class ProgressDto
|
||||
{
|
||||
public int VolumeId { get; set; }
|
||||
public int ChapterId { get; set; }
|
||||
public int PageNum { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
/// <summary>
|
||||
/// For Book reader, this can be an optional string of the id of a part marker, to help resume reading position
|
||||
/// on pages that combine multiple "chapters".
|
||||
/// </summary>
|
||||
public string BookScrollId { get; set; }
|
||||
}
|
||||
}
|
||||
7
API/Data/BookmarkRepository.cs
Normal file
7
API/Data/BookmarkRepository.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace API.Data
|
||||
{
|
||||
public class BookmarkRepository
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.ChangeTracking;
|
|||
|
||||
namespace API.Data
|
||||
{
|
||||
public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
||||
public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
||||
IdentityUserClaim<int>, AppUserRole, IdentityUserLogin<int>,
|
||||
IdentityRoleClaim<int>, IdentityUserToken<int>>
|
||||
{
|
||||
|
|
@ -17,10 +17,10 @@ namespace API.Data
|
|||
ChangeTracker.Tracked += OnEntityTracked;
|
||||
ChangeTracker.StateChanged += OnEntityStateChanged;
|
||||
}
|
||||
|
||||
|
||||
public DbSet<Library> Library { get; set; }
|
||||
public DbSet<Series> Series { get; set; }
|
||||
|
||||
|
||||
public DbSet<Chapter> Chapter { get; set; }
|
||||
public DbSet<Volume> Volume { get; set; }
|
||||
public DbSet<AppUser> AppUser { get; set; }
|
||||
|
|
@ -31,18 +31,19 @@ namespace API.Data
|
|||
public DbSet<AppUserPreferences> AppUserPreferences { get; set; }
|
||||
public DbSet<SeriesMetadata> SeriesMetadata { get; set; }
|
||||
public DbSet<CollectionTag> CollectionTag { get; set; }
|
||||
public DbSet<AppUserBookmark> AppUserBookmark { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
base.OnModelCreating(builder);
|
||||
|
||||
|
||||
|
||||
builder.Entity<AppUser>()
|
||||
.HasMany(ur => ur.UserRoles)
|
||||
.WithOne(u => u.User)
|
||||
.HasForeignKey(ur => ur.UserId)
|
||||
.IsRequired();
|
||||
|
||||
|
||||
builder.Entity<AppRole>()
|
||||
.HasMany(ur => ur.UserRoles)
|
||||
.WithOne(u => u.Role)
|
||||
|
|
@ -50,7 +51,7 @@ namespace API.Data
|
|||
.IsRequired();
|
||||
}
|
||||
|
||||
|
||||
|
||||
void OnEntityTracked(object sender, EntityTrackedEventArgs e)
|
||||
{
|
||||
if (!e.FromQuery && e.Entry.State == EntityState.Added && e.Entry.Entity is IEntityDate entity)
|
||||
|
|
@ -58,7 +59,7 @@ namespace API.Data
|
|||
entity.Created = DateTime.Now;
|
||||
entity.LastModified = DateTime.Now;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
void OnEntityStateChanged(object sender, EntityStateChangedEventArgs e)
|
||||
|
|
@ -67,4 +68,4 @@ namespace API.Data
|
|||
entity.LastModified = DateTime.Now;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
913
API/Data/Migrations/20210809210326_BookmarkPages.Designer.cs
generated
Normal file
913
API/Data/Migrations/20210809210326_BookmarkPages.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,913 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using API.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(DataContext))]
|
||||
[Migration("20210809210326_BookmarkPages")]
|
||||
partial class BookmarkPages
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "5.0.8");
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex");
|
||||
|
||||
b.ToTable("AspNetRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUser", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("LastActive")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex");
|
||||
|
||||
b.ToTable("AspNetUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AppUserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ChapterId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Page")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("VolumeId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AppUserId");
|
||||
|
||||
b.ToTable("AppUserBookmark");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AppUserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AutoCloseMenu")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("BookReaderDarkMode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("BookReaderFontFamily")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("BookReaderFontSize")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("BookReaderLineSpacing")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("BookReaderMargin")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("BookReaderReadingDirection")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("BookReaderTapToPaginate")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("PageSplitOption")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ReaderMode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ReadingDirection")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ScalingOption")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("SiteDarkMode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AppUserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("AppUserPreferences");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserProgress", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AppUserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("BookScrollId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ChapterId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastModified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("PagesRead")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("VolumeId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AppUserId");
|
||||
|
||||
b.ToTable("AppUserProgresses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserRating", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AppUserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Rating")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Review")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AppUserId");
|
||||
|
||||
b.ToTable("AppUserRating");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserRole", b =>
|
||||
{
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("RoleId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte[]>("CoverImage")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsSpecial")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("LastModified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Number")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Pages")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Range")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("VolumeId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("VolumeId");
|
||||
|
||||
b.ToTable("Chapter");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.CollectionTag", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte[]>("CoverImage")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<string>("NormalizedTitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Promoted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Summary")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Id", "Promoted")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CollectionTag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.FolderPath", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("LastScanned")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("LibraryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.ToTable("FolderPath");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Library", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CoverImage")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastModified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Library");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.MangaFile", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ChapterId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("FilePath")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Format")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("LastModified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Pages")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChapterId");
|
||||
|
||||
b.ToTable("MangaFile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Series", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte[]>("CoverImage")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Format")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("LastModified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("LibraryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("LocalizedName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OriginalName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Pages")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SortName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Summary")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.HasIndex("Name", "NormalizedName", "LocalizedName", "LibraryId", "Format")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.SeriesMetadata", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SeriesId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Id", "SeriesId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("SeriesMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ServerSetting", b =>
|
||||
{
|
||||
b.Property<int>("Key")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("ServerSetting");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte[]>("CoverImage")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastModified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Number")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Pages")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("Volume");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AppUserLibrary", b =>
|
||||
{
|
||||
b.Property<int>("AppUsersId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("LibrariesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("AppUsersId", "LibrariesId");
|
||||
|
||||
b.HasIndex("LibrariesId");
|
||||
|
||||
b.ToTable("AppUserLibrary");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CollectionTagSeriesMetadata", b =>
|
||||
{
|
||||
b.Property<int>("CollectionTagsId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SeriesMetadatasId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("CollectionTagsId", "SeriesMetadatasId");
|
||||
|
||||
b.HasIndex("SeriesMetadatasId");
|
||||
|
||||
b.ToTable("CollectionTagSeriesMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("RoleId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
|
||||
{
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||
.WithMany("Bookmarks")
|
||||
.HasForeignKey("AppUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("AppUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||
.WithOne("UserPreferences")
|
||||
.HasForeignKey("API.Entities.AppUserPreferences", "AppUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("AppUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserProgress", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||
.WithMany("Progresses")
|
||||
.HasForeignKey("AppUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("AppUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserRating", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||
.WithMany("Ratings")
|
||||
.HasForeignKey("AppUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("AppUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserRole", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppRole", "Role")
|
||||
.WithMany("UserRoles")
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.AppUser", "User")
|
||||
.WithMany("UserRoles")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Role");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Volume", "Volume")
|
||||
.WithMany("Chapters")
|
||||
.HasForeignKey("VolumeId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Volume");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.FolderPath", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Library", "Library")
|
||||
.WithMany("Folders")
|
||||
.HasForeignKey("LibraryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Library");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.MangaFile", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Chapter", "Chapter")
|
||||
.WithMany("Files")
|
||||
.HasForeignKey("ChapterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Chapter");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Series", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Library", "Library")
|
||||
.WithMany("Series")
|
||||
.HasForeignKey("LibraryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Library");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.SeriesMetadata", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Series", "Series")
|
||||
.WithOne("Metadata")
|
||||
.HasForeignKey("API.Entities.SeriesMetadata", "SeriesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Series", "Series")
|
||||
.WithMany("Volumes")
|
||||
.HasForeignKey("SeriesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AppUserLibrary", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AppUsersId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.Library", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("LibrariesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CollectionTagSeriesMetadata", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.CollectionTag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("CollectionTagsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.SeriesMetadata", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SeriesMetadatasId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||
{
|
||||
b.Navigation("UserRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUser", b =>
|
||||
{
|
||||
b.Navigation("Bookmarks");
|
||||
|
||||
b.Navigation("Progresses");
|
||||
|
||||
b.Navigation("Ratings");
|
||||
|
||||
b.Navigation("UserPreferences");
|
||||
|
||||
b.Navigation("UserRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||
{
|
||||
b.Navigation("Files");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Library", b =>
|
||||
{
|
||||
b.Navigation("Folders");
|
||||
|
||||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Series", b =>
|
||||
{
|
||||
b.Navigation("Metadata");
|
||||
|
||||
b.Navigation("Volumes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||
{
|
||||
b.Navigation("Chapters");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
44
API/Data/Migrations/20210809210326_BookmarkPages.cs
Normal file
44
API/Data/Migrations/20210809210326_BookmarkPages.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
public partial class BookmarkPages : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AppUserBookmark",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Page = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
VolumeId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SeriesId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ChapterId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
AppUserId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AppUserBookmark", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AppUserBookmark_AspNetUsers_AppUserId",
|
||||
column: x => x.AppUserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AppUserBookmark_AppUserId",
|
||||
table: "AppUserBookmark",
|
||||
column: "AppUserId");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AppUserBookmark");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ namespace API.Data.Migrations
|
|||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "5.0.4");
|
||||
.HasAnnotation("ProductVersion", "5.0.8");
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||
{
|
||||
|
|
@ -118,6 +118,34 @@ namespace API.Data.Migrations
|
|||
b.ToTable("AspNetUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AppUserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ChapterId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Page")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("VolumeId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AppUserId");
|
||||
|
||||
b.ToTable("AppUserBookmark");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
@ -641,6 +669,17 @@ namespace API.Data.Migrations
|
|||
b.ToTable("AspNetUserTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||
.WithMany("Bookmarks")
|
||||
.HasForeignKey("AppUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("AppUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||
|
|
@ -832,6 +871,8 @@ namespace API.Data.Migrations
|
|||
|
||||
modelBuilder.Entity("API.Entities.AppUser", b =>
|
||||
{
|
||||
b.Navigation("Bookmarks");
|
||||
|
||||
b.Navigation("Progresses");
|
||||
|
||||
b.Navigation("Ratings");
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ namespace API.Data
|
|||
}
|
||||
|
||||
public ISeriesRepository SeriesRepository => new SeriesRepository(_context, _mapper);
|
||||
public IUserRepository UserRepository => new UserRepository(_context, _userManager);
|
||||
public IUserRepository UserRepository => new UserRepository(_context, _userManager, _mapper);
|
||||
public ILibraryRepository LibraryRepository => new LibraryRepository(_context, _mapper);
|
||||
|
||||
public IVolumeRepository VolumeRepository => new VolumeRepository(_context, _mapper);
|
||||
|
|
@ -56,4 +56,4 @@ namespace API.Data
|
|||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ using API.Constants;
|
|||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Interfaces;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
|
|
@ -14,18 +16,20 @@ namespace API.Data
|
|||
{
|
||||
private readonly DataContext _context;
|
||||
private readonly UserManager<AppUser> _userManager;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public UserRepository(DataContext context, UserManager<AppUser> userManager)
|
||||
public UserRepository(DataContext context, UserManager<AppUser> userManager, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_userManager = userManager;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public void Update(AppUser user)
|
||||
{
|
||||
_context.Entry(user).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
|
||||
public void Update(AppUserPreferences preferences)
|
||||
{
|
||||
_context.Entry(preferences).State = EntityState.Modified;
|
||||
|
|
@ -45,6 +49,7 @@ namespace API.Data
|
|||
{
|
||||
return await _context.Users
|
||||
.Include(u => u.Progresses)
|
||||
.Include(u => u.Bookmarks)
|
||||
.SingleOrDefaultAsync(x => x.UserName == username);
|
||||
}
|
||||
|
||||
|
|
@ -71,6 +76,36 @@ namespace API.Data
|
|||
.SingleOrDefaultAsync(p => p.AppUser.UserName == username);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForSeries(int userId, int seriesId)
|
||||
{
|
||||
return await _context.AppUserBookmark
|
||||
.Where(x => x.AppUserId == userId && x.SeriesId == seriesId)
|
||||
.OrderBy(x => x.Page)
|
||||
.AsNoTracking()
|
||||
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForVolume(int userId, int volumeId)
|
||||
{
|
||||
return await _context.AppUserBookmark
|
||||
.Where(x => x.AppUserId == userId && x.VolumeId == volumeId)
|
||||
.OrderBy(x => x.Page)
|
||||
.AsNoTracking()
|
||||
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForChapter(int userId, int chapterId)
|
||||
{
|
||||
return await _context.AppUserBookmark
|
||||
.Where(x => x.AppUserId == userId && x.ChapterId == chapterId)
|
||||
.OrderBy(x => x.Page)
|
||||
.AsNoTracking()
|
||||
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<MemberDto>> GetMembersAsync()
|
||||
{
|
||||
return await _context.Users
|
||||
|
|
@ -97,4 +132,4 @@ namespace API.Data
|
|||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
using API.Interfaces;
|
||||
using AutoMapper;
|
||||
|
|
@ -38,7 +40,6 @@ namespace API.Data
|
|||
.SingleOrDefaultAsync(c => c.Id == chapterId);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns Chapters for a volume id.
|
||||
/// </summary>
|
||||
|
|
@ -79,13 +80,30 @@ namespace API.Data
|
|||
return chapter;
|
||||
}
|
||||
|
||||
public async Task<IList<MangaFile>> GetFilesForChapter(int chapterId)
|
||||
/// <summary>
|
||||
/// Returns non-tracked files for a given chapterId
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IList<MangaFile>> GetFilesForChapterAsync(int chapterId)
|
||||
{
|
||||
return await _context.MangaFile
|
||||
.Where(c => chapterId == c.ChapterId)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns non-tracked files for a set of chapterIds
|
||||
/// </summary>
|
||||
/// <param name="chapterIds"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IList<MangaFile>> GetFilesForChaptersAsync(IReadOnlyList<int> chapterIds)
|
||||
{
|
||||
return await _context.MangaFile
|
||||
.Where(c => chapterIds.Contains(c.ChapterId))
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<MangaFile>> GetFilesForVolume(int volumeId)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ namespace API.Entities
|
|||
public ICollection<AppUserProgress> Progresses { get; set; }
|
||||
public ICollection<AppUserRating> Ratings { get; set; }
|
||||
public AppUserPreferences UserPreferences { get; set; }
|
||||
|
||||
public ICollection<AppUserBookmark> Bookmarks { get; set; }
|
||||
|
||||
[ConcurrencyCheck]
|
||||
public uint RowVersion { get; set; }
|
||||
|
||||
|
|
@ -26,4 +27,4 @@ namespace API.Entities
|
|||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
22
API/Entities/AppUserBookmark.cs
Normal file
22
API/Entities/AppUserBookmark.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace API.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a saved page in a Chapter entity for a given user.
|
||||
/// </summary>
|
||||
public class AppUserBookmark
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int Page { get; set; }
|
||||
public int VolumeId { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
public int ChapterId { get; set; }
|
||||
|
||||
|
||||
// Relationships
|
||||
[JsonIgnore]
|
||||
public AppUser AppUser { get; set; }
|
||||
public int AppUserId { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -16,11 +16,11 @@ namespace API.Helpers
|
|||
CreateMap<Volume, VolumeDto>();
|
||||
|
||||
CreateMap<MangaFile, MangaFileDto>();
|
||||
|
||||
|
||||
CreateMap<Chapter, ChapterDto>();
|
||||
|
||||
CreateMap<Series, SeriesDto>();
|
||||
|
||||
|
||||
CreateMap<CollectionTag, CollectionTagDto>();
|
||||
|
||||
CreateMap<SeriesMetadata, SeriesMetadataDto>();
|
||||
|
|
@ -29,18 +29,20 @@ namespace API.Helpers
|
|||
|
||||
CreateMap<AppUserPreferences, UserPreferencesDto>();
|
||||
|
||||
CreateMap<AppUserBookmark, BookmarkDto>();
|
||||
|
||||
CreateMap<Series, SearchResultDto>()
|
||||
.ForMember(dest => dest.SeriesId,
|
||||
opt => opt.MapFrom(src => src.Id))
|
||||
.ForMember(dest => dest.LibraryName,
|
||||
opt => opt.MapFrom(src => src.Library.Name));
|
||||
|
||||
|
||||
|
||||
|
||||
CreateMap<Library, LibraryDto>()
|
||||
.ForMember(dest => dest.Folders,
|
||||
opt =>
|
||||
opt =>
|
||||
opt.MapFrom(src => src.Folders.Select(x => x.Path).ToList()));
|
||||
|
||||
|
||||
CreateMap<AppUser, MemberDto>()
|
||||
.AfterMap((ps, pst, context) => context.Mapper.Map(ps.Libraries, pst.Libraries));
|
||||
|
||||
|
|
@ -50,4 +52,4 @@ namespace API.Helpers
|
|||
.ConvertUsing<ServerSettingConverter>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,5 +16,8 @@ namespace API.Interfaces
|
|||
Task<AppUserRating> GetUserRating(int seriesId, int userId);
|
||||
void AddRatingTracking(AppUserRating userRating);
|
||||
Task<AppUserPreferences> GetPreferencesAsync(string username);
|
||||
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForSeries(int userId, int seriesId);
|
||||
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForVolume(int userId, int volumeId);
|
||||
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForChapter(int userId, int chapterId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,10 @@ namespace API.Interfaces
|
|||
void Update(Volume volume);
|
||||
Task<Chapter> GetChapterAsync(int chapterId);
|
||||
Task<ChapterDto> GetChapterDtoAsync(int chapterId);
|
||||
Task<IList<MangaFile>> GetFilesForChapter(int chapterId);
|
||||
Task<IList<MangaFile>> GetFilesForChapterAsync(int chapterId);
|
||||
Task<IList<MangaFile>> GetFilesForChaptersAsync(IReadOnlyList<int> chapterIds);
|
||||
Task<IList<Chapter>> GetChaptersAsync(int volumeId);
|
||||
Task<byte[]> GetChapterCoverImageAsync(int chapterId);
|
||||
Task<IList<MangaFile>> GetFilesForVolume(int volumeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,5 +36,6 @@ namespace API.Interfaces.Services
|
|||
|
||||
void EnsureCacheDirectory();
|
||||
string GetCachedEpubFile(int chapterId, Chapter chapter);
|
||||
public void ExtractChapterFiles(string extractPath, IReadOnlyList<MangaFile> files);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ namespace API.Interfaces.Services
|
|||
/// <returns></returns>
|
||||
string[] GetFilesWithExtension(string path, string searchPatternExpression = "");
|
||||
Task<byte[]> ReadFileAsync(string path);
|
||||
bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath);
|
||||
bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath, string prepend = "");
|
||||
bool Exists(string directory);
|
||||
|
||||
IEnumerable<string> GetFiles(string path, string searchPatternExpression = "",
|
||||
|
|
|
|||
|
|
@ -224,17 +224,16 @@ namespace API.Services
|
|||
|
||||
public async Task<Tuple<byte[], string>> CreateZipForDownload(IEnumerable<string> files, string tempFolder)
|
||||
{
|
||||
var tempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp");
|
||||
var dateString = DateTime.Now.ToShortDateString().Replace("/", "_");
|
||||
|
||||
var tempLocation = Path.Join(tempDirectory, $"{tempFolder}_{dateString}");
|
||||
var tempLocation = Path.Join(DirectoryService.TempDirectory, $"{tempFolder}_{dateString}");
|
||||
DirectoryService.ExistOrCreate(tempLocation);
|
||||
if (!_directoryService.CopyFilesToDirectory(files, tempLocation))
|
||||
{
|
||||
throw new KavitaException("Unable to copy files to temp directory archive download.");
|
||||
}
|
||||
|
||||
var zipPath = Path.Join(tempDirectory, $"kavita_{tempFolder}_{dateString}.zip");
|
||||
var zipPath = Path.Join(DirectoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.zip");
|
||||
try
|
||||
{
|
||||
ZipFile.CreateFromDirectory(tempLocation, zipPath);
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ namespace API.Services
|
|||
|
||||
public void EnsureCacheDirectory()
|
||||
{
|
||||
if (!DirectoryService.ExistOrCreate(CacheDirectory))
|
||||
if (!DirectoryService.ExistOrCreate(DirectoryService.CacheDirectory))
|
||||
{
|
||||
_logger.LogError("Cache directory {CacheDirectory} is not accessible or does not exist. Creating...", CacheDirectory);
|
||||
}
|
||||
|
|
@ -60,58 +60,77 @@ namespace API.Services
|
|||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Caches the files for the given chapter to CacheDirectory
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns>This will always return the Chapter for the chpaterId</returns>
|
||||
public async Task<Chapter> Ensure(int chapterId)
|
||||
{
|
||||
EnsureCacheDirectory();
|
||||
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(chapterId);
|
||||
var files = chapter.Files.ToList();
|
||||
var fileCount = files.Count;
|
||||
var extractPath = GetCachePath(chapterId);
|
||||
var extraPath = "";
|
||||
var removeNonImages = true;
|
||||
|
||||
if (Directory.Exists(extractPath))
|
||||
if (!Directory.Exists(extractPath))
|
||||
{
|
||||
return chapter;
|
||||
var files = chapter.Files.ToList();
|
||||
ExtractChapterFiles(extractPath, files);
|
||||
}
|
||||
|
||||
return chapter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is an internal method for cache service for extracting chapter files to disk. The code is structured
|
||||
/// for cache service, but can be re-used (download bookmarks)
|
||||
/// </summary>
|
||||
/// <param name="extractPath"></param>
|
||||
/// <param name="files"></param>
|
||||
/// <returns></returns>
|
||||
public void ExtractChapterFiles(string extractPath, IReadOnlyList<MangaFile> files)
|
||||
{
|
||||
var removeNonImages = true;
|
||||
var fileCount = files.Count;
|
||||
var extraPath = "";
|
||||
var extractDi = new DirectoryInfo(extractPath);
|
||||
|
||||
if (files.Count > 0 && files[0].Format == MangaFormat.Image)
|
||||
{
|
||||
DirectoryService.ExistOrCreate(extractPath);
|
||||
if (files.Count == 1)
|
||||
{
|
||||
_directoryService.CopyFileToDirectory(files[0].FilePath, extractPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
_directoryService.CopyDirectoryToDirectory(Path.GetDirectoryName(files[0].FilePath), extractPath, Parser.Parser.ImageFileExtensions);
|
||||
}
|
||||
DirectoryService.ExistOrCreate(extractPath);
|
||||
if (files.Count == 1)
|
||||
{
|
||||
_directoryService.CopyFileToDirectory(files[0].FilePath, extractPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
_directoryService.CopyDirectoryToDirectory(Path.GetDirectoryName(files[0].FilePath), extractPath,
|
||||
Parser.Parser.ImageFileExtensions);
|
||||
}
|
||||
|
||||
extractDi.Flatten();
|
||||
return chapter;
|
||||
extractDi.Flatten();
|
||||
}
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
if (fileCount > 1)
|
||||
{
|
||||
extraPath = file.Id + string.Empty;
|
||||
}
|
||||
if (fileCount > 1)
|
||||
{
|
||||
extraPath = file.Id + string.Empty;
|
||||
}
|
||||
|
||||
if (file.Format == MangaFormat.Archive)
|
||||
{
|
||||
_archiveService.ExtractArchive(file.FilePath, Path.Join(extractPath, extraPath));
|
||||
} else if (file.Format == MangaFormat.Pdf)
|
||||
{
|
||||
_bookService.ExtractPdfImages(file.FilePath, Path.Join(extractPath, extraPath));
|
||||
} else if (file.Format == MangaFormat.Epub)
|
||||
{
|
||||
removeNonImages = false;
|
||||
DirectoryService.ExistOrCreate(extractPath);
|
||||
_directoryService.CopyFileToDirectory(files[0].FilePath, extractPath);
|
||||
}
|
||||
if (file.Format == MangaFormat.Archive)
|
||||
{
|
||||
_archiveService.ExtractArchive(file.FilePath, Path.Join(extractPath, extraPath));
|
||||
}
|
||||
else if (file.Format == MangaFormat.Pdf)
|
||||
{
|
||||
_bookService.ExtractPdfImages(file.FilePath, Path.Join(extractPath, extraPath));
|
||||
}
|
||||
else if (file.Format == MangaFormat.Epub)
|
||||
{
|
||||
removeNonImages = false;
|
||||
DirectoryService.ExistOrCreate(extractPath);
|
||||
_directoryService.CopyFileToDirectory(files[0].FilePath, extractPath);
|
||||
}
|
||||
}
|
||||
|
||||
extractDi.Flatten();
|
||||
|
|
@ -119,9 +138,6 @@ namespace API.Services
|
|||
{
|
||||
extractDi.RemoveNonImages();
|
||||
}
|
||||
|
||||
|
||||
return chapter;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -173,7 +189,7 @@ namespace API.Services
|
|||
{
|
||||
// Calculate what chapter the page belongs to
|
||||
var pagesSoFar = 0;
|
||||
var chapterFiles = chapter.Files ?? await _unitOfWork.VolumeRepository.GetFilesForChapter(chapter.Id);
|
||||
var chapterFiles = chapter.Files ?? await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapter.Id);
|
||||
foreach (var mangaFile in chapterFiles)
|
||||
{
|
||||
if (page <= (mangaFile.Pages + pagesSoFar))
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ namespace API.Services
|
|||
private static readonly Regex ExcludeDirectories = new Regex(
|
||||
@"@eaDir|\.DS_Store",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
public static readonly string TempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp");
|
||||
public static readonly string LogDirectory = Path.Join(Directory.GetCurrentDirectory(), "logs");
|
||||
public static readonly string CacheDirectory = Path.Join(Directory.GetCurrentDirectory(), "cache");
|
||||
|
||||
public DirectoryService(ILogger<DirectoryService> logger)
|
||||
{
|
||||
|
|
@ -247,33 +250,40 @@ namespace API.Services
|
|||
}
|
||||
}
|
||||
|
||||
public bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath)
|
||||
/// <summary>
|
||||
/// Copies files to a destination directory. If the destination directory doesn't exist, this will create it.
|
||||
/// </summary>
|
||||
/// <param name="filePaths"></param>
|
||||
/// <param name="directoryPath"></param>
|
||||
/// <param name="prepend">An optional string to prepend to the target file's name</param>
|
||||
/// <returns></returns>
|
||||
public bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath, string prepend = "")
|
||||
{
|
||||
string currentFile = null;
|
||||
try
|
||||
{
|
||||
foreach (var file in filePaths)
|
||||
{
|
||||
currentFile = file;
|
||||
var fileInfo = new FileInfo(file);
|
||||
if (fileInfo.Exists)
|
||||
{
|
||||
fileInfo.CopyTo(Path.Join(directoryPath, fileInfo.Name));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Tried to copy {File} but it doesn't exist", file);
|
||||
}
|
||||
ExistOrCreate(directoryPath);
|
||||
string currentFile = null;
|
||||
try
|
||||
{
|
||||
foreach (var file in filePaths)
|
||||
{
|
||||
currentFile = file;
|
||||
var fileInfo = new FileInfo(file);
|
||||
if (fileInfo.Exists)
|
||||
{
|
||||
fileInfo.CopyTo(Path.Join(directoryPath, prepend + fileInfo.Name));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Tried to copy {File} but it doesn't exist", file);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unable to copy {File} to {DirectoryPath}", currentFile, directoryPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unable to copy {File} to {DirectoryPath}", currentFile, directoryPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public IEnumerable<string> ListDirectory(string rootPath)
|
||||
|
|
@ -404,5 +414,23 @@ namespace API.Services
|
|||
return fileCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to delete the files passed to it. Swallows exceptions.
|
||||
/// </summary>
|
||||
/// <param name="files">Full path of files to delete</param>
|
||||
public static void DeleteFiles(IEnumerable<string> files)
|
||||
{
|
||||
foreach (var file in files)
|
||||
{
|
||||
try
|
||||
{
|
||||
new FileInfo(file).Delete();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
/* Swallow exception */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue