mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-08-07 09:01:25 -04:00
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
@ -7,15 +7,15 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="5.0.5" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="5.0.8" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
|
||||||
<PackageReference Include="NSubstitute" Version="4.2.2" />
|
<PackageReference Include="NSubstitute" Version="4.2.2" />
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="coverlet.collector" Version="3.0.3">
|
<PackageReference Include="coverlet.collector" Version="3.1.0">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
@ -35,35 +35,35 @@
|
|||||||
<PackageReference Include="ExCSS" Version="4.1.0" />
|
<PackageReference Include="ExCSS" Version="4.1.0" />
|
||||||
<PackageReference Include="Flurl" Version="3.0.2" />
|
<PackageReference Include="Flurl" Version="3.0.2" />
|
||||||
<PackageReference Include="Flurl.Http" Version="3.2.0" />
|
<PackageReference Include="Flurl.Http" Version="3.2.0" />
|
||||||
<PackageReference Include="Hangfire" Version="1.7.20" />
|
<PackageReference Include="Hangfire" Version="1.7.24" />
|
||||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.20" />
|
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.24" />
|
||||||
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
||||||
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.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="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.4" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.8" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.4" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.8" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.4" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.8" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
<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>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.4" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.8" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
|
||||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.0.0" />
|
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.1.3" />
|
||||||
<PackageReference Include="NetVips" Version="2.0.1" />
|
<PackageReference Include="NetVips" Version="2.0.1" />
|
||||||
<PackageReference Include="NetVips.Native" Version="8.11.0" />
|
<PackageReference Include="NetVips.Native" Version="8.11.0" />
|
||||||
<PackageReference Include="NReco.Logging.File" Version="1.1.1" />
|
<PackageReference Include="NReco.Logging.File" Version="1.1.2" />
|
||||||
<PackageReference Include="Sentry.AspNetCore" Version="3.8.2" />
|
<PackageReference Include="Sentry.AspNetCore" Version="3.8.3" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.28.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>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</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.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" />
|
<PackageReference Include="VersOne.Epub" Version="3.0.3.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@ -77,22 +77,38 @@
|
|||||||
<None Remove="Hangfire-log.db" />
|
<None Remove="Hangfire-log.db" />
|
||||||
<None Remove="obj\**" />
|
<None Remove="obj\**" />
|
||||||
<None Remove="wwwroot\**" />
|
<None Remove="wwwroot\**" />
|
||||||
|
<None Remove="cache\**" />
|
||||||
|
<None Remove="backups\**" />
|
||||||
|
<None Remove="logs\**" />
|
||||||
|
<None Remove="temp\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Remove="Interfaces\IMetadataService.cs" />
|
<Compile Remove="Interfaces\IMetadataService.cs" />
|
||||||
<Compile Remove="obj\**" />
|
<Compile Remove="obj\**" />
|
||||||
<Compile Remove="wwwroot\**" />
|
<Compile Remove="wwwroot\**" />
|
||||||
|
<Compile Remove="cache\**" />
|
||||||
|
<Compile Remove="backups\**" />
|
||||||
|
<Compile Remove="logs\**" />
|
||||||
|
<Compile Remove="temp\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Remove="obj\**" />
|
<EmbeddedResource Remove="obj\**" />
|
||||||
<EmbeddedResource Remove="wwwroot\**" />
|
<EmbeddedResource Remove="wwwroot\**" />
|
||||||
|
<EmbeddedResource Remove="cache\**" />
|
||||||
|
<EmbeddedResource Remove="backups\**" />
|
||||||
|
<EmbeddedResource Remove="logs\**" />
|
||||||
|
<EmbeddedResource Remove="temp\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Remove="obj\**" />
|
<Content Remove="obj\**" />
|
||||||
<Content Remove="wwwroot\**" />
|
<Content Remove="wwwroot\**" />
|
||||||
|
<Content Remove="cache\**" />
|
||||||
|
<Content Remove="backups\**" />
|
||||||
|
<Content Remove="logs\**" />
|
||||||
|
<Content Remove="temp\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -3,7 +3,11 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using API.Comparators;
|
||||||
|
using API.DTOs;
|
||||||
|
using API.DTOs.Downloads;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Interfaces;
|
using API.Interfaces;
|
||||||
using API.Interfaces.Services;
|
using API.Interfaces.Services;
|
||||||
@ -21,12 +25,16 @@ namespace API.Controllers
|
|||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly IArchiveService _archiveService;
|
private readonly IArchiveService _archiveService;
|
||||||
private readonly IDirectoryService _directoryService;
|
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;
|
_unitOfWork = unitOfWork;
|
||||||
_archiveService = archiveService;
|
_archiveService = archiveService;
|
||||||
_directoryService = directoryService;
|
_directoryService = directoryService;
|
||||||
|
_cacheService = cacheService;
|
||||||
|
_numericComparer = new NumericComparer();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("volume-size")]
|
[HttpGet("volume-size")]
|
||||||
@ -39,7 +47,7 @@ namespace API.Controllers
|
|||||||
[HttpGet("chapter-size")]
|
[HttpGet("chapter-size")]
|
||||||
public async Task<ActionResult<long>> GetChapterSize(int chapterId)
|
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)));
|
return Ok(DirectoryService.GetTotalSize(files.Select(c => c.FilePath)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +104,7 @@ namespace API.Controllers
|
|||||||
[HttpGet("chapter")]
|
[HttpGet("chapter")]
|
||||||
public async Task<ActionResult> DownloadChapter(int chapterId)
|
public async Task<ActionResult> DownloadChapter(int chapterId)
|
||||||
{
|
{
|
||||||
var files = await _unitOfWork.VolumeRepository.GetFilesForChapter(chapterId);
|
var files = await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapterId);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (files.Count == 1)
|
if (files.Count == 1)
|
||||||
@ -132,5 +140,62 @@ namespace API.Controllers
|
|||||||
return BadRequest(ex.Message);
|
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 library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryForUserDto.Id);
|
||||||
|
|
||||||
var originalFolders = library.Folders.Select(x => x.Path);
|
var originalFolders = library.Folders.Select(x => x.Path).ToList();
|
||||||
var differenceBetweenFolders = originalFolders.Except(libraryForUserDto.Folders);
|
|
||||||
|
|
||||||
library.Name = libraryForUserDto.Name;
|
library.Name = libraryForUserDto.Name;
|
||||||
library.Folders = libraryForUserDto.Folders.Select(s => new FolderPath() {Path = s}).ToList();
|
library.Folders = libraryForUserDto.Folders.Select(s => new FolderPath() {Path = s}).ToList();
|
||||||
@ -212,9 +211,9 @@ namespace API.Controllers
|
|||||||
_unitOfWork.LibraryRepository.Update(library);
|
_unitOfWork.LibraryRepository.Update(library);
|
||||||
|
|
||||||
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was a critical issue updating the 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();
|
return Ok();
|
||||||
|
@ -50,15 +50,16 @@ namespace API.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("chapter-info")]
|
[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
|
// PERF: Write this in one DB call
|
||||||
var chapter = await _cacheService.Ensure(chapterId);
|
var chapter = await _cacheService.Ensure(chapterId);
|
||||||
if (chapter == null) return BadRequest("Could not find Chapter");
|
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");
|
if (volume == null) return BadRequest("Could not find Volume");
|
||||||
var (_, mangaFile) = await _cacheService.GetCachedPagePath(chapter, 0);
|
var mangaFile = (await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapterId)).First();
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
||||||
|
|
||||||
return Ok(new ChapterInfoDto()
|
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")]
|
[HttpPost("mark-read")]
|
||||||
public async Task<ActionResult> MarkRead(MarkReadDto markReadDto)
|
public async Task<ActionResult> MarkRead(MarkReadDto markReadDto)
|
||||||
@ -232,21 +210,45 @@ namespace API.Controllers
|
|||||||
return BadRequest("Could not save progress");
|
return BadRequest("Could not save progress");
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("bookmark")]
|
[HttpGet("get-progress")]
|
||||||
public async Task<ActionResult> Bookmark(BookmarkDto bookmarkDto)
|
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());
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
|
|
||||||
// Don't let user bookmark past total pages.
|
// Don't let user save past total pages.
|
||||||
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(bookmarkDto.ChapterId);
|
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(progressDto.ChapterId);
|
||||||
if (bookmarkDto.PageNum > chapter.Pages)
|
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
|
// 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>();
|
user.Progresses ??= new List<AppUserProgress>();
|
||||||
var userProgress =
|
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)
|
if (userProgress == null)
|
||||||
{
|
{
|
||||||
user.Progresses.Add(new AppUserProgress
|
user.Progresses.Add(new AppUserProgress
|
||||||
{
|
{
|
||||||
PagesRead = bookmarkDto.PageNum,
|
PagesRead = progressDto.PageNum,
|
||||||
VolumeId = bookmarkDto.VolumeId,
|
VolumeId = progressDto.VolumeId,
|
||||||
SeriesId = bookmarkDto.SeriesId,
|
SeriesId = progressDto.SeriesId,
|
||||||
ChapterId = bookmarkDto.ChapterId,
|
ChapterId = progressDto.ChapterId,
|
||||||
BookScrollId = bookmarkDto.BookScrollId,
|
BookScrollId = progressDto.BookScrollId,
|
||||||
LastModified = DateTime.Now
|
LastModified = DateTime.Now
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
userProgress.PagesRead = bookmarkDto.PageNum;
|
userProgress.PagesRead = progressDto.PageNum;
|
||||||
userProgress.SeriesId = bookmarkDto.SeriesId;
|
userProgress.SeriesId = progressDto.SeriesId;
|
||||||
userProgress.VolumeId = bookmarkDto.VolumeId;
|
userProgress.VolumeId = progressDto.VolumeId;
|
||||||
userProgress.BookScrollId = bookmarkDto.BookScrollId;
|
userProgress.BookScrollId = progressDto.BookScrollId;
|
||||||
userProgress.LastModified = DateTime.Now;
|
userProgress.LastModified = DateTime.Now;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,6 +295,139 @@ namespace API.Controllers
|
|||||||
return BadRequest("Could not save progress");
|
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>
|
/// <summary>
|
||||||
/// Returns the next logical chapter from the series.
|
/// Returns the next logical chapter from the series.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -2,14 +2,10 @@
|
|||||||
{
|
{
|
||||||
public class BookmarkDto
|
public class BookmarkDto
|
||||||
{
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int Page { get; set; }
|
||||||
public int VolumeId { get; set; }
|
public int VolumeId { get; set; }
|
||||||
public int ChapterId { get; set; }
|
|
||||||
public int PageNum { get; set; }
|
|
||||||
public int SeriesId { get; set; }
|
public int SeriesId { get; set; }
|
||||||
/// <summary>
|
public int ChapterId { get; set; }
|
||||||
/// 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; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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
|
namespace API.Data
|
||||||
{
|
{
|
||||||
public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
||||||
IdentityUserClaim<int>, AppUserRole, IdentityUserLogin<int>,
|
IdentityUserClaim<int>, AppUserRole, IdentityUserLogin<int>,
|
||||||
IdentityRoleClaim<int>, IdentityUserToken<int>>
|
IdentityRoleClaim<int>, IdentityUserToken<int>>
|
||||||
{
|
{
|
||||||
@ -17,10 +17,10 @@ namespace API.Data
|
|||||||
ChangeTracker.Tracked += OnEntityTracked;
|
ChangeTracker.Tracked += OnEntityTracked;
|
||||||
ChangeTracker.StateChanged += OnEntityStateChanged;
|
ChangeTracker.StateChanged += OnEntityStateChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DbSet<Library> Library { get; set; }
|
public DbSet<Library> Library { get; set; }
|
||||||
public DbSet<Series> Series { get; set; }
|
public DbSet<Series> Series { get; set; }
|
||||||
|
|
||||||
public DbSet<Chapter> Chapter { get; set; }
|
public DbSet<Chapter> Chapter { get; set; }
|
||||||
public DbSet<Volume> Volume { get; set; }
|
public DbSet<Volume> Volume { get; set; }
|
||||||
public DbSet<AppUser> AppUser { get; set; }
|
public DbSet<AppUser> AppUser { get; set; }
|
||||||
@ -31,18 +31,19 @@ namespace API.Data
|
|||||||
public DbSet<AppUserPreferences> AppUserPreferences { get; set; }
|
public DbSet<AppUserPreferences> AppUserPreferences { get; set; }
|
||||||
public DbSet<SeriesMetadata> SeriesMetadata { get; set; }
|
public DbSet<SeriesMetadata> SeriesMetadata { get; set; }
|
||||||
public DbSet<CollectionTag> CollectionTag { get; set; }
|
public DbSet<CollectionTag> CollectionTag { get; set; }
|
||||||
|
public DbSet<AppUserBookmark> AppUserBookmark { get; set; }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(builder);
|
base.OnModelCreating(builder);
|
||||||
|
|
||||||
|
|
||||||
builder.Entity<AppUser>()
|
builder.Entity<AppUser>()
|
||||||
.HasMany(ur => ur.UserRoles)
|
.HasMany(ur => ur.UserRoles)
|
||||||
.WithOne(u => u.User)
|
.WithOne(u => u.User)
|
||||||
.HasForeignKey(ur => ur.UserId)
|
.HasForeignKey(ur => ur.UserId)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
builder.Entity<AppRole>()
|
builder.Entity<AppRole>()
|
||||||
.HasMany(ur => ur.UserRoles)
|
.HasMany(ur => ur.UserRoles)
|
||||||
.WithOne(u => u.Role)
|
.WithOne(u => u.Role)
|
||||||
@ -50,7 +51,7 @@ namespace API.Data
|
|||||||
.IsRequired();
|
.IsRequired();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void OnEntityTracked(object sender, EntityTrackedEventArgs e)
|
void OnEntityTracked(object sender, EntityTrackedEventArgs e)
|
||||||
{
|
{
|
||||||
if (!e.FromQuery && e.Entry.State == EntityState.Added && e.Entry.Entity is IEntityDate entity)
|
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.Created = DateTime.Now;
|
||||||
entity.LastModified = DateTime.Now;
|
entity.LastModified = DateTime.Now;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnEntityStateChanged(object sender, EntityStateChangedEventArgs e)
|
void OnEntityStateChanged(object sender, EntityStateChangedEventArgs e)
|
||||||
@ -67,4 +68,4 @@ namespace API.Data
|
|||||||
entity.LastModified = DateTime.Now;
|
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
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "5.0.4");
|
.HasAnnotation("ProductVersion", "5.0.8");
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||||
{
|
{
|
||||||
@ -118,6 +118,34 @@ namespace API.Data.Migrations
|
|||||||
b.ToTable("AspNetUsers");
|
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 =>
|
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@ -641,6 +669,17 @@ namespace API.Data.Migrations
|
|||||||
b.ToTable("AspNetUserTokens");
|
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 =>
|
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||||
@ -832,6 +871,8 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("API.Entities.AppUser", b =>
|
modelBuilder.Entity("API.Entities.AppUser", b =>
|
||||||
{
|
{
|
||||||
|
b.Navigation("Bookmarks");
|
||||||
|
|
||||||
b.Navigation("Progresses");
|
b.Navigation("Progresses");
|
||||||
|
|
||||||
b.Navigation("Ratings");
|
b.Navigation("Ratings");
|
||||||
|
@ -20,7 +20,7 @@ namespace API.Data
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ISeriesRepository SeriesRepository => new SeriesRepository(_context, _mapper);
|
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 ILibraryRepository LibraryRepository => new LibraryRepository(_context, _mapper);
|
||||||
|
|
||||||
public IVolumeRepository VolumeRepository => new VolumeRepository(_context, _mapper);
|
public IVolumeRepository VolumeRepository => new VolumeRepository(_context, _mapper);
|
||||||
@ -56,4 +56,4 @@ namespace API.Data
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@ using API.Constants;
|
|||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Interfaces;
|
using API.Interfaces;
|
||||||
|
using AutoMapper;
|
||||||
|
using AutoMapper.QueryableExtensions;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
@ -14,18 +16,20 @@ namespace API.Data
|
|||||||
{
|
{
|
||||||
private readonly DataContext _context;
|
private readonly DataContext _context;
|
||||||
private readonly UserManager<AppUser> _userManager;
|
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;
|
_context = context;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
|
_mapper = mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Update(AppUser user)
|
public void Update(AppUser user)
|
||||||
{
|
{
|
||||||
_context.Entry(user).State = EntityState.Modified;
|
_context.Entry(user).State = EntityState.Modified;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Update(AppUserPreferences preferences)
|
public void Update(AppUserPreferences preferences)
|
||||||
{
|
{
|
||||||
_context.Entry(preferences).State = EntityState.Modified;
|
_context.Entry(preferences).State = EntityState.Modified;
|
||||||
@ -45,6 +49,7 @@ namespace API.Data
|
|||||||
{
|
{
|
||||||
return await _context.Users
|
return await _context.Users
|
||||||
.Include(u => u.Progresses)
|
.Include(u => u.Progresses)
|
||||||
|
.Include(u => u.Bookmarks)
|
||||||
.SingleOrDefaultAsync(x => x.UserName == username);
|
.SingleOrDefaultAsync(x => x.UserName == username);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,6 +76,36 @@ namespace API.Data
|
|||||||
.SingleOrDefaultAsync(p => p.AppUser.UserName == username);
|
.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()
|
public async Task<IEnumerable<MemberDto>> GetMembersAsync()
|
||||||
{
|
{
|
||||||
return await _context.Users
|
return await _context.Users
|
||||||
@ -97,4 +132,4 @@ namespace API.Data
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
|
using API.DTOs.Reader;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Interfaces;
|
using API.Interfaces;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
@ -38,7 +40,6 @@ namespace API.Data
|
|||||||
.SingleOrDefaultAsync(c => c.Id == chapterId);
|
.SingleOrDefaultAsync(c => c.Id == chapterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns Chapters for a volume id.
|
/// Returns Chapters for a volume id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -79,13 +80,30 @@ namespace API.Data
|
|||||||
return chapter;
|
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
|
return await _context.MangaFile
|
||||||
.Where(c => chapterId == c.ChapterId)
|
.Where(c => chapterId == c.ChapterId)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.ToListAsync();
|
.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)
|
public async Task<IList<MangaFile>> GetFilesForVolume(int volumeId)
|
||||||
{
|
{
|
||||||
|
@ -16,7 +16,8 @@ namespace API.Entities
|
|||||||
public ICollection<AppUserProgress> Progresses { get; set; }
|
public ICollection<AppUserProgress> Progresses { get; set; }
|
||||||
public ICollection<AppUserRating> Ratings { get; set; }
|
public ICollection<AppUserRating> Ratings { get; set; }
|
||||||
public AppUserPreferences UserPreferences { get; set; }
|
public AppUserPreferences UserPreferences { get; set; }
|
||||||
|
public ICollection<AppUserBookmark> Bookmarks { get; set; }
|
||||||
|
|
||||||
[ConcurrencyCheck]
|
[ConcurrencyCheck]
|
||||||
public uint RowVersion { get; set; }
|
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<Volume, VolumeDto>();
|
||||||
|
|
||||||
CreateMap<MangaFile, MangaFileDto>();
|
CreateMap<MangaFile, MangaFileDto>();
|
||||||
|
|
||||||
CreateMap<Chapter, ChapterDto>();
|
CreateMap<Chapter, ChapterDto>();
|
||||||
|
|
||||||
CreateMap<Series, SeriesDto>();
|
CreateMap<Series, SeriesDto>();
|
||||||
|
|
||||||
CreateMap<CollectionTag, CollectionTagDto>();
|
CreateMap<CollectionTag, CollectionTagDto>();
|
||||||
|
|
||||||
CreateMap<SeriesMetadata, SeriesMetadataDto>();
|
CreateMap<SeriesMetadata, SeriesMetadataDto>();
|
||||||
@ -29,18 +29,20 @@ namespace API.Helpers
|
|||||||
|
|
||||||
CreateMap<AppUserPreferences, UserPreferencesDto>();
|
CreateMap<AppUserPreferences, UserPreferencesDto>();
|
||||||
|
|
||||||
|
CreateMap<AppUserBookmark, BookmarkDto>();
|
||||||
|
|
||||||
CreateMap<Series, SearchResultDto>()
|
CreateMap<Series, SearchResultDto>()
|
||||||
.ForMember(dest => dest.SeriesId,
|
.ForMember(dest => dest.SeriesId,
|
||||||
opt => opt.MapFrom(src => src.Id))
|
opt => opt.MapFrom(src => src.Id))
|
||||||
.ForMember(dest => dest.LibraryName,
|
.ForMember(dest => dest.LibraryName,
|
||||||
opt => opt.MapFrom(src => src.Library.Name));
|
opt => opt.MapFrom(src => src.Library.Name));
|
||||||
|
|
||||||
|
|
||||||
CreateMap<Library, LibraryDto>()
|
CreateMap<Library, LibraryDto>()
|
||||||
.ForMember(dest => dest.Folders,
|
.ForMember(dest => dest.Folders,
|
||||||
opt =>
|
opt =>
|
||||||
opt.MapFrom(src => src.Folders.Select(x => x.Path).ToList()));
|
opt.MapFrom(src => src.Folders.Select(x => x.Path).ToList()));
|
||||||
|
|
||||||
CreateMap<AppUser, MemberDto>()
|
CreateMap<AppUser, MemberDto>()
|
||||||
.AfterMap((ps, pst, context) => context.Mapper.Map(ps.Libraries, pst.Libraries));
|
.AfterMap((ps, pst, context) => context.Mapper.Map(ps.Libraries, pst.Libraries));
|
||||||
|
|
||||||
@ -50,4 +52,4 @@ namespace API.Helpers
|
|||||||
.ConvertUsing<ServerSettingConverter>();
|
.ConvertUsing<ServerSettingConverter>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,5 +16,8 @@ namespace API.Interfaces
|
|||||||
Task<AppUserRating> GetUserRating(int seriesId, int userId);
|
Task<AppUserRating> GetUserRating(int seriesId, int userId);
|
||||||
void AddRatingTracking(AppUserRating userRating);
|
void AddRatingTracking(AppUserRating userRating);
|
||||||
Task<AppUserPreferences> GetPreferencesAsync(string username);
|
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);
|
void Update(Volume volume);
|
||||||
Task<Chapter> GetChapterAsync(int chapterId);
|
Task<Chapter> GetChapterAsync(int chapterId);
|
||||||
Task<ChapterDto> GetChapterDtoAsync(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<IList<Chapter>> GetChaptersAsync(int volumeId);
|
||||||
Task<byte[]> GetChapterCoverImageAsync(int chapterId);
|
Task<byte[]> GetChapterCoverImageAsync(int chapterId);
|
||||||
Task<IList<MangaFile>> GetFilesForVolume(int volumeId);
|
Task<IList<MangaFile>> GetFilesForVolume(int volumeId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,5 +36,6 @@ namespace API.Interfaces.Services
|
|||||||
|
|
||||||
void EnsureCacheDirectory();
|
void EnsureCacheDirectory();
|
||||||
string GetCachedEpubFile(int chapterId, Chapter chapter);
|
string GetCachedEpubFile(int chapterId, Chapter chapter);
|
||||||
|
public void ExtractChapterFiles(string extractPath, IReadOnlyList<MangaFile> files);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ namespace API.Interfaces.Services
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
string[] GetFilesWithExtension(string path, string searchPatternExpression = "");
|
string[] GetFilesWithExtension(string path, string searchPatternExpression = "");
|
||||||
Task<byte[]> ReadFileAsync(string path);
|
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);
|
bool Exists(string directory);
|
||||||
|
|
||||||
IEnumerable<string> GetFiles(string path, string searchPatternExpression = "",
|
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)
|
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 dateString = DateTime.Now.ToShortDateString().Replace("/", "_");
|
||||||
|
|
||||||
var tempLocation = Path.Join(tempDirectory, $"{tempFolder}_{dateString}");
|
var tempLocation = Path.Join(DirectoryService.TempDirectory, $"{tempFolder}_{dateString}");
|
||||||
DirectoryService.ExistOrCreate(tempLocation);
|
DirectoryService.ExistOrCreate(tempLocation);
|
||||||
if (!_directoryService.CopyFilesToDirectory(files, tempLocation))
|
if (!_directoryService.CopyFilesToDirectory(files, tempLocation))
|
||||||
{
|
{
|
||||||
throw new KavitaException("Unable to copy files to temp directory archive download.");
|
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
|
try
|
||||||
{
|
{
|
||||||
ZipFile.CreateFromDirectory(tempLocation, zipPath);
|
ZipFile.CreateFromDirectory(tempLocation, zipPath);
|
||||||
|
@ -37,7 +37,7 @@ namespace API.Services
|
|||||||
|
|
||||||
public void EnsureCacheDirectory()
|
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);
|
_logger.LogError("Cache directory {CacheDirectory} is not accessible or does not exist. Creating...", CacheDirectory);
|
||||||
}
|
}
|
||||||
@ -60,58 +60,77 @@ namespace API.Services
|
|||||||
return path;
|
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)
|
public async Task<Chapter> Ensure(int chapterId)
|
||||||
{
|
{
|
||||||
EnsureCacheDirectory();
|
EnsureCacheDirectory();
|
||||||
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(chapterId);
|
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(chapterId);
|
||||||
var files = chapter.Files.ToList();
|
|
||||||
var fileCount = files.Count;
|
|
||||||
var extractPath = GetCachePath(chapterId);
|
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);
|
var extractDi = new DirectoryInfo(extractPath);
|
||||||
|
|
||||||
if (files.Count > 0 && files[0].Format == MangaFormat.Image)
|
if (files.Count > 0 && files[0].Format == MangaFormat.Image)
|
||||||
{
|
{
|
||||||
DirectoryService.ExistOrCreate(extractPath);
|
DirectoryService.ExistOrCreate(extractPath);
|
||||||
if (files.Count == 1)
|
if (files.Count == 1)
|
||||||
{
|
{
|
||||||
_directoryService.CopyFileToDirectory(files[0].FilePath, extractPath);
|
_directoryService.CopyFileToDirectory(files[0].FilePath, extractPath);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_directoryService.CopyDirectoryToDirectory(Path.GetDirectoryName(files[0].FilePath), extractPath, Parser.Parser.ImageFileExtensions);
|
_directoryService.CopyDirectoryToDirectory(Path.GetDirectoryName(files[0].FilePath), extractPath,
|
||||||
}
|
Parser.Parser.ImageFileExtensions);
|
||||||
|
}
|
||||||
|
|
||||||
extractDi.Flatten();
|
extractDi.Flatten();
|
||||||
return chapter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var file in files)
|
foreach (var file in files)
|
||||||
{
|
{
|
||||||
if (fileCount > 1)
|
if (fileCount > 1)
|
||||||
{
|
{
|
||||||
extraPath = file.Id + string.Empty;
|
extraPath = file.Id + string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.Format == MangaFormat.Archive)
|
if (file.Format == MangaFormat.Archive)
|
||||||
{
|
{
|
||||||
_archiveService.ExtractArchive(file.FilePath, Path.Join(extractPath, extraPath));
|
_archiveService.ExtractArchive(file.FilePath, Path.Join(extractPath, extraPath));
|
||||||
} else if (file.Format == MangaFormat.Pdf)
|
}
|
||||||
{
|
else if (file.Format == MangaFormat.Pdf)
|
||||||
_bookService.ExtractPdfImages(file.FilePath, Path.Join(extractPath, extraPath));
|
{
|
||||||
} else if (file.Format == MangaFormat.Epub)
|
_bookService.ExtractPdfImages(file.FilePath, Path.Join(extractPath, extraPath));
|
||||||
{
|
}
|
||||||
removeNonImages = false;
|
else if (file.Format == MangaFormat.Epub)
|
||||||
DirectoryService.ExistOrCreate(extractPath);
|
{
|
||||||
_directoryService.CopyFileToDirectory(files[0].FilePath, extractPath);
|
removeNonImages = false;
|
||||||
}
|
DirectoryService.ExistOrCreate(extractPath);
|
||||||
|
_directoryService.CopyFileToDirectory(files[0].FilePath, extractPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extractDi.Flatten();
|
extractDi.Flatten();
|
||||||
@ -119,9 +138,6 @@ namespace API.Services
|
|||||||
{
|
{
|
||||||
extractDi.RemoveNonImages();
|
extractDi.RemoveNonImages();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return chapter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -173,7 +189,7 @@ namespace API.Services
|
|||||||
{
|
{
|
||||||
// Calculate what chapter the page belongs to
|
// Calculate what chapter the page belongs to
|
||||||
var pagesSoFar = 0;
|
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)
|
foreach (var mangaFile in chapterFiles)
|
||||||
{
|
{
|
||||||
if (page <= (mangaFile.Pages + pagesSoFar))
|
if (page <= (mangaFile.Pages + pagesSoFar))
|
||||||
|
@ -16,6 +16,9 @@ namespace API.Services
|
|||||||
private static readonly Regex ExcludeDirectories = new Regex(
|
private static readonly Regex ExcludeDirectories = new Regex(
|
||||||
@"@eaDir|\.DS_Store",
|
@"@eaDir|\.DS_Store",
|
||||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
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)
|
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;
|
ExistOrCreate(directoryPath);
|
||||||
try
|
string currentFile = null;
|
||||||
{
|
try
|
||||||
foreach (var file in filePaths)
|
{
|
||||||
{
|
foreach (var file in filePaths)
|
||||||
currentFile = file;
|
{
|
||||||
var fileInfo = new FileInfo(file);
|
currentFile = file;
|
||||||
if (fileInfo.Exists)
|
var fileInfo = new FileInfo(file);
|
||||||
{
|
if (fileInfo.Exists)
|
||||||
fileInfo.CopyTo(Path.Join(directoryPath, fileInfo.Name));
|
{
|
||||||
}
|
fileInfo.CopyTo(Path.Join(directoryPath, prepend + fileInfo.Name));
|
||||||
else
|
}
|
||||||
{
|
else
|
||||||
_logger.LogWarning("Tried to copy {File} but it doesn't exist", file);
|
{
|
||||||
}
|
_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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
return true;
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Unable to copy {File} to {DirectoryPath}", currentFile, directoryPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<string> ListDirectory(string rootPath)
|
public IEnumerable<string> ListDirectory(string rootPath)
|
||||||
@ -404,5 +414,23 @@ namespace API.Services
|
|||||||
return fileCount;
|
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 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,8 +11,8 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
|
||||||
<PackageReference Include="Sentry" Version="3.8.2" />
|
<PackageReference Include="Sentry" Version="3.8.3" />
|
||||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.26.0.34506">
|
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.27.0.35380">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" id="modal-basic-title">{{title}} Bookmarks</h4>
|
||||||
|
<button type="button" class="close" aria-label="Close" (click)="close()">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li class="list-group-item">
|
||||||
|
There are {{bookmarks.length}} pages bookmarked over {{uniqueChapters}} files.
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item" *ngIf="bookmarks.length === 0">
|
||||||
|
No bookmarks yet
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" (click)="clearBookmarks()" [disabled]="(isDownloading || isClearing) && bookmarks.length > 0">
|
||||||
|
<span *ngIf="isClearing" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||||
|
<span>Clear{{isClearing ? 'ing...' : ''}}</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" (click)="downloadBookmarks()" [disabled]="(isDownloading || isClearing) && bookmarks.length > 0">
|
||||||
|
<span *ngIf="isDownloading" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||||
|
<span>Download{{isDownloading ? 'ing...' : ''}}</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary" (click)="close()">Close</button>
|
||||||
|
</div>
|
@ -0,0 +1,70 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { ToastrService } from 'ngx-toastr';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { DownloadService } from 'src/app/shared/_services/download.service';
|
||||||
|
import { PageBookmark } from 'src/app/_models/page-bookmark';
|
||||||
|
import { Series } from 'src/app/_models/series';
|
||||||
|
import { ImageService } from 'src/app/_services/image.service';
|
||||||
|
import { ReaderService } from 'src/app/_services/reader.service';
|
||||||
|
import { SeriesService } from 'src/app/_services/series.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-bookmarks-modal',
|
||||||
|
templateUrl: './bookmarks-modal.component.html',
|
||||||
|
styleUrls: ['./bookmarks-modal.component.scss']
|
||||||
|
})
|
||||||
|
export class BookmarksModalComponent implements OnInit {
|
||||||
|
|
||||||
|
@Input() series!: Series;
|
||||||
|
|
||||||
|
bookmarks: Array<PageBookmark> = [];
|
||||||
|
title: string = '';
|
||||||
|
subtitle: string = '';
|
||||||
|
isDownloading: boolean = false;
|
||||||
|
isClearing: boolean = false;
|
||||||
|
|
||||||
|
uniqueChapters: number = 0;
|
||||||
|
|
||||||
|
constructor(public imageService: ImageService, private readerService: ReaderService,
|
||||||
|
public modal: NgbActiveModal, private downloadService: DownloadService,
|
||||||
|
private toastr: ToastrService, private seriesService: SeriesService) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.readerService.getBookmarksForSeries(this.series.id).pipe(take(1)).subscribe(bookmarks => {
|
||||||
|
this.bookmarks = bookmarks;
|
||||||
|
const chapters: {[id: number]: string} = {};
|
||||||
|
this.bookmarks.forEach(bmk => {
|
||||||
|
if (!chapters.hasOwnProperty(bmk.chapterId)) {
|
||||||
|
chapters[bmk.chapterId] = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.uniqueChapters = Object.keys(chapters).length;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.modal.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadBookmarks() {
|
||||||
|
this.isDownloading = true;
|
||||||
|
this.downloadService.downloadBookmarks(this.bookmarks, this.series.name).pipe(take(1)).subscribe(() => {
|
||||||
|
this.isDownloading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clearBookmarks() {
|
||||||
|
this.isClearing = true;
|
||||||
|
this.readerService.clearBookmarks(this.series.id).subscribe(() => {
|
||||||
|
this.isClearing = false;
|
||||||
|
this.init();
|
||||||
|
this.toastr.success(this.series.name + '\'s bookmarks have been removed');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
7
UI/Web/src/app/_models/page-bookmark.ts
Normal file
7
UI/Web/src/app/_models/page-bookmark.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface PageBookmark {
|
||||||
|
id: number;
|
||||||
|
page: number;
|
||||||
|
seriesId: number;
|
||||||
|
volumeId: number;
|
||||||
|
chapterId: number;
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
export interface Bookmark {
|
export interface ProgressBookmark {
|
||||||
pageNum: number;
|
pageNum: number;
|
||||||
chapterId: number;
|
chapterId: number;
|
||||||
bookScrollId?: string;
|
bookScrollId?: string;
|
@ -14,14 +14,15 @@ export enum Action {
|
|||||||
Edit = 4,
|
Edit = 4,
|
||||||
Info = 5,
|
Info = 5,
|
||||||
RefreshMetadata = 6,
|
RefreshMetadata = 6,
|
||||||
Download = 7
|
Download = 7,
|
||||||
|
Bookmarks = 8
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActionItem<T> {
|
export interface ActionItem<T> {
|
||||||
title: string;
|
title: string;
|
||||||
action: Action;
|
action: Action;
|
||||||
callback: (action: Action, data: T) => void;
|
callback: (action: Action, data: T) => void;
|
||||||
|
requiresAdmin: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@ -58,43 +59,50 @@ export class ActionFactoryService {
|
|||||||
this.collectionTagActions.push({
|
this.collectionTagActions.push({
|
||||||
action: Action.Edit,
|
action: Action.Edit,
|
||||||
title: 'Edit',
|
title: 'Edit',
|
||||||
callback: this.dummyCallback
|
callback: this.dummyCallback,
|
||||||
|
requiresAdmin: true
|
||||||
});
|
});
|
||||||
|
|
||||||
this.seriesActions.push({
|
this.seriesActions.push({
|
||||||
action: Action.ScanLibrary,
|
action: Action.ScanLibrary,
|
||||||
title: 'Scan Series',
|
title: 'Scan Series',
|
||||||
callback: this.dummyCallback
|
callback: this.dummyCallback,
|
||||||
|
requiresAdmin: true
|
||||||
});
|
});
|
||||||
|
|
||||||
this.seriesActions.push({
|
this.seriesActions.push({
|
||||||
action: Action.RefreshMetadata,
|
action: Action.RefreshMetadata,
|
||||||
title: 'Refresh Metadata',
|
title: 'Refresh Metadata',
|
||||||
callback: this.dummyCallback
|
callback: this.dummyCallback,
|
||||||
|
requiresAdmin: true
|
||||||
});
|
});
|
||||||
|
|
||||||
this.seriesActions.push({
|
this.seriesActions.push({
|
||||||
action: Action.Delete,
|
action: Action.Delete,
|
||||||
title: 'Delete',
|
title: 'Delete',
|
||||||
callback: this.dummyCallback
|
callback: this.dummyCallback,
|
||||||
|
requiresAdmin: true
|
||||||
});
|
});
|
||||||
|
|
||||||
this.seriesActions.push({
|
this.seriesActions.push({
|
||||||
action: Action.Edit,
|
action: Action.Edit,
|
||||||
title: 'Edit',
|
title: 'Edit',
|
||||||
callback: this.dummyCallback
|
callback: this.dummyCallback,
|
||||||
|
requiresAdmin: true
|
||||||
});
|
});
|
||||||
|
|
||||||
this.libraryActions.push({
|
this.libraryActions.push({
|
||||||
action: Action.ScanLibrary,
|
action: Action.ScanLibrary,
|
||||||
title: 'Scan Library',
|
title: 'Scan Library',
|
||||||
callback: this.dummyCallback
|
callback: this.dummyCallback,
|
||||||
|
requiresAdmin: true
|
||||||
});
|
});
|
||||||
|
|
||||||
this.libraryActions.push({
|
this.libraryActions.push({
|
||||||
action: Action.RefreshMetadata,
|
action: Action.RefreshMetadata,
|
||||||
title: 'Refresh Metadata',
|
title: 'Refresh Metadata',
|
||||||
callback: this.dummyCallback
|
callback: this.dummyCallback,
|
||||||
|
requiresAdmin: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,13 +110,15 @@ export class ActionFactoryService {
|
|||||||
this.volumeActions.push({
|
this.volumeActions.push({
|
||||||
action: Action.Download,
|
action: Action.Download,
|
||||||
title: 'Download',
|
title: 'Download',
|
||||||
callback: this.dummyCallback
|
callback: this.dummyCallback,
|
||||||
|
requiresAdmin: false
|
||||||
});
|
});
|
||||||
|
|
||||||
this.chapterActions.push({
|
this.chapterActions.push({
|
||||||
action: Action.Download,
|
action: Action.Download,
|
||||||
title: 'Download',
|
title: 'Download',
|
||||||
callback: this.dummyCallback
|
callback: this.dummyCallback,
|
||||||
|
requiresAdmin: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -150,12 +160,20 @@ export class ActionFactoryService {
|
|||||||
{
|
{
|
||||||
action: Action.MarkAsRead,
|
action: Action.MarkAsRead,
|
||||||
title: 'Mark as Read',
|
title: 'Mark as Read',
|
||||||
callback: this.dummyCallback
|
callback: this.dummyCallback,
|
||||||
|
requiresAdmin: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: Action.MarkAsUnread,
|
action: Action.MarkAsUnread,
|
||||||
title: 'Mark as Unread',
|
title: 'Mark as Unread',
|
||||||
callback: this.dummyCallback
|
callback: this.dummyCallback,
|
||||||
|
requiresAdmin: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: Action.Bookmarks,
|
||||||
|
title: 'Bookmarks',
|
||||||
|
callback: this.dummyCallback,
|
||||||
|
requiresAdmin: false
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -163,12 +181,14 @@ export class ActionFactoryService {
|
|||||||
{
|
{
|
||||||
action: Action.MarkAsRead,
|
action: Action.MarkAsRead,
|
||||||
title: 'Mark as Read',
|
title: 'Mark as Read',
|
||||||
callback: this.dummyCallback
|
callback: this.dummyCallback,
|
||||||
|
requiresAdmin: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: Action.MarkAsUnread,
|
action: Action.MarkAsUnread,
|
||||||
title: 'Mark as Unread',
|
title: 'Mark as Unread',
|
||||||
callback: this.dummyCallback
|
callback: this.dummyCallback,
|
||||||
|
requiresAdmin: false
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -176,25 +196,29 @@ export class ActionFactoryService {
|
|||||||
{
|
{
|
||||||
action: Action.MarkAsRead,
|
action: Action.MarkAsRead,
|
||||||
title: 'Mark as Read',
|
title: 'Mark as Read',
|
||||||
callback: this.dummyCallback
|
callback: this.dummyCallback,
|
||||||
|
requiresAdmin: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: Action.MarkAsUnread,
|
action: Action.MarkAsUnread,
|
||||||
title: 'Mark as Unread',
|
title: 'Mark as Unread',
|
||||||
callback: this.dummyCallback
|
callback: this.dummyCallback,
|
||||||
|
requiresAdmin: false
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
this.volumeActions.push({
|
this.volumeActions.push({
|
||||||
action: Action.Info,
|
action: Action.Info,
|
||||||
title: 'Info',
|
title: 'Info',
|
||||||
callback: this.dummyCallback
|
callback: this.dummyCallback,
|
||||||
|
requiresAdmin: false
|
||||||
});
|
});
|
||||||
|
|
||||||
this.chapterActions.push({
|
this.chapterActions.push({
|
||||||
action: Action.Info,
|
action: Action.Info,
|
||||||
title: 'Info',
|
title: 'Info',
|
||||||
callback: this.dummyCallback
|
callback: this.dummyCallback,
|
||||||
|
requiresAdmin: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { Injectable, OnDestroy } from '@angular/core';
|
import { Injectable, OnDestroy } from '@angular/core';
|
||||||
|
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { forkJoin, Subject } from 'rxjs';
|
import { forkJoin, Subject } from 'rxjs';
|
||||||
import { take, takeUntil } from 'rxjs/operators';
|
import { take, takeUntil } from 'rxjs/operators';
|
||||||
|
import { BookmarksModalComponent } from '../_modals/bookmarks-modal/bookmarks-modal.component';
|
||||||
import { Chapter } from '../_models/chapter';
|
import { Chapter } from '../_models/chapter';
|
||||||
import { Library } from '../_models/library';
|
import { Library } from '../_models/library';
|
||||||
import { Series } from '../_models/series';
|
import { Series } from '../_models/series';
|
||||||
@ -24,9 +26,10 @@ export type ChapterActionCallback = (chapter: Chapter) => void;
|
|||||||
export class ActionService implements OnDestroy {
|
export class ActionService implements OnDestroy {
|
||||||
|
|
||||||
private readonly onDestroy = new Subject<void>();
|
private readonly onDestroy = new Subject<void>();
|
||||||
|
private bookmarkModalRef: NgbModalRef | null = null;
|
||||||
|
|
||||||
constructor(private libraryService: LibraryService, private seriesService: SeriesService,
|
constructor(private libraryService: LibraryService, private seriesService: SeriesService,
|
||||||
private readerService: ReaderService, private toastr: ToastrService) { }
|
private readerService: ReaderService, private toastr: ToastrService, private modalService: NgbModal) { }
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.onDestroy.next();
|
this.onDestroy.next();
|
||||||
@ -153,7 +156,7 @@ export class ActionService implements OnDestroy {
|
|||||||
* @param callback Optional callback to perform actions after API completes
|
* @param callback Optional callback to perform actions after API completes
|
||||||
*/
|
*/
|
||||||
markVolumeAsUnread(seriesId: number, volume: Volume, callback?: VolumeActionCallback) {
|
markVolumeAsUnread(seriesId: number, volume: Volume, callback?: VolumeActionCallback) {
|
||||||
forkJoin(volume.chapters?.map(chapter => this.readerService.bookmark(seriesId, volume.id, chapter.id, 0))).pipe(takeUntil(this.onDestroy)).subscribe(results => {
|
forkJoin(volume.chapters?.map(chapter => this.readerService.saveProgress(seriesId, volume.id, chapter.id, 0))).pipe(takeUntil(this.onDestroy)).subscribe(results => {
|
||||||
volume.pagesRead = 0;
|
volume.pagesRead = 0;
|
||||||
volume.chapters?.forEach(c => c.pagesRead = 0);
|
volume.chapters?.forEach(c => c.pagesRead = 0);
|
||||||
this.toastr.success('Marked as Unread');
|
this.toastr.success('Marked as Unread');
|
||||||
@ -170,7 +173,7 @@ export class ActionService implements OnDestroy {
|
|||||||
* @param callback Optional callback to perform actions after API completes
|
* @param callback Optional callback to perform actions after API completes
|
||||||
*/
|
*/
|
||||||
markChapterAsRead(seriesId: number, chapter: Chapter, callback?: ChapterActionCallback) {
|
markChapterAsRead(seriesId: number, chapter: Chapter, callback?: ChapterActionCallback) {
|
||||||
this.readerService.bookmark(seriesId, chapter.volumeId, chapter.id, chapter.pages).pipe(take(1)).subscribe(results => {
|
this.readerService.saveProgress(seriesId, chapter.volumeId, chapter.id, chapter.pages).pipe(take(1)).subscribe(results => {
|
||||||
chapter.pagesRead = chapter.pages;
|
chapter.pagesRead = chapter.pages;
|
||||||
this.toastr.success('Marked as Read');
|
this.toastr.success('Marked as Read');
|
||||||
if (callback) {
|
if (callback) {
|
||||||
@ -186,7 +189,7 @@ export class ActionService implements OnDestroy {
|
|||||||
* @param callback Optional callback to perform actions after API completes
|
* @param callback Optional callback to perform actions after API completes
|
||||||
*/
|
*/
|
||||||
markChapterAsUnread(seriesId: number, chapter: Chapter, callback?: ChapterActionCallback) {
|
markChapterAsUnread(seriesId: number, chapter: Chapter, callback?: ChapterActionCallback) {
|
||||||
this.readerService.bookmark(seriesId, chapter.volumeId, chapter.id, chapter.pages).pipe(take(1)).subscribe(results => {
|
this.readerService.saveProgress(seriesId, chapter.volumeId, chapter.id, chapter.pages).pipe(take(1)).subscribe(results => {
|
||||||
chapter.pagesRead = 0;
|
chapter.pagesRead = 0;
|
||||||
this.toastr.success('Marked as unread');
|
this.toastr.success('Marked as unread');
|
||||||
if (callback) {
|
if (callback) {
|
||||||
@ -195,5 +198,23 @@ export class ActionService implements OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
openBookmarkModal(series: Series, callback?: SeriesActionCallback) {
|
||||||
|
if (this.bookmarkModalRef != null) { return; }
|
||||||
|
this.bookmarkModalRef = this.modalService.open(BookmarksModalComponent, { scrollable: true, size: 'lg' });
|
||||||
|
this.bookmarkModalRef.componentInstance.series = series;
|
||||||
|
this.bookmarkModalRef.closed.pipe(take(1)).subscribe(() => {
|
||||||
|
this.bookmarkModalRef = null;
|
||||||
|
if (callback) {
|
||||||
|
callback(series);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.bookmarkModalRef.dismissed.pipe(take(1)).subscribe(() => {
|
||||||
|
this.bookmarkModalRef = null;
|
||||||
|
if (callback) {
|
||||||
|
callback(series);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,10 @@ export class ImageService {
|
|||||||
return this.baseUrl + 'image/chapter-cover?chapterId=' + chapterId;
|
return this.baseUrl + 'image/chapter-cover?chapterId=' + chapterId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBookmarkedImage(chapterId: number, pageNum: number) {
|
||||||
|
return this.baseUrl + 'image/chapter-cover?chapterId=' + chapterId + '&pageNum=' + pageNum;
|
||||||
|
}
|
||||||
|
|
||||||
updateErroredImage(event: any) {
|
updateErroredImage(event: any) {
|
||||||
event.target.src = this.placeholderImage;
|
event.target.src = this.placeholderImage;
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,9 @@ export class MessageHubService {
|
|||||||
this.updateNotificationModalRef.closed.subscribe(() => {
|
this.updateNotificationModalRef.closed.subscribe(() => {
|
||||||
this.updateNotificationModalRef = null;
|
this.updateNotificationModalRef = null;
|
||||||
});
|
});
|
||||||
|
this.updateNotificationModalRef.dismissed.subscribe(() => {
|
||||||
|
this.updateNotificationModalRef = null;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,8 +3,9 @@ import { Injectable } from '@angular/core';
|
|||||||
import { environment } from 'src/environments/environment';
|
import { environment } from 'src/environments/environment';
|
||||||
import { ChapterInfo } from '../manga-reader/_models/chapter-info';
|
import { ChapterInfo } from '../manga-reader/_models/chapter-info';
|
||||||
import { UtilityService } from '../shared/_services/utility.service';
|
import { UtilityService } from '../shared/_services/utility.service';
|
||||||
import { Bookmark } from '../_models/bookmark';
|
|
||||||
import { Chapter } from '../_models/chapter';
|
import { Chapter } from '../_models/chapter';
|
||||||
|
import { PageBookmark } from '../_models/page-bookmark';
|
||||||
|
import { ProgressBookmark } from '../_models/progress-bookmark';
|
||||||
import { Volume } from '../_models/volume';
|
import { Volume } from '../_models/volume';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@ -19,20 +20,44 @@ export class ReaderService {
|
|||||||
|
|
||||||
constructor(private httpClient: HttpClient, private utilityService: UtilityService) { }
|
constructor(private httpClient: HttpClient, private utilityService: UtilityService) { }
|
||||||
|
|
||||||
getBookmark(chapterId: number) {
|
bookmark(seriesId: number, volumeId: number, chapterId: number, page: number) {
|
||||||
return this.httpClient.get<Bookmark>(this.baseUrl + 'reader/get-bookmark?chapterId=' + chapterId);
|
return this.httpClient.post(this.baseUrl + 'reader/bookmark', {seriesId, volumeId, chapterId, page});
|
||||||
|
}
|
||||||
|
|
||||||
|
unbookmark(seriesId: number, volumeId: number, chapterId: number, page: number) {
|
||||||
|
return this.httpClient.post(this.baseUrl + 'reader/unbookmark', {seriesId, volumeId, chapterId, page});
|
||||||
|
}
|
||||||
|
|
||||||
|
getBookmarks(chapterId: number) {
|
||||||
|
return this.httpClient.get<PageBookmark[]>(this.baseUrl + 'reader/get-bookmarks?chapterId=' + chapterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getBookmarksForVolume(volumeId: number) {
|
||||||
|
return this.httpClient.get<PageBookmark[]>(this.baseUrl + 'reader/get-volume-bookmarks?volumeId=' + volumeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getBookmarksForSeries(seriesId: number) {
|
||||||
|
return this.httpClient.get<PageBookmark[]>(this.baseUrl + 'reader/get-series-bookmarks?seriesId=' + seriesId);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearBookmarks(seriesId: number) {
|
||||||
|
return this.httpClient.post(this.baseUrl + 'reader/remove-bookmarks', {seriesId});
|
||||||
|
}
|
||||||
|
|
||||||
|
getProgress(chapterId: number) {
|
||||||
|
return this.httpClient.get<ProgressBookmark>(this.baseUrl + 'reader/get-progress?chapterId=' + chapterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPageUrl(chapterId: number, page: number) {
|
getPageUrl(chapterId: number, page: number) {
|
||||||
return this.baseUrl + 'reader/image?chapterId=' + chapterId + '&page=' + page;
|
return this.baseUrl + 'reader/image?chapterId=' + chapterId + '&page=' + page;
|
||||||
}
|
}
|
||||||
|
|
||||||
getChapterInfo(chapterId: number) {
|
getChapterInfo(seriesId: number, chapterId: number) {
|
||||||
return this.httpClient.get<ChapterInfo>(this.baseUrl + 'reader/chapter-info?chapterId=' + chapterId);
|
return this.httpClient.get<ChapterInfo>(this.baseUrl + 'reader/chapter-info?chapterId=' + chapterId + '&seriesId=' + seriesId);
|
||||||
}
|
}
|
||||||
|
|
||||||
bookmark(seriesId: number, volumeId: number, chapterId: number, page: number, bookScrollId: string | null = null) {
|
saveProgress(seriesId: number, volumeId: number, chapterId: number, page: number, bookScrollId: string | null = null) {
|
||||||
return this.httpClient.post(this.baseUrl + 'reader/bookmark', {seriesId, volumeId, chapterId, pageNum: page, bookScrollId});
|
return this.httpClient.post(this.baseUrl + 'reader/progress', {seriesId, volumeId, chapterId, pageNum: page, bookScrollId});
|
||||||
}
|
}
|
||||||
|
|
||||||
markVolumeRead(seriesId: number, volumeId: number) {
|
markVolumeRead(seriesId: number, volumeId: number) {
|
||||||
|
@ -39,6 +39,7 @@ import { RecentlyAddedComponent } from './recently-added/recently-added.componen
|
|||||||
import { LibraryCardComponent } from './library-card/library-card.component';
|
import { LibraryCardComponent } from './library-card/library-card.component';
|
||||||
import { SeriesCardComponent } from './series-card/series-card.component';
|
import { SeriesCardComponent } from './series-card/series-card.component';
|
||||||
import { InProgressComponent } from './in-progress/in-progress.component';
|
import { InProgressComponent } from './in-progress/in-progress.component';
|
||||||
|
import { BookmarksModalComponent } from './_modals/bookmarks-modal/bookmarks-modal.component';
|
||||||
|
|
||||||
let sentryProviders: any[] = [];
|
let sentryProviders: any[] = [];
|
||||||
|
|
||||||
@ -104,7 +105,8 @@ if (environment.production) {
|
|||||||
RecentlyAddedComponent,
|
RecentlyAddedComponent,
|
||||||
LibraryCardComponent,
|
LibraryCardComponent,
|
||||||
SeriesCardComponent,
|
SeriesCardComponent,
|
||||||
InProgressComponent
|
InProgressComponent,
|
||||||
|
BookmarksModalComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
|
@ -114,15 +114,14 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
currentPageAnchor: string = '';
|
currentPageAnchor: string = '';
|
||||||
intersectionObserver: IntersectionObserver = new IntersectionObserver((entries) => this.handleIntersection(entries), { threshold: [1] });
|
intersectionObserver: IntersectionObserver = new IntersectionObserver((entries) => this.handleIntersection(entries), { threshold: [1] });
|
||||||
/**
|
/**
|
||||||
* Last seen bookmark part path
|
* Last seen progress part path
|
||||||
*/
|
*/
|
||||||
lastSeenScrollPartPath: string = '';
|
lastSeenScrollPartPath: string = '';
|
||||||
|
/**
|
||||||
// Temp hack: Override background color for reader and restore it onDestroy
|
* Hack: Override background color for reader and restore it onDestroy
|
||||||
|
*/
|
||||||
originalBodyColor: string | undefined;
|
originalBodyColor: string | undefined;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
darkModeStyles = `
|
darkModeStyles = `
|
||||||
*:not(input), *:not(select), *:not(code), *:not(:link), *:not(.ngx-toastr) {
|
*:not(input), *:not(select), *:not(code), *:not(:link), *:not(.ngx-toastr) {
|
||||||
color: #dcdcdc !important;
|
color: #dcdcdc !important;
|
||||||
@ -198,11 +197,11 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* After the page has loaded, setup the scroll handler. The scroll handler has 2 parts. One is if there are page anchors setup (aka page anchor elements linked with the
|
* After the page has loaded, setup the scroll handler. The scroll handler has 2 parts. One is if there are page anchors setup (aka page anchor elements linked with the
|
||||||
* table of content) then we calculate what has already been reached and grab the last reached one to bookmark. If page anchors aren't setup (toc missing), then try to bookmark
|
* table of content) then we calculate what has already been reached and grab the last reached one to save progress. If page anchors aren't setup (toc missing), then try to save progress
|
||||||
* based on the last seen scroll part (xpath).
|
* based on the last seen scroll part (xpath).
|
||||||
*/
|
*/
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
// check scroll offset and if offset is after any of the "id" markers, bookmark it
|
// check scroll offset and if offset is after any of the "id" markers, save progress
|
||||||
fromEvent(window, 'scroll')
|
fromEvent(window, 'scroll')
|
||||||
.pipe(debounceTime(200), takeUntil(this.onDestroy)).subscribe((event) => {
|
.pipe(debounceTime(200), takeUntil(this.onDestroy)).subscribe((event) => {
|
||||||
if (this.isLoading) return;
|
if (this.isLoading) return;
|
||||||
@ -215,7 +214,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
const alreadyReached = Object.values(this.pageAnchors).filter((i: number) => i <= verticalOffset);
|
const alreadyReached = Object.values(this.pageAnchors).filter((i: number) => i <= verticalOffset);
|
||||||
if (alreadyReached.length > 0) {
|
if (alreadyReached.length > 0) {
|
||||||
this.currentPageAnchor = Object.keys(this.pageAnchors)[alreadyReached.length - 1];
|
this.currentPageAnchor = Object.keys(this.pageAnchors)[alreadyReached.length - 1];
|
||||||
this.readerService.bookmark(this.seriesId, this.volumeId, this.chapterId, this.pageNum, this.lastSeenScrollPartPath).pipe(take(1)).subscribe(() => {/* No operation */});
|
this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, this.pageNum, this.lastSeenScrollPartPath).pipe(take(1)).subscribe(() => {/* No operation */});
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
this.currentPageAnchor = '';
|
this.currentPageAnchor = '';
|
||||||
@ -223,7 +222,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.lastSeenScrollPartPath !== '') {
|
if (this.lastSeenScrollPartPath !== '') {
|
||||||
this.readerService.bookmark(this.seriesId, this.volumeId, this.chapterId, this.pageNum, this.lastSeenScrollPartPath).pipe(take(1)).subscribe(() => {/* No operation */});
|
this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, this.pageNum, this.lastSeenScrollPartPath).pipe(take(1)).subscribe(() => {/* No operation */});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -279,7 +278,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
forkJoin({
|
forkJoin({
|
||||||
chapter: this.seriesService.getChapter(this.chapterId),
|
chapter: this.seriesService.getChapter(this.chapterId),
|
||||||
bookmark: this.readerService.getBookmark(this.chapterId),
|
progress: this.readerService.getProgress(this.chapterId),
|
||||||
chapters: this.bookService.getBookChapters(this.chapterId),
|
chapters: this.bookService.getBookChapters(this.chapterId),
|
||||||
info: this.bookService.getBookInfo(this.chapterId)
|
info: this.bookService.getBookInfo(this.chapterId)
|
||||||
}).pipe(take(1)).subscribe(results => {
|
}).pipe(take(1)).subscribe(results => {
|
||||||
@ -287,17 +286,17 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.volumeId = results.chapter.volumeId;
|
this.volumeId = results.chapter.volumeId;
|
||||||
this.maxPages = results.chapter.pages;
|
this.maxPages = results.chapter.pages;
|
||||||
this.chapters = results.chapters;
|
this.chapters = results.chapters;
|
||||||
this.pageNum = results.bookmark.pageNum;
|
this.pageNum = results.progress.pageNum;
|
||||||
this.bookTitle = results.info;
|
this.bookTitle = results.info;
|
||||||
|
|
||||||
|
|
||||||
if (this.pageNum >= this.maxPages) {
|
if (this.pageNum >= this.maxPages) {
|
||||||
this.pageNum = this.maxPages - 1;
|
this.pageNum = this.maxPages - 1;
|
||||||
this.readerService.bookmark(this.seriesId, this.volumeId, this.chapterId, this.pageNum).pipe(take(1)).subscribe(() => {/* No operation */});
|
this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, this.pageNum).pipe(take(1)).subscribe(() => {/* No operation */});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user bookmark has part, if so load it so we scroll to it
|
// Check if user progress has part, if so load it so we scroll to it
|
||||||
this.loadPage(results.bookmark.bookScrollId || undefined);
|
this.loadPage(results.progress.bookScrollId || undefined);
|
||||||
}, () => {
|
}, () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.closeReader();
|
this.closeReader();
|
||||||
@ -484,7 +483,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
loadPage(part?: string | undefined, scrollTop?: number | undefined) {
|
loadPage(part?: string | undefined, scrollTop?: number | undefined) {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
this.readerService.bookmark(this.seriesId, this.volumeId, this.chapterId, this.pageNum).pipe(take(1)).subscribe(() => {/* No operation */});
|
this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, this.pageNum).pipe(take(1)).subscribe(() => {/* No operation */});
|
||||||
|
|
||||||
this.bookService.getBookPage(this.chapterId, this.pageNum).pipe(take(1)).subscribe(content => {
|
this.bookService.getBookPage(this.chapterId, this.pageNum).pipe(take(1)).subscribe(content => {
|
||||||
this.page = this.domSanitizer.bypassSecurityTrustHtml(content);
|
this.page = this.domSanitizer.bypassSecurityTrustHtml(content);
|
||||||
|
@ -12,6 +12,9 @@
|
|||||||
{{subtitle}}
|
{{subtitle}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="margin-left: auto; padding-right: 3%;">
|
||||||
|
<button class="btn btn-icon btn-small" role="checkbox" [attr.aria-checked]="pageBookmarked" title="{{pageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}" (click)="bookmarkPage()"><i class="{{pageBookmarked ? 'fa' : 'far'}} fa-bookmark" aria-hidden="true"></i><span class="sr-only">{{pageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}</span></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngIf="isLoading">
|
<ng-container *ngIf="isLoading">
|
||||||
|
@ -183,6 +183,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
* If extended settings area is visible. Blocks auto-closing of menu.
|
* If extended settings area is visible. Blocks auto-closing of menu.
|
||||||
*/
|
*/
|
||||||
settingsOpen: boolean = false;
|
settingsOpen: boolean = false;
|
||||||
|
/**
|
||||||
|
* A map of bookmarked pages to anything. Used for O(1) lookup time if a page is bookmarked or not.
|
||||||
|
*/
|
||||||
|
bookmarks: {[key: string]: number} = {};
|
||||||
|
|
||||||
private readonly onDestroy = new Subject<void>();
|
private readonly onDestroy = new Subject<void>();
|
||||||
|
|
||||||
@ -191,6 +195,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
get pageBookmarked() {
|
||||||
|
return this.bookmarks.hasOwnProperty(this.pageNum);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
get splitIconClass() {
|
get splitIconClass() {
|
||||||
@ -348,13 +355,14 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.pageNum = 1;
|
this.pageNum = 1;
|
||||||
|
|
||||||
forkJoin({
|
forkJoin({
|
||||||
bookmark: this.readerService.getBookmark(this.chapterId),
|
progress: this.readerService.getProgress(this.chapterId),
|
||||||
chapterInfo: this.readerService.getChapterInfo(this.chapterId)
|
chapterInfo: this.readerService.getChapterInfo(this.seriesId, this.chapterId),
|
||||||
|
bookmarks: this.readerService.getBookmarks(this.chapterId)
|
||||||
}).pipe(take(1)).subscribe(results => {
|
}).pipe(take(1)).subscribe(results => {
|
||||||
this.volumeId = results.chapterInfo.volumeId;
|
this.volumeId = results.chapterInfo.volumeId;
|
||||||
this.maxPages = results.chapterInfo.pages;
|
this.maxPages = results.chapterInfo.pages;
|
||||||
|
|
||||||
let page = results.bookmark.pageNum;
|
let page = results.progress.pageNum;
|
||||||
if (page >= this.maxPages) {
|
if (page >= this.maxPages) {
|
||||||
page = this.maxPages - 1;
|
page = this.maxPages - 1;
|
||||||
}
|
}
|
||||||
@ -367,6 +375,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
this.updateTitle(results.chapterInfo);
|
this.updateTitle(results.chapterInfo);
|
||||||
|
|
||||||
|
// From bookmarks, create map of pages to make lookup time O(1)
|
||||||
|
this.bookmarks = {};
|
||||||
|
results.bookmarks.forEach(bookmark => {
|
||||||
|
this.bookmarks[bookmark.page] = 1;
|
||||||
|
});
|
||||||
|
|
||||||
this.readerService.getNextChapter(this.seriesId, this.volumeId, this.chapterId).pipe(take(1)).subscribe(chapterId => {
|
this.readerService.getNextChapter(this.seriesId, this.volumeId, this.chapterId).pipe(take(1)).subscribe(chapterId => {
|
||||||
this.nextChapterId = chapterId;
|
this.nextChapterId = chapterId;
|
||||||
if (chapterId === CHAPTER_ID_DOESNT_EXIST || chapterId === this.chapterId) {
|
if (chapterId === CHAPTER_ID_DOESNT_EXIST || chapterId === this.chapterId) {
|
||||||
@ -747,14 +761,14 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
loadPage() {
|
loadPage() {
|
||||||
if (!this.canvas || !this.ctx) { return; }
|
if (!this.canvas || !this.ctx) { return; }
|
||||||
|
|
||||||
// Due to the fact that we start at image 0, but page 1, we need the last page to be bookmarked as page + 1 to be completed
|
// Due to the fact that we start at image 0, but page 1, we need the last page to have progress as page + 1 to be completed
|
||||||
let pageNum = this.pageNum;
|
let pageNum = this.pageNum;
|
||||||
if (this.pageNum == this.maxPages - 1) {
|
if (this.pageNum == this.maxPages - 1) {
|
||||||
pageNum = this.pageNum + 1;
|
pageNum = this.pageNum + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
this.readerService.bookmark(this.seriesId, this.volumeId, this.chapterId, pageNum).pipe(take(1)).subscribe(() => {/* No operation */});
|
this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, pageNum).pipe(take(1)).subscribe(() => {/* No operation */});
|
||||||
|
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.canvasImage = this.cachedImages.current();
|
this.canvasImage = this.cachedImages.current();
|
||||||
@ -814,13 +828,13 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
if (this.pageNum >= this.maxPages - 10) {
|
if (this.pageNum >= this.maxPages - 10) {
|
||||||
// Tell server to cache the next chapter
|
// Tell server to cache the next chapter
|
||||||
if (this.nextChapterId > 0 && !this.nextChapterPrefetched) {
|
if (this.nextChapterId > 0 && !this.nextChapterPrefetched) {
|
||||||
this.readerService.getChapterInfo(this.nextChapterId).pipe(take(1)).subscribe(res => {
|
this.readerService.getChapterInfo(this.seriesId, this.nextChapterId).pipe(take(1)).subscribe(res => {
|
||||||
this.nextChapterPrefetched = true;
|
this.nextChapterPrefetched = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (this.pageNum <= 10) {
|
} else if (this.pageNum <= 10) {
|
||||||
if (this.prevChapterId > 0 && !this.prevChapterPrefetched) {
|
if (this.prevChapterId > 0 && !this.prevChapterPrefetched) {
|
||||||
this.readerService.getChapterInfo(this.prevChapterId).pipe(take(1)).subscribe(res => {
|
this.readerService.getChapterInfo(this.seriesId, this.prevChapterId).pipe(take(1)).subscribe(res => {
|
||||||
this.prevChapterPrefetched = true;
|
this.prevChapterPrefetched = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -905,7 +919,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
handleWebtoonPageChange(updatedPageNum: number) {
|
handleWebtoonPageChange(updatedPageNum: number) {
|
||||||
this.setPageNum(updatedPageNum);
|
this.setPageNum(updatedPageNum);
|
||||||
this.readerService.bookmark(this.seriesId, this.volumeId, this.chapterId, this.pageNum).pipe(take(1)).subscribe(() => {/* No operation */});
|
this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, this.pageNum).pipe(take(1)).subscribe(() => {/* No operation */});
|
||||||
}
|
}
|
||||||
|
|
||||||
saveSettings() {
|
saveSettings() {
|
||||||
@ -945,4 +959,22 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
this.updateForm();
|
this.updateForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bookmarks the current page for the chapter
|
||||||
|
*/
|
||||||
|
bookmarkPage() {
|
||||||
|
const pageNum = this.pageNum;
|
||||||
|
if (this.pageBookmarked) {
|
||||||
|
// Remove bookmark
|
||||||
|
this.readerService.unbookmark(this.seriesId, this.volumeId, this.chapterId, pageNum).pipe(take(1)).subscribe(() => {
|
||||||
|
delete this.bookmarks[pageNum];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.readerService.bookmark(this.seriesId, this.volumeId, this.chapterId, pageNum).pipe(take(1)).subscribe(() => {
|
||||||
|
this.bookmarks[pageNum] = 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import { ImageService } from 'src/app/_services/image.service';
|
|||||||
import { ActionFactoryService, Action, ActionItem } from 'src/app/_services/action-factory.service';
|
import { ActionFactoryService, Action, 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 { ConfirmService } from '../shared/confirm.service';
|
import { ConfirmService } from '../shared/confirm.service';
|
||||||
|
import { ActionService } from '../_services/action.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-series-card',
|
selector: 'app-series-card',
|
||||||
@ -30,7 +31,8 @@ export class SeriesCardComponent implements OnInit, OnChanges {
|
|||||||
constructor(private accountService: AccountService, private router: Router,
|
constructor(private accountService: AccountService, private router: Router,
|
||||||
private seriesService: SeriesService, private toastr: ToastrService,
|
private seriesService: SeriesService, private toastr: ToastrService,
|
||||||
private modalService: NgbModal, private confirmService: ConfirmService,
|
private modalService: NgbModal, private confirmService: ConfirmService,
|
||||||
public imageService: ImageService, private actionFactoryService: ActionFactoryService) {
|
public imageService: ImageService, private actionFactoryService: ActionFactoryService,
|
||||||
|
private actionService: ActionService) {
|
||||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||||
if (user) {
|
if (user) {
|
||||||
this.isAdmin = this.accountService.hasAdminRole(user);
|
this.isAdmin = this.accountService.hasAdminRole(user);
|
||||||
@ -68,6 +70,9 @@ export class SeriesCardComponent implements OnInit, OnChanges {
|
|||||||
case(Action.Edit):
|
case(Action.Edit):
|
||||||
this.openEditModal(series);
|
this.openEditModal(series);
|
||||||
break;
|
break;
|
||||||
|
case(Action.Bookmarks):
|
||||||
|
this.actionService.openBookmarkModal(series, (series) => {/* No Operation */ });
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -13,12 +13,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row no-gutters">
|
<div class="row no-gutters">
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-primary" (click)="read()" (mouseover)="showBook = true;" (mouseleave)="showBook = false;" [disabled]="isLoading">
|
<button class="btn btn-primary" (click)="read()" [disabled]="isLoading">
|
||||||
<span>
|
<span>
|
||||||
<i class="fa {{showBook ? 'fa-book-open' : 'fa-book'}}"></i>
|
<i class="fa {{showBook ? 'fa-book-open' : 'fa-book'}}"></i>
|
||||||
</span>
|
</span>
|
||||||
|
<span class="read-btn--text"> {{(hasReadingProgress) ? 'Continue' : 'Read'}}</span>
|
||||||
<span class="read-btn--text">{{(hasReadingProgress) ? 'Continue' : 'Read'}}</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-2" *ngIf="isAdmin">
|
<div class="ml-2" *ngIf="isAdmin">
|
||||||
|
@ -150,6 +150,9 @@ export class SeriesDetailComponent implements OnInit {
|
|||||||
case(Action.Delete):
|
case(Action.Delete):
|
||||||
this.deleteSeries(series);
|
this.deleteSeries(series);
|
||||||
break;
|
break;
|
||||||
|
case(Action.Bookmarks):
|
||||||
|
this.actionService.openBookmarkModal(series, (series) => this.actionInProgress = false);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@ import { saveAs } from 'file-saver';
|
|||||||
import { Chapter } from 'src/app/_models/chapter';
|
import { Chapter } from 'src/app/_models/chapter';
|
||||||
import { Volume } from 'src/app/_models/volume';
|
import { Volume } from 'src/app/_models/volume';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
|
import { PageBookmark } from 'src/app/_models/page-bookmark';
|
||||||
|
import { map, take } from 'rxjs/operators';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -85,6 +87,12 @@ export class DownloadService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
downloadBookmarks(bookmarks: PageBookmark[], seriesName: string) {
|
||||||
|
return this.httpClient.post(this.baseUrl + 'download/bookmarks', {bookmarks}, {observe: 'response', responseType: 'blob' as 'text'}).pipe(take(1), map(resp => {
|
||||||
|
this.preformSave(resp.body || '', this.getFilenameFromHeader(resp.headers, seriesName));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
private preformSave(res: string, filename: string) {
|
private preformSave(res: string, filename: string) {
|
||||||
const blob = new Blob([res], {type: 'text/plain;charset=utf-8'});
|
const blob = new Blob([res], {type: 'text/plain;charset=utf-8'});
|
||||||
saveAs(blob, filename);
|
saveAs(blob, filename);
|
||||||
|
@ -2,7 +2,9 @@
|
|||||||
<div ngbDropdown container="body" class="d-inline-block">
|
<div ngbDropdown container="body" class="d-inline-block">
|
||||||
<button [disabled]="disabled" class="btn {{btnClass}}" id="actions-{{labelBy}}" ngbDropdownToggle (click)="preventClick($event)"><i class="fa {{iconClass}}" aria-hidden="true"></i></button>
|
<button [disabled]="disabled" class="btn {{btnClass}}" id="actions-{{labelBy}}" ngbDropdownToggle (click)="preventClick($event)"><i class="fa {{iconClass}}" aria-hidden="true"></i></button>
|
||||||
<div ngbDropdownMenu attr.aria-labelledby="actions-{{labelBy}}">
|
<div ngbDropdownMenu attr.aria-labelledby="actions-{{labelBy}}">
|
||||||
<button ngbDropdownItem *ngFor="let action of actions" (click)="performAction($event, action)">{{action.title}}</button>
|
<button ngbDropdownItem *ngFor="let action of nonAdminActions" (click)="performAction($event, action)">{{action.title}}</button>
|
||||||
|
<div class="dropdown-divider" *ngIf="nonAdminActions.length > 1 && adminActions.length > 1"></div>
|
||||||
|
<button ngbDropdownItem *ngFor="let action of adminActions" (click)="performAction($event, action)">{{action.title}}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
@ -15,9 +15,15 @@ export class CardActionablesComponent implements OnInit {
|
|||||||
@Input() disabled: boolean = false;
|
@Input() disabled: boolean = false;
|
||||||
@Output() actionHandler = new EventEmitter<ActionItem<any>>();
|
@Output() actionHandler = new EventEmitter<ActionItem<any>>();
|
||||||
|
|
||||||
|
adminActions: ActionItem<any>[] = [];
|
||||||
|
nonAdminActions: ActionItem<any>[] = [];
|
||||||
|
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.nonAdminActions = this.actions.filter(item => !item.requiresAdmin);
|
||||||
|
this.adminActions = this.actions.filter(item => item.requiresAdmin);
|
||||||
}
|
}
|
||||||
|
|
||||||
preventClick(event: any) {
|
preventClick(event: any) {
|
||||||
@ -33,4 +39,7 @@ export class CardActionablesComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Insert hr to separate admin actions
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component';
|
import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component';
|
||||||
import { ConfirmConfig } from './confirm-dialog/_models/confirm-config';
|
import { ConfirmConfig } from './confirm-dialog/_models/confirm-config';
|
||||||
|
|
||||||
@ -36,9 +37,12 @@ export class ConfirmService {
|
|||||||
|
|
||||||
const modalRef = this.modalService.open(ConfirmDialogComponent);
|
const modalRef = this.modalService.open(ConfirmDialogComponent);
|
||||||
modalRef.componentInstance.config = config;
|
modalRef.componentInstance.config = config;
|
||||||
modalRef.closed.subscribe(result => {
|
modalRef.closed.pipe(take(1)).subscribe(result => {
|
||||||
return resolve(result);
|
return resolve(result);
|
||||||
});
|
});
|
||||||
|
modalRef.dismissed.pipe(take(1)).subscribe(() => {
|
||||||
|
return reject(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -57,9 +61,12 @@ export class ConfirmService {
|
|||||||
|
|
||||||
const modalRef = this.modalService.open(ConfirmDialogComponent);
|
const modalRef = this.modalService.open(ConfirmDialogComponent);
|
||||||
modalRef.componentInstance.config = config;
|
modalRef.componentInstance.config = config;
|
||||||
modalRef.closed.subscribe(result => {
|
modalRef.closed.pipe(take(1)).subscribe(result => {
|
||||||
return resolve(result);
|
return resolve(result);
|
||||||
});
|
});
|
||||||
|
modalRef.dismissed.pipe(take(1)).subscribe(() => {
|
||||||
|
return reject(false);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,9 +46,10 @@ body {
|
|||||||
font-family: "EBGaramond", "Helvetica Neue", sans-serif;
|
font-family: "EBGaramond", "Helvetica Neue", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
// .disabled, :disabled {
|
|
||||||
// cursor: not-allowed !important;
|
.btn-icon {
|
||||||
// }
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
// Slider handle override
|
// Slider handle override
|
||||||
::ng-deep {
|
::ng-deep {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user