mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
Misc Bugfixes (#1378)
* Fixed an issue where sometimes when loading the next page, the pagination area wouldn't be properly setup due to a missed rendering cycle * Refactored BookController to thin it out and refactor some of the functions to apply IOC. Added some split query statements on a few queries. * Added Split Query to many queries * Added a visual indicator for loading state of PDF. Will spruce up css later. * Added back in logic * Fixed flash of white when refreshing browser * Hooked in a loading progress bar for the pdf reader * Close the pdf reader when pressing ESC
This commit is contained in:
parent
3a10b54422
commit
c650436f57
@ -6,28 +6,22 @@ using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using HtmlAgilityPack;
|
||||
using Kavita.Common;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using VersOne.Epub;
|
||||
|
||||
namespace API.Controllers
|
||||
{
|
||||
public class BookController : BaseApiController
|
||||
{
|
||||
private readonly ILogger<BookController> _logger;
|
||||
private readonly IBookService _bookService;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ICacheService _cacheService;
|
||||
private const string BookApiUrl = "book-resources?file=";
|
||||
|
||||
|
||||
public BookController(ILogger<BookController> logger, IBookService bookService,
|
||||
public BookController(IBookService bookService,
|
||||
IUnitOfWork unitOfWork, ICacheService cacheService)
|
||||
{
|
||||
_logger = logger;
|
||||
_bookService = bookService;
|
||||
_unitOfWork = unitOfWork;
|
||||
_cacheService = cacheService;
|
||||
@ -70,6 +64,8 @@ namespace API.Controllers
|
||||
break;
|
||||
case MangaFormat.Unknown:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
return Ok(new BookInfoDto()
|
||||
@ -97,6 +93,7 @@ namespace API.Controllers
|
||||
[ResponseCache(Duration = 60 * 1, Location = ResponseCacheLocation.Client, NoStore = false)]
|
||||
public async Task<ActionResult> GetBookPageResources(int chapterId, [FromQuery] string file)
|
||||
{
|
||||
if (chapterId <= 0) return BadRequest("Chapter is not valid");
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions);
|
||||
|
||||
@ -120,125 +117,20 @@ namespace API.Controllers
|
||||
[HttpGet("{chapterId}/chapters")]
|
||||
public async Task<ActionResult<ICollection<BookChapterItem>>> GetBookChapters(int chapterId)
|
||||
{
|
||||
if (chapterId <= 0) return BadRequest("Chapter is not valid");
|
||||
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions);
|
||||
var mappings = await _bookService.CreateKeyToPageMappingAsync(book);
|
||||
|
||||
var navItems = await book.GetNavigationAsync();
|
||||
var chaptersList = new List<BookChapterItem>();
|
||||
|
||||
foreach (var navigationItem in navItems)
|
||||
try
|
||||
{
|
||||
if (navigationItem.NestedItems.Count > 0)
|
||||
{
|
||||
var nestedChapters = new List<BookChapterItem>();
|
||||
|
||||
foreach (var nestedChapter in navigationItem.NestedItems)
|
||||
{
|
||||
if (nestedChapter.Link == null) continue;
|
||||
var key = BookService.CleanContentKeys(nestedChapter.Link.ContentFileName);
|
||||
if (mappings.ContainsKey(key))
|
||||
{
|
||||
nestedChapters.Add(new BookChapterItem()
|
||||
{
|
||||
Title = nestedChapter.Title,
|
||||
Page = mappings[key],
|
||||
Part = nestedChapter.Link.Anchor ?? string.Empty,
|
||||
Children = new List<BookChapterItem>()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
CreateToCChapter(navigationItem, nestedChapters, chaptersList, mappings);
|
||||
}
|
||||
|
||||
if (navigationItem.NestedItems.Count == 0)
|
||||
{
|
||||
CreateToCChapter(navigationItem, Array.Empty<BookChapterItem>(), chaptersList, mappings);
|
||||
}
|
||||
return Ok(await _bookService.GenerateTableOfContents(chapter));
|
||||
}
|
||||
|
||||
if (chaptersList.Count == 0)
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
// Generate from TOC
|
||||
var tocPage = book.Content.Html.Keys.FirstOrDefault(k => k.ToUpper().Contains("TOC"));
|
||||
if (tocPage == null) return Ok(chaptersList);
|
||||
|
||||
// Find all anchor tags, for each anchor we get inner text, to lower then title case on UI. Get href and generate page content
|
||||
var doc = new HtmlDocument();
|
||||
var content = await book.Content.Html[tocPage].ReadContentAsync();
|
||||
doc.LoadHtml(content);
|
||||
var anchors = doc.DocumentNode.SelectNodes("//a");
|
||||
if (anchors == null) return Ok(chaptersList);
|
||||
|
||||
foreach (var anchor in anchors)
|
||||
{
|
||||
if (anchor.Attributes.Contains("href"))
|
||||
{
|
||||
var key = BookService.CleanContentKeys(anchor.Attributes["href"].Value).Split("#")[0];
|
||||
if (!mappings.ContainsKey(key))
|
||||
{
|
||||
// Fallback to searching for key (bad epub metadata)
|
||||
var correctedKey = book.Content.Html.Keys.SingleOrDefault(s => s.EndsWith(key));
|
||||
if (!string.IsNullOrEmpty(correctedKey))
|
||||
{
|
||||
key = correctedKey;
|
||||
}
|
||||
}
|
||||
if (!string.IsNullOrEmpty(key) && mappings.ContainsKey(key))
|
||||
{
|
||||
var part = string.Empty;
|
||||
if (anchor.Attributes["href"].Value.Contains('#'))
|
||||
{
|
||||
part = anchor.Attributes["href"].Value.Split("#")[1];
|
||||
}
|
||||
chaptersList.Add(new BookChapterItem()
|
||||
{
|
||||
Title = anchor.InnerText,
|
||||
Page = mappings[key],
|
||||
Part = part,
|
||||
Children = new List<BookChapterItem>()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return Ok(chaptersList);
|
||||
}
|
||||
|
||||
private static void CreateToCChapter(EpubNavigationItemRef navigationItem, IList<BookChapterItem> nestedChapters, IList<BookChapterItem> chaptersList,
|
||||
IReadOnlyDictionary<string, int> mappings)
|
||||
{
|
||||
if (navigationItem.Link == null)
|
||||
{
|
||||
var item = new BookChapterItem()
|
||||
{
|
||||
Title = navigationItem.Title,
|
||||
Children = nestedChapters
|
||||
};
|
||||
if (nestedChapters.Count > 0)
|
||||
{
|
||||
item.Page = nestedChapters[0].Page;
|
||||
}
|
||||
|
||||
chaptersList.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
var groupKey = BookService.CleanContentKeys(navigationItem.Link.ContentFileName);
|
||||
if (mappings.ContainsKey(groupKey))
|
||||
{
|
||||
chaptersList.Add(new BookChapterItem()
|
||||
{
|
||||
Title = navigationItem.Title,
|
||||
Page = mappings[groupKey],
|
||||
Children = nestedChapters
|
||||
});
|
||||
}
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// This returns a single page within the epub book. All html will be rewritten to be scoped within our reader,
|
||||
/// all css is scoped, etc.
|
||||
@ -252,57 +144,17 @@ namespace API.Controllers
|
||||
var chapter = await _cacheService.Ensure(chapterId);
|
||||
var path = _cacheService.GetCachedFile(chapter);
|
||||
|
||||
using var book = await EpubReader.OpenBookAsync(path, BookService.BookReaderOptions);
|
||||
var mappings = await _bookService.CreateKeyToPageMappingAsync(book);
|
||||
|
||||
var counter = 0;
|
||||
var doc = new HtmlDocument {OptionFixNestedTags = true};
|
||||
|
||||
var baseUrl = "//" + Request.Host + Request.PathBase + "/api/";
|
||||
var apiBase = baseUrl + "book/" + chapterId + "/" + BookApiUrl;
|
||||
var bookPages = await book.GetReadingOrderAsync();
|
||||
foreach (var contentFileRef in bookPages)
|
||||
|
||||
try
|
||||
{
|
||||
if (page != counter)
|
||||
{
|
||||
counter++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var content = await contentFileRef.ReadContentAsync();
|
||||
if (contentFileRef.ContentType != EpubContentType.XHTML_1_1) return Ok(content);
|
||||
|
||||
// In more cases than not, due to this being XML not HTML, we need to escape the script tags.
|
||||
content = BookService.EscapeTags(content);
|
||||
|
||||
doc.LoadHtml(content);
|
||||
var body = doc.DocumentNode.SelectSingleNode("//body");
|
||||
|
||||
if (body == null)
|
||||
{
|
||||
if (doc.ParseErrors.Any())
|
||||
{
|
||||
LogBookErrors(book, contentFileRef, doc);
|
||||
return BadRequest("The file is malformed! Cannot read.");
|
||||
}
|
||||
_logger.LogError("{FilePath} has no body tag! Generating one for support. Book may be skewed", book.FilePath);
|
||||
doc.DocumentNode.SelectSingleNode("/html").AppendChild(HtmlNode.CreateNode("<body></body>"));
|
||||
body = doc.DocumentNode.SelectSingleNode("/html/body");
|
||||
}
|
||||
|
||||
return Ok(await _bookService.ScopePage(doc, book, apiBase, body, mappings, page));
|
||||
return Ok(await _bookService.GetBookPage(page, chapterId, path, baseUrl));
|
||||
}
|
||||
|
||||
return BadRequest("Could not find the appropriate html for that page");
|
||||
}
|
||||
|
||||
private void LogBookErrors(EpubBookRef book, EpubContentFileRef contentFileRef, HtmlDocument doc)
|
||||
{
|
||||
_logger.LogError("{FilePath} has an invalid html file (Page {PageName})", book.FilePath, contentFileRef.FileName);
|
||||
foreach (var error in doc.ParseErrors)
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
_logger.LogError("Line {LineNumber}, Reason: {Reason}", error.Line, error.Reason);
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +59,7 @@ public class CollectionTagRepository : ICollectionTagRepository
|
||||
var tagsToDelete = await _context.CollectionTag
|
||||
.Include(c => c.SeriesMetadatas)
|
||||
.Where(c => c.SeriesMetadatas.Count == 0)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
_context.RemoveRange(tagsToDelete);
|
||||
|
||||
@ -112,6 +113,7 @@ public class CollectionTagRepository : ICollectionTagRepository
|
||||
return await _context.CollectionTag
|
||||
.Where(c => c.Id == tagId)
|
||||
.Include(c => c.SeriesMetadatas)
|
||||
.AsSplitQuery()
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
|
@ -55,6 +55,7 @@ public class GenreRepository : IGenreRepository
|
||||
.Include(p => p.SeriesMetadatas)
|
||||
.Include(p => p.Chapters)
|
||||
.Where(p => p.SeriesMetadatas.Count == 0 && p.Chapters.Count == 0 && p.ExternalTag == removeExternal)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
|
||||
_context.Genre.RemoveRange(genresWithNoConnections);
|
||||
@ -67,6 +68,7 @@ public class GenreRepository : IGenreRepository
|
||||
return await _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.SelectMany(s => s.Metadata.Genres)
|
||||
.AsSplitQuery()
|
||||
.Distinct()
|
||||
.OrderBy(p => p.Title)
|
||||
.ProjectTo<GenreTagDto>(_mapper.ConfigurationProvider)
|
||||
|
@ -169,6 +169,7 @@ public class LibraryRepository : ILibraryRepository
|
||||
.Include(f => f.Folders)
|
||||
.OrderBy(l => l.Name)
|
||||
.ProjectTo<LibraryDto>(_mapper.ConfigurationProvider)
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
@ -200,7 +201,7 @@ public class LibraryRepository : ILibraryRepository
|
||||
query = query.Include(l => l.AppUsers);
|
||||
}
|
||||
|
||||
return query;
|
||||
return query.AsSplitQuery();
|
||||
}
|
||||
|
||||
|
||||
@ -259,6 +260,7 @@ public class LibraryRepository : ILibraryRepository
|
||||
.Where(library => library.AppUsers.Contains(user))
|
||||
.Include(l => l.Folders)
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.ProjectTo<LibraryDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
@ -283,6 +285,7 @@ public class LibraryRepository : ILibraryRepository
|
||||
var ret = await _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.Select(s => s.Metadata.Language)
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
@ -302,6 +305,7 @@ public class LibraryRepository : ILibraryRepository
|
||||
{
|
||||
return _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.AsSplitQuery()
|
||||
.Select(s => s.Metadata.PublicationStatus)
|
||||
.Distinct()
|
||||
.AsEnumerable()
|
||||
@ -313,5 +317,4 @@ public class LibraryRepository : ILibraryRepository
|
||||
.OrderBy(s => s.Title);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -54,6 +54,7 @@ public class PersonRepository : IPersonRepository
|
||||
.Include(p => p.SeriesMetadatas)
|
||||
.Include(p => p.ChapterMetadatas)
|
||||
.Where(p => p.SeriesMetadatas.Count == 0 && p.ChapterMetadatas.Count == 0)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
|
||||
_context.Person.RemoveRange(peopleWithNoConnections);
|
||||
@ -69,6 +70,7 @@ public class PersonRepository : IPersonRepository
|
||||
.Distinct()
|
||||
.OrderBy(p => p.Name)
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
@ -96,6 +96,7 @@ public class ReadingListRepository : IReadingListRepository
|
||||
var query = _context.ReadingList
|
||||
.Where(l => l.AppUserId == userId || (includePromoted && l.Promoted ))
|
||||
.Where(l => l.Items.Any(i => i.SeriesId == seriesId))
|
||||
.AsSplitQuery()
|
||||
.OrderBy(l => l.LastModified)
|
||||
.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
|
||||
.AsNoTracking();
|
||||
@ -108,6 +109,7 @@ public class ReadingListRepository : IReadingListRepository
|
||||
return await _context.ReadingList
|
||||
.Where(r => r.Id == readingListId)
|
||||
.Include(r => r.Items.OrderBy(item => item.Order))
|
||||
.AsSplitQuery()
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
@ -116,6 +118,7 @@ public class ReadingListRepository : IReadingListRepository
|
||||
var userLibraries = _context.Library
|
||||
.Include(l => l.AppUsers)
|
||||
.Where(library => library.AppUsers.Any(user => user.Id == userId))
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.Select(library => library.Id)
|
||||
.ToList();
|
||||
@ -165,6 +168,7 @@ public class ReadingListRepository : IReadingListRepository
|
||||
})
|
||||
.Where(o => userLibraries.Contains(o.LibraryId))
|
||||
.OrderBy(rli => rli.Order)
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
|
||||
|
@ -279,6 +279,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
.Include(l => l.AppUsers)
|
||||
.Where(library => library.AppUsers.Any(user => user.Id == userId))
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Select(library => library.Id)
|
||||
.ToListAsync();
|
||||
}
|
||||
@ -485,6 +486,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
var volumes = await _context.Volume
|
||||
.Where(v => seriesIds.Contains(v.SeriesId))
|
||||
.Include(v => v.Chapters)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
|
||||
IList<int> chapterIds = new List<int>();
|
||||
@ -509,6 +511,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
var volumes = await _context.Volume
|
||||
.Where(v => seriesIds.Contains(v.SeriesId))
|
||||
.Include(v => v.Chapters)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
|
||||
var seriesChapters = new Dictionary<int, IList<int>>();
|
||||
@ -532,10 +535,12 @@ public class SeriesRepository : ISeriesRepository
|
||||
{
|
||||
var userProgress = await _context.AppUserProgresses
|
||||
.Where(p => p.AppUserId == userId && series.Select(s => s.Id).Contains(p.SeriesId))
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
|
||||
var userRatings = await _context.AppUserRating
|
||||
.Where(r => r.AppUserId == userId && series.Select(s => s.Id).Contains(r.SeriesId))
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var s in series)
|
||||
@ -804,6 +809,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
var userLibraries = _context.Library
|
||||
.Include(l => l.AppUsers)
|
||||
.Where(library => library.AppUsers.Any(user => user.Id == userId))
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.Select(library => library.Id)
|
||||
.ToList();
|
||||
@ -829,6 +835,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
.Include(v => v.Chapters)
|
||||
.ThenInclude(c => c.Files)
|
||||
.SelectMany(v => v.Chapters.SelectMany(c => c.Files))
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
@ -838,6 +845,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
var allowedLibraries = _context.Library
|
||||
.Include(l => l.AppUsers)
|
||||
.Where(library => library.AppUsers.Any(x => x.Id == userId))
|
||||
.AsSplitQuery()
|
||||
.Select(l => l.Id);
|
||||
|
||||
return await _context.Series
|
||||
@ -920,6 +928,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
return await _context.SeriesMetadata
|
||||
.Where(sm => seriesIds.Contains(sm.SeriesId))
|
||||
.Include(sm => sm.CollectionTags)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
@ -993,6 +1002,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
{
|
||||
return _context.AppUser
|
||||
.Where(u => u.Id == userId)
|
||||
.AsSplitQuery()
|
||||
.SelectMany(l => l.Libraries.Select(lib => lib.Id));
|
||||
}
|
||||
|
||||
|
@ -54,6 +54,7 @@ public class TagRepository : ITagRepository
|
||||
.Include(p => p.SeriesMetadatas)
|
||||
.Include(p => p.Chapters)
|
||||
.Where(p => p.SeriesMetadatas.Count == 0 && p.Chapters.Count == 0 && p.ExternalTag == removeExternal)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
|
||||
_context.Tag.RemoveRange(tagsWithNoConnections);
|
||||
@ -66,6 +67,7 @@ public class TagRepository : ITagRepository
|
||||
return await _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.SelectMany(s => s.Metadata.Tags)
|
||||
.AsSplitQuery()
|
||||
.Distinct()
|
||||
.OrderBy(t => t.Title)
|
||||
.AsNoTracking()
|
||||
|
@ -205,6 +205,7 @@ public class UserRepository : IUserRepository
|
||||
return await _context.Users
|
||||
.Include(u => u.ReadingLists)
|
||||
.ThenInclude(l => l.Items)
|
||||
.AsSplitQuery()
|
||||
.SingleOrDefaultAsync(x => x.UserName == username);
|
||||
}
|
||||
|
||||
@ -244,6 +245,7 @@ public class UserRepository : IUserRepository
|
||||
{
|
||||
return await _context.Library
|
||||
.Include(l => l.AppUsers)
|
||||
.AsSplitQuery()
|
||||
.AnyAsync(library => library.AppUsers.Any(user => user.Id == userId));
|
||||
}
|
||||
|
||||
@ -362,6 +364,7 @@ public class UserRepository : IUserRepository
|
||||
Folders = l.Folders.Select(x => x.Path).ToList()
|
||||
}).ToList()
|
||||
})
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
@ -390,6 +393,7 @@ public class UserRepository : IUserRepository
|
||||
Folders = l.Folders.Select(x => x.Path).ToList()
|
||||
}).ToList()
|
||||
})
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
@ -62,6 +62,7 @@ public class VolumeRepository : IVolumeRepository
|
||||
.Where(c => volumeId == c.VolumeId)
|
||||
.Include(c => c.Files)
|
||||
.SelectMany(c => c.Files)
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
@ -106,7 +107,7 @@ public class VolumeRepository : IVolumeRepository
|
||||
|
||||
if (includeChapters)
|
||||
{
|
||||
query = query.Include(v => v.Chapters);
|
||||
query = query.Include(v => v.Chapters).AsSplitQuery();
|
||||
}
|
||||
return await query.ToListAsync();
|
||||
}
|
||||
@ -123,6 +124,7 @@ public class VolumeRepository : IVolumeRepository
|
||||
.Where(vol => vol.Id == volumeId)
|
||||
.Include(vol => vol.Chapters)
|
||||
.ThenInclude(c => c.Files)
|
||||
.AsSplitQuery()
|
||||
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
|
||||
.SingleAsync(vol => vol.Id == volumeId);
|
||||
|
||||
@ -143,6 +145,7 @@ public class VolumeRepository : IVolumeRepository
|
||||
.Where(vol => vol.SeriesId == seriesId)
|
||||
.Include(vol => vol.Chapters)
|
||||
.ThenInclude(c => c.Files)
|
||||
.AsSplitQuery()
|
||||
.OrderBy(vol => vol.Number)
|
||||
.ToListAsync();
|
||||
}
|
||||
@ -157,6 +160,7 @@ public class VolumeRepository : IVolumeRepository
|
||||
return await _context.Volume
|
||||
.Include(vol => vol.Chapters)
|
||||
.ThenInclude(c => c.Files)
|
||||
.AsSplitQuery()
|
||||
.SingleOrDefaultAsync(vol => vol.Id == volumeId);
|
||||
}
|
||||
|
||||
@ -220,6 +224,4 @@ public class VolumeRepository : IVolumeRepository
|
||||
v.PagesRead = userProgress.Where(p => p.VolumeId == v.Id).Sum(p => p.PagesRead);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -7,6 +7,8 @@ using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using API.Data.Metadata;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Parser;
|
||||
using Docnet.Core;
|
||||
@ -15,6 +17,7 @@ using Docnet.Core.Models;
|
||||
using Docnet.Core.Readers;
|
||||
using ExCSS;
|
||||
using HtmlAgilityPack;
|
||||
using Kavita.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IO;
|
||||
using SixLabors.ImageSharp;
|
||||
@ -51,6 +54,9 @@ namespace API.Services
|
||||
void ExtractPdfImages(string fileFilePath, string targetDirectory);
|
||||
|
||||
Task<string> ScopePage(HtmlDocument doc, EpubBookRef book, string apiBase, HtmlNode body, Dictionary<string, int> mappings, int page);
|
||||
Task<ICollection<BookChapterItem>> GenerateTableOfContents(Chapter chapter);
|
||||
|
||||
Task<string> GetBookPage(int page, int chapterId, string cachedEpubPath, string baseUrl);
|
||||
}
|
||||
|
||||
public class BookService : IBookService
|
||||
@ -61,6 +67,7 @@ namespace API.Services
|
||||
private readonly StylesheetParser _cssParser = new ();
|
||||
private static readonly RecyclableMemoryStreamManager StreamManager = new ();
|
||||
private const string CssScopeClass = ".book-content";
|
||||
private const string BookApiUrl = "book-resources?file=";
|
||||
public static readonly EpubReaderOptions BookReaderOptions = new()
|
||||
{
|
||||
PackageReaderOptions = new PackageReaderOptions()
|
||||
@ -681,6 +688,182 @@ namespace API.Services
|
||||
return PrepareFinalHtml(doc, body);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This will return a list of mappings from ID -> page num. ID will be the xhtml key and page num will be the reading order
|
||||
/// this is used to rewrite anchors in the book text so that we always load properly in our reader.
|
||||
/// </summary>
|
||||
/// <param name="chapter">Chapter with at least one file</param>
|
||||
/// <returns></returns>
|
||||
public async Task<ICollection<BookChapterItem>> GenerateTableOfContents(Chapter chapter)
|
||||
{
|
||||
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookReaderOptions);
|
||||
var mappings = await CreateKeyToPageMappingAsync(book);
|
||||
|
||||
var navItems = await book.GetNavigationAsync();
|
||||
var chaptersList = new List<BookChapterItem>();
|
||||
|
||||
foreach (var navigationItem in navItems)
|
||||
{
|
||||
if (navigationItem.NestedItems.Count == 0)
|
||||
{
|
||||
CreateToCChapter(navigationItem, Array.Empty<BookChapterItem>(), chaptersList, mappings);
|
||||
continue;
|
||||
}
|
||||
|
||||
var nestedChapters = new List<BookChapterItem>();
|
||||
|
||||
foreach (var nestedChapter in navigationItem.NestedItems.Where(n => n.Link != null))
|
||||
{
|
||||
var key = BookService.CleanContentKeys(nestedChapter.Link.ContentFileName);
|
||||
if (mappings.ContainsKey(key))
|
||||
{
|
||||
nestedChapters.Add(new BookChapterItem()
|
||||
{
|
||||
Title = nestedChapter.Title,
|
||||
Page = mappings[key],
|
||||
Part = nestedChapter.Link.Anchor ?? string.Empty,
|
||||
Children = new List<BookChapterItem>()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
CreateToCChapter(navigationItem, nestedChapters, chaptersList, mappings);
|
||||
}
|
||||
|
||||
if (chaptersList.Count != 0) return chaptersList;
|
||||
// Generate from TOC
|
||||
var tocPage = book.Content.Html.Keys.FirstOrDefault(k => k.ToUpper().Contains("TOC"));
|
||||
if (tocPage == null) return chaptersList;
|
||||
|
||||
// Find all anchor tags, for each anchor we get inner text, to lower then title case on UI. Get href and generate page content
|
||||
var doc = new HtmlDocument();
|
||||
var content = await book.Content.Html[tocPage].ReadContentAsync();
|
||||
doc.LoadHtml(content);
|
||||
var anchors = doc.DocumentNode.SelectNodes("//a");
|
||||
if (anchors == null) return chaptersList;
|
||||
|
||||
foreach (var anchor in anchors)
|
||||
{
|
||||
if (!anchor.Attributes.Contains("href")) continue;
|
||||
|
||||
var key = BookService.CleanContentKeys(anchor.Attributes["href"].Value).Split("#")[0];
|
||||
if (!mappings.ContainsKey(key))
|
||||
{
|
||||
// Fallback to searching for key (bad epub metadata)
|
||||
var correctedKey = book.Content.Html.Keys.SingleOrDefault(s => s.EndsWith(key));
|
||||
if (!string.IsNullOrEmpty(correctedKey))
|
||||
{
|
||||
key = correctedKey;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(key) || !mappings.ContainsKey(key)) continue;
|
||||
var part = string.Empty;
|
||||
if (anchor.Attributes["href"].Value.Contains('#'))
|
||||
{
|
||||
part = anchor.Attributes["href"].Value.Split("#")[1];
|
||||
}
|
||||
chaptersList.Add(new BookChapterItem()
|
||||
{
|
||||
Title = anchor.InnerText,
|
||||
Page = mappings[key],
|
||||
Part = part,
|
||||
Children = new List<BookChapterItem>()
|
||||
});
|
||||
}
|
||||
|
||||
return chaptersList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This returns a single page within the epub book. All html will be rewritten to be scoped within our reader,
|
||||
/// all css is scoped, etc.
|
||||
/// </summary>
|
||||
/// <param name="page">The requested page</param>
|
||||
/// <param name="chapterId">The chapterId</param>
|
||||
/// <param name="cachedEpubPath">The path to the cached epub file</param>
|
||||
/// <param name="baseUrl">The API base for Kavita, to rewrite urls to so we load though our endpoint</param>
|
||||
/// <returns>Full epub HTML Page, scoped to Kavita's reader</returns>
|
||||
/// <exception cref="KavitaException">All exceptions throw this</exception>
|
||||
public async Task<string> GetBookPage(int page, int chapterId, string cachedEpubPath, string baseUrl)
|
||||
{
|
||||
using var book = await EpubReader.OpenBookAsync(cachedEpubPath, BookReaderOptions);
|
||||
var mappings = await CreateKeyToPageMappingAsync(book);
|
||||
var apiBase = baseUrl + "book/" + chapterId + "/" + BookApiUrl;
|
||||
|
||||
var counter = 0;
|
||||
var doc = new HtmlDocument {OptionFixNestedTags = true};
|
||||
|
||||
|
||||
var bookPages = await book.GetReadingOrderAsync();
|
||||
foreach (var contentFileRef in bookPages)
|
||||
{
|
||||
if (page != counter)
|
||||
{
|
||||
counter++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var content = await contentFileRef.ReadContentAsync();
|
||||
if (contentFileRef.ContentType != EpubContentType.XHTML_1_1) return content;
|
||||
|
||||
// In more cases than not, due to this being XML not HTML, we need to escape the script tags.
|
||||
content = BookService.EscapeTags(content);
|
||||
|
||||
doc.LoadHtml(content);
|
||||
var body = doc.DocumentNode.SelectSingleNode("//body");
|
||||
|
||||
if (body == null)
|
||||
{
|
||||
if (doc.ParseErrors.Any())
|
||||
{
|
||||
LogBookErrors(book, contentFileRef, doc);
|
||||
throw new KavitaException("The file is malformed! Cannot read.");
|
||||
}
|
||||
_logger.LogError("{FilePath} has no body tag! Generating one for support. Book may be skewed", book.FilePath);
|
||||
doc.DocumentNode.SelectSingleNode("/html").AppendChild(HtmlNode.CreateNode("<body></body>"));
|
||||
body = doc.DocumentNode.SelectSingleNode("/html/body");
|
||||
}
|
||||
|
||||
return await ScopePage(doc, book, apiBase, body, mappings, page);
|
||||
}
|
||||
|
||||
throw new KavitaException("Could not find the appropriate html for that page");
|
||||
}
|
||||
|
||||
private static void CreateToCChapter(EpubNavigationItemRef navigationItem, IList<BookChapterItem> nestedChapters, IList<BookChapterItem> chaptersList,
|
||||
IReadOnlyDictionary<string, int> mappings)
|
||||
{
|
||||
if (navigationItem.Link == null)
|
||||
{
|
||||
var item = new BookChapterItem()
|
||||
{
|
||||
Title = navigationItem.Title,
|
||||
Children = nestedChapters
|
||||
};
|
||||
if (nestedChapters.Count > 0)
|
||||
{
|
||||
item.Page = nestedChapters[0].Page;
|
||||
}
|
||||
|
||||
chaptersList.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
var groupKey = CleanContentKeys(navigationItem.Link.ContentFileName);
|
||||
if (mappings.ContainsKey(groupKey))
|
||||
{
|
||||
chaptersList.Add(new BookChapterItem()
|
||||
{
|
||||
Title = navigationItem.Title,
|
||||
Page = mappings[groupKey],
|
||||
Children = nestedChapters
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the cover image to covers directory and returns file path back
|
||||
/// </summary>
|
||||
@ -743,6 +926,12 @@ namespace API.Services
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an image raster of a page within a PDF
|
||||
/// </summary>
|
||||
/// <param name="docReader"></param>
|
||||
/// <param name="pageNumber"></param>
|
||||
/// <param name="stream"></param>
|
||||
private static void GetPdfPage(IDocReader docReader, int pageNumber, Stream stream)
|
||||
{
|
||||
using var pageReader = docReader.GetPageReader(pageNumber);
|
||||
@ -784,5 +973,14 @@ namespace API.Services
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
private void LogBookErrors(EpubBookRef book, EpubContentFileRef contentFileRef, HtmlDocument doc)
|
||||
{
|
||||
_logger.LogError("{FilePath} has an invalid html file (Page {PageName})", book.FilePath, contentFileRef.FileName);
|
||||
foreach (var error in doc.ParseErrors)
|
||||
{
|
||||
_logger.LogError("Line {LineNumber}, Reason: {Reason}", error.Line, error.Reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -165,6 +165,8 @@ public class StatsService : IStatsService
|
||||
LibraryId = s.LibraryId,
|
||||
Count = _context.Library.Where(l => l.Id == s.LibraryId).SelectMany(l => l.Series).Count()
|
||||
})
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.MaxAsync(d => d.Count);
|
||||
}
|
||||
|
||||
@ -176,12 +178,16 @@ public class StatsService : IStatsService
|
||||
v.SeriesId,
|
||||
Count = _context.Series.Where(s => s.Id == v.SeriesId).SelectMany(s => s.Volumes).Count()
|
||||
})
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.MaxAsync(d => d.Count);
|
||||
}
|
||||
|
||||
private Task<int> MaxChaptersInASeries()
|
||||
{
|
||||
return _context.Series
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.MaxAsync(s => s.Volumes
|
||||
.Where(v => v.Number == 0)
|
||||
.SelectMany(v => v.Chapters)
|
||||
|
@ -1080,6 +1080,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
} else {
|
||||
this.canvasImage.src = this.getPageUrl(this.pageNum);
|
||||
}
|
||||
this.canvasImage.onload = () => {
|
||||
this.cdRef.markForCheck();
|
||||
};
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
@ -1114,8 +1118,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
console.log('prevChapterId', this.prevChapterId);
|
||||
|
||||
if (this.prevChapterId === CHAPTER_ID_NOT_FETCHED || this.prevChapterId === this.chapterId) {
|
||||
this.readerService.getPrevChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {
|
||||
this.prevChapterId = chapterId;
|
||||
@ -1127,7 +1129,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
loadChapter(chapterId: number, direction: 'Next' | 'Prev') {
|
||||
console.log('chapterId: ', chapterId);
|
||||
if (chapterId > 0) {
|
||||
this.isLoading = true;
|
||||
this.cdRef.markForCheck();
|
||||
|
@ -55,13 +55,6 @@
|
||||
}
|
||||
|
||||
|
||||
// .download {
|
||||
// width: 80px;
|
||||
// height: 80px;
|
||||
// }
|
||||
|
||||
|
||||
|
||||
.btn-icon {
|
||||
color: white;
|
||||
}
|
||||
|
@ -1,4 +1,16 @@
|
||||
<div class="{{theme}}">
|
||||
|
||||
<ng-container *ngIf="isLoading">
|
||||
<div class="loading mx-auto" style="min-width: 200px; width: 600px;">
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
Loading...PDFs may take longer than expected
|
||||
</div>
|
||||
<div class="progress-container row g-0 align-items-center">
|
||||
<div class="progress" style="height: 5px;">
|
||||
<div class="progress-bar" role="progressbar" [ngStyle]="{'width': loadPrecent + '%'}" [attr.aria-valuenow]="loadPrecent" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ngx-extended-pdf-viewer
|
||||
#pdfViewer
|
||||
[src]="readerService.downloadPdf(this.chapterId)"
|
||||
@ -23,6 +35,9 @@
|
||||
[customToolbar]="multiToolbar"
|
||||
|
||||
(pageChange)="saveProgress()"
|
||||
(pdfLoadingStarts)="updateLoading(true)"
|
||||
(pdfLoaded)="updateLoading(false)"
|
||||
(progress)="updateLoadProgress($event)"
|
||||
>
|
||||
|
||||
</ngx-extended-pdf-viewer>
|
||||
|
@ -10,3 +10,11 @@
|
||||
::ng-deep #presentationMode {
|
||||
margin: 3px 0 4px !important;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
width: 100%;
|
||||
}
|
||||
.progress-bar {
|
||||
// NOTE: We have to override due to theme variables not being available
|
||||
background-color: #3B9E76;
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { PageViewModeType } from 'ngx-extended-pdf-viewer';
|
||||
import { NgxExtendedPdfViewerService, PageViewModeType, ProgressBarEvent } from 'ngx-extended-pdf-viewer';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { Subject, take } from 'rxjs';
|
||||
import { BookService } from 'src/app/book-reader/book.service';
|
||||
import { KEY_CODES } from 'src/app/shared/_services/utility.service';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { User } from 'src/app/_models/user';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
@ -62,7 +63,11 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
|
||||
backgroundColor: string = this.themeMap[this.theme].background;
|
||||
fontColor: string = this.themeMap[this.theme].font;
|
||||
|
||||
isLoading: boolean = false;
|
||||
isLoading: boolean = true;
|
||||
/**
|
||||
* How much of the current document is loaded
|
||||
*/
|
||||
loadPrecent: number = 0;
|
||||
|
||||
/**
|
||||
* This can't be updated dynamically:
|
||||
@ -76,12 +81,19 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
|
||||
private seriesService: SeriesService, public readerService: ReaderService,
|
||||
private navService: NavService, private toastr: ToastrService,
|
||||
private bookService: BookService, private themeService: ThemeService,
|
||||
private readonly cdRef: ChangeDetectorRef) {
|
||||
private readonly cdRef: ChangeDetectorRef, private pdfViewerService: NgxExtendedPdfViewerService) {
|
||||
this.navService.hideNavBar();
|
||||
this.themeService.clearThemes();
|
||||
this.navService.hideSideNav();
|
||||
}
|
||||
|
||||
@HostListener('window:keyup', ['$event'])
|
||||
handleKeyPress(event: KeyboardEvent) {
|
||||
if (event.key === KEY_CODES.ESC_KEY) {
|
||||
this.closeReader();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.themeService.currentTheme$.pipe(take(1)).subscribe(theme => {
|
||||
this.themeService.setTheme(theme.name);
|
||||
@ -141,13 +153,12 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.seriesService.getChapter(this.chapterId).subscribe(chapter => {
|
||||
this.maxPages = chapter.pages;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
if (this.currentPage >= this.maxPages) {
|
||||
this.currentPage = this.maxPages - 1;
|
||||
this.saveProgress();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
}
|
||||
@ -193,4 +204,14 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
|
||||
this.readerService.closeReader(this.readingListMode, this.readingListId);
|
||||
}
|
||||
|
||||
updateLoading(state: boolean) {
|
||||
this.isLoading = state;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateLoadProgress(event: ProgressBarEvent) {
|
||||
this.loadPrecent = event.percent;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,203 +1,4 @@
|
||||
/* Theme for the light mode of Kavita */
|
||||
/* Default styles for Kavita */
|
||||
:root {
|
||||
--color-scheme: light;
|
||||
--primary-color: #4ac694;
|
||||
--primary-color-dark-shade: #3B9E76;
|
||||
--primary-color-darker-shade: #338A67;
|
||||
--primary-color-darkest-shade: #25624A;
|
||||
--error-color: #ff4136;
|
||||
--bs-body-bg: #fff;
|
||||
--body-text-color: #333;
|
||||
--btn-icon-filter: none;
|
||||
|
||||
/* Navbar */
|
||||
--navbar-bg-color: black;
|
||||
--navbar-text-color: white;
|
||||
--navbar-fa-icon-color: white;
|
||||
--navbar-btn-hover-outline-color: rgba(255, 255, 255, 1);
|
||||
|
||||
/* Inputs */
|
||||
--input-bg-color: #fff;
|
||||
--input-focused-border-color: #ccc;
|
||||
--input-bg-readonly-color: rgba(0,0,0,0.2);
|
||||
--input-placeholder-color: #aeaeae;
|
||||
--input-border-color: #ccc;
|
||||
--input-range-color: var(--primary-color);
|
||||
--input-range-active-color: var(--primary-color-darker-shade);
|
||||
|
||||
/* Buttons */
|
||||
--btn-primary-text-color: black;
|
||||
--btn-primary-bg-color: white;
|
||||
--btn-primary-border-color: black;
|
||||
--btn-primary-hover-text-color: white;
|
||||
--btn-primary-hover-bg-color: black;
|
||||
--btn-primary-hover-border-color: black;
|
||||
--btn-alt-bg-color: #424c72;
|
||||
--btn-alt-border-color: #444f75;
|
||||
--btn-alt-hover-bg-color: #3b4466;
|
||||
--btn-alt-focus-bg-color: #343c59;
|
||||
--btn-alt-focus-boxshadow-color: rgb(68 79 117 / 50%);
|
||||
--btn-fa-icon-color: black;
|
||||
--btn-disabled-bg-color: #020202;
|
||||
--btn-disabled-text-color: white;
|
||||
--btn-disabled-border-color: #6c757d;
|
||||
|
||||
/* Nav */
|
||||
--nav-link-active-text-color: white;
|
||||
--nav-link-bg-color: rgba(74, 198, 148, 0.9);
|
||||
--nav-tab-active-text-color: white;
|
||||
--nav-tab-text-color: var(--body-text-color);
|
||||
--nav-tab-bg-color: rgba(74, 198, 148, 0.9);
|
||||
--nav-tab-hover-border-color: #4ac694;
|
||||
--nav-link-text-color: black;
|
||||
--nav-link-hover-text-color: var(--primary-color);
|
||||
--nav-tab-border-hover-color: transparent;
|
||||
|
||||
/* Side Nav */
|
||||
--side-nav-bg-color: rgba(255,255,255,0.6);
|
||||
--side-nav-mobile-bg-color: rgb(255,255,255);
|
||||
--side-nav-openclose-transition: 0.15s ease-in-out;
|
||||
--side-nav-box-shadow: none;
|
||||
--side-nav-mobile-box-shadow: 3px 0em 5px 10em rgb(0 0 0 / 50%);
|
||||
--side-nav-hover-text-color: white;
|
||||
--side-nav-hover-bg-color: black;
|
||||
--side-nav-color: black;
|
||||
--side-nav-border-radius: 5px;
|
||||
--side-nav-border: 1px solid rgba(0,0,0,0.2);
|
||||
--side-nav-border-closed: 1px solid transparent;
|
||||
--side-nav-companion-bar-transistion: 0.5s linear;
|
||||
--side-nav-border-transition: 0.5s ease-in-out;
|
||||
--side-nav-bg-color-transition: 0.5s ease-in-out;
|
||||
--side-nav-closed-bg-color: transparent;
|
||||
--side-nav-item-active-color: var(--primary-color);
|
||||
--side-nav-active-bg-color: rgba(0,0,0,0.5);
|
||||
--side-nav-item-active-text-color: white;
|
||||
--side-nav-overlay-color: rgba(0,0,0,0.5);
|
||||
|
||||
|
||||
/* Checkboxes */
|
||||
--checkbox-checked-bg-color: var(--primary-color);
|
||||
--checkbox-bg-color: white;
|
||||
--checkbox-border-color: var(--primary-color);
|
||||
--checkbox-focus-border-color: var(--input-border-color);
|
||||
|
||||
/* Tagbadge */
|
||||
--tagbadge-bg-color: #c9c9c9;
|
||||
|
||||
/* Toasts */
|
||||
--toast-success-bg-color: rgba(74, 198, 148, 0.9);
|
||||
--toast-error-bg-color: #BD362F;
|
||||
--toast-info-bg-color: #2F96B4;
|
||||
--toast-warning-bg-color: #F89406;
|
||||
|
||||
/* Rating star */
|
||||
--ratingstar-star-empty: #b0c4de;
|
||||
--ratingstar-star-filled: var(--primary-color);
|
||||
|
||||
/* Global */
|
||||
--accent-bg-color: rgba(206, 206, 206, 0.5); // Drawer had: var(--bs-body-bg)
|
||||
--accent-text-color: grey;
|
||||
--accent-text-size: 0.8rem;
|
||||
--hr-color: rgba(239, 239, 239, 0.125);
|
||||
--grid-breakpoints-xs: $grid-breakpoint-xs;
|
||||
--grid-breakpoints-sm: $grid-breakpoint-sm;
|
||||
--grid-breakpoints-md: $grid-breakpoint-md;
|
||||
--grid-breakpoints-lg: $grid-breakpoint-lg;
|
||||
--grid-breakpoints-xl: $grid-breakpoint-xl;
|
||||
--body-font-family: "EBGaramond", "Helvetica Neue", sans-serif;
|
||||
--brand-font-family: "Spartan", sans-serif;
|
||||
--text-muted-color: #aaa;
|
||||
|
||||
/* Breadcrumb */
|
||||
--breadcrumb-bg-color: #eaeaea;
|
||||
--breadcrumb-item-text-color: var(--body-text-color);
|
||||
|
||||
/* Card */
|
||||
--card-text-color: #000;
|
||||
--card-border-width: 0 1px 1px 1px;
|
||||
--card-border-style: solid;
|
||||
--card-border-color: #ccc;
|
||||
--card-progress-bar-color: var(--primary-color);
|
||||
--card-overlay-bg-color: rgba(0, 0, 0, 0);
|
||||
--card-overlay-hover-bg-color: rgba(0, 0, 0, 0.2);
|
||||
|
||||
/* List items */
|
||||
--list-group-item-text-color: var(--body-text-color);
|
||||
--list-group-item-bg-color: white;
|
||||
--list-group-hover-text-color: var(--body-text-color);
|
||||
--list-group-hover-bg-color: #eaeaea;
|
||||
--list-group-item-border-color: rgba(239, 239, 239, 0.125);
|
||||
--list-group-active-border-color: none;
|
||||
|
||||
/* Dropdown */
|
||||
--dropdown-item-hover-text-color: white;
|
||||
--dropdown-item-hover-bg-color: var(--primary-color);
|
||||
--dropdown-overlay-color: rgba(0,0,0,0.5);
|
||||
--dropdown-item-bg-color: white;
|
||||
|
||||
/* Manga Reader */
|
||||
--manga-reader-overlay-filter: blur(10px);
|
||||
--manga-reader-overlay-bg-color: rgba(0,0,0,0.5);
|
||||
--manga-reader-overlay-text-color: white;
|
||||
--manga-reader-bg-color: black;
|
||||
--manga-reader-next-highlight-bg-color: rgba(65, 225, 100, 0.5);
|
||||
--manga-reader-prev-highlight-bg-color: rgba(65, 105, 225, 0.5);
|
||||
|
||||
/* Radios */
|
||||
--radio-accent-color: var(--primary-color);
|
||||
--radio-hover-accent-color: var(--primary-color-dark-shade);
|
||||
|
||||
/* Carousel */
|
||||
--carousel-header-text-color: black;
|
||||
--carousel-header-text-decoration: none;
|
||||
--carousel-hover-header-text-decoration: underline;
|
||||
|
||||
/** Drawer */
|
||||
--drawer-background-color: white; // TODO: Use bg
|
||||
--drawer-bg-color: white;
|
||||
--drawer-text-color: black;
|
||||
|
||||
/* Pagination */
|
||||
--pagination-active-link-border-color: var(--primary-color);
|
||||
--pagination-active-link-bg-color: var(--primary-color);
|
||||
--pagination-active-link-text-color: white;
|
||||
--pagination-link-border-color: rgba(239, 239, 239, 1);
|
||||
--pagination-link-text-color: black;
|
||||
--pagination-link-bg-color: white;
|
||||
--pagination-focus-border-color: var(--primary-color);
|
||||
--pagination-link-hover-color: var(--primary-color);
|
||||
|
||||
/** Event Widget */
|
||||
--event-widget-bg-color: white;
|
||||
--event-widget-item-bg-color: lightgrey;
|
||||
--event-widget-text-color: black;
|
||||
--event-widget-item-border-color: lightgrey;
|
||||
--event-widget-border-color: lightgrey;
|
||||
|
||||
/* Popover */
|
||||
--popover-body-bg-color: var(--navbar-bg-color);
|
||||
--popover-body-text-color: var(--navbar-text-color);
|
||||
--popover-outerarrow-color: lightgrey;
|
||||
--popover-arrow-color: lightgrey;
|
||||
--popover-bg-color: lightgrey;
|
||||
--popover-border-color: lightgrey;
|
||||
|
||||
/* Accordion */
|
||||
--accordion-header-text-color: rgba(74, 198, 148, 0.9);
|
||||
--accordion-header-bg-color: var(--bs-body-bg);
|
||||
--accordion-body-bg-color: var(--bs-body-bg);
|
||||
--accordion-active-body-bg-color: var(--bs-body-bg);
|
||||
--accordion-body-border-color: rgba(239, 239, 239, 0.125);
|
||||
--accordion-body-text-color: var(--body-text-color);
|
||||
--accordion-header-collapsed-text-color: var(--body-text-color);
|
||||
--accordion-header-collapsed-bg-color: var(--bs-body-bg);
|
||||
--accordion-button-focus-border-color: rgba(74, 198, 148, 0.9);
|
||||
--accordion-button-focus-box-shadow: unset;
|
||||
|
||||
/* Search */
|
||||
--search-result-text-lite-color: rgba(0,0,0,1);
|
||||
|
||||
/* Bulk Selection */
|
||||
--bulk-selection-text-color: var(--navbar-text-color);
|
||||
--bulk-selection-highlight-text-color: var(--primary-color);
|
||||
}
|
||||
@import './dark.scss'; // Just re-import variables from dark since that's all we support
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user