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:
Joseph Milazzo 2021-08-10 18:18:07 -05:00 committed by GitHub
parent d1d7df9291
commit e9ec6671d5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 1860 additions and 241 deletions

View file

@ -224,17 +224,16 @@ namespace API.Services
public async Task<Tuple<byte[], string>> CreateZipForDownload(IEnumerable<string> files, string tempFolder)
{
var tempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp");
var dateString = DateTime.Now.ToShortDateString().Replace("/", "_");
var tempLocation = Path.Join(tempDirectory, $"{tempFolder}_{dateString}");
var tempLocation = Path.Join(DirectoryService.TempDirectory, $"{tempFolder}_{dateString}");
DirectoryService.ExistOrCreate(tempLocation);
if (!_directoryService.CopyFilesToDirectory(files, tempLocation))
{
throw new KavitaException("Unable to copy files to temp directory archive download.");
}
var zipPath = Path.Join(tempDirectory, $"kavita_{tempFolder}_{dateString}.zip");
var zipPath = Path.Join(DirectoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.zip");
try
{
ZipFile.CreateFromDirectory(tempLocation, zipPath);

View file

@ -37,7 +37,7 @@ namespace API.Services
public void EnsureCacheDirectory()
{
if (!DirectoryService.ExistOrCreate(CacheDirectory))
if (!DirectoryService.ExistOrCreate(DirectoryService.CacheDirectory))
{
_logger.LogError("Cache directory {CacheDirectory} is not accessible or does not exist. Creating...", CacheDirectory);
}
@ -60,58 +60,77 @@ namespace API.Services
return path;
}
/// <summary>
/// Caches the files for the given chapter to CacheDirectory
/// </summary>
/// <param name="chapterId"></param>
/// <returns>This will always return the Chapter for the chpaterId</returns>
public async Task<Chapter> Ensure(int chapterId)
{
EnsureCacheDirectory();
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(chapterId);
var files = chapter.Files.ToList();
var fileCount = files.Count;
var extractPath = GetCachePath(chapterId);
var extraPath = "";
var removeNonImages = true;
if (Directory.Exists(extractPath))
if (!Directory.Exists(extractPath))
{
return chapter;
var files = chapter.Files.ToList();
ExtractChapterFiles(extractPath, files);
}
return chapter;
}
/// <summary>
/// This is an internal method for cache service for extracting chapter files to disk. The code is structured
/// for cache service, but can be re-used (download bookmarks)
/// </summary>
/// <param name="extractPath"></param>
/// <param name="files"></param>
/// <returns></returns>
public void ExtractChapterFiles(string extractPath, IReadOnlyList<MangaFile> files)
{
var removeNonImages = true;
var fileCount = files.Count;
var extraPath = "";
var extractDi = new DirectoryInfo(extractPath);
if (files.Count > 0 && files[0].Format == MangaFormat.Image)
{
DirectoryService.ExistOrCreate(extractPath);
if (files.Count == 1)
{
_directoryService.CopyFileToDirectory(files[0].FilePath, extractPath);
}
else
{
_directoryService.CopyDirectoryToDirectory(Path.GetDirectoryName(files[0].FilePath), extractPath, Parser.Parser.ImageFileExtensions);
}
DirectoryService.ExistOrCreate(extractPath);
if (files.Count == 1)
{
_directoryService.CopyFileToDirectory(files[0].FilePath, extractPath);
}
else
{
_directoryService.CopyDirectoryToDirectory(Path.GetDirectoryName(files[0].FilePath), extractPath,
Parser.Parser.ImageFileExtensions);
}
extractDi.Flatten();
return chapter;
extractDi.Flatten();
}
foreach (var file in files)
{
if (fileCount > 1)
{
extraPath = file.Id + string.Empty;
}
if (fileCount > 1)
{
extraPath = file.Id + string.Empty;
}
if (file.Format == MangaFormat.Archive)
{
_archiveService.ExtractArchive(file.FilePath, Path.Join(extractPath, extraPath));
} else if (file.Format == MangaFormat.Pdf)
{
_bookService.ExtractPdfImages(file.FilePath, Path.Join(extractPath, extraPath));
} else if (file.Format == MangaFormat.Epub)
{
removeNonImages = false;
DirectoryService.ExistOrCreate(extractPath);
_directoryService.CopyFileToDirectory(files[0].FilePath, extractPath);
}
if (file.Format == MangaFormat.Archive)
{
_archiveService.ExtractArchive(file.FilePath, Path.Join(extractPath, extraPath));
}
else if (file.Format == MangaFormat.Pdf)
{
_bookService.ExtractPdfImages(file.FilePath, Path.Join(extractPath, extraPath));
}
else if (file.Format == MangaFormat.Epub)
{
removeNonImages = false;
DirectoryService.ExistOrCreate(extractPath);
_directoryService.CopyFileToDirectory(files[0].FilePath, extractPath);
}
}
extractDi.Flatten();
@ -119,9 +138,6 @@ namespace API.Services
{
extractDi.RemoveNonImages();
}
return chapter;
}
@ -173,7 +189,7 @@ namespace API.Services
{
// Calculate what chapter the page belongs to
var pagesSoFar = 0;
var chapterFiles = chapter.Files ?? await _unitOfWork.VolumeRepository.GetFilesForChapter(chapter.Id);
var chapterFiles = chapter.Files ?? await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapter.Id);
foreach (var mangaFile in chapterFiles)
{
if (page <= (mangaFile.Pages + pagesSoFar))

View file

@ -16,6 +16,9 @@ namespace API.Services
private static readonly Regex ExcludeDirectories = new Regex(
@"@eaDir|\.DS_Store",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static readonly string TempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp");
public static readonly string LogDirectory = Path.Join(Directory.GetCurrentDirectory(), "logs");
public static readonly string CacheDirectory = Path.Join(Directory.GetCurrentDirectory(), "cache");
public DirectoryService(ILogger<DirectoryService> logger)
{
@ -247,33 +250,40 @@ namespace API.Services
}
}
public bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath)
/// <summary>
/// Copies files to a destination directory. If the destination directory doesn't exist, this will create it.
/// </summary>
/// <param name="filePaths"></param>
/// <param name="directoryPath"></param>
/// <param name="prepend">An optional string to prepend to the target file's name</param>
/// <returns></returns>
public bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath, string prepend = "")
{
string currentFile = null;
try
{
foreach (var file in filePaths)
{
currentFile = file;
var fileInfo = new FileInfo(file);
if (fileInfo.Exists)
{
fileInfo.CopyTo(Path.Join(directoryPath, fileInfo.Name));
}
else
{
_logger.LogWarning("Tried to copy {File} but it doesn't exist", file);
}
ExistOrCreate(directoryPath);
string currentFile = null;
try
{
foreach (var file in filePaths)
{
currentFile = file;
var fileInfo = new FileInfo(file);
if (fileInfo.Exists)
{
fileInfo.CopyTo(Path.Join(directoryPath, prepend + fileInfo.Name));
}
else
{
_logger.LogWarning("Tried to copy {File} but it doesn't exist", file);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Unable to copy {File} to {DirectoryPath}", currentFile, directoryPath);
return false;
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Unable to copy {File} to {DirectoryPath}", currentFile, directoryPath);
return false;
}
return true;
return true;
}
public IEnumerable<string> ListDirectory(string rootPath)
@ -404,5 +414,23 @@ namespace API.Services
return fileCount;
}
/// <summary>
/// Attempts to delete the files passed to it. Swallows exceptions.
/// </summary>
/// <param name="files">Full path of files to delete</param>
public static void DeleteFiles(IEnumerable<string> files)
{
foreach (var file in files)
{
try
{
new FileInfo(file).Delete();
}
catch (Exception)
{
/* Swallow exception */
}
}
}
}
}