mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-06-04 22:25:36 -04:00
Book Reader Redesign with e-ink focus (#1246)
* Refactored the drawer into offcanvas component. Had to write some hacks to emulate how bootstrap's javascript implementation works as ngBootstrap doesn't have a component yet. * Cleaned up some of the code * Rewrote drawer to align it with the new design * First pass, refactored table of content into it's own component * Refactored all of the settings logic into a separate component. Everything is broken. * More settings on on reactive form * More code cleanup on settings * Misc fixes around the drawer code. Fixed a bug where range sliders were inheriting background color of normal text inputs * Fixed dark mode with book reader. We now clear the theme from the main app so book reader is self-contained. Styles for dark mode are injected into the reading-section. Styles that were previously in scss are now only for the actual menu system. * Cleaned up drawer styling on header * Removed an ngIf statement for click to paginate * Tweaked the accent style to have smaller font size and adjusted style on light mode. Cleaned up some clearTimeout code in a further effort to streamline codebase. * Refactored Dark mode into a basic theme. Currently styles are hardcoded. * Patched book theme in from themes branch * Patched in the backend for Book Theme (not tested yet) * Fixed a bug in seeding code for book themes. Started integration of themes into the reader settings * Everything except managing themes is working. Themes are a bit shakey, having second thoughts if we should have them or not. * Reverted the ability to do custom user book themes. Code is stable with system themes. * Stablize the Styles (#1128) * Fixed a bug where adding multiple series to reading list would throw an error on UI, but it was successful. * When a series has a reading list, we now show the connection on Series detail. * Removed all baseurl code from UI and not-connected component since we no longer use it. * Fixed tag badges not showing a border. Added last read time to the series detail page * Fixed up error interceptor to remove no-connection code * Changed implementation for series detail. Book libraries will never send chapters back. Volume 0 volumes will not be sent in volumes ever. Fixed up more renaming logic on books to send more accurate representations to the UI. * Cleaned up the selected tab and tab display logic * Fixed a bad where statement in reading lists for series * Fixed up tab logic again * Fixed a small margin on search backdrop * Made badge expander button smaller to align with badges * Fixed a few UIs due to .form-group and .form-row being removed * Updated Theme component page to help with style testing * Added more components to theme tester * Cleaned up some styling * Fixed opacity on search item hover * Bump versions by dotnet-bump-version. * Tweaked the accordion styles for light mode * Set dark book theme as default. Refactored resetSettings to be much cleaner * Started the refactor to allow book themes to affect global css variables * Fixed some issues with my css variable declarations * Fixed a close model state update * Lots of work, but dark mode on the book reader is basically done. We have to code the themes much like the site themes * Some black theme enhancements * Started working on column layout in book reader. * Cleaned up the CSS on Reader Settings * Hooked up reading direction * Got column and double column layout working * Implemented some basic virtual paging and hooked in book color theme and layout mode into user preferences. * Migration wrote, can edit page layout and color theme on book reader. Removed book dark mode since no longer needed. Fixed a bug on login/register forms where when input is focused, text is white and not black. * When loading book reader, apply column layout. * Lots of work around 2 column layout, working on images not splitting. Still not working, committing so i can merge develop in and validate code with new manga reader. * Fixed images being split into 2 BUT regression on each page boundary, total reading height is smaller and smaller * Fixed some rendering bugs where toggling column layouts would shrink images on screen constantly. Fixed a bug where bottom bar wouldn't render on column layout in some conditions (this might need to be reworked) * Started progress on progress work * Updated .NET to 6.0.4 * Fixed a bug where DataContextModelSnapshot was being removed on build thus new migrations were broken. * Tweaked the code around progress saving so that we don't loose track of last scroll element on page load * Trying to restore progress, but stuck * Extra merge stuff * Fixed a bug where volumes that are a range fail to generate series detail * No gutters on whole app. Book reader backend now applies the image class automatically at the backend. * Added wiki documentation into invite user flow and register admin user to help users understand email isn't required and they can host their own service. * Removed bottom padding * Refactored the document height to be set and removed on nav service, so the book reader and manga reader aren't broken. * Fixed the height of the action bar to simplify logic and keep the code cleaner. Refactored book service image scoping to be much more streamlined and efficient * Fixed the height of action bar to 62px and adjusted code to use the hardcoded px. (code commented) * Removed commented out code from fixed action bar height * Progress restoration seems to be working * Code cleanup * Ensure the bottom action bar is at the bottom of the viewport on small pages * Fixed book fonts not setting properly and added OpenDyslexic font. * Fixed up some font issues * Updated drawer so all sections are open by default * Switched some LINQ to use MinBy * When navigating between pages and column layout, adjust the shift for the user. * Removed some debug code * Blacklist .qpkg folders and don't scan Recently-Snapshot or recycle folders. * Renamed the scale width to be scoped to kavita to avoid conflicts. * Refactored ngx-sliders out to use normal range instead. Changed up the preferences to separate image and book settinngs into own accordion. * updated user preferences for new migration options (not committed yet) * Removed some debug code * Remove console.logs * Migration committed, let's release this to users. * A lot of crazy code just to ensure that when you close drawer the toggle reflectst that state.
This commit is contained in:
parent
641e0a71e9
commit
2723a6cd10
@ -25,7 +25,7 @@ namespace API.Tests.Services;
|
|||||||
|
|
||||||
public class SiteThemeServiceTests
|
public class SiteThemeServiceTests
|
||||||
{
|
{
|
||||||
private readonly ILogger<SiteThemeService> _logger = Substitute.For<ILogger<SiteThemeService>>();
|
private readonly ILogger<ThemeService> _logger = Substitute.For<ILogger<ThemeService>>();
|
||||||
private readonly IEventHub _messageHub = Substitute.For<IEventHub>();
|
private readonly IEventHub _messageHub = Substitute.For<IEventHub>();
|
||||||
|
|
||||||
private readonly DbConnection _connection;
|
private readonly DbConnection _connection;
|
||||||
@ -135,7 +135,7 @@ public class SiteThemeServiceTests
|
|||||||
var filesystem = CreateFileSystem();
|
var filesystem = CreateFileSystem();
|
||||||
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData(""));
|
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData(""));
|
||||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||||
var siteThemeService = new SiteThemeService(ds, _unitOfWork, _messageHub);
|
var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub);
|
||||||
await siteThemeService.Scan();
|
await siteThemeService.Scan();
|
||||||
|
|
||||||
Assert.NotNull(await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("custom"));
|
Assert.NotNull(await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("custom"));
|
||||||
@ -148,7 +148,7 @@ public class SiteThemeServiceTests
|
|||||||
var filesystem = CreateFileSystem();
|
var filesystem = CreateFileSystem();
|
||||||
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData(""));
|
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData(""));
|
||||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||||
var siteThemeService = new SiteThemeService(ds, _unitOfWork, _messageHub);
|
var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub);
|
||||||
await siteThemeService.Scan();
|
await siteThemeService.Scan();
|
||||||
|
|
||||||
Assert.NotNull(await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("custom"));
|
Assert.NotNull(await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("custom"));
|
||||||
@ -167,7 +167,7 @@ public class SiteThemeServiceTests
|
|||||||
var filesystem = CreateFileSystem();
|
var filesystem = CreateFileSystem();
|
||||||
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData(""));
|
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData(""));
|
||||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||||
var siteThemeService = new SiteThemeService(ds, _unitOfWork, _messageHub);
|
var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub);
|
||||||
await siteThemeService.Scan();
|
await siteThemeService.Scan();
|
||||||
|
|
||||||
Assert.NotNull(await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("custom"));
|
Assert.NotNull(await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("custom"));
|
||||||
@ -188,7 +188,7 @@ public class SiteThemeServiceTests
|
|||||||
var filesystem = CreateFileSystem();
|
var filesystem = CreateFileSystem();
|
||||||
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123"));
|
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123"));
|
||||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||||
var siteThemeService = new SiteThemeService(ds, _unitOfWork, _messageHub);
|
var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub);
|
||||||
|
|
||||||
_context.SiteTheme.Add(new SiteTheme()
|
_context.SiteTheme.Add(new SiteTheme()
|
||||||
{
|
{
|
||||||
@ -213,7 +213,7 @@ public class SiteThemeServiceTests
|
|||||||
var filesystem = CreateFileSystem();
|
var filesystem = CreateFileSystem();
|
||||||
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123"));
|
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123"));
|
||||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||||
var siteThemeService = new SiteThemeService(ds, _unitOfWork, _messageHub);
|
var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub);
|
||||||
|
|
||||||
_context.SiteTheme.Add(new SiteTheme()
|
_context.SiteTheme.Add(new SiteTheme()
|
||||||
{
|
{
|
||||||
@ -241,7 +241,7 @@ public class SiteThemeServiceTests
|
|||||||
var filesystem = CreateFileSystem();
|
var filesystem = CreateFileSystem();
|
||||||
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123"));
|
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123"));
|
||||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||||
var siteThemeService = new SiteThemeService(ds, _unitOfWork, _messageHub);
|
var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub);
|
||||||
|
|
||||||
_context.SiteTheme.Add(new SiteTheme()
|
_context.SiteTheme.Add(new SiteTheme()
|
||||||
{
|
{
|
||||||
|
@ -94,16 +94,12 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Remove="Interfaces\IMetadataService.cs" />
|
|
||||||
<Compile Remove="obj\**" />
|
<Compile Remove="obj\**" />
|
||||||
<Compile Remove="cache\**" />
|
<Compile Remove="cache\**" />
|
||||||
<Compile Remove="backups\**" />
|
<Compile Remove="backups\**" />
|
||||||
<Compile Remove="logs\**" />
|
<Compile Remove="logs\**" />
|
||||||
<Compile Remove="temp\**" />
|
<Compile Remove="temp\**" />
|
||||||
<Compile Remove="covers\**" />
|
<Compile Remove="covers\**" />
|
||||||
<Compile Remove="DTOs\Email\SmtpConfig.cs" />
|
|
||||||
<Compile Remove="DTOs\Email\EmailOptionsDto.cs" />
|
|
||||||
<Compile Remove="Helpers\Converters\SmtpConverter.cs" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.DTOs.Theme;
|
using API.DTOs.Theme;
|
||||||
|
using API.Extensions;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Tasks;
|
using API.Services.Tasks;
|
||||||
using Kavita.Common;
|
using Kavita.Common;
|
||||||
@ -13,13 +14,13 @@ namespace API.Controllers;
|
|||||||
public class ThemeController : BaseApiController
|
public class ThemeController : BaseApiController
|
||||||
{
|
{
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly ISiteThemeService _siteThemeService;
|
private readonly IThemeService _themeService;
|
||||||
private readonly ITaskScheduler _taskScheduler;
|
private readonly ITaskScheduler _taskScheduler;
|
||||||
|
|
||||||
public ThemeController(IUnitOfWork unitOfWork, ISiteThemeService siteThemeService, ITaskScheduler taskScheduler)
|
public ThemeController(IUnitOfWork unitOfWork, IThemeService themeService, ITaskScheduler taskScheduler)
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_siteThemeService = siteThemeService;
|
_themeService = themeService;
|
||||||
_taskScheduler = taskScheduler;
|
_taskScheduler = taskScheduler;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,9 +40,9 @@ public class ThemeController : BaseApiController
|
|||||||
|
|
||||||
[Authorize("RequireAdminRole")]
|
[Authorize("RequireAdminRole")]
|
||||||
[HttpPost("update-default")]
|
[HttpPost("update-default")]
|
||||||
public async Task<ActionResult> UpdateDefault(UpdateDefaultSiteThemeDto dto)
|
public async Task<ActionResult> UpdateDefault(UpdateDefaultThemeDto dto)
|
||||||
{
|
{
|
||||||
await _siteThemeService.UpdateDefault(dto.ThemeId);
|
await _themeService.UpdateDefault(dto.ThemeId);
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,7 +55,7 @@ public class ThemeController : BaseApiController
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return Ok(await _siteThemeService.GetContent(themeId));
|
return Ok(await _themeService.GetContent(themeId));
|
||||||
}
|
}
|
||||||
catch (KavitaException ex)
|
catch (KavitaException ex)
|
||||||
{
|
{
|
||||||
|
@ -82,11 +82,12 @@ namespace API.Controllers
|
|||||||
existingPreferences.BookReaderMargin = preferencesDto.BookReaderMargin;
|
existingPreferences.BookReaderMargin = preferencesDto.BookReaderMargin;
|
||||||
existingPreferences.BookReaderLineSpacing = preferencesDto.BookReaderLineSpacing;
|
existingPreferences.BookReaderLineSpacing = preferencesDto.BookReaderLineSpacing;
|
||||||
existingPreferences.BookReaderFontFamily = preferencesDto.BookReaderFontFamily;
|
existingPreferences.BookReaderFontFamily = preferencesDto.BookReaderFontFamily;
|
||||||
existingPreferences.BookReaderDarkMode = preferencesDto.BookReaderDarkMode;
|
|
||||||
existingPreferences.BookReaderFontSize = preferencesDto.BookReaderFontSize;
|
existingPreferences.BookReaderFontSize = preferencesDto.BookReaderFontSize;
|
||||||
existingPreferences.BookReaderTapToPaginate = preferencesDto.BookReaderTapToPaginate;
|
existingPreferences.BookReaderTapToPaginate = preferencesDto.BookReaderTapToPaginate;
|
||||||
existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection;
|
existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection;
|
||||||
preferencesDto.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
|
preferencesDto.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
|
||||||
|
existingPreferences.BookThemeName = preferencesDto.BookReaderThemeName;
|
||||||
|
existingPreferences.PageLayoutMode = preferencesDto.BookReaderLayoutMode;
|
||||||
existingPreferences.Theme = await _unitOfWork.SiteThemeRepository.GetThemeById(preferencesDto.Theme.Id);
|
existingPreferences.Theme = await _unitOfWork.SiteThemeRepository.GetThemeById(preferencesDto.Theme.Id);
|
||||||
|
|
||||||
// TODO: Remove this code - this overrides layout mode to be single until the mode is released
|
// TODO: Remove this code - this overrides layout mode to be single until the mode is released
|
||||||
|
@ -4,6 +4,9 @@ using API.Services;
|
|||||||
|
|
||||||
namespace API.DTOs.Theme;
|
namespace API.DTOs.Theme;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a set of css overrides the user can upload to Kavita and will load into webui
|
||||||
|
/// </summary>
|
||||||
public class SiteThemeDto
|
public class SiteThemeDto
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
namespace API.DTOs.Theme;
|
namespace API.DTOs.Theme;
|
||||||
|
|
||||||
public class UpdateDefaultSiteThemeDto
|
public class UpdateDefaultThemeDto
|
||||||
{
|
{
|
||||||
public int ThemeId { get; set; }
|
public int ThemeId { get; set; }
|
||||||
}
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using API.Entities;
|
using API.DTOs.Theme;
|
||||||
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
|
||||||
namespace API.DTOs
|
namespace API.DTOs
|
||||||
@ -74,5 +75,7 @@ namespace API.DTOs
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Should default to Dark</remarks>
|
/// <remarks>Should default to Dark</remarks>
|
||||||
public SiteTheme Theme { get; set; }
|
public SiteTheme Theme { get; set; }
|
||||||
|
public string BookReaderThemeName { get; set; }
|
||||||
|
public BookPageLayoutMode BookReaderLayoutMode { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,33 +44,40 @@ namespace API.Data
|
|||||||
public DbSet<SeriesRelation> SeriesRelation { get; set; }
|
public DbSet<SeriesRelation> SeriesRelation { get; set; }
|
||||||
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(builder);
|
||||||
|
|
||||||
|
|
||||||
modelBuilder.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();
|
||||||
|
|
||||||
modelBuilder.Entity<AppRole>()
|
builder.Entity<AppRole>()
|
||||||
.HasMany(ur => ur.UserRoles)
|
.HasMany(ur => ur.UserRoles)
|
||||||
.WithOne(u => u.Role)
|
.WithOne(u => u.Role)
|
||||||
.HasForeignKey(ur => ur.RoleId)
|
.HasForeignKey(ur => ur.RoleId)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
modelBuilder.Entity<SeriesRelation>()
|
builder.Entity<SeriesRelation>()
|
||||||
.HasOne(pt => pt.Series)
|
.HasOne(pt => pt.Series)
|
||||||
.WithMany(p => p.Relations)
|
.WithMany(p => p.Relations)
|
||||||
.HasForeignKey(pt => pt.SeriesId)
|
.HasForeignKey(pt => pt.SeriesId)
|
||||||
.OnDelete(DeleteBehavior.ClientCascade);
|
.OnDelete(DeleteBehavior.ClientCascade);
|
||||||
|
|
||||||
modelBuilder.Entity<SeriesRelation>()
|
builder.Entity<SeriesRelation>()
|
||||||
.HasOne(pt => pt.TargetSeries)
|
.HasOne(pt => pt.TargetSeries)
|
||||||
.WithMany(t => t.RelationOf)
|
.WithMany(t => t.RelationOf)
|
||||||
.HasForeignKey(pt => pt.TargetSeriesId);
|
.HasForeignKey(pt => pt.TargetSeriesId);
|
||||||
|
|
||||||
|
builder.Entity<AppUserPreferences>()
|
||||||
|
.Property(b => b.BookThemeName)
|
||||||
|
.HasDefaultValue("Dark");
|
||||||
|
builder.Entity<AppUserPreferences>()
|
||||||
|
.Property(b => b.BackgroundColor)
|
||||||
|
.HasDefaultValue("#000000");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
1523
API/Data/Migrations/20220508162841_BookReaderUpdate.Designer.cs
generated
Normal file
1523
API/Data/Migrations/20220508162841_BookReaderUpdate.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
56
API/Data/Migrations/20220508162841_BookReaderUpdate.cs
Normal file
56
API/Data/Migrations/20220508162841_BookReaderUpdate.cs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class BookReaderUpdate : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "BookReaderDarkMode",
|
||||||
|
table: "AppUserPreferences",
|
||||||
|
newName: "PageLayoutMode");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "BackgroundColor",
|
||||||
|
table: "AppUserPreferences",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true,
|
||||||
|
defaultValue: "#000000",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "TEXT",
|
||||||
|
oldNullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "BookThemeName",
|
||||||
|
table: "AppUserPreferences",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true,
|
||||||
|
defaultValue: "Dark");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "BookThemeName",
|
||||||
|
table: "AppUserPreferences");
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "PageLayoutMode",
|
||||||
|
table: "AppUserPreferences",
|
||||||
|
newName: "BookReaderDarkMode");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "BackgroundColor",
|
||||||
|
table: "AppUserPreferences",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "TEXT",
|
||||||
|
oldNullable: true,
|
||||||
|
oldDefaultValue: "#000000");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -15,7 +15,7 @@ namespace API.Data.Migrations
|
|||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.3");
|
modelBuilder.HasAnnotation("ProductVersion", "6.0.4");
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||||
{
|
{
|
||||||
@ -166,10 +166,9 @@ namespace API.Data.Migrations
|
|||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("BackgroundColor")
|
b.Property<string>("BackgroundColor")
|
||||||
.HasColumnType("TEXT");
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
b.Property<bool>("BookReaderDarkMode")
|
.HasDefaultValue("#000000");
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("BookReaderFontFamily")
|
b.Property<string>("BookReaderFontFamily")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
@ -189,9 +188,17 @@ namespace API.Data.Migrations
|
|||||||
b.Property<bool>("BookReaderTapToPaginate")
|
b.Property<bool>("BookReaderTapToPaginate")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("BookThemeName")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("Dark");
|
||||||
|
|
||||||
b.Property<int>("LayoutMode")
|
b.Property<int>("LayoutMode")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("PageLayoutMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int>("PageSplitOption")
|
b.Property<int>("PageSplitOption")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
@ -19,7 +19,6 @@ public interface ISiteThemeRepository
|
|||||||
Task<SiteThemeDto> GetThemeDtoByName(string themeName);
|
Task<SiteThemeDto> GetThemeDtoByName(string themeName);
|
||||||
Task<SiteTheme> GetDefaultTheme();
|
Task<SiteTheme> GetDefaultTheme();
|
||||||
Task<IEnumerable<SiteTheme>> GetThemes();
|
Task<IEnumerable<SiteTheme>> GetThemes();
|
||||||
|
|
||||||
Task<SiteTheme> GetThemeById(int themeId);
|
Task<SiteTheme> GetThemeById(int themeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
@ -25,6 +25,7 @@ namespace API.Entities
|
|||||||
/// </example>
|
/// </example>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ReaderMode ReaderMode { get; set; }
|
public ReaderMode ReaderMode { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Manga Reader Option: Allow the menu to close after 6 seconds without interaction
|
/// Manga Reader Option: Allow the menu to close after 6 seconds without interaction
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -42,10 +43,6 @@ namespace API.Entities
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string BackgroundColor { get; set; } = "#000000";
|
public string BackgroundColor { get; set; } = "#000000";
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Book Reader Option: Should the background color be dark
|
|
||||||
/// </summary>
|
|
||||||
public bool BookReaderDarkMode { get; set; } = true;
|
|
||||||
/// <summary>
|
|
||||||
/// Book Reader Option: Override extra Margin
|
/// Book Reader Option: Override extra Margin
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int BookReaderMargin { get; set; } = 15;
|
public int BookReaderMargin { get; set; } = 15;
|
||||||
@ -74,7 +71,17 @@ namespace API.Entities
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Should default to Dark</remarks>
|
/// <remarks>Should default to Dark</remarks>
|
||||||
public SiteTheme Theme { get; set; }
|
public SiteTheme Theme { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Book Reader Option: The color theme to decorate the book contents
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Should default to Dark</remarks>
|
||||||
|
public string BookThemeName { get; set; } = "Dark";
|
||||||
|
/// <summary>
|
||||||
|
/// Book Reader Option: The way a page from a book is rendered. Default is as book dictates, 1 column is fit to height,
|
||||||
|
/// 2 column is fit to height, 2 columns
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Defaults to Default</remarks>
|
||||||
|
public BookPageLayoutMode PageLayoutMode { get; set; } = BookPageLayoutMode.Default;
|
||||||
|
|
||||||
|
|
||||||
public AppUser AppUser { get; set; }
|
public AppUser AppUser { get; set; }
|
||||||
|
13
API/Entities/Enums/BookPageLayoutMode.cs
Normal file
13
API/Entities/Enums/BookPageLayoutMode.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace API.Entities.Enums;
|
||||||
|
|
||||||
|
public enum BookPageLayoutMode
|
||||||
|
{
|
||||||
|
[Description("Default")]
|
||||||
|
Default = 0,
|
||||||
|
[Description("1 Column")]
|
||||||
|
Column1 = 1,
|
||||||
|
[Description("2 Column")]
|
||||||
|
Column2 = 2
|
||||||
|
}
|
15
API/Entities/Interfaces/ITheme.cs
Normal file
15
API/Entities/Interfaces/ITheme.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using API.Entities.Enums.Theme;
|
||||||
|
|
||||||
|
namespace API.Entities.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A theme in some kind
|
||||||
|
/// </summary>
|
||||||
|
public interface ITheme
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string NormalizedName { get; set; }
|
||||||
|
public string FileName { get; set; }
|
||||||
|
public bool IsDefault { get; set; }
|
||||||
|
public ThemeProvider Provider { get; set; }
|
||||||
|
}
|
@ -8,7 +8,7 @@ namespace API.Entities;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a set of css overrides the user can upload to Kavita and will load into webui
|
/// Represents a set of css overrides the user can upload to Kavita and will load into webui
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SiteTheme : IEntityDate
|
public class SiteTheme : IEntityDate, ITheme
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -23,6 +23,7 @@ public class SiteTheme : IEntityDate
|
|||||||
/// File path to the content. Stored under <see cref="DirectoryService.SiteThemeDirectory"/>.
|
/// File path to the content. Stored under <see cref="DirectoryService.SiteThemeDirectory"/>.
|
||||||
/// Must be a .css file
|
/// Must be a .css file
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>System provided themes use an alternative location as they are packaged with the app</remarks>
|
||||||
public string FileName { get; set; }
|
public string FileName { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Only one theme can have this. Will auto-set this as default for new user accounts
|
/// Only one theme can have this. Will auto-set this as default for new user accounts
|
||||||
|
@ -40,7 +40,7 @@ namespace API.Extensions
|
|||||||
services.AddScoped<IAccountService, AccountService>();
|
services.AddScoped<IAccountService, AccountService>();
|
||||||
services.AddScoped<IEmailService, EmailService>();
|
services.AddScoped<IEmailService, EmailService>();
|
||||||
services.AddScoped<IBookmarkService, BookmarkService>();
|
services.AddScoped<IBookmarkService, BookmarkService>();
|
||||||
services.AddScoped<ISiteThemeService, SiteThemeService>();
|
services.AddScoped<IThemeService, ThemeService>();
|
||||||
services.AddScoped<ISeriesService, SeriesService>();
|
services.AddScoped<ISeriesService, SeriesService>();
|
||||||
|
|
||||||
|
|
||||||
|
@ -107,7 +107,13 @@ namespace API.Helpers
|
|||||||
CreateMap<AppUserPreferences, UserPreferencesDto>()
|
CreateMap<AppUserPreferences, UserPreferencesDto>()
|
||||||
.ForMember(dest => dest.Theme,
|
.ForMember(dest => dest.Theme,
|
||||||
opt =>
|
opt =>
|
||||||
opt.MapFrom(src => src.Theme));
|
opt.MapFrom(src => src.Theme))
|
||||||
|
.ForMember(dest => dest.BookReaderThemeName,
|
||||||
|
opt =>
|
||||||
|
opt.MapFrom(src => src.BookThemeName))
|
||||||
|
.ForMember(dest => dest.BookReaderLayoutMode,
|
||||||
|
opt =>
|
||||||
|
opt.MapFrom(src => src.PageLayoutMode));
|
||||||
|
|
||||||
|
|
||||||
CreateMap<AppUserBookmark, BookmarkDto>();
|
CreateMap<AppUserBookmark, BookmarkDto>();
|
||||||
|
@ -43,7 +43,7 @@ namespace API.Parser
|
|||||||
MatchOptions, RegexTimeout);
|
MatchOptions, RegexTimeout);
|
||||||
|
|
||||||
|
|
||||||
private static readonly string XmlRegexExtensions = @"\.xml";
|
private const string XmlRegexExtensions = @"\.xml";
|
||||||
private static readonly Regex ImageRegex = new Regex(ImageFileExtensions,
|
private static readonly Regex ImageRegex = new Regex(ImageFileExtensions,
|
||||||
MatchOptions, RegexTimeout);
|
MatchOptions, RegexTimeout);
|
||||||
private static readonly Regex ArchiveFileRegex = new Regex(ArchiveFileExtensions,
|
private static readonly Regex ArchiveFileRegex = new Regex(ArchiveFileExtensions,
|
||||||
@ -999,7 +999,7 @@ namespace API.Parser
|
|||||||
|
|
||||||
public static bool HasBlacklistedFolderInPath(string path)
|
public static bool HasBlacklistedFolderInPath(string path)
|
||||||
{
|
{
|
||||||
return path.Contains("__MACOSX") || path.StartsWith("@Recently-Snapshot") || path.StartsWith("@recycle") || path.StartsWith("._");
|
return path.Contains("__MACOSX") || path.StartsWith("@Recently-Snapshot") || path.StartsWith("@recycle") || path.StartsWith("._") || path.Contains(".qpkg");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -183,6 +183,7 @@ namespace API.Services
|
|||||||
|
|
||||||
EscapeFontFamilyReferences(ref stylesheetHtml, apiBase, prepend);
|
EscapeFontFamilyReferences(ref stylesheetHtml, apiBase, prepend);
|
||||||
|
|
||||||
|
|
||||||
// Check if there are any background images and rewrite those urls
|
// Check if there are any background images and rewrite those urls
|
||||||
EscapeCssImageReferences(ref stylesheetHtml, apiBase, book);
|
EscapeCssImageReferences(ref stylesheetHtml, apiBase, book);
|
||||||
|
|
||||||
@ -246,67 +247,62 @@ namespace API.Services
|
|||||||
private static void ScopeImages(HtmlDocument doc, EpubBookRef book, string apiBase)
|
private static void ScopeImages(HtmlDocument doc, EpubBookRef book, string apiBase)
|
||||||
{
|
{
|
||||||
var images = doc.DocumentNode.SelectNodes("//img");
|
var images = doc.DocumentNode.SelectNodes("//img");
|
||||||
if (images != null)
|
if (images == null) return;
|
||||||
{
|
|
||||||
foreach (var image in images)
|
foreach (var image in images)
|
||||||
{
|
{
|
||||||
if (image.Name != "img") continue;
|
if (image.Name != "img") continue;
|
||||||
|
|
||||||
// Need to do for xlink:href
|
string key = null;
|
||||||
if (image.Attributes["src"] != null)
|
if (image.Attributes["src"] != null)
|
||||||
{
|
{
|
||||||
var imageFile = image.Attributes["src"].Value;
|
key = "src";
|
||||||
|
}
|
||||||
|
else if (image.Attributes["xlink:href"] != null)
|
||||||
|
{
|
||||||
|
key = "xlink:href";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(key)) continue;
|
||||||
|
|
||||||
|
var imageFile = GetKeyForImage(book, image.Attributes[key].Value);
|
||||||
|
image.Attributes.Remove(key);
|
||||||
|
image.Attributes.Add(key, $"{apiBase}" + imageFile);
|
||||||
|
|
||||||
|
// Add a custom class that the reader uses to ensure images stay within reader
|
||||||
|
image.AddClass("kavita-scale-width");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the image key associated with the file. Contains some basic fallback logic.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="book"></param>
|
||||||
|
/// <param name="imageFile"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private static string GetKeyForImage(EpubBookRef book, string imageFile)
|
||||||
|
{
|
||||||
if (!book.Content.Images.ContainsKey(imageFile))
|
if (!book.Content.Images.ContainsKey(imageFile))
|
||||||
{
|
{
|
||||||
// TODO: Refactor the Key code to a method to allow the hacks to be tested
|
|
||||||
var correctedKey = book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile));
|
var correctedKey = book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile));
|
||||||
if (correctedKey != null)
|
if (correctedKey != null)
|
||||||
{
|
{
|
||||||
imageFile = correctedKey;
|
imageFile = correctedKey;
|
||||||
} else if (imageFile.StartsWith(".."))
|
}
|
||||||
|
else if (imageFile.StartsWith(".."))
|
||||||
{
|
{
|
||||||
// There are cases where the key is defined static like OEBPS/Images/1-4.jpg but reference is ../Images/1-4.jpg
|
// There are cases where the key is defined static like OEBPS/Images/1-4.jpg but reference is ../Images/1-4.jpg
|
||||||
correctedKey = book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile.Replace("..", string.Empty)));
|
correctedKey =
|
||||||
|
book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile.Replace("..", string.Empty)));
|
||||||
if (correctedKey != null)
|
if (correctedKey != null)
|
||||||
{
|
{
|
||||||
imageFile = correctedKey;
|
imageFile = correctedKey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
image.Attributes.Remove("src");
|
return imageFile;
|
||||||
image.Attributes.Add("src", $"{apiBase}" + imageFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
images = doc.DocumentNode.SelectNodes("//image");
|
|
||||||
if (images != null)
|
|
||||||
{
|
|
||||||
foreach (var image in images)
|
|
||||||
{
|
|
||||||
if (image.Name != "image") continue;
|
|
||||||
|
|
||||||
if (image.Attributes["xlink:href"] != null)
|
|
||||||
{
|
|
||||||
var imageFile = image.Attributes["xlink:href"].Value;
|
|
||||||
if (!book.Content.Images.ContainsKey(imageFile))
|
|
||||||
{
|
|
||||||
var correctedKey = book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile));
|
|
||||||
if (correctedKey != null)
|
|
||||||
{
|
|
||||||
imageFile = correctedKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
image.Attributes.Remove("xlink:href");
|
|
||||||
image.Attributes.Add("xlink:href", $"{apiBase}" + imageFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string PrepareFinalHtml(HtmlDocument doc, HtmlNode body)
|
private static string PrepareFinalHtml(HtmlDocument doc, HtmlNode body)
|
||||||
|
@ -69,7 +69,7 @@ namespace API.Services
|
|||||||
private readonly ILogger<DirectoryService> _logger;
|
private readonly ILogger<DirectoryService> _logger;
|
||||||
|
|
||||||
private static readonly Regex ExcludeDirectories = new Regex(
|
private static readonly Regex ExcludeDirectories = new Regex(
|
||||||
@"@eaDir|\.DS_Store|\.qpkg",
|
@"@eaDir|\.DS_Store|\.qpkg|__MACOSX|@Recently-Snapshot|@recycle",
|
||||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
private static readonly Regex FileCopyAppend = new Regex(@"\(\d+\)",
|
private static readonly Regex FileCopyAppend = new Regex(@"\(\d+\)",
|
||||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
@ -62,7 +62,7 @@ public class MetadataService : IMetadataService
|
|||||||
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
|
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
|
||||||
private async Task<bool> UpdateChapterCoverImage(Chapter chapter, bool forceUpdate)
|
private async Task<bool> UpdateChapterCoverImage(Chapter chapter, bool forceUpdate)
|
||||||
{
|
{
|
||||||
var firstFile = chapter.Files.OrderBy(x => x.Chapter).FirstOrDefault();
|
var firstFile = chapter.Files.MinBy(x => x.Chapter);
|
||||||
|
|
||||||
if (!_cacheHelper.ShouldUpdateCoverImage(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, chapter.CoverImage), firstFile, chapter.Created, forceUpdate, chapter.CoverImageLocked))
|
if (!_cacheHelper.ShouldUpdateCoverImage(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, chapter.CoverImage), firstFile, chapter.Created, forceUpdate, chapter.CoverImageLocked))
|
||||||
return false;
|
return false;
|
||||||
@ -97,12 +97,13 @@ public class MetadataService : IMetadataService
|
|||||||
null, volume.Created, forceUpdate)) return false;
|
null, volume.Created, forceUpdate)) return false;
|
||||||
|
|
||||||
volume.Chapters ??= new List<Chapter>();
|
volume.Chapters ??= new List<Chapter>();
|
||||||
var firstChapter = volume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting).FirstOrDefault();
|
var firstChapter = volume.Chapters.MinBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting);
|
||||||
if (firstChapter == null) return false;
|
if (firstChapter == null) return false;
|
||||||
|
|
||||||
volume.CoverImage = firstChapter.CoverImage;
|
volume.CoverImage = firstChapter.CoverImage;
|
||||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(volume.Id, MessageFactoryEntityTypes.Volume), false);
|
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(volume.Id, MessageFactoryEntityTypes.Volume), false);
|
||||||
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,8 +134,7 @@ public class MetadataService : IMetadataService
|
|||||||
|
|
||||||
if (!_cacheHelper.CoverImageExists(coverImage))
|
if (!_cacheHelper.CoverImageExists(coverImage))
|
||||||
{
|
{
|
||||||
coverImage = series.Volumes[0].Chapters.OrderBy(c => double.Parse(c.Number), _chapterSortComparerForInChapterSorting)
|
coverImage = series.Volumes[0].Chapters.MinBy(c => double.Parse(c.Number), _chapterSortComparerForInChapterSorting)?.CoverImage;
|
||||||
.FirstOrDefault()?.CoverImage;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
series.CoverImage = firstCover?.CoverImage ?? coverImage;
|
series.CoverImage = firstCover?.CoverImage ?? coverImage;
|
||||||
|
@ -36,7 +36,7 @@ public class TaskScheduler : ITaskScheduler
|
|||||||
|
|
||||||
private readonly IStatsService _statsService;
|
private readonly IStatsService _statsService;
|
||||||
private readonly IVersionUpdaterService _versionUpdaterService;
|
private readonly IVersionUpdaterService _versionUpdaterService;
|
||||||
private readonly ISiteThemeService _siteThemeService;
|
private readonly IThemeService _themeService;
|
||||||
|
|
||||||
public static BackgroundJobServer Client => new BackgroundJobServer();
|
public static BackgroundJobServer Client => new BackgroundJobServer();
|
||||||
private static readonly Random Rnd = new Random();
|
private static readonly Random Rnd = new Random();
|
||||||
@ -45,7 +45,7 @@ public class TaskScheduler : ITaskScheduler
|
|||||||
public TaskScheduler(ICacheService cacheService, ILogger<TaskScheduler> logger, IScannerService scannerService,
|
public TaskScheduler(ICacheService cacheService, ILogger<TaskScheduler> logger, IScannerService scannerService,
|
||||||
IUnitOfWork unitOfWork, IMetadataService metadataService, IBackupService backupService,
|
IUnitOfWork unitOfWork, IMetadataService metadataService, IBackupService backupService,
|
||||||
ICleanupService cleanupService, IStatsService statsService, IVersionUpdaterService versionUpdaterService,
|
ICleanupService cleanupService, IStatsService statsService, IVersionUpdaterService versionUpdaterService,
|
||||||
ISiteThemeService siteThemeService)
|
IThemeService themeService)
|
||||||
{
|
{
|
||||||
_cacheService = cacheService;
|
_cacheService = cacheService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@ -56,7 +56,7 @@ public class TaskScheduler : ITaskScheduler
|
|||||||
_cleanupService = cleanupService;
|
_cleanupService = cleanupService;
|
||||||
_statsService = statsService;
|
_statsService = statsService;
|
||||||
_versionUpdaterService = versionUpdaterService;
|
_versionUpdaterService = versionUpdaterService;
|
||||||
_siteThemeService = siteThemeService;
|
_themeService = themeService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ScheduleTasks()
|
public async Task ScheduleTasks()
|
||||||
@ -131,7 +131,7 @@ public class TaskScheduler : ITaskScheduler
|
|||||||
public void ScanSiteThemes()
|
public void ScanSiteThemes()
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Starting Site Theme scan");
|
_logger.LogInformation("Starting Site Theme scan");
|
||||||
BackgroundJob.Enqueue(() => _siteThemeService.Scan());
|
BackgroundJob.Enqueue(() => _themeService.Scan());
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -149,6 +149,7 @@ public class TaskScheduler : ITaskScheduler
|
|||||||
public void ScanLibrary(int libraryId)
|
public void ScanLibrary(int libraryId)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Enqueuing library scan for: {LibraryId}", libraryId);
|
_logger.LogInformation("Enqueuing library scan for: {LibraryId}", libraryId);
|
||||||
|
// TODO: If a library scan is already queued up for libraryId, don't do anything
|
||||||
BackgroundJob.Enqueue(() => _scannerService.ScanLibrary(libraryId));
|
BackgroundJob.Enqueue(() => _scannerService.ScanLibrary(libraryId));
|
||||||
// When we do a scan, force cache to re-unpack in case page numbers change
|
// When we do a scan, force cache to re-unpack in case page numbers change
|
||||||
BackgroundJob.Enqueue(() => _cleanupService.CleanupCacheDirectory());
|
BackgroundJob.Enqueue(() => _cleanupService.CleanupCacheDirectory());
|
||||||
|
@ -301,6 +301,9 @@ public class ScannerService : IScannerService
|
|||||||
|
|
||||||
await CleanupDbEntities();
|
await CleanupDbEntities();
|
||||||
|
|
||||||
|
// await _eventHub.SendMessageAsync(SignalREvents.NotificationProgress,
|
||||||
|
// MessageFactory.ScanLibraryProgressEvent(libraryId, 1F));
|
||||||
|
|
||||||
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, false));
|
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -712,7 +715,7 @@ public class ScannerService : IScannerService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BUG: The issue here is that people is just from chapter, but series metadata might already have some people on it
|
// NOTE: The issue here is that people is just from chapter, but series metadata might already have some people on it
|
||||||
// I might be able to filter out people that are in locked fields?
|
// I might be able to filter out people that are in locked fields?
|
||||||
var people = chapters.SelectMany(c => c.People).ToList();
|
var people = chapters.SelectMany(c => c.People).ToList();
|
||||||
PersonHelper.KeepOnlySamePeopleBetweenLists(series.Metadata.People,
|
PersonHelper.KeepOnlySamePeopleBetweenLists(series.Metadata.People,
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
@ -7,24 +6,23 @@ using API.Entities;
|
|||||||
using API.Entities.Enums.Theme;
|
using API.Entities.Enums.Theme;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
using Kavita.Common;
|
using Kavita.Common;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
|
||||||
|
|
||||||
namespace API.Services.Tasks;
|
namespace API.Services.Tasks;
|
||||||
|
|
||||||
public interface ISiteThemeService
|
public interface IThemeService
|
||||||
{
|
{
|
||||||
Task<string> GetContent(int themeId);
|
Task<string> GetContent(int themeId);
|
||||||
Task Scan();
|
Task Scan();
|
||||||
Task UpdateDefault(int themeId);
|
Task UpdateDefault(int themeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SiteThemeService : ISiteThemeService
|
public class ThemeService : IThemeService
|
||||||
{
|
{
|
||||||
private readonly IDirectoryService _directoryService;
|
private readonly IDirectoryService _directoryService;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly IEventHub _eventHub;
|
private readonly IEventHub _eventHub;
|
||||||
|
|
||||||
public SiteThemeService(IDirectoryService directoryService, IUnitOfWork unitOfWork, IEventHub eventHub)
|
public ThemeService(IDirectoryService directoryService, IUnitOfWork unitOfWork, IEventHub eventHub)
|
||||||
{
|
{
|
||||||
_directoryService = directoryService;
|
_directoryService = directoryService;
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
@ -36,7 +34,6 @@ public class SiteThemeService : ISiteThemeService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="themeId"></param>
|
/// <param name="themeId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
/// <exception cref="KavitaException"></exception>
|
|
||||||
public async Task<string> GetContent(int themeId)
|
public async Task<string> GetContent(int themeId)
|
||||||
{
|
{
|
||||||
var theme = await _unitOfWork.SiteThemeRepository.GetThemeDto(themeId);
|
var theme = await _unitOfWork.SiteThemeRepository.GetThemeDto(themeId);
|
||||||
@ -55,7 +52,8 @@ public class SiteThemeService : ISiteThemeService
|
|||||||
{
|
{
|
||||||
_directoryService.ExistOrCreate(_directoryService.SiteThemeDirectory);
|
_directoryService.ExistOrCreate(_directoryService.SiteThemeDirectory);
|
||||||
var reservedNames = Seed.DefaultThemes.Select(t => t.NormalizedName).ToList();
|
var reservedNames = Seed.DefaultThemes.Select(t => t.NormalizedName).ToList();
|
||||||
var themeFiles = _directoryService.GetFilesWithExtension(Parser.Parser.NormalizePath(_directoryService.SiteThemeDirectory), @"\.css")
|
var themeFiles = _directoryService
|
||||||
|
.GetFilesWithExtension(Parser.Parser.NormalizePath(_directoryService.SiteThemeDirectory), @"\.css")
|
||||||
.Where(name => !reservedNames.Contains(Parser.Parser.Normalize(name))).ToList();
|
.Where(name => !reservedNames.Contains(Parser.Parser.Normalize(name))).ToList();
|
||||||
|
|
||||||
var allThemes = (await _unitOfWork.SiteThemeRepository.GetThemes()).ToList();
|
var allThemes = (await _unitOfWork.SiteThemeRepository.GetThemes()).ToList();
|
||||||
@ -91,7 +89,8 @@ public class SiteThemeService : ISiteThemeService
|
|||||||
});
|
});
|
||||||
|
|
||||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||||
MessageFactory.SiteThemeProgressEvent(_directoryService.FileSystem.Path.GetFileName(themeFile), themeName, ProgressEventType.Updated));
|
MessageFactory.SiteThemeProgressEvent(_directoryService.FileSystem.Path.GetFileName(themeFile), themeName,
|
||||||
|
ProgressEventType.Updated));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -117,9 +116,9 @@ public class SiteThemeService : ISiteThemeService
|
|||||||
|
|
||||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||||
MessageFactory.SiteThemeProgressEvent("", "", ProgressEventType.Ended));
|
MessageFactory.SiteThemeProgressEvent("", "", ProgressEventType.Ended));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes the theme and any references to it from Pref and sets them to the default at the time.
|
/// Removes the theme and any references to it from Pref and sets them to the default at the time.
|
||||||
/// This commits to DB.
|
/// This commits to DB.
|
||||||
|
@ -66,6 +66,10 @@ namespace API.SignalR
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private const string SiteThemeProgress = "SiteThemeProgress";
|
private const string SiteThemeProgress = "SiteThemeProgress";
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// A custom book theme was removed or added
|
||||||
|
/// </summary>
|
||||||
|
private const string BookThemeProgress = "BookThemeProgress";
|
||||||
|
/// <summary>
|
||||||
/// A type of event that has progress (determinate or indeterminate).
|
/// A type of event that has progress (determinate or indeterminate).
|
||||||
/// The underlying event will have a name to give details on how to handle.
|
/// The underlying event will have a name to give details on how to handle.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -367,5 +371,21 @@ namespace API.SignalR
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static SignalRMessage BookThemeProgressEvent(string subtitle, string themeName, string eventType)
|
||||||
|
{
|
||||||
|
return new SignalRMessage()
|
||||||
|
{
|
||||||
|
Name = BookThemeProgress,
|
||||||
|
Title = "Scanning Book Theme",
|
||||||
|
SubTitle = subtitle,
|
||||||
|
EventType = eventType,
|
||||||
|
Progress = ProgressType.Indeterminate,
|
||||||
|
Body = new
|
||||||
|
{
|
||||||
|
ThemeName = themeName,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
5
UI/Web/src/app/_models/book-page-layout-mode.ts
Normal file
5
UI/Web/src/app/_models/book-page-layout-mode.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export enum BookPageLayoutMode {
|
||||||
|
Default = 0,
|
||||||
|
Column1 = 1,
|
||||||
|
Column2 = 2,
|
||||||
|
}
|
@ -1,3 +0,0 @@
|
|||||||
export interface SiteThemeProgressEvent {
|
|
||||||
themeName: string;
|
|
||||||
}
|
|
3
UI/Web/src/app/_models/events/theme-progress-event.ts
Normal file
3
UI/Web/src/app/_models/events/theme-progress-event.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export interface ThemeProgressEvent {
|
||||||
|
themeName: string;
|
||||||
|
}
|
26
UI/Web/src/app/_models/preferences/book-theme.ts
Normal file
26
UI/Web/src/app/_models/preferences/book-theme.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { ThemeProvider } from "./site-theme";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Theme for the the book reader contents
|
||||||
|
*/
|
||||||
|
export interface BookTheme {
|
||||||
|
name: string;
|
||||||
|
provider: ThemeProvider;
|
||||||
|
/**
|
||||||
|
* Main color (usually background color) that represents the theme
|
||||||
|
*/
|
||||||
|
colorHash: string;
|
||||||
|
isDefault: boolean;
|
||||||
|
/**
|
||||||
|
* Is this theme providing dark mode to the reader aka Should we style the reader controls to be dark mode
|
||||||
|
*/
|
||||||
|
isDarkTheme: boolean;
|
||||||
|
/**
|
||||||
|
* Used to identify the theme on style tag
|
||||||
|
*/
|
||||||
|
selector: string;
|
||||||
|
/**
|
||||||
|
* Inner HTML
|
||||||
|
*/
|
||||||
|
content: string;
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
import { LayoutMode } from 'src/app/manga-reader/_models/layout-mode';
|
import { LayoutMode } from 'src/app/manga-reader/_models/layout-mode';
|
||||||
|
import { BookPageLayoutMode } from '../book-page-layout-mode';
|
||||||
import { PageSplitOption } from './page-split-option';
|
import { PageSplitOption } from './page-split-option';
|
||||||
import { ReaderMode } from './reader-mode';
|
import { ReaderMode } from './reader-mode';
|
||||||
import { ReadingDirection } from './reading-direction';
|
import { ReadingDirection } from './reading-direction';
|
||||||
@ -18,13 +19,14 @@ export interface Preferences {
|
|||||||
showScreenHints: boolean;
|
showScreenHints: boolean;
|
||||||
|
|
||||||
// Book Reader
|
// Book Reader
|
||||||
bookReaderDarkMode: boolean;
|
|
||||||
bookReaderMargin: number;
|
bookReaderMargin: number;
|
||||||
bookReaderLineSpacing: number;
|
bookReaderLineSpacing: number;
|
||||||
bookReaderFontSize: number;
|
bookReaderFontSize: number;
|
||||||
bookReaderFontFamily: string;
|
bookReaderFontFamily: string;
|
||||||
bookReaderTapToPaginate: boolean;
|
bookReaderTapToPaginate: boolean;
|
||||||
bookReaderReadingDirection: ReadingDirection;
|
bookReaderReadingDirection: ReadingDirection;
|
||||||
|
bookReaderThemeName: string;
|
||||||
|
bookReaderLayoutMode: BookPageLayoutMode;
|
||||||
|
|
||||||
// Global
|
// Global
|
||||||
theme: SiteTheme;
|
theme: SiteTheme;
|
||||||
@ -35,3 +37,4 @@ export const scalingOptions = [{text: 'Automatic', value: ScalingOption.Automati
|
|||||||
export const pageSplitOptions = [{text: 'Fit to Screen', value: PageSplitOption.FitSplit}, {text: 'Right to Left', value: PageSplitOption.SplitRightToLeft}, {text: 'Left to Right', value: PageSplitOption.SplitLeftToRight}, {text: 'No Split', value: PageSplitOption.NoSplit}];
|
export const pageSplitOptions = [{text: 'Fit to Screen', value: PageSplitOption.FitSplit}, {text: 'Right to Left', value: PageSplitOption.SplitRightToLeft}, {text: 'Left to Right', value: PageSplitOption.SplitLeftToRight}, {text: 'No Split', value: PageSplitOption.NoSplit}];
|
||||||
export const readingModes = [{text: 'Left to Right', value: ReaderMode.LeftRight}, {text: 'Up to Down', value: ReaderMode.UpDown}, {text: 'Webtoon', value: ReaderMode.Webtoon}];
|
export const readingModes = [{text: 'Left to Right', value: ReaderMode.LeftRight}, {text: 'Up to Down', value: ReaderMode.UpDown}, {text: 'Webtoon', value: ReaderMode.Webtoon}];
|
||||||
export const layoutModes = [{text: 'Single', value: LayoutMode.Single}, {text: 'Double', value: LayoutMode.Double}, {text: 'Double (Manga)', value: LayoutMode.DoubleReversed}];
|
export const layoutModes = [{text: 'Single', value: LayoutMode.Single}, {text: 'Double', value: LayoutMode.Double}, {text: 'Double (Manga)', value: LayoutMode.DoubleReversed}];
|
||||||
|
export const bookLayoutModes = [{text: 'Default', value: BookPageLayoutMode.Default}, {text: '1 Column', value: BookPageLayoutMode.Column1}, {text: '2 Column', value: BookPageLayoutMode.Column2}];
|
||||||
|
@ -6,7 +6,7 @@ import { BehaviorSubject, ReplaySubject } from 'rxjs';
|
|||||||
import { environment } from 'src/environments/environment';
|
import { environment } from 'src/environments/environment';
|
||||||
import { LibraryModifiedEvent } from '../_models/events/library-modified-event';
|
import { LibraryModifiedEvent } from '../_models/events/library-modified-event';
|
||||||
import { NotificationProgressEvent } from '../_models/events/notification-progress-event';
|
import { NotificationProgressEvent } from '../_models/events/notification-progress-event';
|
||||||
import { SiteThemeProgressEvent } from '../_models/events/site-theme-progress-event';
|
import { ThemeProgressEvent } from '../_models/events/theme-progress-event';
|
||||||
import { User } from '../_models/user';
|
import { User } from '../_models/user';
|
||||||
|
|
||||||
export enum EVENTS {
|
export enum EVENTS {
|
||||||
@ -157,7 +157,7 @@ export class MessageHubService {
|
|||||||
this.hubConnection.on(EVENTS.SiteThemeProgress, resp => {
|
this.hubConnection.on(EVENTS.SiteThemeProgress, resp => {
|
||||||
this.messagesSource.next({
|
this.messagesSource.next({
|
||||||
event: EVENTS.SiteThemeProgress,
|
event: EVENTS.SiteThemeProgress,
|
||||||
payload: resp.body as SiteThemeProgressEvent
|
payload: resp.body as ThemeProgressEvent
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -19,4 +19,11 @@ export class ScrollService {
|
|||||||
behavior: 'smooth'
|
behavior: 'smooth'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scrollToX(left: number, el: Element | Window = window) {
|
||||||
|
el.scroll({
|
||||||
|
left: left,
|
||||||
|
behavior: 'auto'
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,13 +10,13 @@ import { SiteTheme, ThemeProvider } from '../_models/preferences/site-theme';
|
|||||||
import { EVENTS, MessageHubService } from './message-hub.service';
|
import { EVENTS, MessageHubService } from './message-hub.service';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class ThemeService implements OnDestroy {
|
export class ThemeService implements OnDestroy {
|
||||||
|
|
||||||
public defaultTheme: string = 'dark';
|
public defaultTheme: string = 'dark';
|
||||||
|
public defaultBookTheme: string = 'Dark';
|
||||||
|
|
||||||
private currentThemeSource = new ReplaySubject<SiteTheme>(1);
|
private currentThemeSource = new ReplaySubject<SiteTheme>(1);
|
||||||
public currentTheme$ = this.currentThemeSource.asObservable();
|
public currentTheme$ = this.currentThemeSource.asObservable();
|
||||||
@ -47,7 +47,7 @@ export class ThemeService implements OnDestroy {
|
|||||||
if (notificationEvent.name !== EVENTS.SiteThemeProgress) return;
|
if (notificationEvent.name !== EVENTS.SiteThemeProgress) return;
|
||||||
|
|
||||||
if (notificationEvent.eventType === 'ended') {
|
if (notificationEvent.eventType === 'ended') {
|
||||||
this.getThemes().subscribe(() => {});
|
if (notificationEvent.name === EVENTS.SiteThemeProgress) this.getThemes().subscribe(() => {});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -61,6 +61,10 @@ export class ThemeService implements OnDestroy {
|
|||||||
return getComputedStyle(this.document.body).getPropertyValue('--color-scheme').trim();
|
return getComputedStyle(this.document.body).getPropertyValue('--color-scheme').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCssVariable(variable: string) {
|
||||||
|
return getComputedStyle(this.document.body).getPropertyValue(variable).trim();
|
||||||
|
}
|
||||||
|
|
||||||
isDarkTheme() {
|
isDarkTheme() {
|
||||||
return this.getColorScheme().toLowerCase() === 'dark';
|
return this.getColorScheme().toLowerCase() === 'dark';
|
||||||
}
|
}
|
||||||
@ -73,6 +77,13 @@ export class ThemeService implements OnDestroy {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used in book reader to remove all themes so book reader can provide custom theming options
|
||||||
|
*/
|
||||||
|
clearThemes() {
|
||||||
|
this.unsetThemes();
|
||||||
|
}
|
||||||
|
|
||||||
setDefault(themeId: number) {
|
setDefault(themeId: number) {
|
||||||
return this.httpClient.post(this.baseUrl + 'theme/update-default', {themeId: themeId}).pipe(map(() => {
|
return this.httpClient.post(this.baseUrl + 'theme/update-default', {themeId: themeId}).pipe(map(() => {
|
||||||
// Refresh the cache when a default state is changed
|
// Refresh the cache when a default state is changed
|
||||||
@ -84,7 +95,24 @@ export class ThemeService implements OnDestroy {
|
|||||||
return this.httpClient.post(this.baseUrl + 'theme/scan', {});
|
return this.httpClient.post(this.baseUrl + 'theme/scan', {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the book theme on the body tag so css variable overrides can take place
|
||||||
|
* @param selector brtheme- prefixed string
|
||||||
|
*/
|
||||||
|
setBookTheme(selector: string) {
|
||||||
|
this.unsetBookThemes();
|
||||||
|
this.renderer.addClass(this.document.querySelector('body'), selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearBookTheme() {
|
||||||
|
this.unsetBookThemes();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the theme as active. Will inject a style tag into document to load a custom theme and apply the selector to the body
|
||||||
|
* @param themeName
|
||||||
|
*/
|
||||||
setTheme(themeName: string) {
|
setTheme(themeName: string) {
|
||||||
const theme = this.themeCache.find(t => t.name.toLowerCase() === themeName.toLowerCase());
|
const theme = this.themeCache.find(t => t.name.toLowerCase() === themeName.toLowerCase());
|
||||||
if (theme) {
|
if (theme) {
|
||||||
@ -132,4 +160,10 @@ export class ThemeService implements OnDestroy {
|
|||||||
private unsetThemes() {
|
private unsetThemes() {
|
||||||
this.themeCache.forEach(theme => this.document.body.classList.remove(theme.selector));
|
this.themeCache.forEach(theme => this.document.body.classList.remove(theme.selector));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private unsetBookThemes() {
|
||||||
|
Array.from(this.document.body.classList).filter(cls => cls.startsWith('brtheme-')).forEach(c => this.document.body.classList.remove(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
39
UI/Web/src/app/_services/toggle.service.ts
Normal file
39
UI/Web/src/app/_services/toggle.service.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { NavigationStart, Router } from '@angular/router';
|
||||||
|
import { filter, ReplaySubject, take } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ToggleService {
|
||||||
|
|
||||||
|
toggleState: boolean = false;
|
||||||
|
|
||||||
|
|
||||||
|
private toggleStateSource: ReplaySubject<boolean> = new ReplaySubject<boolean>(1);
|
||||||
|
public toggleState$ = this.toggleStateSource.asObservable();
|
||||||
|
|
||||||
|
constructor(router: Router) {
|
||||||
|
router.events
|
||||||
|
.pipe(filter(event => event instanceof NavigationStart))
|
||||||
|
.subscribe((event) => {
|
||||||
|
this.toggleState = false;
|
||||||
|
});
|
||||||
|
this.toggleStateSource.next(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
this.toggleState = !this.toggleState;
|
||||||
|
this.toggleStateSource.pipe(take(1)).subscribe(state => {
|
||||||
|
this.toggleState = !state;
|
||||||
|
console.log('Toggle: ', this.toggleState)
|
||||||
|
this.toggleStateSource.next(this.toggleState);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
set(state: boolean) {
|
||||||
|
this.toggleState = state;
|
||||||
|
this.toggleStateSource.next(state);
|
||||||
|
}
|
||||||
|
}
|
114
UI/Web/src/app/book-reader/_models/book-black-theme.ts
Normal file
114
UI/Web/src/app/book-reader/_models/book-black-theme.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
// Important note about themes. Must have one section with .reader-container that contains color, background-color and rest of the styles must be scoped to .book-content
|
||||||
|
export const BookBlackTheme = `
|
||||||
|
:root .brtheme-black {
|
||||||
|
/* General */
|
||||||
|
--color-scheme: dark;
|
||||||
|
--bs-body-color: black;
|
||||||
|
--hr-color: rgba(239, 239, 239, 0.125);
|
||||||
|
--accent-bg-color: rgba(1, 4, 9, 0.5);
|
||||||
|
--accent-text-color: lightgrey;
|
||||||
|
--body-text-color: #efefef;
|
||||||
|
--btn-icon-filter: invert(1) grayscale(100%) brightness(200%);
|
||||||
|
|
||||||
|
/* Drawer */
|
||||||
|
--drawer-bg-color: #292929;
|
||||||
|
--drawer-text-color: white;
|
||||||
|
|
||||||
|
/* Accordion */
|
||||||
|
--accordion-header-text-color: rgba(74, 198, 148, 0.9);
|
||||||
|
--accordion-header-bg-color: rgba(52, 60, 70, 0.5);
|
||||||
|
--accordion-body-bg-color: #292929;
|
||||||
|
--accordion-body-border-color: rgba(239, 239, 239, 0.125);
|
||||||
|
--accordion-body-text-color: var(--body-text-color);
|
||||||
|
--accordion-header-collapsed-text-color: rgba(74, 198, 148, 0.9);
|
||||||
|
--accordion-header-collapsed-bg-color: #292929;
|
||||||
|
--accordion-button-focus-border-color: unset;
|
||||||
|
--accordion-button-focus-box-shadow: unset;
|
||||||
|
--accordion-active-body-bg-color: #292929;
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
--btn-focus-boxshadow-color: rgb(255 255 255 / 50%);
|
||||||
|
--btn-primary-text-color: white;
|
||||||
|
--btn-primary-bg-color: var(--primary-color);
|
||||||
|
--btn-primary-border-color: var(--primary-color);
|
||||||
|
--btn-primary-hover-text-color: white;
|
||||||
|
--btn-primary-hover-bg-color: var(--primary-color-darker-shade);
|
||||||
|
--btn-primary-hover-border-color: var(--primary-color-darker-shade);
|
||||||
|
--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(255 255 255 / 50%);
|
||||||
|
--btn-fa-icon-color: white;
|
||||||
|
--btn-disabled-bg-color: #343a40;
|
||||||
|
--btn-disabled-text-color: white;
|
||||||
|
--btn-disabled-border-color: #6c757d;
|
||||||
|
|
||||||
|
/* Nav (Tabs) */
|
||||||
|
--nav-tab-border-color: rgba(44, 118, 88, 0.7);
|
||||||
|
--nav-tab-text-color: var(--body-text-color);
|
||||||
|
--nav-tab-bg-color: var(--primary-color);
|
||||||
|
--nav-tab-hover-border-color: var(--primary-color);
|
||||||
|
--nav-tab-active-text-color: white;
|
||||||
|
--nav-tab-border-hover-color: transparent;
|
||||||
|
--nav-tab-hover-text-color: var(--body-text-color);
|
||||||
|
--nav-tab-hover-bg-color: transparent;
|
||||||
|
--nav-tab-border-top: rgba(44, 118, 88, 0.7);
|
||||||
|
--nav-tab-border-left: rgba(44, 118, 88, 0.7);
|
||||||
|
--nav-tab-border-bottom: rgba(44, 118, 88, 0.7);
|
||||||
|
--nav-tab-border-right: rgba(44, 118, 88, 0.7);
|
||||||
|
--nav-tab-hover-border-top: rgba(44, 118, 88, 0.7);
|
||||||
|
--nav-tab-hover-border-left: rgba(44, 118, 88, 0.7);
|
||||||
|
--nav-tab-hover-border-bottom: var(--bs-body-bg);
|
||||||
|
--nav-tab-hover-border-right: rgba(44, 118, 88, 0.7);
|
||||||
|
--nav-tab-active-hover-bg-color: var(--primary-color);
|
||||||
|
--nav-link-bg-color: var(--primary-color);
|
||||||
|
--nav-link-active-text-color: white;
|
||||||
|
--nav-link-text-color: white;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Reading Bar */
|
||||||
|
--br-actionbar-button-text-color: white;
|
||||||
|
--br-actionbar-button-hover-border-color: #6c757d;
|
||||||
|
--br-actionbar-bg-color: black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.book-content *:not(input), .book-content *:not(select), .book-content *:not(code), .book-content *:not(:link), .book-content *:not(.ngx-toastr) {
|
||||||
|
color: #dcdcdc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-content code {
|
||||||
|
color: #e83e8c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-content :link, .book-content a {
|
||||||
|
color: #8db2e5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-content img, .book-content img[src] {
|
||||||
|
z-index: 1;
|
||||||
|
filter: brightness(0.85) !important;
|
||||||
|
background-color: initial !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-container {
|
||||||
|
color: #dcdcdc !important;
|
||||||
|
background-image: none !important;
|
||||||
|
background-color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-content *:not(code), .book-content *:not(a) {
|
||||||
|
background-color: black;
|
||||||
|
box-shadow: none;
|
||||||
|
text-shadow: none;
|
||||||
|
border-radius: unset;
|
||||||
|
color: #dcdcdc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-content :visited, .book-content :visited *, .book-content :visited *[class] {color: rgb(211, 138, 138) !important}
|
||||||
|
.book-content :link:not(cite), :link .book-content *:not(cite) {color: #8db2e5 !important}
|
||||||
|
`;
|
119
UI/Web/src/app/book-reader/_models/book-dark-theme.ts
Normal file
119
UI/Web/src/app/book-reader/_models/book-dark-theme.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
// Important note about themes. Styles must be scoped to .book-content if not css variable overrides
|
||||||
|
export const BookDarkTheme = `
|
||||||
|
:root .brtheme-dark {
|
||||||
|
/* General */
|
||||||
|
--color-scheme: dark;
|
||||||
|
--bs-body-color: #292929;
|
||||||
|
--hr-color: rgba(239, 239, 239, 0.125);
|
||||||
|
--accent-bg-color: rgba(1, 4, 9, 0.5);
|
||||||
|
--accent-text-color: lightgrey;
|
||||||
|
--body-text-color: #efefef;
|
||||||
|
--btn-icon-filter: invert(1) grayscale(100%) brightness(200%);
|
||||||
|
|
||||||
|
/* Drawer */
|
||||||
|
--drawer-bg-color: #292929;
|
||||||
|
--drawer-text-color: white;
|
||||||
|
|
||||||
|
/* Accordion */
|
||||||
|
--accordion-header-text-color: rgba(74, 198, 148, 0.9);
|
||||||
|
--accordion-header-bg-color: rgba(52, 60, 70, 0.5);
|
||||||
|
--accordion-body-bg-color: #292929;
|
||||||
|
--accordion-body-border-color: rgba(239, 239, 239, 0.125);
|
||||||
|
--accordion-body-text-color: var(--body-text-color);
|
||||||
|
--accordion-header-collapsed-text-color: rgba(74, 198, 148, 0.9);
|
||||||
|
--accordion-header-collapsed-bg-color: #292929;
|
||||||
|
--accordion-button-focus-border-color: unset;
|
||||||
|
--accordion-button-focus-box-shadow: unset;
|
||||||
|
--accordion-active-body-bg-color: #292929;
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
--btn-focus-boxshadow-color: rgb(255 255 255 / 50%);
|
||||||
|
--btn-primary-text-color: white;
|
||||||
|
--btn-primary-bg-color: var(--primary-color);
|
||||||
|
--btn-primary-border-color: var(--primary-color);
|
||||||
|
--btn-primary-hover-text-color: white;
|
||||||
|
--btn-primary-hover-bg-color: var(--primary-color-darker-shade);
|
||||||
|
--btn-primary-hover-border-color: var(--primary-color-darker-shade);
|
||||||
|
--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(255 255 255 / 50%);
|
||||||
|
--btn-fa-icon-color: white;
|
||||||
|
--btn-disabled-bg-color: #343a40;
|
||||||
|
--btn-disabled-text-color: white;
|
||||||
|
--btn-disabled-border-color: #6c757d;
|
||||||
|
|
||||||
|
/* Nav (Tabs) */
|
||||||
|
--nav-tab-border-color: rgba(44, 118, 88, 0.7);
|
||||||
|
--nav-tab-text-color: var(--body-text-color);
|
||||||
|
--nav-tab-bg-color: var(--primary-color);
|
||||||
|
--nav-tab-hover-border-color: var(--primary-color);
|
||||||
|
--nav-tab-active-text-color: white;
|
||||||
|
--nav-tab-border-hover-color: transparent;
|
||||||
|
--nav-tab-hover-text-color: var(--body-text-color);
|
||||||
|
--nav-tab-hover-bg-color: transparent;
|
||||||
|
--nav-tab-border-top: rgba(44, 118, 88, 0.7);
|
||||||
|
--nav-tab-border-left: rgba(44, 118, 88, 0.7);
|
||||||
|
--nav-tab-border-bottom: rgba(44, 118, 88, 0.7);
|
||||||
|
--nav-tab-border-right: rgba(44, 118, 88, 0.7);
|
||||||
|
--nav-tab-hover-border-top: rgba(44, 118, 88, 0.7);
|
||||||
|
--nav-tab-hover-border-left: rgba(44, 118, 88, 0.7);
|
||||||
|
--nav-tab-hover-border-bottom: var(--bs-body-bg);
|
||||||
|
--nav-tab-hover-border-right: rgba(44, 118, 88, 0.7);
|
||||||
|
--nav-tab-active-hover-bg-color: var(--primary-color);
|
||||||
|
--nav-link-bg-color: var(--primary-color);
|
||||||
|
--nav-link-active-text-color: white;
|
||||||
|
--nav-link-text-color: white;
|
||||||
|
|
||||||
|
/* Checkboxes/Switch */
|
||||||
|
--checkbox-checked-bg-color: var(--primary-color);
|
||||||
|
--checkbox-border-color: var(--input-focused-border-color);
|
||||||
|
--checkbox-focus-border-color: var(--primary-color);
|
||||||
|
--checkbox-focus-boxshadow-color: rgb(255 255 255 / 50%);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Reading Bar */
|
||||||
|
--br-actionbar-button-text-color: white;
|
||||||
|
--br-actionbar-button-hover-border-color: #6c757d;
|
||||||
|
--br-actionbar-bg-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.book-content *:not(input), .book-content *:not(select), .book-content *:not(code), .book-content *:not(:link), .book-content *:not(.ngx-toastr) {
|
||||||
|
color: #dcdcdc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-content code {
|
||||||
|
color: #e83e8c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-content :link, .book-content a {
|
||||||
|
color: #8db2e5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-content img, .book-content img[src] {
|
||||||
|
z-index: 1;
|
||||||
|
filter: brightness(0.85) !important;
|
||||||
|
background-color: initial !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-container {
|
||||||
|
color: #dcdcdc !important;
|
||||||
|
background-image: none !important;
|
||||||
|
background-color: #292929 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-content *:not(code), .book-content *:not(a) {
|
||||||
|
background-color: #292929;
|
||||||
|
box-shadow: none;
|
||||||
|
text-shadow: none;
|
||||||
|
border-radius: unset;
|
||||||
|
color: #dcdcdc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-content :visited, .book-content :visited *, .book-content :visited *[class] {color: rgb(211, 138, 138) !important}
|
||||||
|
.book-content :link:not(cite), :link .book-content *:not(cite) {color: #8db2e5 !important}
|
||||||
|
`;
|
7
UI/Web/src/app/book-reader/_models/book-white-theme.ts
Normal file
7
UI/Web/src/app/book-reader/_models/book-white-theme.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// Important note about themes. Must have one section with .reader-container that contains color, background-color and rest of the styles must be scoped to .book-content
|
||||||
|
export const BookWhiteTheme = `
|
||||||
|
:root() .brtheme-white {
|
||||||
|
--brtheme-link-text-color: green;
|
||||||
|
--brtheme-bg-color: lightgrey;
|
||||||
|
}
|
||||||
|
`;
|
15
UI/Web/src/app/book-reader/_models/theme-font.ts
Normal file
15
UI/Web/src/app/book-reader/_models/theme-font.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* A font family to inject into the book reader
|
||||||
|
*/
|
||||||
|
export interface ThemeFont {
|
||||||
|
/**
|
||||||
|
* Name/Font-family
|
||||||
|
*/
|
||||||
|
fontFamily: string;
|
||||||
|
/**
|
||||||
|
* Where the font is loaded from?
|
||||||
|
*/
|
||||||
|
fontSrc: string;
|
||||||
|
format: 'truetype';
|
||||||
|
|
||||||
|
}
|
@ -5,12 +5,14 @@ import { BookReaderRoutingModule } from './book-reader.router.module';
|
|||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
import { SafeStylePipe } from './safe-style.pipe';
|
import { SafeStylePipe } from './safe-style.pipe';
|
||||||
import { ReactiveFormsModule } from '@angular/forms';
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
import { NgbProgressbarModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
|
||||||
import { PipeModule } from '../pipe/pipe.module';
|
import { PipeModule } from '../pipe/pipe.module';
|
||||||
|
import { NgbAccordionModule, NgbNavModule, NgbProgressbarModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { TableOfContentsComponent } from './table-of-contents/table-of-contents.component';
|
||||||
|
import { ReaderSettingsComponent } from './reader-settings/reader-settings.component';
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [BookReaderComponent, SafeStylePipe],
|
declarations: [BookReaderComponent, SafeStylePipe, TableOfContentsComponent, ReaderSettingsComponent],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
BookReaderRoutingModule,
|
BookReaderRoutingModule,
|
||||||
@ -19,6 +21,9 @@ import { PipeModule } from '../pipe/pipe.module';
|
|||||||
NgbProgressbarModule,
|
NgbProgressbarModule,
|
||||||
NgbTooltipModule,
|
NgbTooltipModule,
|
||||||
PipeModule,
|
PipeModule,
|
||||||
|
NgbTooltipModule,
|
||||||
|
NgbAccordionModule, // Drawer
|
||||||
|
NgbNavModule, // Drawer
|
||||||
], exports: [
|
], exports: [
|
||||||
BookReaderComponent,
|
BookReaderComponent,
|
||||||
SafeStylePipe
|
SafeStylePipe
|
||||||
|
@ -2,74 +2,11 @@
|
|||||||
<div class="fixed-top" #stickyTop>
|
<div class="fixed-top" #stickyTop>
|
||||||
<a class="visually-hidden-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">Skip to main content</a>
|
<a class="visually-hidden-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">Skip to main content</a>
|
||||||
<ng-container [ngTemplateOutlet]="actionBar"></ng-container>
|
<ng-container [ngTemplateOutlet]="actionBar"></ng-container>
|
||||||
<app-drawer #commentDrawer="drawer" [isOpen]="drawerOpen" [style.--drawer-width]="'300px'" [options]="{topOffset: topOffset}" [style.--drawer-background-color]="drawerBackgroundColor" (drawerClosed)="closeDrawer()">
|
<app-drawer #commentDrawer="drawer" [(isOpen)]="drawerOpen" [options]="{topOffset: topOffset}">
|
||||||
<div header>
|
<h5 header>
|
||||||
<h2 style="margin-top: 0.5rem">Book Settings
|
Book Settings
|
||||||
<!-- Temp use times rather than btn-close due to some styling issue -->
|
</h5>
|
||||||
<button type="button" class="btn btn-icon" style="font-size: 2rem" aria-label="Close" (click)="commentDrawer.close()">×</button>
|
<div subheader>
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div body class="drawer-body">
|
|
||||||
<div class="control-container">
|
|
||||||
<div class="controls">
|
|
||||||
|
|
||||||
<form [formGroup]="settingsForm">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="library-type" class="form-label">Font Family</label>
|
|
||||||
<select class="form-control" id="library-type" formControlName="bookReaderFontFamily">
|
|
||||||
<option [value]="opt" *ngFor="let opt of fontFamilies; let i = index">{{opt | titlecase}}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="controls">
|
|
||||||
<label id="fontsize" class="form-label">Font Size</label>
|
|
||||||
<button (click)="updateFontSize(-10)" class="btn btn-icon" title="Decrease" aria-labelledby="fontsize"><i class="fa fa-minus" aria-hidden="true"></i></button>
|
|
||||||
<span>{{pageStyles['font-size']}}</span>
|
|
||||||
<button (click)="updateFontSize(10)" class="btn btn-icon" title="Increase" aria-labelledby="fontsize"><i class="fa fa-plus" aria-hidden="true"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="controls">
|
|
||||||
<label id="linespacing" class="form-label">Line Spacing</label>
|
|
||||||
<button (click)="updateLineSpacing(-10)" class="btn btn-icon" title="Decrease" aria-labelledby="linespacing"><i class="fa fa-minus" aria-hidden="true"></i></button>
|
|
||||||
<span>{{pageStyles['line-height']}}</span>
|
|
||||||
<button (click)="updateLineSpacing(10)" class="btn btn-icon" title="Increase" aria-labelledby="linespacing"><i class="fa fa-plus" aria-hidden="true"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="controls">
|
|
||||||
<label id="margin" class="form-label">Margin</label>
|
|
||||||
<button (click)="updateMargin(-5)" class="btn btn-icon" title="Remove Margin" aria-labelledby="margin"><i class="fa fa-minus" aria-hidden="true"></i></button>
|
|
||||||
<span>{{pageStyles['margin-right']}}</span>
|
|
||||||
<button (click)="updateMargin(5)" class="btn btn-icon" title="Add Margin" aria-labelledby="margin"><i class="fa fa-plus" aria-hidden="true"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="controls">
|
|
||||||
<label id="readingdirection" class="form-label">Reading Direction</label>
|
|
||||||
<button (click)="toggleReadingDirection()" class="btn btn-icon" aria-labelledby="readingdirection" title="{{readingDirection === 0 ? 'Left to Right' : 'Right to Left'}}"><i class="fa {{readingDirection === 0 ? 'fa-arrow-right' : 'fa-arrow-left'}} " aria-hidden="true"></i><span class="d-none d-sm-block"> {{readingDirection === 0 ? 'Left to Right' : 'Right to Left'}}</span></button>
|
|
||||||
</div>
|
|
||||||
<div class="controls">
|
|
||||||
<label id="darkmode" class="form-label">Dark Mode</label>
|
|
||||||
<button (click)="toggleDarkMode(false)" class="btn btn-icon" aria-labelledby="darkmode" title="Off"><i class="fa fa-sun" aria-hidden="true"></i></button>
|
|
||||||
<button (click)="toggleDarkMode(true)" class="btn btn-icon" aria-labelledby="darkmode" title="On"><i class="fa fa-moon" aria-hidden="true"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="controls">
|
|
||||||
<label id="tap-pagination" class="form-label">Tap Pagination <i class="fa fa-info-circle" aria-hidden="true" placement="top" [ngbTooltip]="tapPaginationTooltip" role="button" tabindex="0" aria-describedby="tap-pagination-help"></i></label>
|
|
||||||
<ng-template #tapPaginationTooltip>The ability to click the sides of the page to page left and right</ng-template>
|
|
||||||
<span class="visually-hidden" id="tap-pagination-help">The ability to click the sides of the page to page left and right</span>
|
|
||||||
<button (click)="toggleClickToPaginate()" class="btn btn-icon" aria-labelledby="tap-pagination"><i class="fa fa-arrows-alt-h {{clickToPaginate ? 'icon-primary-color' : ''}}" aria-hidden="true"></i> {{clickToPaginate ? 'On' : 'Off'}}</button>
|
|
||||||
</div>
|
|
||||||
<div class="controls">
|
|
||||||
<label id="fullscreen" class="form-label">Fullscreen <i class="fa fa-info-circle" aria-hidden="true" placement="top" [ngbTooltip]="fullscreenTooltip" role="button" tabindex="0" aria-describedby="fullscreen-help"></i></label>
|
|
||||||
<ng-template #fullscreenTooltip>Put reader in fullscreen mode</ng-template>
|
|
||||||
<span class="visually-hidden" id="fullscreen-help">
|
|
||||||
<ng-container [ngTemplateOutlet]="fullscreenTooltip"></ng-container>
|
|
||||||
</span>
|
|
||||||
<button (click)="toggleFullscreen()" class="btn btn-icon" aria-labelledby="fullscreen">
|
|
||||||
<i class="fa {{this.isFullscreen ? 'fa-compress-alt' : 'fa-expand-alt'}} {{isFullscreen ? 'icon-primary-color' : ''}}" aria-hidden="true"></i>
|
|
||||||
<span *ngIf="darkMode"> {{isFullscreen ? 'Exit' : 'Enter'}}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="row g-0 justify-content-between">
|
|
||||||
<button (click)="resetSettings()" class="btn btn-primary col">Reset to Defaults</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<button class="btn btn-small btn-icon col-1" style="padding-left: 0px" [disabled]="prevChapterDisabled" (click)="loadPrevChapter()" title="Prev Chapter/Volume"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
|
<button class="btn btn-small btn-icon col-1" style="padding-left: 0px" [disabled]="prevChapterDisabled" (click)="loadPrevChapter()" title="Prev Chapter/Volume"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
|
||||||
<div class="col-1 page-stub ps-1">{{pageNum}}</div>
|
<div class="col-1 page-stub ps-1">{{pageNum}}</div>
|
||||||
@ -79,51 +16,56 @@
|
|||||||
<div class="col-1 btn-icon page-stub pe-1" (click)="goToPage(maxPages - 1)" title="Go to last page">{{maxPages - 1}}</div>
|
<div class="col-1 btn-icon page-stub pe-1" (click)="goToPage(maxPages - 1)" title="Go to last page">{{maxPages - 1}}</div>
|
||||||
<button class="btn btn-small btn-icon col-1" style="padding-right: 0px; padding-left: 0px" [disabled]="nextChapterDisabled" (click)="loadNextChapter()" title="Next Chapter/Volume"><i class="fa fa-fast-forward" aria-hidden="true"></i></button>
|
<button class="btn btn-small btn-icon col-1" style="padding-right: 0px; padding-left: 0px" [disabled]="nextChapterDisabled" (click)="loadNextChapter()" title="Next Chapter/Volume"><i class="fa fa-fast-forward" aria-hidden="true"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-of-contents">
|
|
||||||
<h3>Table of Contents</h3>
|
|
||||||
<div *ngIf="chapters.length === 0">
|
|
||||||
<em>This book does not have Table of Contents set in the metadata or a toc file</em>
|
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="chapters.length === 1; else nestedChildren">
|
<div body class="drawer-body">
|
||||||
<ul>
|
<!-- TODO: Center align the tab pills -->
|
||||||
<li *ngFor="let chapter of chapters[0].children">
|
<nav role="navigation">
|
||||||
<a href="javascript:void(0);" (click)="loadChapterPage(chapter.page, chapter.part)">{{chapter.title}}</a>
|
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="nav nav-pills mb-2" [destroyOnHide]="false">
|
||||||
</li>
|
<li [ngbNavItem]="TabID.Settings">
|
||||||
</ul>
|
<a ngbNavLink>Settings</a>
|
||||||
</div>
|
<ng-template ngbNavContent>
|
||||||
<ng-template #nestedChildren>
|
<app-reader-settings
|
||||||
<ul *ngFor="let chapterGroup of chapters" class="chapter-title">
|
(colorThemeUpdate)="setOverrideStyles($event)"
|
||||||
<li class="{{chapterGroup.page == pageNum ? 'active': ''}}" (click)="loadChapterPage(chapterGroup.page, '')">
|
(styleUpdate)="updateReaderStyles($event)"
|
||||||
{{chapterGroup.title}}
|
(clickToPaginateChanged)="showPaginationOverlay($event)"
|
||||||
</li>
|
(fullscreen)="toggleFullscreen()"
|
||||||
<ul *ngFor="let chapter of chapterGroup.children">
|
(layoutModeUpdate)="updateLayoutMode($event)"
|
||||||
<li class="{{cleanIdSelector(chapter.part) === currentPageAnchor ? 'active' : ''}}">
|
(readingDirection)="readingDirection = $event"
|
||||||
<a href="javascript:void(0);" (click)="loadChapterPage(chapter.page, chapter.part)">{{chapter.title}}</a>
|
></app-reader-settings>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</ul>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</div>
|
</li>
|
||||||
|
|
||||||
|
<li [ngbNavItem]="TabID.TableOfContents">
|
||||||
|
<a ngbNavLink>Table of Contents</a>
|
||||||
|
<ng-template ngbNavContent>
|
||||||
|
<app-table-of-contents [chapters]="chapters" [chapterId]="chapterId" [pageNum]="pageNum" [currentPageAnchor]="currentPageAnchor" (loadChapter)="loadChapterPage($event)"></app-table-of-contents>
|
||||||
|
</ng-template>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<div [ngbNavOutlet]="nav" class="mt-3"></div>
|
||||||
</div>
|
</div>
|
||||||
</app-drawer>
|
</app-drawer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div #readingSection class="reading-section" [ngStyle]="{'padding-top': topOffset + 20 + 'px'}"
|
<div #readingSection class="reading-section {{ColumnLayout}}" [ngStyle]="{'padding-top': '62px'}"
|
||||||
[@isLoading]="isLoading ? true : false" (click)="handleReaderClick($event)">
|
[@isLoading]="isLoading ? true : false">
|
||||||
|
|
||||||
<div #readingHtml class="book-content" [ngStyle]="{'padding-bottom': topOffset + 20 + 'px', 'margin': '0px 0px'}"
|
<div #readingHtml class="book-content" [ngStyle]="{'max-height': ColumnHeight, 'column-width': ColumnWidth}"
|
||||||
[innerHtml]="page" *ngIf="page !== undefined"></div>
|
[innerHtml]="page" *ngIf="page !== undefined"></div>
|
||||||
|
|
||||||
<div class="left {{clickOverlayClass('left')}} no-observe" [ngStyle]="{'padding-top': topOffset + 'px'}" (click)="prevPage()" *ngIf="clickToPaginate" tabindex="-1"></div>
|
<ng-container *ngIf="clickToPaginate">
|
||||||
<div class="{{scrollbarNeeded ? 'right-with-scrollbar' : 'right'}} {{clickOverlayClass('right')}} no-observe" [ngStyle]="{'padding-top': topOffset + 'px'}" (click)="nextPage()" *ngIf="clickToPaginate" tabindex="-1"></div>
|
<div class="left {{clickOverlayClass('left')}} no-observe" [ngStyle]="{'padding-top': topOffset + 'px'}" (click)="prevPage()" tabindex="-1"></div>
|
||||||
|
<div class="{{scrollbarNeeded ? 'right-with-scrollbar' : 'right'}} {{clickOverlayClass('right')}} no-observe" [ngStyle]="{'padding-top': topOffset + 'px'}" (click)="nextPage()" tabindex="-1"></div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<div *ngIf="page !== undefined && scrollbarNeeded" (click)="$event.stopPropagation();">
|
<div *ngIf="page !== undefined && (scrollbarNeeded || layoutMode !== BookPageLayoutMode.Default)" (click)="$event.stopPropagation();">
|
||||||
<ng-container [ngTemplateOutlet]="actionBar"></ng-container>
|
<ng-container [ngTemplateOutlet]="actionBar"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-template #actionBar>
|
<ng-template #actionBar>
|
||||||
<div class="reading-bar row g-0 justify-content-between">
|
<div class="action-bar row g-0 justify-content-between">
|
||||||
<button class="btn btn-outline-secondary btn-icon col-2 col-xs-1" (click)="prevPage()"
|
<button class="btn btn-outline-secondary btn-icon col-2 col-xs-1" (click)="prevPage()"
|
||||||
[disabled]="IsPrevDisabled"
|
[disabled]="IsPrevDisabled"
|
||||||
title="{{readingDirection === ReadingDirection.LeftToRight ? 'Previous' : 'Next'}} Page">
|
title="{{readingDirection === ReadingDirection.LeftToRight ? 'Previous' : 'Next'}} Page">
|
||||||
@ -139,8 +81,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #showTitle>
|
<ng-template #showTitle>
|
||||||
<span class="book-title-text">{{bookTitle}}</span>
|
|
||||||
<span *ngIf="incognitoMode" (click)="turnOffIncognito()" role="button" aria-label="Incognito mode is on. Toggle to turn off.">(<i class="fa fa-glasses" aria-hidden="true"></i><span class="visually-hidden">Incognito Mode</span>)</span>
|
<span *ngIf="incognitoMode" (click)="turnOffIncognito()" role="button" aria-label="Incognito mode is on. Toggle to turn off.">(<i class="fa fa-glasses" aria-hidden="true"></i><span class="visually-hidden">Incognito Mode</span>)</span>
|
||||||
|
<span class="book-title-text ms-1" [title]="bookTitle">{{bookTitle}}</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-secondary col-2 col-xs-1" (click)="closeReader()"><i class="fa fa-times-circle" aria-hidden="true"></i><span class="d-none d-sm-block"> Close</span></button>
|
<button class="btn btn-secondary col-2 col-xs-1" (click)="closeReader()"><i class="fa fa-times-circle" aria-hidden="true"></i><span class="d-none d-sm-block"> Close</span></button>
|
||||||
|
@ -28,123 +28,61 @@
|
|||||||
src: url(../../../assets/fonts/RocknRoll_One/RocknRollOne-Regular.ttf) format("truetype");
|
src: url(../../../assets/fonts/RocknRoll_One/RocknRollOne-Regular.ttf) format("truetype");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "OpenDyslexic2";
|
||||||
|
src: url(../../../assets/fonts/OpenDyslexic2/OpenDyslexic-Regular.otf) format("opentype");
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--br-actionbar-button-text-color: #6c757d;
|
||||||
|
--accordion-body-bg-color: black;
|
||||||
|
--accordion-header-bg-color: grey;
|
||||||
|
--br-actionbar-button-hover-border-color: #6c757d;
|
||||||
|
--br-actionbar-bg-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
$dark-form-background-no-opacity: rgb(1, 4, 9);
|
$dark-form-background-no-opacity: rgb(1, 4, 9);
|
||||||
$primary-color: #0062cc;
|
$primary-color: #0062cc;
|
||||||
|
|
||||||
|
|
||||||
|
// Drawer
|
||||||
.control-container {
|
.control-container {
|
||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-of-contents li {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-stub {
|
.page-stub {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
padding-left: 2px;
|
padding-left: 2px;
|
||||||
padding-right: 2px;
|
padding-right: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drawer End
|
||||||
|
|
||||||
.fixed-top {
|
.fixed-top {
|
||||||
z-index: 1022;
|
z-index: 1022;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark-mode {
|
.dark-mode .overlay {
|
||||||
|
opacity: 0;
|
||||||
color: #dcdcdc !important;
|
|
||||||
background-image: none !important;
|
|
||||||
background-color: #292929 !important;
|
|
||||||
|
|
||||||
*:not(code), *:not(a) {
|
|
||||||
background-color: #292929;
|
|
||||||
box-shadow: none;
|
|
||||||
text-shadow: none;
|
|
||||||
border-radius: unset;
|
|
||||||
color: #dcdcdc !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
*:not(input), *:not(code), *:not(:link) {
|
|
||||||
color: #dcdcdc !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
.action-bar {
|
||||||
color: #e83e8c !important;
|
background-color: var(--br-actionbar-bg-color);
|
||||||
}
|
|
||||||
|
|
||||||
.btn-icon {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
:link, a {
|
|
||||||
color: #8db2e5 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
img, img[src] {
|
|
||||||
z-index: 1;
|
|
||||||
filter: brightness(0.85) !important;
|
|
||||||
background-color: initial !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:visited, :visited *, :visited *[class] {color: rgb(211, 138, 138) !important}
|
|
||||||
:link:not(cite), :link *:not(cite) {color: #8db2e5 !important}
|
|
||||||
}
|
|
||||||
|
|
||||||
.reading-bar {
|
|
||||||
background-color: white;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 0 6px 0 rgb(0 0 0 / 70%);
|
box-shadow: 0 0 6px 0 rgb(0 0 0 / 70%);
|
||||||
}
|
max-height: 62px;
|
||||||
|
|
||||||
.dark-mode {
|
.book-title-text {
|
||||||
.reading-bar, .book-title, .drawer-body, .drawer-container {
|
text-align: center;
|
||||||
background-color: $dark-form-background-no-opacity;
|
text-overflow: ellipsis;
|
||||||
}
|
|
||||||
button {
|
|
||||||
background-color: $dark-form-background-no-opacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
&.btn-secondary {
|
|
||||||
border-color: transparent;
|
|
||||||
|
|
||||||
&:hover, &:focus {
|
|
||||||
border-color: #545b62;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.btn-outline-secondary {
|
|
||||||
border-color: transparent;
|
|
||||||
|
|
||||||
&:hover, &:focus {
|
|
||||||
border-color: #545b62;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
background-color: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
background-color: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
::ng-deep .dark-mode .drawer-container {
|
|
||||||
.header, body, *:not(.progress-bar) {
|
|
||||||
background-color: $dark-form-background-no-opacity !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media(max-width: 875px) {
|
@media(max-width: 875px) {
|
||||||
.book-title {
|
.book-title {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-title {
|
.book-title {
|
||||||
@ -152,11 +90,15 @@ $primary-color: #0062cc;
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.reading-section {
|
.reading-section {
|
||||||
max-height: 100vh;
|
max-height: 100vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
//overflow: auto; // This will break progress reporting
|
//overflow: auto; // This will break progress reporting
|
||||||
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reader-container {
|
.reader-container {
|
||||||
@ -166,6 +108,40 @@ $primary-color: #0062cc;
|
|||||||
|
|
||||||
.book-content {
|
.book-content {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
padding-top: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
margin: 0px 0px;
|
||||||
|
height: calc(var(--vh)*100); // This will ensure bottom bar extends to the bottom of the screen
|
||||||
|
|
||||||
|
a, :link {
|
||||||
|
color: var(--brtheme-link-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
background-color: var(--brtheme-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// This is essentially fitting the text to height and when you press next you are scrolling over by page width
|
||||||
|
.column-layout-1 {
|
||||||
|
.book-content {
|
||||||
|
column-count: 1;
|
||||||
|
column-gap: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-layout-2 {
|
||||||
|
.book-content {
|
||||||
|
column-count: 2;
|
||||||
|
column-gap: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A bunch of resets so books render correctly
|
// A bunch of resets so books render correctly
|
||||||
@ -175,18 +151,15 @@ $primary-color: #0062cc;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer-body {
|
|
||||||
padding-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chapter-title {
|
// This is applied to images in the backend
|
||||||
padding-inline-start: 0px
|
::ng-deep .kavita-scale-width {
|
||||||
}
|
|
||||||
|
|
||||||
::ng-deep .scale-width {
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
object-position: top center;
|
object-position: top center;
|
||||||
|
break-inside: avoid;
|
||||||
|
break-before: column;
|
||||||
|
max-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -195,9 +168,8 @@ $primary-color: #0062cc;
|
|||||||
color: $primary-color;
|
color: $primary-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark-mode .overlay {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.right {
|
.right {
|
||||||
@ -246,14 +218,17 @@ $primary-color: #0062cc;
|
|||||||
animation: fadein .5s both;
|
animation: fadein .5s both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
&.btn-secondary {
|
&.btn-secondary {
|
||||||
color: #6c757d;
|
color: var(--br-actionbar-button-text-color);
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
background-color: unset;
|
background-color: unset;
|
||||||
|
|
||||||
&:hover, &:focus {
|
&:hover, &:focus {
|
||||||
border-color: #545b62;
|
border-color: var(--br-actionbar-button-hover-border-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,18 +237,18 @@ $primary-color: #0062cc;
|
|||||||
background-color: unset;
|
background-color: unset;
|
||||||
|
|
||||||
&:hover, &:focus {
|
&:hover, &:focus {
|
||||||
border-color: #545b62;
|
border-color: var(--br-actionbar-button-hover-border-color); // #545b62;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
background-color: unset;
|
background-color: unset;
|
||||||
color: #6c757d;
|
color: var(--br-actionbar-button-text-color); // #6c757d;
|
||||||
}
|
}
|
||||||
|
|
||||||
i {
|
i {
|
||||||
background-color: unset;
|
background-color: unset;
|
||||||
color: #6c757d;
|
color: var(--br-actionbar-button-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
|
@ -1,18 +1,15 @@
|
|||||||
import { AfterViewInit, Component, ElementRef, HostListener, Inject, OnDestroy, OnInit, Renderer2, RendererStyleFlags2, ViewChild } from '@angular/core';
|
import { AfterViewInit, Component, ElementRef, HostListener, Inject, OnDestroy, OnInit, Renderer2, RendererStyleFlags2, ViewChild } from '@angular/core';
|
||||||
import {DOCUMENT, Location} from '@angular/common';
|
import {DOCUMENT, Location} from '@angular/common';
|
||||||
import { FormControl, FormGroup } from '@angular/forms';
|
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { forkJoin, fromEvent, Subject } from 'rxjs';
|
import { forkJoin, fromEvent, Subject } from 'rxjs';
|
||||||
import { debounceTime, take, takeUntil } from 'rxjs/operators';
|
import { debounceTime, filter, take, takeUntil, tap } from 'rxjs/operators';
|
||||||
import { Chapter } from 'src/app/_models/chapter';
|
import { Chapter } from 'src/app/_models/chapter';
|
||||||
import { User } from 'src/app/_models/user';
|
|
||||||
import { AccountService } from 'src/app/_services/account.service';
|
import { AccountService } from 'src/app/_services/account.service';
|
||||||
import { NavService } from 'src/app/_services/nav.service';
|
import { NavService } from 'src/app/_services/nav.service';
|
||||||
import { ReaderService } from 'src/app/_services/reader.service';
|
import { ReaderService } from 'src/app/_services/reader.service';
|
||||||
import { SeriesService } from 'src/app/_services/series.service';
|
import { SeriesService } from 'src/app/_services/series.service';
|
||||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||||
|
|
||||||
import { BookService } from '../book.service';
|
import { BookService } from '../book.service';
|
||||||
import { KEY_CODES, UtilityService } from 'src/app/shared/_services/utility.service';
|
import { KEY_CODES, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||||
import { BookChapterItem } from '../_models/book-chapter-item';
|
import { BookChapterItem } from '../_models/book-chapter-item';
|
||||||
@ -23,16 +20,19 @@ import { ReadingDirection } from 'src/app/_models/preferences/reading-direction'
|
|||||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||||
import { LibraryService } from 'src/app/_services/library.service';
|
import { LibraryService } from 'src/app/_services/library.service';
|
||||||
import { LibraryType } from 'src/app/_models/library';
|
import { LibraryType } from 'src/app/_models/library';
|
||||||
|
import { BookTheme } from 'src/app/_models/preferences/book-theme';
|
||||||
|
import { BookPageLayoutMode } from 'src/app/_models/book-page-layout-mode';
|
||||||
|
import { PageStyle } from '../reader-settings/reader-settings.component';
|
||||||
|
import { User } from 'src/app/_models/user';
|
||||||
|
import { LayoutMode } from 'src/app/manga-reader/_models/layout-mode';
|
||||||
import { ThemeService } from 'src/app/_services/theme.service';
|
import { ThemeService } from 'src/app/_services/theme.service';
|
||||||
import { ScrollService } from 'src/app/_services/scroll.service';
|
import { ScrollService } from 'src/app/_services/scroll.service';
|
||||||
|
import { PAGING_DIRECTION } from 'src/app/manga-reader/_models/reader-enums';
|
||||||
|
|
||||||
|
|
||||||
interface PageStyle {
|
enum TabID {
|
||||||
'font-family': string;
|
Settings = 1,
|
||||||
'font-size': string;
|
TableOfContents = 2
|
||||||
'line-height': string;
|
|
||||||
'margin-left': string;
|
|
||||||
'margin-right': string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HistoryPoint {
|
interface HistoryPoint {
|
||||||
@ -92,25 +92,38 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
readingListMode: boolean = false;
|
readingListMode: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The actual pages from the epub, used for showing on table of contents. This must be here as we need access to it for scroll anchors
|
||||||
|
*/
|
||||||
chapters: Array<BookChapterItem> = [];
|
chapters: Array<BookChapterItem> = [];
|
||||||
|
/**
|
||||||
|
* Current Page
|
||||||
|
*/
|
||||||
pageNum = 0;
|
pageNum = 0;
|
||||||
|
/**
|
||||||
|
* Max Pages
|
||||||
|
*/
|
||||||
maxPages = 1;
|
maxPages = 1;
|
||||||
|
/**
|
||||||
|
* This allows for exploration into different chapters
|
||||||
|
*/
|
||||||
adhocPageHistory: Stack<HistoryPoint> = new Stack<HistoryPoint>();
|
adhocPageHistory: Stack<HistoryPoint> = new Stack<HistoryPoint>();
|
||||||
/**
|
/**
|
||||||
* A stack of the chapter ids we come across during continuous reading mode. When we traverse a boundary, we use this to avoid extra API calls.
|
* A stack of the chapter ids we come across during continuous reading mode. When we traverse a boundary, we use this to avoid extra API calls.
|
||||||
* @see Stack
|
* @see Stack
|
||||||
*/
|
*/
|
||||||
continuousChaptersStack: Stack<number> = new Stack();
|
continuousChaptersStack: Stack<number> = new Stack(); // TODO: See if this can be moved into reader service so we can reduce code duplication between readers
|
||||||
|
|
||||||
user!: User;
|
activeTabId: TabID = TabID.Settings;
|
||||||
|
|
||||||
drawerOpen = false;
|
drawerOpen = false;
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
bookTitle: string = '';
|
bookTitle: string = '';
|
||||||
settingsForm: FormGroup = new FormGroup({});
|
|
||||||
clickToPaginate = false;
|
|
||||||
|
|
||||||
|
clickToPaginate = false;
|
||||||
|
/**
|
||||||
|
* The boolean that decides if the clickToPaginate overlay is visible or not.
|
||||||
|
*/
|
||||||
clickToPaginateVisualOverlay = false;
|
clickToPaginateVisualOverlay = false;
|
||||||
clickToPaginateVisualOverlayTimeout: any = undefined; // For animation
|
clickToPaginateVisualOverlayTimeout: any = undefined; // For animation
|
||||||
clickToPaginateVisualOverlayTimeout2: any = undefined; // For kicking off animation, giving enough time to render html
|
clickToPaginateVisualOverlayTimeout2: any = undefined; // For kicking off animation, giving enough time to render html
|
||||||
@ -157,20 +170,17 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
nextPageDisabled = false;
|
nextPageDisabled = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal property used to capture all the different css properties to render on all elements
|
* Internal property used to capture all the different css properties to render on all elements. This is a cached version that is updated from reader-settings component
|
||||||
*/
|
*/
|
||||||
pageStyles!: PageStyle;
|
pageStyles!: PageStyle;
|
||||||
/**
|
|
||||||
* List of all font families user can select from
|
|
||||||
*/
|
|
||||||
fontFamilies: Array<string> = [];
|
|
||||||
|
|
||||||
|
|
||||||
darkMode = false;
|
darkMode = true;
|
||||||
backgroundColor: string = 'white';
|
backgroundColor: string = 'white';
|
||||||
readerStyles: string = '';
|
/**
|
||||||
darkModeStyleElem!: HTMLElement;
|
* Offset for drawer and rendering canvas. Fixed to 62px.
|
||||||
topOffset: number = 0; // Offset for drawer and rendering canvas
|
*/
|
||||||
|
topOffset: number = 62;
|
||||||
/**
|
/**
|
||||||
* Used for showing/hiding bottom action bar. Calculates if there is enough scroll to show it.
|
* Used for showing/hiding bottom action bar. Calculates if there is enough scroll to show it.
|
||||||
* Will hide if all content in book is absolute positioned
|
* Will hide if all content in book is absolute positioned
|
||||||
@ -191,36 +201,40 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
libraryType: LibraryType = LibraryType.Book;
|
libraryType: LibraryType = LibraryType.Book;
|
||||||
|
|
||||||
/**
|
|
||||||
* Hack: Override background color for reader and restore it onDestroy
|
|
||||||
*/
|
|
||||||
originalBodyColor: string | undefined;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the web browser is in fullscreen mode
|
* If the web browser is in fullscreen mode
|
||||||
*/
|
*/
|
||||||
isFullscreen: boolean = false;
|
isFullscreen: boolean = false;
|
||||||
|
|
||||||
darkModeStyles = `
|
/**
|
||||||
*:not(input), *:not(select), *:not(code), *:not(:link), *:not(.ngx-toastr) {
|
* How to render the page content
|
||||||
color: #dcdcdc !important;
|
*/
|
||||||
|
layoutMode: BookPageLayoutMode = BookPageLayoutMode.Default;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Width of the document (in non-column layout), used for column layout virtual paging
|
||||||
|
*/
|
||||||
|
windowWidth: number = 0;
|
||||||
|
windowHeight: number = 0;
|
||||||
|
|
||||||
|
user!: User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to keep track of direction user is paging, to help with virtual paging on column layout
|
||||||
|
*/
|
||||||
|
pagingDirection: PAGING_DIRECTION = PAGING_DIRECTION.FORWARD;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
get BookPageLayoutMode() {
|
||||||
|
return BookPageLayoutMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
get TabID(): typeof TabID {
|
||||||
color: #e83e8c !important;
|
return TabID;
|
||||||
}
|
}
|
||||||
|
|
||||||
:link, a {
|
|
||||||
color: #8db2e5 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
img, img[src] {
|
|
||||||
z-index: 1;
|
|
||||||
filter: brightness(0.85) !important;
|
|
||||||
background-color: initial !important;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
get ReadingDirection(): typeof ReadingDirection {
|
get ReadingDirection(): typeof ReadingDirection {
|
||||||
return ReadingDirection;
|
return ReadingDirection;
|
||||||
}
|
}
|
||||||
@ -252,9 +266,36 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
return this.pageNum === 0;
|
return this.pageNum === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
get drawerBackgroundColor() {
|
get ColumnWidth() {
|
||||||
return this.darkMode ? '#010409': '#fff';
|
switch (this.layoutMode) {
|
||||||
|
case BookPageLayoutMode.Default:
|
||||||
|
return 'unset';
|
||||||
|
case BookPageLayoutMode.Column1:
|
||||||
|
return (this.windowWidth /2) + 'px';
|
||||||
|
case BookPageLayoutMode.Column2:
|
||||||
|
return ((this.windowWidth / 4)) + 'px';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get ColumnHeight() {
|
||||||
|
if (this.layoutMode !== BookPageLayoutMode.Default) {
|
||||||
|
// Take the height after page loads, subtract the top/bottom bar
|
||||||
|
return this.windowHeight - (this.topOffset *2) + 'px';
|
||||||
|
}
|
||||||
|
return 'unset';
|
||||||
|
}
|
||||||
|
|
||||||
|
get ColumnLayout() {
|
||||||
|
switch (this.layoutMode) {
|
||||||
|
case BookPageLayoutMode.Default:
|
||||||
|
return '';
|
||||||
|
case BookPageLayoutMode.Column1:
|
||||||
|
return 'column-layout-1';
|
||||||
|
case BookPageLayoutMode.Column2:
|
||||||
|
return 'column-layout-2';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService,
|
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService,
|
||||||
private seriesService: SeriesService, private readerService: ReaderService, private location: Location,
|
private seriesService: SeriesService, private readerService: ReaderService, private location: Location,
|
||||||
@ -263,49 +304,8 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
private scrollService: ScrollService, private utilityService: UtilityService, private libraryService: LibraryService,
|
private scrollService: ScrollService, private utilityService: UtilityService, private libraryService: LibraryService,
|
||||||
@Inject(DOCUMENT) private document: Document, private themeService: ThemeService) {
|
@Inject(DOCUMENT) private document: Document, private themeService: ThemeService) {
|
||||||
this.navService.hideNavBar();
|
this.navService.hideNavBar();
|
||||||
|
this.themeService.clearThemes();
|
||||||
this.navService.hideSideNav();
|
this.navService.hideSideNav();
|
||||||
|
|
||||||
this.darkModeStyleElem = this.renderer.createElement('style');
|
|
||||||
this.darkModeStyleElem.innerHTML = this.darkModeStyles;
|
|
||||||
this.fontFamilies = this.bookService.getFontFamilies();
|
|
||||||
|
|
||||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
|
||||||
if (user) {
|
|
||||||
this.user = user;
|
|
||||||
|
|
||||||
if (this.user.preferences.bookReaderFontFamily === undefined) {
|
|
||||||
this.user.preferences.bookReaderFontFamily = 'default';
|
|
||||||
}
|
|
||||||
if (this.user.preferences.bookReaderFontSize === undefined) {
|
|
||||||
this.user.preferences.bookReaderFontSize = 100;
|
|
||||||
}
|
|
||||||
if (this.user.preferences.bookReaderLineSpacing === undefined) {
|
|
||||||
this.user.preferences.bookReaderLineSpacing = 100;
|
|
||||||
}
|
|
||||||
if (this.user.preferences.bookReaderMargin === undefined) {
|
|
||||||
this.user.preferences.bookReaderMargin = 0;
|
|
||||||
}
|
|
||||||
if (this.user.preferences.bookReaderReadingDirection === undefined) {
|
|
||||||
this.user.preferences.bookReaderReadingDirection = ReadingDirection.LeftToRight;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.readingDirection = this.user.preferences.bookReaderReadingDirection;
|
|
||||||
|
|
||||||
this.clickToPaginate = this.user.preferences.bookReaderTapToPaginate;
|
|
||||||
|
|
||||||
this.settingsForm.addControl('bookReaderFontFamily', new FormControl(user.preferences.bookReaderFontFamily, []));
|
|
||||||
|
|
||||||
this.settingsForm.get('bookReaderFontFamily')!.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(changes => {
|
|
||||||
this.updateFontFamily(changes);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const bodyNode = this.document.querySelector('body');
|
|
||||||
if (bodyNode !== undefined && bodyNode !== null) {
|
|
||||||
this.originalBodyColor = bodyNode.style.background;
|
|
||||||
}
|
|
||||||
this.resetSettings();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -316,9 +316,17 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
// check scroll offset and if offset is after any of the "id" markers, save progress
|
// check scroll offset and if offset is after any of the "id" markers, save progress
|
||||||
fromEvent(this.reader.nativeElement, 'scroll')
|
fromEvent(this.reader.nativeElement, '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;
|
||||||
|
|
||||||
|
this.handleScrollEvent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleScrollEvent() {
|
||||||
// Highlight the current chapter we are on
|
// Highlight the current chapter we are on
|
||||||
if (Object.keys(this.pageAnchors).length !== 0) {
|
if (Object.keys(this.pageAnchors).length !== 0) {
|
||||||
// get the height of the document so we can capture markers that are halfway on the document viewport
|
// get the height of the document so we can capture markers that are halfway on the document viewport
|
||||||
@ -334,24 +342,14 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
|
|
||||||
// Find the element that is on screen to bookmark against
|
// Find the element that is on screen to bookmark against
|
||||||
const intersectingEntries = Array.from(this.readingSectionElemRef.nativeElement.querySelectorAll('div,o,p,ul,li,a,img,h1,h2,h3,h4,h5,h6,span'))
|
|
||||||
|
const intersectingEntries = Array.from(this.readingHtml.nativeElement.querySelectorAll('div,o,p,ul,li,a,img,h1,h2,h3,h4,h5,h6,span'))
|
||||||
.filter(element => !element.classList.contains('no-observe'))
|
.filter(element => !element.classList.contains('no-observe'))
|
||||||
.filter(entry => {
|
.filter(entry => {
|
||||||
return this.utilityService.isInViewport(entry, this.topOffset);
|
return this.utilityService.isInViewport(entry, this.topOffset);
|
||||||
});
|
});
|
||||||
|
|
||||||
intersectingEntries.sort((a: Element, b: Element) => {
|
intersectingEntries.sort(this.sortElements);
|
||||||
const aTop = a.getBoundingClientRect().top;
|
|
||||||
const bTop = b.getBoundingClientRect().top;
|
|
||||||
if (aTop < bTop) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (aTop > bTop) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (intersectingEntries.length > 0) {
|
if (intersectingEntries.length > 0) {
|
||||||
let path = this.getXPathTo(intersectingEntries[0]);
|
let path = this.getXPathTo(intersectingEntries[0]);
|
||||||
@ -365,7 +363,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
if (this.lastSeenScrollPartPath !== '') {
|
if (this.lastSeenScrollPartPath !== '') {
|
||||||
this.saveProgress();
|
this.saveProgress();
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
saveProgress() {
|
saveProgress() {
|
||||||
@ -381,28 +378,17 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
const bodyNode = this.document.querySelector('body');
|
this.clearTimeout(this.clickToPaginateVisualOverlayTimeout);
|
||||||
if (bodyNode !== undefined && bodyNode !== null && this.originalBodyColor !== undefined) {
|
this.clearTimeout(this.clickToPaginateVisualOverlayTimeout2);
|
||||||
bodyNode.style.background = this.originalBodyColor;
|
|
||||||
|
this.themeService.clearBookTheme();
|
||||||
|
|
||||||
this.themeService.currentTheme$.pipe(take(1)).subscribe(theme => {
|
this.themeService.currentTheme$.pipe(take(1)).subscribe(theme => {
|
||||||
this.themeService.setTheme(theme.name);
|
this.themeService.setTheme(theme.name);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
this.navService.showNavBar();
|
this.navService.showNavBar();
|
||||||
this.navService.showSideNav();
|
this.navService.showSideNav();
|
||||||
|
|
||||||
const head = this.document.querySelector('head');
|
|
||||||
this.renderer.removeChild(head, this.darkModeStyleElem);
|
|
||||||
|
|
||||||
if (this.clickToPaginateVisualOverlayTimeout !== undefined) {
|
|
||||||
clearTimeout(this.clickToPaginateVisualOverlayTimeout);
|
|
||||||
this.clickToPaginateVisualOverlayTimeout = undefined;
|
|
||||||
}
|
|
||||||
if (this.clickToPaginateVisualOverlayTimeout2 !== undefined) {
|
|
||||||
clearTimeout(this.clickToPaginateVisualOverlayTimeout2);
|
|
||||||
this.clickToPaginateVisualOverlayTimeout2 = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.readerService.exitFullscreen();
|
this.readerService.exitFullscreen();
|
||||||
|
|
||||||
this.onDestroy.next();
|
this.onDestroy.next();
|
||||||
@ -439,8 +425,13 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||||
|
if (user) {
|
||||||
|
this.user = user;
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.nextChapterId = CHAPTER_ID_NOT_FETCHED;
|
this.nextChapterId = CHAPTER_ID_NOT_FETCHED;
|
||||||
@ -449,6 +440,8 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.prevChapterDisabled = false;
|
this.prevChapterDisabled = false;
|
||||||
this.nextChapterPrefetched = false;
|
this.nextChapterPrefetched = false;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
this.bookService.getBookInfo(this.chapterId).subscribe(info => {
|
this.bookService.getBookInfo(this.chapterId).subscribe(info => {
|
||||||
this.bookTitle = info.bookTitle;
|
this.bookTitle = info.bookTitle;
|
||||||
|
|
||||||
@ -463,12 +456,14 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
chapter: this.seriesService.getChapter(this.chapterId),
|
chapter: this.seriesService.getChapter(this.chapterId),
|
||||||
progress: this.readerService.getProgress(this.chapterId),
|
progress: this.readerService.getProgress(this.chapterId),
|
||||||
chapters: this.bookService.getBookChapters(this.chapterId),
|
chapters: this.bookService.getBookChapters(this.chapterId),
|
||||||
}).pipe(take(1)).subscribe(results => {
|
}).subscribe(results => {
|
||||||
this.chapter = results.chapter;
|
this.chapter = results.chapter;
|
||||||
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.progress.pageNum;
|
this.pageNum = results.progress.pageNum;
|
||||||
|
if (results.progress.bookScrollId) this.lastSeenScrollPartPath = results.progress.bookScrollId;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
this.continuousChaptersStack.push(this.chapterId);
|
this.continuousChaptersStack.push(this.chapterId);
|
||||||
@ -477,6 +472,8 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.libraryType = type;
|
this.libraryType = type;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.updateLayoutMode(this.user.preferences.bookReaderLayoutMode || BookPageLayoutMode.Default);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (this.pageNum >= this.maxPages) {
|
if (this.pageNum >= this.maxPages) {
|
||||||
@ -505,8 +502,18 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}, 200);
|
}, 200);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('window:resize', ['$event'])
|
||||||
|
onResize(event: any){
|
||||||
|
// Update the window Height
|
||||||
|
this.windowHeight = Math.max(this.readingSectionElemRef.nativeElement.clientHeight, window.innerHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('window:orientationchange', ['$event'])
|
||||||
|
onOrientationChange() {
|
||||||
|
// Update the window Height
|
||||||
|
this.windowHeight = Math.max(this.readingSectionElemRef.nativeElement.clientHeight, window.innerHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('window:keydown', ['$event'])
|
@HostListener('window:keydown', ['$event'])
|
||||||
@ -598,9 +605,9 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadChapterPage(pageNum: number, part: string) {
|
loadChapterPage(event: {pageNum: number, part: string}) {
|
||||||
this.setPageNum(pageNum);
|
this.setPageNum(event.pageNum);
|
||||||
this.loadPage('id("' + part + '")');
|
this.loadPage('id("' + event.part + '")');
|
||||||
}
|
}
|
||||||
|
|
||||||
closeReader() {
|
closeReader() {
|
||||||
@ -611,30 +618,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resetSettings() {
|
|
||||||
const windowWidth = window.innerWidth
|
|
||||||
|| this.document.documentElement.clientWidth
|
|
||||||
|| this.document.body.clientWidth;
|
|
||||||
|
|
||||||
let margin = '15%';
|
|
||||||
if (windowWidth <= 700) {
|
|
||||||
margin = '5%';
|
|
||||||
}
|
|
||||||
if (this.user) {
|
|
||||||
if (windowWidth > 700) {
|
|
||||||
margin = this.user.preferences.bookReaderMargin + '%';
|
|
||||||
}
|
|
||||||
this.pageStyles = {'font-family': this.user.preferences.bookReaderFontFamily, 'font-size': this.user.preferences.bookReaderFontSize + '%', 'margin-left': margin, 'margin-right': margin, 'line-height': this.user.preferences.bookReaderLineSpacing + '%'};
|
|
||||||
|
|
||||||
this.toggleDarkMode(this.user.preferences.bookReaderDarkMode);
|
|
||||||
} else {
|
|
||||||
this.pageStyles = {'font-family': 'default', 'font-size': '100%', 'margin-left': margin, 'margin-right': margin, 'line-height': '100%'};
|
|
||||||
this.toggleDarkMode(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.settingsForm.get('bookReaderFontFamily')?.setValue(this.user.preferences.bookReaderFontFamily);
|
|
||||||
this.updateReaderStyles();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a click handler for any anchors that have 'kavita-page'. If 'kavita-page' present, changes page to kavita-page and optionally passes a part value
|
* Adds a click handler for any anchors that have 'kavita-page'. If 'kavita-page' present, changes page to kavita-page and optionally passes a part value
|
||||||
@ -669,6 +652,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
promptForPage() {
|
promptForPage() {
|
||||||
const question = 'There are ' + (this.maxPages - 1) + ' pages. What page do you want to go to?';
|
const question = 'There are ' + (this.maxPages - 1) + ' pages. What page do you want to go to?';
|
||||||
const goToPageNum = window.prompt(question, '');
|
const goToPageNum = window.prompt(question, '');
|
||||||
@ -698,50 +682,21 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
this.pageNum = page;
|
this.pageNum = page;
|
||||||
this.loadPage();
|
this.loadPage();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanIdSelector(id: string) {
|
|
||||||
const tokens = id.split('/');
|
|
||||||
if (tokens.length > 0) {
|
|
||||||
return tokens[0];
|
|
||||||
}
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
getPageMarkers(ids: Array<string>) {
|
|
||||||
try {
|
|
||||||
return this.document.querySelectorAll(ids.map(id => '#' + this.cleanIdSelector(id)).join(', '));
|
|
||||||
} catch (Exception) {
|
|
||||||
// Fallback to anchors instead. Some books have ids that are not valid for querySelectors, so anchors should be used instead
|
|
||||||
return this.document.querySelectorAll(ids.map(id => '[href="#' + id + '"]').join(', '));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupPageAnchors() {
|
|
||||||
this.pageAnchors = {};
|
|
||||||
this.currentPageAnchor = '';
|
|
||||||
const ids = this.chapters.map(item => item.children).flat().filter(item => item.page === this.pageNum).map(item => item.part).filter(item => item.length > 0);
|
|
||||||
if (ids.length > 0) {
|
|
||||||
const elems = this.getPageMarkers(ids);
|
|
||||||
elems.forEach(elem => {
|
|
||||||
this.pageAnchors[elem.id] = elem.getBoundingClientRect().top;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadPage(part?: string | undefined, scrollTop?: number | undefined) {
|
loadPage(part?: string | undefined, scrollTop?: number | undefined) {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
this.saveProgress();
|
|
||||||
|
|
||||||
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); // PERF: Potential optimization to prefetch next/prev page and store in localStorage
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.addLinkClickHandlers();
|
this.addLinkClickHandlers();
|
||||||
this.updateReaderStyles();
|
this.updateReaderStyles(this.pageStyles);
|
||||||
// We need to get the offset after we ensure the title has rendered
|
this.updateReaderStyles(this.pageStyles);
|
||||||
requestAnimationFrame(() => this.topOffset = this.stickyTopElemRef.nativeElement?.getBoundingClientRect().height);
|
|
||||||
|
|
||||||
const imgs = this.readingSectionElemRef.nativeElement.querySelectorAll('img');
|
const imgs = this.readingSectionElemRef.nativeElement.querySelectorAll('img');
|
||||||
if (imgs === null || imgs.length === 0) {
|
if (imgs === null || imgs.length === 0) {
|
||||||
@ -749,23 +704,47 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply scaling class to all images to ensure they scale down to max width to not blow out the reader
|
|
||||||
Array.from(imgs).forEach(img => this.renderer.addClass(img, 'scale-width'));
|
|
||||||
|
|
||||||
Promise.all(Array.from(imgs)
|
Promise.all(Array.from(imgs)
|
||||||
.filter(img => !img.complete)
|
.filter(img => !img.complete)
|
||||||
.map(img => new Promise(resolve => { img.onload = img.onerror = resolve; })))
|
.map(img => new Promise(resolve => { img.onload = img.onerror = resolve; })))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.setupPage(part, scrollTop);
|
this.setupPage(part, scrollTop);
|
||||||
|
this.updateImagesWithHeight();
|
||||||
});
|
});
|
||||||
}, 10);
|
}, 10);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a max-height inline css property on each image in the page if the layout mode is column-based, else it removes the property
|
||||||
|
*/
|
||||||
|
updateImagesWithHeight() {
|
||||||
|
const images = this.readingSectionElemRef.nativeElement.querySelectorAll('img') || [];
|
||||||
|
|
||||||
|
if (this.layoutMode !== BookPageLayoutMode.Default) {
|
||||||
|
const height = this.ColumnHeight;
|
||||||
|
Array.from(images).forEach(img => {
|
||||||
|
this.renderer.setStyle(img, 'max-height', height);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Array.from(images).forEach(img => {
|
||||||
|
this.renderer.removeStyle(img, 'max-height');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setupPage(part?: string | undefined, scrollTop?: number | undefined) {
|
setupPage(part?: string | undefined, scrollTop?: number | undefined) {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.scrollbarNeeded = this.readingHtml.nativeElement.clientHeight > this.readingSectionElemRef.nativeElement.clientHeight;
|
this.scrollbarNeeded = this.readingHtml.nativeElement.clientHeight > this.readingSectionElemRef.nativeElement.clientHeight;
|
||||||
|
|
||||||
|
// Virtual Paging stuff
|
||||||
|
this.windowWidth = window.innerWidth
|
||||||
|
|| this.document.documentElement.clientWidth
|
||||||
|
|| this.document.body.clientWidth;
|
||||||
|
|
||||||
|
this.windowHeight = Math.max(this.readingSectionElemRef.nativeElement.clientHeight, this.windowHeight);
|
||||||
|
this.updateLayoutMode(this.layoutMode || BookPageLayoutMode.Default);
|
||||||
|
|
||||||
// Find all the part ids and their top offset
|
// Find all the part ids and their top offset
|
||||||
this.setupPageAnchors();
|
this.setupPageAnchors();
|
||||||
|
|
||||||
@ -775,16 +754,25 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
} else if (scrollTop !== undefined && scrollTop !== 0) {
|
} else if (scrollTop !== undefined && scrollTop !== 0) {
|
||||||
this.scrollService.scrollTo(scrollTop, this.reader.nativeElement);
|
this.scrollService.scrollTo(scrollTop, this.reader.nativeElement);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
if (this.layoutMode === BookPageLayoutMode.Default) {
|
||||||
this.scrollService.scrollTo(0, this.reader.nativeElement);
|
this.scrollService.scrollTo(0, this.reader.nativeElement);
|
||||||
|
} else {
|
||||||
|
this.reader.nativeElement.children
|
||||||
|
// We need to check if we are paging back, because we need to adjust the scroll
|
||||||
|
if (this.pagingDirection === PAGING_DIRECTION.BACKWARDS) {
|
||||||
|
this.scrollService.scrollToX(this.readingHtml.nativeElement.scrollWidth, this.readingHtml.nativeElement);
|
||||||
|
} else {
|
||||||
|
this.scrollService.scrollToX(0, this.readingHtml.nativeElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// we need to click the document before arrow keys will scroll down.
|
// we need to click the document before arrow keys will scroll down.
|
||||||
this.reader.nativeElement.focus();
|
this.reader.nativeElement.focus();
|
||||||
|
this.saveProgress();
|
||||||
}
|
}
|
||||||
|
|
||||||
setPageNum(pageNum: number) {
|
|
||||||
this.pageNum = Math.max(Math.min(pageNum, this.maxPages), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
goBack() {
|
goBack() {
|
||||||
if (!this.adhocPageHistory.isEmpty()) {
|
if (!this.adhocPageHistory.isEmpty()) {
|
||||||
@ -796,20 +784,43 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clickOverlayClass(side: 'right' | 'left') {
|
setPageNum(pageNum: number) {
|
||||||
if (!this.clickToPaginateVisualOverlay) {
|
this.pageNum = Math.max(Math.min(pageNum, this.maxPages), 0);
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.readingDirection === ReadingDirection.LeftToRight) {
|
if (this.pageNum >= this.maxPages - 10) {
|
||||||
return side === 'right' ? 'highlight' : 'highlight-2';
|
// Tell server to cache the next chapter
|
||||||
|
if (this.nextChapterId > 0 && !this.nextChapterPrefetched) {
|
||||||
|
this.readerService.getChapterInfo(this.nextChapterId).pipe(take(1)).subscribe(res => {
|
||||||
|
this.nextChapterPrefetched = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (this.pageNum <= 10) {
|
||||||
|
if (this.prevChapterId > 0 && !this.prevChapterPrefetched) {
|
||||||
|
this.readerService.getChapterInfo(this.prevChapterId).pipe(take(1)).subscribe(res => {
|
||||||
|
this.prevChapterPrefetched = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return side === 'right' ? 'highlight-2' : 'highlight';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
prevPage() {
|
prevPage() {
|
||||||
const oldPageNum = this.pageNum;
|
const oldPageNum = this.pageNum;
|
||||||
|
|
||||||
|
this.pagingDirection = PAGING_DIRECTION.BACKWARDS;
|
||||||
|
|
||||||
|
// We need to handle virtual paging before we increment the actual page
|
||||||
|
if (this.layoutMode !== BookPageLayoutMode.Default) {
|
||||||
|
|
||||||
|
const scrollOffset = this.readingHtml.nativeElement.scrollLeft;
|
||||||
|
const pageWidth = this.readingSectionElemRef.nativeElement.clientWidth - (this.readingSectionElemRef.nativeElement.clientWidth*(parseInt(this.pageStyles['margin-left'], 10) / 100))*2 + 20;
|
||||||
|
|
||||||
|
if (scrollOffset - pageWidth >= 0) {
|
||||||
|
this.scrollService.scrollToX(scrollOffset - pageWidth, this.readingHtml.nativeElement);
|
||||||
|
this.saveProgress();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.readingDirection === ReadingDirection.LeftToRight) {
|
if (this.readingDirection === ReadingDirection.LeftToRight) {
|
||||||
this.setPageNum(this.pageNum - 1);
|
this.setPageNum(this.pageNum - 1);
|
||||||
} else {
|
} else {
|
||||||
@ -824,6 +835,8 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
if (oldPageNum === this.pageNum) { return; }
|
if (oldPageNum === this.pageNum) { return; }
|
||||||
|
|
||||||
|
// If prev and in default layout, need to handle somehow
|
||||||
|
|
||||||
this.loadPage();
|
this.loadPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -832,6 +845,25 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.pagingDirection = PAGING_DIRECTION.FORWARD;
|
||||||
|
|
||||||
|
// We need to handle virtual paging before we increment the actual page
|
||||||
|
if (this.layoutMode !== BookPageLayoutMode.Default) {
|
||||||
|
|
||||||
|
const scrollOffset = this.readingHtml.nativeElement.scrollLeft;
|
||||||
|
const totalScroll = this.readingHtml.nativeElement.scrollWidth;
|
||||||
|
const pageWidth = this.readingSectionElemRef.nativeElement.clientWidth - (this.readingSectionElemRef.nativeElement.clientWidth*(parseInt(this.pageStyles['margin-left'], 10) / 100))*2 + 20;
|
||||||
|
|
||||||
|
|
||||||
|
if (scrollOffset + pageWidth < totalScroll) {
|
||||||
|
this.scrollService.scrollToX(scrollOffset + pageWidth, this.readingHtml.nativeElement);
|
||||||
|
this.handleScrollEvent();
|
||||||
|
this.saveProgress();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const oldPageNum = this.pageNum;
|
const oldPageNum = this.pageNum;
|
||||||
if (oldPageNum + 1 === this.maxPages) {
|
if (oldPageNum + 1 === this.maxPages) {
|
||||||
// Move to next volume/chapter automatically
|
// Move to next volume/chapter automatically
|
||||||
@ -846,67 +878,16 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.setPageNum(this.pageNum - 1);
|
this.setPageNum(this.pageNum - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (oldPageNum === this.pageNum) { return; }
|
if (oldPageNum === this.pageNum) { return; }
|
||||||
|
|
||||||
|
|
||||||
this.loadPage();
|
this.loadPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFontSize(amount: number) {
|
|
||||||
let val = parseInt(this.pageStyles['font-size'].substr(0, this.pageStyles['font-size'].length - 1), 10);
|
|
||||||
|
|
||||||
if (val + amount > 300 || val + amount < 50) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pageStyles['font-size'] = val + amount + '%';
|
|
||||||
this.updateReaderStyles();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFontFamily(familyName: string) {
|
|
||||||
if (familyName === null) familyName = '';
|
|
||||||
let cleanedName = familyName.replace(' ', '_').replace('!important', '').trim();
|
|
||||||
if (cleanedName === 'default') {
|
|
||||||
this.pageStyles['font-family'] = 'inherit';
|
|
||||||
} else {
|
|
||||||
this.pageStyles['font-family'] = "'" + cleanedName + "'";
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateReaderStyles();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateMargin(amount: number) {
|
|
||||||
let cleanedValue = this.pageStyles['margin-left'].replace('%', '').replace('!important', '').trim();
|
|
||||||
let val = parseInt(cleanedValue, 10);
|
|
||||||
|
|
||||||
if (val + amount > 30 || val + amount < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pageStyles['margin-left'] = (val + amount) + '%';
|
|
||||||
this.pageStyles['margin-right'] = (val + amount) + '%';
|
|
||||||
|
|
||||||
this.updateReaderStyles();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateLineSpacing(amount: number) {
|
|
||||||
const cleanedValue = parseInt(this.pageStyles['line-height'].replace('%', '').replace('!important', '').trim(), 10);
|
|
||||||
|
|
||||||
if (cleanedValue + amount > 250 || cleanedValue + amount < 100) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pageStyles['line-height'] = (cleanedValue + amount) + '%';
|
|
||||||
|
|
||||||
this.updateReaderStyles();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies styles onto the html of the book page
|
* Applies styles onto the html of the book page
|
||||||
*/
|
*/
|
||||||
updateReaderStyles() {
|
updateReaderStyles(pageStyles: PageStyle) {
|
||||||
|
this.pageStyles = pageStyles;
|
||||||
if (this.readingHtml === undefined || !this.readingHtml.nativeElement) return;
|
if (this.readingHtml === undefined || !this.readingHtml.nativeElement) return;
|
||||||
|
|
||||||
// Line Height must be placed on each element in the page
|
// Line Height must be placed on each element in the page
|
||||||
@ -940,71 +921,34 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setOverrideStyles(theme: BookTheme) {
|
||||||
|
// TODO: Put optimization in to avoid any work if the theme is the same as selected (or have reading settings control handle that)
|
||||||
|
|
||||||
toggleDarkMode(force?: boolean) {
|
// Remove all themes
|
||||||
if (force !== undefined) {
|
Array.from(this.document.querySelectorAll('style[id^="brtheme-"]')).forEach(elem => elem.remove());
|
||||||
this.darkMode = force;
|
|
||||||
} else {
|
|
||||||
this.darkMode = !this.darkMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setOverrideStyles();
|
this.darkMode = theme.isDarkTheme;
|
||||||
}
|
|
||||||
|
|
||||||
toggleReadingDirection() {
|
const styleElem = this.renderer.createElement('style');
|
||||||
if (this.readingDirection === ReadingDirection.LeftToRight) {
|
styleElem.id = theme.selector;
|
||||||
this.readingDirection = ReadingDirection.RightToLeft;
|
styleElem.innerHTML = theme.content;
|
||||||
} else {
|
|
||||||
this.readingDirection = ReadingDirection.LeftToRight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getDarkModeBackgroundColor() {
|
|
||||||
return this.darkMode ? '#292929' : '#fff';
|
|
||||||
}
|
|
||||||
|
|
||||||
setOverrideStyles() {
|
this.renderer.appendChild(this.document.querySelector('.reading-section'), styleElem);
|
||||||
const bodyNode = this.document.querySelector('body');
|
// I need to also apply the selector onto the body so that any css variables will take effect
|
||||||
if (bodyNode !== undefined && bodyNode !== null) {
|
this.themeService.setBookTheme(theme.selector);
|
||||||
if (this.themeService.isDarkTheme()) {
|
|
||||||
bodyNode.classList.remove('bg-dark');
|
|
||||||
}
|
|
||||||
|
|
||||||
bodyNode.style.background = this.getDarkModeBackgroundColor();
|
|
||||||
}
|
|
||||||
this.backgroundColor = this.getDarkModeBackgroundColor();
|
|
||||||
const head = this.document.querySelector('head');
|
|
||||||
if (this.darkMode) {
|
|
||||||
this.renderer.appendChild(head, this.darkModeStyleElem)
|
|
||||||
} else {
|
|
||||||
this.renderer.removeChild(head, this.darkModeStyleElem);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleDrawer() {
|
toggleDrawer() {
|
||||||
this.topOffset = this.stickyTopElemRef.nativeElement?.offsetHeight;
|
|
||||||
this.drawerOpen = !this.drawerOpen;
|
this.drawerOpen = !this.drawerOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
closeDrawer() {
|
|
||||||
this.drawerOpen = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleReaderClick(event: MouseEvent) {
|
|
||||||
if (this.drawerOpen) {
|
|
||||||
this.closeDrawer();
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
scrollTo(partSelector: string) {
|
scrollTo(partSelector: string) {
|
||||||
if (partSelector.startsWith('#')) {
|
if (partSelector.startsWith('#')) {
|
||||||
partSelector = partSelector.substr(1, partSelector.length);
|
partSelector = partSelector.substr(1, partSelector.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
let element = null;
|
let element: Element | null = null;
|
||||||
if (partSelector.startsWith('//') || partSelector.startsWith('id(')) {
|
if (partSelector.startsWith('//') || partSelector.startsWith('id(')) {
|
||||||
// Part selector is a XPATH
|
// Part selector is a XPATH
|
||||||
element = this.getElementFromXPath(partSelector);
|
element = this.getElementFromXPath(partSelector);
|
||||||
@ -1013,37 +957,16 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (element === null) return;
|
if (element === null) return;
|
||||||
|
|
||||||
|
if (this.layoutMode === BookPageLayoutMode.Default) {
|
||||||
const fromTopOffset = element.getBoundingClientRect().top + window.pageYOffset + TOP_OFFSET;
|
const fromTopOffset = element.getBoundingClientRect().top + window.pageYOffset + TOP_OFFSET;
|
||||||
// We need to use a delay as webkit browsers (aka apple devices) don't always have the document rendered by this point
|
// We need to use a delay as webkit browsers (aka apple devices) don't always have the document rendered by this point
|
||||||
setTimeout(() => this.scrollService.scrollTo(fromTopOffset, this.reader.nativeElement), 10);
|
setTimeout(() => this.scrollService.scrollTo(fromTopOffset, this.reader.nativeElement), 10);
|
||||||
|
} else {
|
||||||
|
setTimeout(() => (element as Element).scrollIntoView({'block': 'start', 'inline': 'start'}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleClickToPaginate() {
|
|
||||||
this.clickToPaginate = !this.clickToPaginate;
|
|
||||||
|
|
||||||
if (this.clickToPaginateVisualOverlayTimeout2 !== undefined) {
|
|
||||||
clearTimeout(this.clickToPaginateVisualOverlayTimeout2);
|
|
||||||
this.clickToPaginateVisualOverlayTimeout2 = undefined;
|
|
||||||
}
|
|
||||||
if (!this.clickToPaginate) { return; }
|
|
||||||
|
|
||||||
this.clickToPaginateVisualOverlayTimeout2 = setTimeout(() => {
|
|
||||||
this.showClickToPaginateVisualOverlay();
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
showClickToPaginateVisualOverlay() {
|
|
||||||
this.clickToPaginateVisualOverlay = true;
|
|
||||||
|
|
||||||
if (this.clickToPaginateVisualOverlay && this.clickToPaginateVisualOverlayTimeout !== undefined) {
|
|
||||||
clearTimeout(this.clickToPaginateVisualOverlayTimeout);
|
|
||||||
this.clickToPaginateVisualOverlayTimeout = undefined;
|
|
||||||
}
|
|
||||||
this.clickToPaginateVisualOverlayTimeout = setTimeout(() => {
|
|
||||||
this.clickToPaginateVisualOverlay = false;
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
getElementFromXPath(path: string) {
|
getElementFromXPath(path: string) {
|
||||||
const node = this.document.evaluate(path, this.document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
const node = this.document.evaluate(path, this.document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
||||||
@ -1095,10 +1018,101 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.readerService.enterFullscreen(this.reader.nativeElement, () => {
|
this.readerService.enterFullscreen(this.reader.nativeElement, () => {
|
||||||
this.isFullscreen = true;
|
this.isFullscreen = true;
|
||||||
// HACK: This is a bug with how browsers change the background color for fullscreen mode
|
// HACK: This is a bug with how browsers change the background color for fullscreen mode
|
||||||
|
this.renderer.setStyle(this.reader.nativeElement, 'background', this.themeService.getCssVariable('--bs-body-color'));
|
||||||
if (!this.darkMode) {
|
if (!this.darkMode) {
|
||||||
this.renderer.setStyle(this.reader.nativeElement, 'background', 'white');
|
this.renderer.setStyle(this.reader.nativeElement, 'background', 'white');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateLayoutMode(mode: BookPageLayoutMode) {
|
||||||
|
this.layoutMode = mode;
|
||||||
|
|
||||||
|
// Remove any max-heights from column layout
|
||||||
|
this.updateImagesWithHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table of Contents
|
||||||
|
cleanIdSelector(id: string) {
|
||||||
|
const tokens = id.split('/');
|
||||||
|
if (tokens.length > 0) {
|
||||||
|
return tokens[0];
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPageMarkers(ids: Array<string>) {
|
||||||
|
try {
|
||||||
|
return this.document.querySelectorAll(ids.map(id => '#' + this.cleanIdSelector(id)).join(', '));
|
||||||
|
} catch (Exception) {
|
||||||
|
// Fallback to anchors instead. Some books have ids that are not valid for querySelectors, so anchors should be used instead
|
||||||
|
return this.document.querySelectorAll(ids.map(id => '[href="#' + id + '"]').join(', '));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupPageAnchors() {
|
||||||
|
this.pageAnchors = {};
|
||||||
|
this.currentPageAnchor = '';
|
||||||
|
const ids = this.chapters.map(item => item.children).flat().filter(item => item.page === this.pageNum).map(item => item.part).filter(item => item.length > 0);
|
||||||
|
if (ids.length > 0) {
|
||||||
|
const elems = this.getPageMarkers(ids);
|
||||||
|
elems.forEach(elem => {
|
||||||
|
this.pageAnchors[elem.id] = elem.getBoundingClientRect().top;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings Handlers
|
||||||
|
showPaginationOverlay(clickToPaginate: boolean) {
|
||||||
|
this.clickToPaginate = clickToPaginate;
|
||||||
|
|
||||||
|
// if (this.clickToPaginateVisualOverlayTimeout2 !== undefined) {
|
||||||
|
// clearTimeout(this.clickToPaginateVisualOverlayTimeout2);
|
||||||
|
// this.clickToPaginateVisualOverlayTimeout2 = undefined;
|
||||||
|
// }
|
||||||
|
this.clearTimeout(this.clickToPaginateVisualOverlayTimeout2);
|
||||||
|
if (!clickToPaginate) { return; }
|
||||||
|
|
||||||
|
this.clickToPaginateVisualOverlayTimeout2 = setTimeout(() => {
|
||||||
|
this.showClickToPaginateVisualOverlay();
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(timeoutId: number | undefined) {
|
||||||
|
if (timeoutId !== undefined) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showClickToPaginateVisualOverlay() {
|
||||||
|
this.clickToPaginateVisualOverlay = true;
|
||||||
|
|
||||||
|
if (this.clickToPaginateVisualOverlay && this.clickToPaginateVisualOverlayTimeout !== undefined) {
|
||||||
|
clearTimeout(this.clickToPaginateVisualOverlayTimeout);
|
||||||
|
this.clickToPaginateVisualOverlayTimeout = undefined;
|
||||||
|
}
|
||||||
|
this.clickToPaginateVisualOverlayTimeout = setTimeout(() => {
|
||||||
|
this.clickToPaginateVisualOverlay = false;
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsible for returning the class to show an overlay or not
|
||||||
|
* @param side
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
clickOverlayClass(side: 'right' | 'left') {
|
||||||
|
// TODO: See if we can use RXjs or a component to manage this
|
||||||
|
if (!this.clickToPaginateVisualOverlay) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.readingDirection === ReadingDirection.LeftToRight) {
|
||||||
|
return side === 'right' ? 'highlight' : 'highlight-2';
|
||||||
|
}
|
||||||
|
return side === 'right' ? 'highlight-2' : 'highlight';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,16 @@ export interface BookPage {
|
|||||||
html: string;
|
html: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FontFamily {
|
||||||
|
/**
|
||||||
|
* What the user should see
|
||||||
|
*/
|
||||||
|
title: string;
|
||||||
|
/**
|
||||||
|
* The actual font face
|
||||||
|
*/
|
||||||
|
family: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -20,8 +30,10 @@ export class BookService {
|
|||||||
|
|
||||||
constructor(private http: HttpClient) { }
|
constructor(private http: HttpClient) { }
|
||||||
|
|
||||||
getFontFamilies() {
|
getFontFamilies(): Array<FontFamily> {
|
||||||
return ['default', 'EBGaramond', 'Fira Sans', 'Lato', 'Libre Baskerville', 'Merriweather', 'Nanum Gothic', 'RocknRoll One'];
|
return [{title: 'default', family: 'default'}, {title: 'EBGaramond', family: 'EBGaramond'}, {title: 'Fira Sans', family: 'Fira_Sans'},
|
||||||
|
{title: 'Lato', family: 'Lato'}, {title: 'Libre Baskerville', family: 'Libre_Baskerville'}, {title: 'Merriweather', family: 'Merriweather'},
|
||||||
|
{title: 'Nanum Gothic', family: 'Nanum_Gothic'}, {title: 'RocknRoll One', family: 'RocknRoll_One'}, {title: 'Open Dyslexic', family: 'OpenDyslexic2'}];
|
||||||
}
|
}
|
||||||
|
|
||||||
getBookChapters(chapterId: number) {
|
getBookChapters(chapterId: number) {
|
||||||
|
@ -0,0 +1,133 @@
|
|||||||
|
<!-- IDEA: Move the whole reader drawer into this component and have it self contained -->
|
||||||
|
<form [formGroup]="settingsForm">
|
||||||
|
<ngb-accordion [closeOthers]="false" #acc="ngbAccordion" [activeIds]="['general-panel', 'reader-panel', 'color-panel']">
|
||||||
|
<ngb-panel id="general-panel" title="General Settings">
|
||||||
|
<ng-template ngbPanelHeader>
|
||||||
|
<h2 class="accordion-header">
|
||||||
|
<button class="accordion-button" ngbPanelToggle type="button" [attr.aria-expanded]="acc.isExpanded('general-panel')" aria-controls="collapseOne">
|
||||||
|
General Settings
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template ngbPanelContent>
|
||||||
|
<div class="control-container">
|
||||||
|
<div class="controls">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="library-type" class="form-label">Font Family</label>
|
||||||
|
<select class="form-select" id="library-type" formControlName="bookReaderFontFamily">
|
||||||
|
<option [value]="opt" *ngFor="let opt of fontOptions; let i = index">{{opt | titlecase}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row g-0 controls">
|
||||||
|
<label for="fontsize" class="form-label col-6">Font Size</label>
|
||||||
|
<span class="col-6 float-end" style="display: inline-flex;">
|
||||||
|
<i class="fa-solid fa-font" style="font-size: 12px;"></i>
|
||||||
|
<input type="range" class="form-range ms-2 me-2" id="fontsize" min="50" max="300" step="10" formControlName="bookReaderFontSize" [ngbTooltip]="settingsForm.get('bookReaderFontSize')?.value + '%'">
|
||||||
|
<i class="fa-solid fa-font" style="font-size: 24px;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-0 controls">
|
||||||
|
<label for="linespacing" class="form-label col-6">Line Spacing</label>
|
||||||
|
<span class="col-6 float-end" style="display: inline-flex;">
|
||||||
|
1x
|
||||||
|
<input type="range" class="form-range ms-2 me-2" id="linespacing" min="100" max="200" step="10" formControlName="bookReaderLineSpacing" [ngbTooltip]="settingsForm.get('bookReaderLineSpacing')?.value + '%'">
|
||||||
|
2.5x
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-0 controls">
|
||||||
|
<label for="margin" class="form-label col-6">Margin</label>
|
||||||
|
<span class="col-6 float-end" style="display: inline-flex;">
|
||||||
|
<i class="fa-solid fa-outdent"></i>
|
||||||
|
<input type="range" class="form-range ms-2 me-2" id="margin" min="0" max="30" step="5" formControlName="bookReaderMargin" [ngbTooltip]="settingsForm.get('bookReaderMargin')?.value + '%'">
|
||||||
|
<i class="fa-solid fa-indent"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-0 justify-content-between mt-2">
|
||||||
|
<button (click)="resetSettings()" class="btn btn-primary col">Reset to Defaults</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
</ngb-panel>
|
||||||
|
|
||||||
|
<ngb-panel id="reader-panel" title="Reader Settings">
|
||||||
|
<ng-template ngbPanelHeader>
|
||||||
|
<h2 class="accordion-header">
|
||||||
|
<button class="accordion-button" ngbPanelToggle type="button" [attr.aria-expanded]="acc.isExpanded('reader-panel')" aria-controls="collapseOne">
|
||||||
|
Reader Settings
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template ngbPanelContent>
|
||||||
|
<div class="controls">
|
||||||
|
<label id="readingdirection" class="form-label">Reading Direction</label>
|
||||||
|
<button (click)="toggleReadingDirection()" class="btn btn-icon" aria-labelledby="readingdirection" title="{{readingDirectionModel === ReadingDirection.LeftToRight ? 'Left to Right' : 'Right to Left'}}">
|
||||||
|
<i class="fa {{readingDirectionModel === ReadingDirection.LeftToRight ? 'fa-arrow-right' : 'fa-arrow-left'}} " aria-hidden="true"></i>
|
||||||
|
<span class="phone-hidden"> {{readingDirectionModel === ReadingDirection.LeftToRight ? 'Left to Right' : 'Right to Left'}}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<label for="tap-pagination" class="form-label">Tap Pagination</label>
|
||||||
|
<div class="accent" id="tap-pagination-help">Click the edges of the screen to paginate</div>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input type="checkbox" id="tap-pagination" formControlName="bookReaderTapToPaginate" class="form-check-input" aria-labelledby="tap-pagination-help">
|
||||||
|
<label>{{settingsForm.get('bookReaderTapToPaginate')?.value ? 'On' : 'Off'}} </label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<label id="fullscreen" class="form-label">Fullscreen <i class="fa fa-info-circle" aria-hidden="true" placement="top" [ngbTooltip]="fullscreenTooltip" role="button" tabindex="0" aria-describedby="fullscreen-help"></i></label>
|
||||||
|
<ng-template #fullscreenTooltip>Put reader in fullscreen mode</ng-template>
|
||||||
|
<span class="visually-hidden" id="fullscreen-help">
|
||||||
|
<ng-container [ngTemplateOutlet]="fullscreenTooltip"></ng-container>
|
||||||
|
</span>
|
||||||
|
<button (click)="toggleFullscreen()" class="btn btn-icon" aria-labelledby="fullscreen">
|
||||||
|
<i class="fa {{this.isFullscreen ? 'fa-compress-alt' : 'fa-expand-alt'}} {{isFullscreen ? 'icon-primary-color' : ''}}" aria-hidden="true"></i>
|
||||||
|
<span *ngIf="activeTheme?.isDarkTheme"> {{isFullscreen ? 'Exit' : 'Enter'}}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<label id="layout-mode" class="form-label">Layout Mode</label>
|
||||||
|
<br>
|
||||||
|
<div class="btn-group d-flex justify-content-center" role="group" aria-label="Layout Mode">
|
||||||
|
<input type="radio" formControlName="layoutMode" [value]="BookPageLayoutMode.Default" class="btn-check" id="layout-mode-default" autocomplete="off">
|
||||||
|
<label class="btn btn-outline-primary" for="layout-mode-default">Default</label>
|
||||||
|
|
||||||
|
<input type="radio" formControlName="layoutMode" [value]="BookPageLayoutMode.Column1" class="btn-check" id="layout-mode-col1" autocomplete="off">
|
||||||
|
<label class="btn btn-outline-primary" for="layout-mode-col1">1 Column</label>
|
||||||
|
|
||||||
|
<input type="radio" formControlName="layoutMode" [value]="BookPageLayoutMode.Column2" class="btn-check" id="layout-mode-col2" autocomplete="off">
|
||||||
|
<label class="btn btn-outline-primary" for="layout-mode-col2">2 Column</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</ng-template>
|
||||||
|
</ngb-panel>
|
||||||
|
|
||||||
|
<ngb-panel id="color-panel" title="Color Theme">
|
||||||
|
<ng-template ngbPanelHeader>
|
||||||
|
<h2 class="accordion-header">
|
||||||
|
<button class="accordion-button" ngbPanelToggle type="button" [attr.aria-expanded]="acc.isExpanded('color-panel')" aria-controls="collapseOne">
|
||||||
|
Color Theme
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template ngbPanelContent>
|
||||||
|
<div class="controls">
|
||||||
|
<ng-container *ngFor="let theme of themes">
|
||||||
|
<button class="btn btn-icon" (click)="setTheme(theme.name)" [ngClass]="{'active': activeTheme?.name === theme.name}">
|
||||||
|
<div class="dot" [ngStyle]="{'background-color': theme.colorHash}"></div>
|
||||||
|
{{theme.name}}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</ngb-panel>
|
||||||
|
|
||||||
|
</ngb-accordion>
|
||||||
|
</form>
|
@ -0,0 +1,9 @@
|
|||||||
|
.dot {
|
||||||
|
height: 25px;
|
||||||
|
width: 25px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
border: 1px solid var(--primary-color);
|
||||||
|
}
|
@ -0,0 +1,272 @@
|
|||||||
|
import { DOCUMENT } from '@angular/common';
|
||||||
|
import { Component, EventEmitter, Inject, OnDestroy, OnInit, Output } from '@angular/core';
|
||||||
|
import { FormControl, FormGroup } from '@angular/forms';
|
||||||
|
import { Subject, take, takeUntil } from 'rxjs';
|
||||||
|
import { BookPageLayoutMode } from 'src/app/_models/book-page-layout-mode';
|
||||||
|
import { BookTheme } from 'src/app/_models/preferences/book-theme';
|
||||||
|
import { ReadingDirection } from 'src/app/_models/preferences/reading-direction';
|
||||||
|
import { ThemeProvider } from 'src/app/_models/preferences/site-theme';
|
||||||
|
import { User } from 'src/app/_models/user';
|
||||||
|
import { AccountService } from 'src/app/_services/account.service';
|
||||||
|
import { ThemeService } from 'src/app/_services/theme.service';
|
||||||
|
import { BookService, FontFamily } from '../book.service';
|
||||||
|
import { BookBlackTheme } from '../_models/book-black-theme';
|
||||||
|
import { BookDarkTheme } from '../_models/book-dark-theme';
|
||||||
|
import { BookWhiteTheme } from '../_models/book-white-theme';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for book reader. Do not use for other components
|
||||||
|
*/
|
||||||
|
export interface PageStyle {
|
||||||
|
'font-family': string;
|
||||||
|
'font-size': string;
|
||||||
|
'line-height': string;
|
||||||
|
'margin-left': string;
|
||||||
|
'margin-right': string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bookColorThemes = [
|
||||||
|
{
|
||||||
|
name: 'Dark',
|
||||||
|
colorHash: '#292929',
|
||||||
|
isDarkTheme: true,
|
||||||
|
isDefault: true,
|
||||||
|
provider: ThemeProvider.System,
|
||||||
|
selector: 'brtheme-dark',
|
||||||
|
content: BookDarkTheme
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Black',
|
||||||
|
colorHash: '#000000',
|
||||||
|
isDarkTheme: true,
|
||||||
|
isDefault: false,
|
||||||
|
provider: ThemeProvider.System,
|
||||||
|
selector: 'brtheme-black',
|
||||||
|
content: BookBlackTheme
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'White',
|
||||||
|
colorHash: '#FFFFFF',
|
||||||
|
isDarkTheme: false,
|
||||||
|
isDefault: false,
|
||||||
|
provider: ThemeProvider.System,
|
||||||
|
selector: 'brtheme-white',
|
||||||
|
content: BookWhiteTheme
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const mobileBreakpointMarginOverride = 700;
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-reader-settings',
|
||||||
|
templateUrl: './reader-settings.component.html',
|
||||||
|
styleUrls: ['./reader-settings.component.scss']
|
||||||
|
})
|
||||||
|
export class ReaderSettingsComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Outputs when clickToPaginate is changed
|
||||||
|
*/
|
||||||
|
@Output() clickToPaginateChanged: EventEmitter<boolean> = new EventEmitter();
|
||||||
|
/**
|
||||||
|
* Outputs when a style is updated and the reader needs to render it
|
||||||
|
*/
|
||||||
|
@Output() styleUpdate: EventEmitter<PageStyle> = new EventEmitter();
|
||||||
|
/**
|
||||||
|
* Outputs when a theme/dark mode is updated
|
||||||
|
*/
|
||||||
|
@Output() colorThemeUpdate: EventEmitter<BookTheme> = new EventEmitter();
|
||||||
|
/**
|
||||||
|
* Outputs when a layout mode is updated
|
||||||
|
*/
|
||||||
|
@Output() layoutModeUpdate: EventEmitter<BookPageLayoutMode> = new EventEmitter();
|
||||||
|
/**
|
||||||
|
* Outputs when fullscreen is toggled
|
||||||
|
*/
|
||||||
|
@Output() fullscreen: EventEmitter<void> = new EventEmitter();
|
||||||
|
/**
|
||||||
|
* Outputs when reading direction is changed
|
||||||
|
*/
|
||||||
|
@Output() readingDirection: EventEmitter<ReadingDirection> = new EventEmitter();
|
||||||
|
|
||||||
|
user!: User;
|
||||||
|
/**
|
||||||
|
* List of all font families user can select from
|
||||||
|
*/
|
||||||
|
fontOptions: Array<string> = [];
|
||||||
|
fontFamilies: Array<FontFamily> = [];
|
||||||
|
/**
|
||||||
|
* Internal property used to capture all the different css properties to render on all elements
|
||||||
|
*/
|
||||||
|
pageStyles!: PageStyle;
|
||||||
|
|
||||||
|
readingDirectionModel: ReadingDirection = ReadingDirection.LeftToRight;
|
||||||
|
|
||||||
|
activeTheme: BookTheme | undefined;
|
||||||
|
|
||||||
|
isFullscreen: boolean = false;
|
||||||
|
|
||||||
|
settingsForm: FormGroup = new FormGroup({});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* System provided themes
|
||||||
|
*/
|
||||||
|
themes: Array<BookTheme> = bookColorThemes;
|
||||||
|
|
||||||
|
|
||||||
|
private onDestroy: Subject<void> = new Subject();
|
||||||
|
|
||||||
|
|
||||||
|
get BookPageLayoutMode(): typeof BookPageLayoutMode {
|
||||||
|
return BookPageLayoutMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
get ReadingDirection() {
|
||||||
|
return ReadingDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
constructor(private bookService: BookService, private accountService: AccountService, @Inject(DOCUMENT) private document: Document, private themeService: ThemeService) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
|
||||||
|
this.fontFamilies = this.bookService.getFontFamilies();
|
||||||
|
this.fontOptions = this.fontFamilies.map(f => f.title);
|
||||||
|
|
||||||
|
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||||
|
if (user) {
|
||||||
|
this.user = user;
|
||||||
|
|
||||||
|
if (this.user.preferences.bookReaderFontFamily === undefined) {
|
||||||
|
this.user.preferences.bookReaderFontFamily = 'default';
|
||||||
|
}
|
||||||
|
if (this.user.preferences.bookReaderFontSize === undefined || this.user.preferences.bookReaderFontSize < 50) {
|
||||||
|
this.user.preferences.bookReaderFontSize = 100;
|
||||||
|
}
|
||||||
|
if (this.user.preferences.bookReaderLineSpacing === undefined || this.user.preferences.bookReaderLineSpacing < 100) {
|
||||||
|
this.user.preferences.bookReaderLineSpacing = 100;
|
||||||
|
}
|
||||||
|
if (this.user.preferences.bookReaderMargin === undefined) {
|
||||||
|
this.user.preferences.bookReaderMargin = 0;
|
||||||
|
}
|
||||||
|
if (this.user.preferences.bookReaderReadingDirection === undefined) {
|
||||||
|
this.user.preferences.bookReaderReadingDirection = ReadingDirection.LeftToRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.readingDirectionModel = this.user.preferences.bookReaderReadingDirection;
|
||||||
|
|
||||||
|
this.settingsForm.addControl('bookReaderFontFamily', new FormControl(this.user.preferences.bookReaderFontFamily, []));
|
||||||
|
this.settingsForm.get('bookReaderFontFamily')!.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(fontName => {
|
||||||
|
const familyName = this.fontFamilies.filter(f => f.title === fontName)[0].family;
|
||||||
|
if (familyName === 'default') {
|
||||||
|
this.pageStyles['font-family'] = 'inherit';
|
||||||
|
} else {
|
||||||
|
this.pageStyles['font-family'] = "'" + familyName + "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.styleUpdate.emit(this.pageStyles);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.settingsForm.addControl('bookReaderFontSize', new FormControl(this.user.preferences.bookReaderFontSize, []));
|
||||||
|
this.settingsForm.get('bookReaderFontSize')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(value => {
|
||||||
|
this.pageStyles['font-size'] = value + '%';
|
||||||
|
this.styleUpdate.emit(this.pageStyles);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.settingsForm.addControl('bookReaderTapToPaginate', new FormControl(this.user.preferences.bookReaderTapToPaginate, []));
|
||||||
|
this.settingsForm.get('bookReaderTapToPaginate')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(value => {
|
||||||
|
this.clickToPaginateChanged.emit(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
this.settingsForm.addControl('bookReaderLineSpacing', new FormControl(this.user.preferences.bookReaderLineSpacing, []));
|
||||||
|
this.settingsForm.get('bookReaderLineSpacing')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(value => {
|
||||||
|
this.pageStyles['line-height'] = value + '%';
|
||||||
|
this.styleUpdate.emit(this.pageStyles);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.settingsForm.addControl('bookReaderMargin', new FormControl(this.user.preferences.bookReaderMargin, []));
|
||||||
|
this.settingsForm.get('bookReaderMargin')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(value => {
|
||||||
|
this.pageStyles['margin-left'] = value + '%';
|
||||||
|
this.pageStyles['margin-right'] = value + '%';
|
||||||
|
this.styleUpdate.emit(this.pageStyles);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.settingsForm.addControl('layoutMode', new FormControl(this.user.preferences.bookReaderLayoutMode || BookPageLayoutMode.Default, []));
|
||||||
|
this.settingsForm.get('layoutMode')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((layoutMode: BookPageLayoutMode) => {
|
||||||
|
console.log(layoutMode);
|
||||||
|
this.layoutModeUpdate.emit(layoutMode);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setTheme(this.user.preferences.bookReaderThemeName || this.themeService.defaultBookTheme);
|
||||||
|
this.resetSettings();
|
||||||
|
} else {
|
||||||
|
this.resetSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.onDestroy.next();
|
||||||
|
this.onDestroy.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
resetSettings() {
|
||||||
|
if (this.user) {
|
||||||
|
this.setPageStyles(this.user.preferences.bookReaderFontFamily, this.user.preferences.bookReaderFontSize + '%', this.user.preferences.bookReaderMargin + '%', this.user.preferences.bookReaderLineSpacing + '%');
|
||||||
|
} else {
|
||||||
|
this.setPageStyles();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.settingsForm.get('bookReaderFontFamily')?.setValue(this.user.preferences.bookReaderFontFamily);
|
||||||
|
this.styleUpdate.emit(this.pageStyles);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal method to be used by resetSettings. Pass items in with quantifiers
|
||||||
|
*/
|
||||||
|
setPageStyles(fontFamily?: string, fontSize?: string, margin?: string, lineHeight?: string, colorTheme?: string) {
|
||||||
|
const windowWidth = window.innerWidth
|
||||||
|
|| this.document.documentElement.clientWidth
|
||||||
|
|| this.document.body.clientWidth;
|
||||||
|
|
||||||
|
|
||||||
|
let defaultMargin = '15%';
|
||||||
|
if (windowWidth <= mobileBreakpointMarginOverride) {
|
||||||
|
defaultMargin = '5%';
|
||||||
|
}
|
||||||
|
this.pageStyles = {
|
||||||
|
'font-family': fontFamily || this.pageStyles['font-family'] || 'default',
|
||||||
|
'font-size': fontSize || this.pageStyles['font-size'] || '100%',
|
||||||
|
'margin-left': margin || this.pageStyles['margin-left'] || defaultMargin,
|
||||||
|
'margin-right': margin || this.pageStyles['margin-right'] || defaultMargin,
|
||||||
|
'line-height': lineHeight || this.pageStyles['line-height'] || '100%'
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
setTheme(themeName: string) {
|
||||||
|
const theme = this.themes.find(t => t.name === themeName);
|
||||||
|
this.activeTheme = theme;
|
||||||
|
this.colorThemeUpdate.emit(theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleReadingDirection() {
|
||||||
|
if (this.readingDirectionModel === ReadingDirection.LeftToRight) {
|
||||||
|
this.readingDirectionModel = ReadingDirection.RightToLeft;
|
||||||
|
} else {
|
||||||
|
this.readingDirectionModel = ReadingDirection.LeftToRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.readingDirection.emit(this.readingDirectionModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleFullscreen() {
|
||||||
|
this.fullscreen.emit();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
<div class="table-of-contents">
|
||||||
|
<h3>Table of Contents</h3>
|
||||||
|
<div *ngIf="chapters.length === 0">
|
||||||
|
<em>This book does not have Table of Contents set in the metadata or a toc file</em>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="chapters.length === 1; else nestedChildren">
|
||||||
|
<ul>
|
||||||
|
<li *ngFor="let chapter of chapters[0].children">
|
||||||
|
<a href="javascript:void(0);" (click)="loadChapterPage(chapter.page, chapter.part)">{{chapter.title}}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<ng-template #nestedChildren>
|
||||||
|
<ul *ngFor="let chapterGroup of chapters" class="chapter-title">
|
||||||
|
<li class="{{chapterGroup.page == pageNum ? 'active': ''}}" (click)="loadChapterPage(chapterGroup.page, '')">
|
||||||
|
{{chapterGroup.title}}
|
||||||
|
</li>
|
||||||
|
<ul *ngFor="let chapter of chapterGroup.children">
|
||||||
|
<li class="{{cleanIdSelector(chapter.part) === currentPageAnchor ? 'active' : ''}}">
|
||||||
|
<a href="javascript:void(0);" (click)="loadChapterPage(chapter.page, chapter.part)">{{chapter.title}}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</ul>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
@ -0,0 +1,11 @@
|
|||||||
|
.table-of-contents li {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter-title {
|
||||||
|
padding-inline-start: 0px
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
import { AfterViewInit, Component, ElementRef, EventEmitter, Inject, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { BookChapterItem } from '../_models/book-chapter-item';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-table-of-contents',
|
||||||
|
templateUrl: './table-of-contents.component.html',
|
||||||
|
styleUrls: ['./table-of-contents.component.scss']
|
||||||
|
})
|
||||||
|
export class TableOfContentsComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
@Input() chapterId!: number;
|
||||||
|
@Input() pageNum!: number;
|
||||||
|
@Input() currentPageAnchor!: string;
|
||||||
|
@Input() chapters:Array<BookChapterItem> = [];
|
||||||
|
|
||||||
|
@Output() loadChapter: EventEmitter<{pageNum: number, part: string}> = new EventEmitter();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private onDestroy: Subject<void> = new Subject();
|
||||||
|
|
||||||
|
|
||||||
|
pageAnchors: {[n: string]: number } = {};
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.onDestroy.next();
|
||||||
|
this.onDestroy.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanIdSelector(id: string) {
|
||||||
|
const tokens = id.split('/');
|
||||||
|
if (tokens.length > 0) {
|
||||||
|
return tokens[0];
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadChapterPage(pageNum: number, part: string) {
|
||||||
|
this.loadChapter.emit({pageNum, part});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,21 +1,21 @@
|
|||||||
|
<ng-container *ngIf="toggleService.toggleState$ | async as isOpen">
|
||||||
<div class="phone-hidden">
|
<div class="phone-hidden">
|
||||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="filteringCollapsed">
|
<div #collapse="ngbCollapse" [ngbCollapse]="!isOpen" (ngbCollapseChange)="setToggle($event)">
|
||||||
<ng-container [ngTemplateOutlet]="filterSection"></ng-container>
|
<ng-container [ngTemplateOutlet]="filterSection"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="not-phone-hidden">
|
<div class="not-phone-hidden">
|
||||||
<app-drawer #commentDrawer="drawer" [isOpen]="!filteringCollapsed" [style.--drawer-width]="'300px'" [options]="{topOffset: 56}" (drawerClosed)="filteringCollapsed = !filteringCollapsed">
|
<app-drawer #commentDrawer="drawer" [isOpen]="isOpen" [options]="{topOffset: 56}" (drawerClosed)="toggleService.set(false)">
|
||||||
<div header>
|
<h5 header>
|
||||||
<h2 style="margin-top: 0.5rem">Book Settings
|
Filter
|
||||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
</h5>
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div body class="drawer-body">
|
<div body class="drawer-body">
|
||||||
<ng-container [ngTemplateOutlet]="filterSection"></ng-container>
|
<ng-container [ngTemplateOutlet]="filterSection"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
</app-drawer>
|
</app-drawer>
|
||||||
</div>
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<ng-template #filterSection>
|
<ng-template #filterSection>
|
||||||
<ng-template #globalFilterTooltip>This is library agnostic</ng-template>
|
<ng-template #globalFilterTooltip>This is library agnostic</ng-template>
|
||||||
|
@ -2,7 +2,7 @@ import { Component, ContentChild, EventEmitter, Input, OnDestroy, OnInit, Output
|
|||||||
import { FormControl, FormGroup } from '@angular/forms';
|
import { FormControl, FormGroup } from '@angular/forms';
|
||||||
import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { distinctUntilChanged, forkJoin, map, Observable, of, ReplaySubject, Subject, takeUntil } from 'rxjs';
|
import { distinctUntilChanged, forkJoin, map, Observable, of, ReplaySubject, Subject, takeUntil } from 'rxjs';
|
||||||
import { Breakpoint, UtilityService } from '../shared/_services/utility.service';
|
import { UtilityService } from '../shared/_services/utility.service';
|
||||||
import { TypeaheadSettings } from '../typeahead/typeahead-settings';
|
import { TypeaheadSettings } from '../typeahead/typeahead-settings';
|
||||||
import { CollectionTag } from '../_models/collection-tag';
|
import { CollectionTag } from '../_models/collection-tag';
|
||||||
import { Genre } from '../_models/genre';
|
import { Genre } from '../_models/genre';
|
||||||
@ -17,8 +17,8 @@ import { Tag } from '../_models/tag';
|
|||||||
import { CollectionTagService } from '../_services/collection-tag.service';
|
import { CollectionTagService } from '../_services/collection-tag.service';
|
||||||
import { LibraryService } from '../_services/library.service';
|
import { LibraryService } from '../_services/library.service';
|
||||||
import { MetadataService } from '../_services/metadata.service';
|
import { MetadataService } from '../_services/metadata.service';
|
||||||
import { NavService } from '../_services/nav.service';
|
|
||||||
import { SeriesService } from '../_services/series.service';
|
import { SeriesService } from '../_services/series.service';
|
||||||
|
import { ToggleService } from '../_services/toggle.service';
|
||||||
import { FilterSettings } from './filter-settings';
|
import { FilterSettings } from './filter-settings';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -43,7 +43,6 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||||||
@Output() applyFilter: EventEmitter<FilterEvent> = new EventEmitter();
|
@Output() applyFilter: EventEmitter<FilterEvent> = new EventEmitter();
|
||||||
|
|
||||||
@ContentChild('[ngbCollapse]') collapse!: NgbCollapse;
|
@ContentChild('[ngbCollapse]') collapse!: NgbCollapse;
|
||||||
//@ContentChild('commentDrawer') commentDrawer:
|
|
||||||
|
|
||||||
|
|
||||||
formatSettings: TypeaheadSettings<FilterItem<MangaFormat>> = new TypeaheadSettings();
|
formatSettings: TypeaheadSettings<FilterItem<MangaFormat>> = new TypeaheadSettings();
|
||||||
@ -87,7 +86,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(private libraryService: LibraryService, private metadataService: MetadataService, private seriesService: SeriesService,
|
constructor(private libraryService: LibraryService, private metadataService: MetadataService, private seriesService: SeriesService,
|
||||||
private utilityService: UtilityService, private collectionTagService: CollectionTagService) {
|
private utilityService: UtilityService, private collectionTagService: CollectionTagService, public toggleService: ToggleService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@ -98,6 +97,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||||||
if (this.filterOpen) {
|
if (this.filterOpen) {
|
||||||
this.filterOpen.pipe(takeUntil(this.onDestroy)).subscribe(openState => {
|
this.filterOpen.pipe(takeUntil(this.onDestroy)).subscribe(openState => {
|
||||||
this.filteringCollapsed = !openState;
|
this.filteringCollapsed = !openState;
|
||||||
|
this.toggleService.set(!this.filteringCollapsed);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,6 +161,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||||||
close() {
|
close() {
|
||||||
this.filterOpen.emit(false);
|
this.filterOpen.emit(false);
|
||||||
this.filteringCollapsed = true;
|
this.filteringCollapsed = true;
|
||||||
|
this.toggleService.set(!this.filteringCollapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
@ -213,6 +214,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||||||
this.resetTypeaheads.next(false); // Pass false to ensure we reset to the preset and not to an empty typeahead
|
this.resetTypeaheads.next(false); // Pass false to ensure we reset to the preset and not to an empty typeahead
|
||||||
if (this.filterSettings.openByDefault) {
|
if (this.filterSettings.openByDefault) {
|
||||||
this.filteringCollapsed = false;
|
this.filteringCollapsed = false;
|
||||||
|
this.toggleService.set(!this.filteringCollapsed);
|
||||||
}
|
}
|
||||||
this.apply();
|
this.apply();
|
||||||
});
|
});
|
||||||
@ -598,4 +600,14 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||||||
this.updateApplied++;
|
this.updateApplied++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleSelected() {
|
||||||
|
//this.filteringCollapsed = !this.filteringCollapsed;
|
||||||
|
this.toggleService.toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
setToggle(event: any) {
|
||||||
|
console.log('set toggle', event);
|
||||||
|
this.toggleService.set(!this.filteringCollapsed);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -4,5 +4,5 @@
|
|||||||
|
|
||||||
.custom-input {
|
.custom-input {
|
||||||
background-color: #fff !important;
|
background-color: #fff !important;
|
||||||
color: black;
|
color: black !important;
|
||||||
}
|
}
|
@ -12,7 +12,7 @@ a {
|
|||||||
|
|
||||||
.custom-input {
|
.custom-input {
|
||||||
background-color: #fff !important;
|
background-color: #fff !important;
|
||||||
color: black;
|
color: black !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invalid-feedback {
|
.invalid-feedback {
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
<div
|
<div class="offcanvas offcanvas-{{position}}" [class.show]="isOpen" tabindex="-1" id="offcanvas" aria-labelledby="offcanvasLabel"
|
||||||
class="drawer-container"
|
[ngStyle]="{'top': options.topOffset + 'px', 'visibility': isOpen ? 'visible' : 'hidden'}" role="dialog">
|
||||||
[ngStyle]="{'position': 'fixed','top': options.topOffset + 'px','height': 'calc(100% - ' + options.topOffset + 'px)'}"
|
<div class="offcanvas-header">
|
||||||
[class.is-open]="isOpen"
|
<div class="offcanvas-title" id="offcanvasLabel">
|
||||||
[class.position-right]="position === 'right'"
|
|
||||||
[class.position-left]="position === 'left'"
|
|
||||||
[class.position-bottom]="position === 'bottom'"
|
|
||||||
>
|
|
||||||
<div class="header">
|
|
||||||
<ng-content select="[header]"></ng-content>
|
<ng-content select="[header]"></ng-content>
|
||||||
</div>
|
</div>
|
||||||
<div class="body">
|
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close" (click)="close()"></button>
|
||||||
|
</div>
|
||||||
|
<div class="offcanvas-header row hide-if-empty">
|
||||||
|
<ng-content select="[subheader]"></ng-content>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="offcanvas-body">
|
||||||
<ng-content select="[body]"></ng-content>
|
<ng-content select="[body]"></ng-content>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- TODO: Replace this with official component when released. This is some hacky solution to mimic Bootstrap 5 Javascript code -->
|
||||||
|
<div class="offcanvas-backdrop fade show" *ngIf="isOpen" (click)="close()"></div>
|
||||||
|
@ -1,56 +1,8 @@
|
|||||||
:host {
|
.offcanvas {
|
||||||
--drawer-height: 100vh;
|
color: var(--drawer-text-color);
|
||||||
--drawer-width: 400px;
|
background-color: var(--drawer-bg-color);
|
||||||
--drawer-top-offset: 0px;
|
|
||||||
//--drawer-background-color: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer-container {
|
.hide-if-empty:empty {
|
||||||
position: absolute;
|
display: none !important;
|
||||||
top: var(--drawer-top-offset);
|
|
||||||
right: 0;
|
|
||||||
width: var(--drawer-width);
|
|
||||||
height: 100vh;
|
|
||||||
background: var(--drawer-background-color, #fff);
|
|
||||||
transition: all 300ms;
|
|
||||||
box-shadow: 0 6px 4px 2px rgb(0 0 0 / 70%);
|
|
||||||
padding: 10px 10px;
|
|
||||||
z-index: 1021;
|
|
||||||
overflow: auto;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
|
|
||||||
&.position-right {
|
|
||||||
right: calc(-1 * var(--drawer-width));
|
|
||||||
&.is-open {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.position-left {
|
|
||||||
left: calc(-1 * var(--drawer-width));
|
|
||||||
&.is-open {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.position-bottom {
|
|
||||||
//top: calc(-1 * var(--drawer-height));
|
|
||||||
top: 100vh;
|
|
||||||
height: 0px;
|
|
||||||
&.is-open {
|
|
||||||
//bottom: 0;
|
|
||||||
top: 100vh;
|
|
||||||
height: var(--drawer-height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.position-top {
|
|
||||||
//bottom: calc(-1 * var(--drawer-height));
|
|
||||||
top: 0px;
|
|
||||||
height: 0px;
|
|
||||||
&.is-open {
|
|
||||||
//bottom: 0;
|
|
||||||
top: 0px;
|
|
||||||
height: var(--drawer-height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -14,18 +14,20 @@ export class DrawerOptions {
|
|||||||
exportAs: "drawer"
|
exportAs: "drawer"
|
||||||
})
|
})
|
||||||
export class DrawerComponent {
|
export class DrawerComponent {
|
||||||
|
|
||||||
@Input() isOpen = false;
|
@Input() isOpen = false;
|
||||||
@Input() width: number = 400;
|
@Input() width: number = 400;
|
||||||
/**
|
/**
|
||||||
* Side of the screen the drawer should animate from
|
* Side of the screen the drawer should animate from
|
||||||
*/
|
*/
|
||||||
@Input() position: 'left' | 'right' | 'bottom' = 'left';
|
@Input() position: 'start' | 'end' | 'bottom' | 'top' = 'start';
|
||||||
@Input() options: Partial<DrawerOptions> = new DrawerOptions();
|
@Input() options: Partial<DrawerOptions> = new DrawerOptions();
|
||||||
@Output() drawerClosed = new EventEmitter();
|
@Output() drawerClosed = new EventEmitter();
|
||||||
|
@Output() isOpenChange: EventEmitter<boolean> = new EventEmitter();
|
||||||
|
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.drawerClosed.emit();
|
this.isOpen = false;
|
||||||
|
this.isOpenChange.emit(false);
|
||||||
|
this.drawerClosed.emit(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,8 @@
|
|||||||
<ng-content select="[main]"></ng-content>
|
<ng-content select="[main]"></ng-content>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button *ngIf="hasFilter" class="btn btn-{{filterActive ? 'primary' : 'secondary'}} btn-small" (click)="toggleFilter()" [attr.aria-expanded]="filterOpen" placement="left" ngbTooltip="{{filterOpen ? 'Open' : 'Close'}} Filtering and Sorting" attr.aria-label="{{filterOpen ? 'Open' : 'Close'}} Filtering and Sorting">
|
<!-- This can get out of sync with offcanvas component. We might need a service to handle communication and service resets to collapsed on route changes -->
|
||||||
|
<button *ngIf="hasFilter" class="btn btn-{{filterActive ? 'primary' : 'secondary'}} btn-small" (click)="toggleService.toggle()" [attr.aria-expanded]="filterOpen" placement="left" ngbTooltip="{{filterOpen ? 'Open' : 'Close'}} Filtering and Sorting" attr.aria-label="{{filterOpen ? 'Open' : 'Close'}} Filtering and Sorting">
|
||||||
<i class="fa fa-filter" aria-hidden="true"></i>
|
<i class="fa fa-filter" aria-hidden="true"></i>
|
||||||
<span class="visually-hidden">Sort / Filter</span>
|
<span class="visually-hidden">Sort / Filter</span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -2,6 +2,7 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angu
|
|||||||
import { Subject, takeUntil } from 'rxjs';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||||
import { NavService } from 'src/app/_services/nav.service';
|
import { NavService } from 'src/app/_services/nav.service';
|
||||||
|
import { ToggleService } from 'src/app/_services/toggle.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This should go on all pages which have the side nav present and is not Settings related.
|
* This should go on all pages which have the side nav present and is not Settings related.
|
||||||
@ -39,7 +40,7 @@ export class SideNavCompanionBarComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private onDestroy: Subject<void> = new Subject();
|
private onDestroy: Subject<void> = new Subject();
|
||||||
|
|
||||||
constructor(private navService: NavService, private utilityService: UtilityService) {
|
constructor(private navService: NavService, private utilityService: UtilityService, public toggleService: ToggleService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
|
<h4>Site Themes</h4>
|
||||||
<ng-container *ngFor="let theme of (themeService.themes$ | async)">
|
<ng-container *ngFor="let theme of (themeService.themes$ | async)">
|
||||||
<div class="card col-auto me-3 mb-3" style="width: 18rem;">
|
<div class="card col-auto me-3 mb-3" style="width: 18rem;">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@ -20,6 +21,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
|
|||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { distinctUntilChanged, Subject, take, takeUntil } from 'rxjs';
|
import { distinctUntilChanged, Subject, take, takeUntil } from 'rxjs';
|
||||||
import { ThemeService } from 'src/app/_services/theme.service';
|
import { ThemeService } from 'src/app/_services/theme.service';
|
||||||
|
import { BookTheme } from 'src/app/_models/preferences/book-theme';
|
||||||
import { SiteTheme, ThemeProvider } from 'src/app/_models/preferences/site-theme';
|
import { SiteTheme, ThemeProvider } from 'src/app/_models/preferences/site-theme';
|
||||||
import { User } from 'src/app/_models/user';
|
import { User } from 'src/app/_models/user';
|
||||||
import { AccountService } from 'src/app/_services/account.service';
|
import { AccountService } from 'src/app/_services/account.service';
|
||||||
|
@ -13,23 +13,18 @@
|
|||||||
These are global settings that are bound to your account.
|
These are global settings that are bound to your account.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ngb-accordion [closeOthers]="true" activeIds="reading-panel" #acc="ngbAccordion">
|
<form [formGroup]="settingsForm" *ngIf="user !== undefined">
|
||||||
<ngb-panel id="reading-panel" title="Reading">
|
|
||||||
|
<ngb-accordion [closeOthers]="true" [activeIds]="AccordionPanelID.ImageReader" #acc="ngbAccordion">
|
||||||
|
<ngb-panel [id]="AccordionPanelID.ImageReader" title="Image Reader">
|
||||||
<ng-template ngbPanelHeader>
|
<ng-template ngbPanelHeader>
|
||||||
<!-- <div class="d-flex align-items-center justify-content-between">
|
|
||||||
<button ngbPanelToggle class="btn container-fluid text-start ps-0 accordion-header">Reading</button>
|
|
||||||
<span class="pull-right"><i class="fa fa-angle-{{acc.isExpanded('reading-panel') ? 'down' : 'up'}}" aria-hidden="true"></i></span>
|
|
||||||
</div> -->
|
|
||||||
<h2 class="accordion-header">
|
<h2 class="accordion-header">
|
||||||
<button class="accordion-button" ngbPanelToggle type="button" [attr.aria-expanded]="acc.isExpanded('reading-panel')" aria-controls="collapseOne">
|
<button class="accordion-button" ngbPanelToggle type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.ImageReader)" aria-controls="collapseOne">
|
||||||
Reading
|
Image Reader
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template ngbPanelContent>
|
<ng-template ngbPanelContent>
|
||||||
<form [formGroup]="settingsForm" *ngIf="user !== undefined">
|
|
||||||
<h3 id="manga-header">Image Reader</h3>
|
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||||
<label for="settings-reading-direction" class="form-label">Reading Direction</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="readingDirectionTooltip" role="button" tabindex="0"></i>
|
<label for="settings-reading-direction" class="form-label">Reading Direction</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="readingDirectionTooltip" role="button" tabindex="0"></i>
|
||||||
@ -90,20 +85,15 @@
|
|||||||
|
|
||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3 mt-1">
|
||||||
<label id="auto-close-label" class="form-label"></label>
|
|
||||||
<div class="mb-3">
|
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
<input type="checkbox" id="auto-close" formControlName="autoCloseMenu" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
|
<input type="checkbox" id="auto-close" formControlName="autoCloseMenu" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
|
||||||
<label class="form-check-label" for="auto-close">Auto Close Menu</label>
|
<label class="form-check-label" for="auto-close">Auto Close Menu</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||||
<div class="mb-3">
|
<div class="mb-3 mt-1">
|
||||||
<label id="show-screen-hints-label" class="form-label"></label>
|
|
||||||
<div class="mb-3">
|
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
<input type="checkbox" id="show-screen-hints" formControlName="showScreenHints" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
|
<input type="checkbox" id="show-screen-hints" formControlName="showScreenHints" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
|
||||||
<label class="form-check-label" for="show-screen-hints">Show Screen Hints</label>
|
<label class="form-check-label" for="show-screen-hints">Show Screen Hints</label>
|
||||||
@ -111,21 +101,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<hr>
|
|
||||||
<h3>Book Reader</h3>
|
|
||||||
<div class="row g-0">
|
|
||||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
|
||||||
<label id="dark-mode-label" class="form-label"></label>
|
|
||||||
<div class="mb-3">
|
|
||||||
<div class="form-check form-switch">
|
|
||||||
<input type="checkbox" id="dark-mode" formControlName="bookReaderDarkMode" class="form-check-input" [value]="true" aria-labelledby="dark-mode-label">
|
|
||||||
<label class="form-check-label" for="dark-mode">Dark Mode</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
|
||||||
|
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">Reset</button>
|
||||||
|
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.touched && !settingsForm.dirty">Save</button>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</ngb-panel>
|
||||||
|
|
||||||
|
<ngb-panel id="AccordionPanelID.BookReader" title="Book Reader">
|
||||||
|
<ng-template ngbPanelHeader>
|
||||||
|
<h2 class="accordion-header">
|
||||||
|
<button class="accordion-button" ngbPanelToggle type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.BookReader)" aria-controls="collapseOne">
|
||||||
|
Book Reader
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template ngbPanelContent>
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||||
<label id="taptopaginate-label" class="form-label"></label>
|
<label id="taptopaginate-label" class="form-label"></label>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
@ -140,7 +134,7 @@
|
|||||||
|
|
||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||||
<label for="settings-book-reading-direction" class="form-label">Book Reading Direction</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookReadingDirectionTooltip" role="button" tabindex="0"></i>
|
<label for="settings-book-reading-direction" class="form-label">Reading Direction</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookReadingDirectionTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #bookReadingDirectionTooltip>Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</ng-template>
|
<ng-template #bookReadingDirectionTooltip>Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</ng-template>
|
||||||
<span class="visually-hidden" id="settings-reading-direction-help">Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</span>
|
<span class="visually-hidden" id="settings-reading-direction-help">Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</span>
|
||||||
<select class="form-select" aria-describedby="settings-reading-direction-help" formControlName="bookReaderReadingDirection">
|
<select class="form-select" aria-describedby="settings-reading-direction-help" formControlName="bookReaderReadingDirection">
|
||||||
@ -159,38 +153,67 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||||
|
<label for="settings-book-layout-mode" class="form-label">Layout Mode</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookLayoutModeTooltip" role="button" tabindex="0"></i>
|
||||||
|
<ng-template #bookLayoutModeTooltip>How content should be laid out. Default is as the book packs it. 1 or 2 Column fits to the height of the device and fits 1 or 2 columns of text per page</ng-template>
|
||||||
|
<span class="visually-hidden" id="settings-book-layout-mode-help"><ng-container [ngTemplateOutlet]="bookLayoutModeTooltip"></ng-container></span>
|
||||||
|
<select class="form-select" aria-describedby="settings-book-layout-mode-help" formControlName="bookReaderLayoutMode" id="settings-book-layout-mode">
|
||||||
|
<option *ngFor="let opt of bookLayoutModes" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||||
|
<label for="settings-color-theme-option" class="form-label">Color Theme</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookColorThemeTooltip" role="button" tabindex="0"></i>
|
||||||
|
<ng-template #bookColorThemeTooltip>What color theme to apply to the book reader content and menuing</ng-template>
|
||||||
|
<span class="visually-hidden" id="settings-color-theme-option-help"><ng-container [ngTemplateOutlet]="bookColorThemeTooltip"></ng-container></span>
|
||||||
|
<select class="form-select" aria-describedby="settings-color-theme-option-help" formControlName="bookReaderThemeName" id="settings-color-theme-option">
|
||||||
|
<option *ngFor="let opt of bookColorThemes" [value]="opt.name">{{opt.name | titlecase}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||||
<label id="font-size" class="form-label">Font Size</label>
|
<label for="fontsize" class="form-label range-label">Font Size</label>
|
||||||
<div class="custom-slider"><ngx-slider [options]="bookReaderFontSizeOptions" formControlName="bookReaderFontSize" aria-labelledby="font-size"></ngx-slider></div>
|
<input type="range" class="form-range" id="fontsize"
|
||||||
|
min="50" max="300" step="10" formControlName="bookReaderFontSize">
|
||||||
|
<span class="range-text">{{settingsForm.get('bookReaderFontSize')?.value + '%'}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||||
<label class="form-label">Line Height</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookLineHeightOptionTooltip" role="button" tabindex="0"></i>
|
<div class="range-label">
|
||||||
|
<label class="form-label" for="linespacing">Line Height</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookLineHeightOptionTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #bookLineHeightOptionTooltip>How much spacing between the lines of the book</ng-template>
|
<ng-template #bookLineHeightOptionTooltip>How much spacing between the lines of the book</ng-template>
|
||||||
<span class="visually-hidden" id="settings-booklineheight-option-help">How much spacing between the lines of the book</span>
|
<span class="visually-hidden" id="settings-booklineheight-option-help">How much spacing between the lines of the book</span>
|
||||||
<div class="custom-slider"><ngx-slider [options]="bookReaderLineSpacingOptions" formControlName="bookReaderLineSpacing" aria-describedby="settings-booklineheight-option-help"></ngx-slider></div>
|
</div>
|
||||||
|
<input type="range" class="form-range" id="linespacing" min="100" max="200" step="10"
|
||||||
|
formControlName="bookReaderLineSpacing" aria-describedby="settings-booklineheight-option-help">
|
||||||
|
<span class="range-text">{{settingsForm.get('bookReaderLineSpacing')?.value + '%'}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||||
|
<div class="range-label">
|
||||||
<label class="form-label">Margin</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookReaderMarginOptionTooltip" role="button" tabindex="0"></i>
|
<label class="form-label">Margin</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookReaderMarginOptionTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #bookReaderMarginOptionTooltip>How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.</ng-template>
|
<ng-template #bookReaderMarginOptionTooltip>How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.</ng-template>
|
||||||
<span class="visually-hidden" id="settings-bookmargin-option-help">How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.</span>
|
<span class="visually-hidden" id="settings-bookmargin-option-help">How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.</span>
|
||||||
<div class="custom-slider"><ngx-slider [options]="bookReaderMarginOptions" formControlName="bookReaderMargin" aria-describedby="bookmargin"></ngx-slider></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<input type="range" class="form-range" id="margin" min="0" max="30" step="5" formControlName="bookReaderMargin" aria-describedby="bookmargin">
|
||||||
|
<span class="range-text">{{settingsForm.get('bookReaderMargin')?.value + '%'}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
|
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
|
||||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">Reset</button>
|
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">Reset</button>
|
||||||
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.touched && !settingsForm.dirty">Save</button>
|
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.touched && !settingsForm.dirty">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ngb-panel>
|
</ngb-panel>
|
||||||
</ngb-accordion>
|
</ngb-accordion>
|
||||||
|
</form>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="tab.fragment === 'password'">
|
<ng-container *ngIf="tab.fragment === 'password'">
|
||||||
<ng-container *ngIf="(isAdmin || hasChangePasswordRole); else noPermission">
|
<ng-container *ngIf="(isAdmin || hasChangePasswordRole); else noPermission">
|
||||||
|
@ -5,3 +5,14 @@
|
|||||||
.container {
|
.container {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-range {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
.range-label {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.range-text {
|
||||||
|
vertical-align: top;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
@ -2,17 +2,22 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
|
|||||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
import { Options } from '@angular-slider/ngx-slider';
|
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { BookService } from 'src/app/book-reader/book.service';
|
import { BookService } from 'src/app/book-reader/book.service';
|
||||||
import { readingDirections, scalingOptions, pageSplitOptions, readingModes, Preferences, layoutModes } from 'src/app/_models/preferences/preferences';
|
import { readingDirections, scalingOptions, pageSplitOptions, readingModes, Preferences, bookLayoutModes, layoutModes } from 'src/app/_models/preferences/preferences';
|
||||||
import { User } from 'src/app/_models/user';
|
import { User } from 'src/app/_models/user';
|
||||||
import { AccountService } from 'src/app/_services/account.service';
|
import { AccountService } from 'src/app/_services/account.service';
|
||||||
import { NavService } from 'src/app/_services/nav.service';
|
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { SettingsService } from 'src/app/admin/settings.service';
|
import { SettingsService } from 'src/app/admin/settings.service';
|
||||||
|
import { bookColorThemes } from 'src/app/book-reader/reader-settings/reader-settings.component';
|
||||||
|
import { BookPageLayoutMode } from 'src/app/_models/book-page-layout-mode';
|
||||||
import { forkJoin } from 'rxjs';
|
import { forkJoin } from 'rxjs';
|
||||||
|
|
||||||
|
enum AccordionPanelID {
|
||||||
|
ImageReader = 'image-reader',
|
||||||
|
BookReader = 'book-reader'
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-user-preferences',
|
selector: 'app-user-preferences',
|
||||||
templateUrl: './user-preferences.component.html',
|
templateUrl: './user-preferences.component.html',
|
||||||
@ -25,6 +30,8 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
pageSplitOptions = pageSplitOptions;
|
pageSplitOptions = pageSplitOptions;
|
||||||
readingModes = readingModes;
|
readingModes = readingModes;
|
||||||
layoutModes = layoutModes;
|
layoutModes = layoutModes;
|
||||||
|
bookLayoutModes = bookLayoutModes;
|
||||||
|
bookColorThemes = bookColorThemes;
|
||||||
|
|
||||||
settingsForm: FormGroup = new FormGroup({});
|
settingsForm: FormGroup = new FormGroup({});
|
||||||
passwordChangeForm: FormGroup = new FormGroup({});
|
passwordChangeForm: FormGroup = new FormGroup({});
|
||||||
@ -36,22 +43,6 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
resetPasswordErrors: string[] = [];
|
resetPasswordErrors: string[] = [];
|
||||||
|
|
||||||
obserableHandles: Array<any> = [];
|
obserableHandles: Array<any> = [];
|
||||||
|
|
||||||
bookReaderLineSpacingOptions: Options = {
|
|
||||||
floor: 100,
|
|
||||||
ceil: 250,
|
|
||||||
step: 10,
|
|
||||||
};
|
|
||||||
bookReaderMarginOptions: Options = {
|
|
||||||
floor: 0,
|
|
||||||
ceil: 30,
|
|
||||||
step: 5,
|
|
||||||
};
|
|
||||||
bookReaderFontSizeOptions: Options = {
|
|
||||||
floor: 50,
|
|
||||||
ceil: 300,
|
|
||||||
step: 10,
|
|
||||||
};
|
|
||||||
fontFamilies: Array<string> = [];
|
fontFamilies: Array<string> = [];
|
||||||
|
|
||||||
tabs: Array<{title: string, fragment: string}> = [
|
tabs: Array<{title: string, fragment: string}> = [
|
||||||
@ -64,12 +55,14 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
opdsEnabled: boolean = false;
|
opdsEnabled: boolean = false;
|
||||||
makeUrl: (val: string) => string = (val: string) => {return this.transformKeyToOpdsUrl(val)};
|
makeUrl: (val: string) => string = (val: string) => {return this.transformKeyToOpdsUrl(val)};
|
||||||
|
|
||||||
backgroundColor: any; // TODO: Hook into user pref
|
get AccordionPanelID() {
|
||||||
|
return AccordionPanelID;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(private accountService: AccountService, private toastr: ToastrService, private bookService: BookService,
|
constructor(private accountService: AccountService, private toastr: ToastrService, private bookService: BookService,
|
||||||
private navService: NavService, private titleService: Title, private route: ActivatedRoute, private settingsService: SettingsService,
|
private titleService: Title, private route: ActivatedRoute, private settingsService: SettingsService,
|
||||||
private router: Router) {
|
private router: Router) {
|
||||||
this.fontFamilies = this.bookService.getFontFamilies();
|
this.fontFamilies = this.bookService.getFontFamilies().map(f => f.title);
|
||||||
|
|
||||||
this.route.fragment.subscribe(frag => {
|
this.route.fragment.subscribe(frag => {
|
||||||
const tab = this.tabs.filter(item => item.fragment === frag);
|
const tab = this.tabs.filter(item => item.fragment === frag);
|
||||||
@ -113,13 +106,14 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
this.settingsForm.addControl('showScreenHints', new FormControl(this.user.preferences.showScreenHints, []));
|
this.settingsForm.addControl('showScreenHints', new FormControl(this.user.preferences.showScreenHints, []));
|
||||||
this.settingsForm.addControl('readerMode', new FormControl(this.user.preferences.readerMode, []));
|
this.settingsForm.addControl('readerMode', new FormControl(this.user.preferences.readerMode, []));
|
||||||
this.settingsForm.addControl('layoutMode', new FormControl(this.user.preferences.layoutMode, []));
|
this.settingsForm.addControl('layoutMode', new FormControl(this.user.preferences.layoutMode, []));
|
||||||
this.settingsForm.addControl('bookReaderDarkMode', new FormControl(this.user.preferences.bookReaderDarkMode, []));
|
|
||||||
this.settingsForm.addControl('bookReaderFontFamily', new FormControl(this.user.preferences.bookReaderFontFamily, []));
|
this.settingsForm.addControl('bookReaderFontFamily', new FormControl(this.user.preferences.bookReaderFontFamily, []));
|
||||||
this.settingsForm.addControl('bookReaderFontSize', new FormControl(this.user.preferences.bookReaderFontSize, []));
|
this.settingsForm.addControl('bookReaderFontSize', new FormControl(this.user.preferences.bookReaderFontSize, []));
|
||||||
this.settingsForm.addControl('bookReaderLineSpacing', new FormControl(this.user.preferences.bookReaderLineSpacing, []));
|
this.settingsForm.addControl('bookReaderLineSpacing', new FormControl(this.user.preferences.bookReaderLineSpacing, []));
|
||||||
this.settingsForm.addControl('bookReaderMargin', new FormControl(this.user.preferences.bookReaderMargin, []));
|
this.settingsForm.addControl('bookReaderMargin', new FormControl(this.user.preferences.bookReaderMargin, []));
|
||||||
this.settingsForm.addControl('bookReaderReadingDirection', new FormControl(this.user.preferences.bookReaderReadingDirection, []));
|
this.settingsForm.addControl('bookReaderReadingDirection', new FormControl(this.user.preferences.bookReaderReadingDirection, []));
|
||||||
this.settingsForm.addControl('bookReaderTapToPaginate', new FormControl(!!this.user.preferences.bookReaderTapToPaginate, []));
|
this.settingsForm.addControl('bookReaderTapToPaginate', new FormControl(!!this.user.preferences.bookReaderTapToPaginate, []));
|
||||||
|
this.settingsForm.addControl('bookReaderLayoutMode', new FormControl(this.user.preferences.bookReaderLayoutMode || BookPageLayoutMode.Default, []));
|
||||||
|
this.settingsForm.addControl('bookReaderThemeName', new FormControl(this.user?.preferences.bookReaderThemeName || bookColorThemes[0].name, []));
|
||||||
|
|
||||||
this.settingsForm.addControl('theme', new FormControl(this.user.preferences.theme, []));
|
this.settingsForm.addControl('theme', new FormControl(this.user.preferences.theme, []));
|
||||||
});
|
});
|
||||||
@ -149,13 +143,14 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
this.settingsForm.get('readerMode')?.setValue(this.user.preferences.readerMode);
|
this.settingsForm.get('readerMode')?.setValue(this.user.preferences.readerMode);
|
||||||
this.settingsForm.get('layoutMode')?.setValue(this.user.preferences.layoutMode);
|
this.settingsForm.get('layoutMode')?.setValue(this.user.preferences.layoutMode);
|
||||||
this.settingsForm.get('pageSplitOption')?.setValue(this.user.preferences.pageSplitOption);
|
this.settingsForm.get('pageSplitOption')?.setValue(this.user.preferences.pageSplitOption);
|
||||||
this.settingsForm.get('bookReaderDarkMode')?.setValue(this.user.preferences.bookReaderDarkMode);
|
|
||||||
this.settingsForm.get('bookReaderFontFamily')?.setValue(this.user.preferences.bookReaderFontFamily);
|
this.settingsForm.get('bookReaderFontFamily')?.setValue(this.user.preferences.bookReaderFontFamily);
|
||||||
this.settingsForm.get('bookReaderFontSize')?.setValue(this.user.preferences.bookReaderFontSize);
|
this.settingsForm.get('bookReaderFontSize')?.setValue(this.user.preferences.bookReaderFontSize);
|
||||||
this.settingsForm.get('bookReaderLineSpacing')?.setValue(this.user.preferences.bookReaderLineSpacing);
|
this.settingsForm.get('bookReaderLineSpacing')?.setValue(this.user.preferences.bookReaderLineSpacing);
|
||||||
this.settingsForm.get('bookReaderMargin')?.setValue(this.user.preferences.bookReaderMargin);
|
this.settingsForm.get('bookReaderMargin')?.setValue(this.user.preferences.bookReaderMargin);
|
||||||
this.settingsForm.get('bookReaderTapToPaginate')?.setValue(this.user.preferences.bookReaderTapToPaginate);
|
this.settingsForm.get('bookReaderTapToPaginate')?.setValue(this.user.preferences.bookReaderTapToPaginate);
|
||||||
this.settingsForm.get('bookReaderReadingDirection')?.setValue(this.user.preferences.bookReaderReadingDirection);
|
this.settingsForm.get('bookReaderReadingDirection')?.setValue(this.user.preferences.bookReaderReadingDirection);
|
||||||
|
this.settingsForm.get('bookReaderLayoutMode')?.setValue(this.user.preferences.bookReaderLayoutMode);
|
||||||
|
this.settingsForm.get('bookReaderThemeName')?.setValue(this.user.preferences.bookReaderThemeName);
|
||||||
this.settingsForm.get('theme')?.setValue(this.user.preferences.theme);
|
this.settingsForm.get('theme')?.setValue(this.user.preferences.theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,16 +171,18 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
readerMode: parseInt(modelSettings.readerMode, 10),
|
readerMode: parseInt(modelSettings.readerMode, 10),
|
||||||
layoutMode: parseInt(modelSettings.layoutMode, 10),
|
layoutMode: parseInt(modelSettings.layoutMode, 10),
|
||||||
showScreenHints: modelSettings.showScreenHints,
|
showScreenHints: modelSettings.showScreenHints,
|
||||||
backgroundColor: this.user.preferences.backgroundColor,
|
backgroundColor: modelSettings.backgroundColor, // this.user.preferences.backgroundColor,
|
||||||
bookReaderDarkMode: modelSettings.bookReaderDarkMode,
|
|
||||||
bookReaderFontFamily: modelSettings.bookReaderFontFamily,
|
bookReaderFontFamily: modelSettings.bookReaderFontFamily,
|
||||||
bookReaderLineSpacing: modelSettings.bookReaderLineSpacing,
|
bookReaderLineSpacing: modelSettings.bookReaderLineSpacing,
|
||||||
bookReaderFontSize: modelSettings.bookReaderFontSize,
|
bookReaderFontSize: modelSettings.bookReaderFontSize,
|
||||||
bookReaderMargin: modelSettings.bookReaderMargin,
|
bookReaderMargin: modelSettings.bookReaderMargin,
|
||||||
bookReaderTapToPaginate: modelSettings.bookReaderTapToPaginate,
|
bookReaderTapToPaginate: modelSettings.bookReaderTapToPaginate,
|
||||||
bookReaderReadingDirection: parseInt(modelSettings.bookReaderReadingDirection, 10),
|
bookReaderReadingDirection: parseInt(modelSettings.bookReaderReadingDirection, 10),
|
||||||
|
bookReaderLayoutMode: parseInt(modelSettings.bookReaderLayoutMode, 10),
|
||||||
|
bookReaderThemeName: modelSettings.bookReaderThemeName,
|
||||||
theme: modelSettings.theme
|
theme: modelSettings.theme
|
||||||
};
|
};
|
||||||
|
|
||||||
this.obserableHandles.push(this.accountService.updatePreferences(data).subscribe((updatedPrefs) => {
|
this.obserableHandles.push(this.accountService.updatePreferences(data).subscribe((updatedPrefs) => {
|
||||||
this.toastr.success('Server settings updated');
|
this.toastr.success('Server settings updated');
|
||||||
if (this.user) {
|
if (this.user) {
|
||||||
|
@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { UserPreferencesComponent } from './user-preferences/user-preferences.component';
|
import { UserPreferencesComponent } from './user-preferences/user-preferences.component';
|
||||||
import { NgbAccordionModule, NgbNavModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbAccordionModule, NgbNavModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { ReactiveFormsModule } from '@angular/forms';
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
import { NgxSliderModule } from '@angular-slider/ngx-slider';
|
|
||||||
import { UserSettingsRoutingModule } from './user-settings-routing.module';
|
import { UserSettingsRoutingModule } from './user-settings-routing.module';
|
||||||
import { ApiKeyComponent } from './api-key/api-key.component';
|
import { ApiKeyComponent } from './api-key/api-key.component';
|
||||||
import { PipeModule } from '../pipe/pipe.module';
|
import { PipeModule } from '../pipe/pipe.module';
|
||||||
@ -29,7 +28,6 @@ import { SidenavModule } from '../sidenav/sidenav.module';
|
|||||||
NgbNavModule,
|
NgbNavModule,
|
||||||
NgbTooltipModule,
|
NgbTooltipModule,
|
||||||
|
|
||||||
NgxSliderModule,
|
|
||||||
ColorPickerModule, // User prefernces background color
|
ColorPickerModule, // User prefernces background color
|
||||||
|
|
||||||
PipeModule,
|
PipeModule,
|
||||||
|
BIN
UI/Web/src/assets/fonts/OpenDyslexic2/OpenDyslexic-Bold.otf
Normal file
BIN
UI/Web/src/assets/fonts/OpenDyslexic2/OpenDyslexic-Bold.otf
Normal file
Binary file not shown.
Binary file not shown.
BIN
UI/Web/src/assets/fonts/OpenDyslexic2/OpenDyslexic-Italic.otf
Normal file
BIN
UI/Web/src/assets/fonts/OpenDyslexic2/OpenDyslexic-Italic.otf
Normal file
Binary file not shown.
BIN
UI/Web/src/assets/fonts/OpenDyslexic2/OpenDyslexic-Regular.otf
Normal file
BIN
UI/Web/src/assets/fonts/OpenDyslexic2/OpenDyslexic-Regular.otf
Normal file
Binary file not shown.
@ -4,12 +4,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.accordion-item {
|
.accordion-item {
|
||||||
background-color: var(--accordion-body-bg-color); //$dark-card-color;
|
background-color: var(--accordion-body-bg-color);
|
||||||
color: var(--accordion-body-text-color);
|
color: var(--accordion-body-text-color);
|
||||||
border-color: var(--accordion-body-border-color);
|
border-color: var(--accordion-body-border-color);
|
||||||
|
|
||||||
div[role="tabpanel"] {
|
div[role="tabpanel"] {
|
||||||
background-color: var(--accordion-header-bg-color);
|
background-color: var(--accordion-header-bg-color);
|
||||||
|
|
||||||
|
.accordion-body {
|
||||||
|
background-color: var(--accordion-active-body-bg-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
input, .form-control {
|
input:not([type="range"]), .form-control {
|
||||||
background-color: var(--input-bg-color);
|
background-color: var(--input-bg-color);
|
||||||
color: var(--input-text-color);
|
color: var(--input-text-color);
|
||||||
border-color: var(--input-border-color);
|
border-color: var(--input-border-color);
|
||||||
@ -23,3 +23,11 @@ input, .form-control {
|
|||||||
color: var(--input-placeholder-color);
|
color: var(--input-placeholder-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-range::-webkit-slider-thumb:active {
|
||||||
|
background-color: var(--input-range-active-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-range::-webkit-slider-thumb {
|
||||||
|
background-color: var(--input-range-color);
|
||||||
|
}
|
||||||
|
@ -74,7 +74,7 @@
|
|||||||
--toast-info-bg-color: #2F96B4;
|
--toast-info-bg-color: #2F96B4;
|
||||||
--toast-warning-bg-color: #F89406;
|
--toast-warning-bg-color: #F89406;
|
||||||
|
|
||||||
/* Checkboxes */
|
/* Checkboxes/Switch */
|
||||||
--checkbox-checked-bg-color: var(--primary-color);
|
--checkbox-checked-bg-color: var(--primary-color);
|
||||||
--checkbox-border-color: var(--input-focused-border-color);
|
--checkbox-border-color: var(--input-focused-border-color);
|
||||||
--checkbox-focus-border-color: var(--primary-color);
|
--checkbox-focus-border-color: var(--primary-color);
|
||||||
@ -150,14 +150,15 @@
|
|||||||
|
|
||||||
/* Accordion */
|
/* Accordion */
|
||||||
--accordion-header-text-color: rgba(74, 198, 148, 0.9);
|
--accordion-header-text-color: rgba(74, 198, 148, 0.9);
|
||||||
--accordion-header-bg-color: rgba(52, 60, 70, 0.5); /* This is a good accent color */
|
--accordion-header-bg-color: rgba(52, 60, 70, 0.5);
|
||||||
--accordion-body-bg-color: rgba(22,27,34,0.5);
|
--accordion-body-bg-color: #292929;
|
||||||
--accordion-body-border-color: rgba(239, 239, 239, 0.125);
|
--accordion-body-border-color: rgba(239, 239, 239, 0.125);
|
||||||
--accordion-body-text-color: var(--body-text-color);
|
--accordion-body-text-color: var(--body-text-color);
|
||||||
--accordion-header-collapsed-text-color: rgba(74, 198, 148, 0.9);
|
--accordion-header-collapsed-text-color: rgba(74, 198, 148, 0.9);
|
||||||
--accordion-header-collapsed-bg-color: rgba(22,27,34,0.5);
|
--accordion-header-collapsed-bg-color: #292929;
|
||||||
--accordion-button-focus-border-color: unset;
|
--accordion-button-focus-border-color: unset;
|
||||||
--accordion-button-focus-box-shadow: unset;
|
--accordion-button-focus-box-shadow: unset;
|
||||||
|
--accordion-active-body-bg-color: #292929;
|
||||||
|
|
||||||
/* Breadcrumb */
|
/* Breadcrumb */
|
||||||
--breadcrumb-bg-color: #292d32;
|
--breadcrumb-bg-color: #292d32;
|
||||||
@ -190,6 +191,8 @@
|
|||||||
|
|
||||||
/* Slider */
|
/* Slider */
|
||||||
--slider-text-color: white;
|
--slider-text-color: white;
|
||||||
|
--input-range-color: var(--primary-color);
|
||||||
|
--input-range-active-color: var(--primary-color-darker-shade);
|
||||||
|
|
||||||
/* Manga Reader */
|
/* Manga Reader */
|
||||||
--manga-reader-overlay-filter: blur(10px);
|
--manga-reader-overlay-filter: blur(10px);
|
||||||
@ -210,7 +213,9 @@
|
|||||||
--carousel-hover-header-text-decoration: none;
|
--carousel-hover-header-text-decoration: none;
|
||||||
|
|
||||||
/** Drawer */
|
/** Drawer */
|
||||||
--drawer-background-color: black;
|
--drawer-background-color: black; // TODO: Remove this for bg
|
||||||
|
--drawer-bg-color: #292929;
|
||||||
|
--drawer-text-color: white;
|
||||||
|
|
||||||
/** Event Widget */
|
/** Event Widget */
|
||||||
--event-widget-bg-color: rgb(1, 4, 9);
|
--event-widget-bg-color: rgb(1, 4, 9);
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
--color-scheme: light;
|
--color-scheme: light;
|
||||||
--primary-color: black;
|
--primary-color: black;
|
||||||
--primary-color-dark-shade: #3B9E76;
|
--primary-color-dark-shade: #3B9E76;
|
||||||
|
--primary-color-darker-shade: #338A67;
|
||||||
|
--primary-color-darkest-shade: #25624A;
|
||||||
--error-color: #ff4136;
|
--error-color: #ff4136;
|
||||||
--bs-body-bg: #fff;
|
--bs-body-bg: #fff;
|
||||||
--body-text-color: black;
|
--body-text-color: black;
|
||||||
@ -20,6 +22,8 @@
|
|||||||
--input-bg-readonly-color: unset;
|
--input-bg-readonly-color: unset;
|
||||||
--input-placeholder-color: #aeaeae;
|
--input-placeholder-color: #aeaeae;
|
||||||
--input-border-color: #ccc;
|
--input-border-color: #ccc;
|
||||||
|
--input-range-color: var(--primary-color);
|
||||||
|
--input-range-active-color: var(--primary-color-darker-shade);
|
||||||
|
|
||||||
/* Buttons */
|
/* Buttons */
|
||||||
--btn-primary-text-color: black;
|
--btn-primary-text-color: black;
|
||||||
@ -145,7 +149,9 @@
|
|||||||
--carousel-hover-header-text-decoration: none;
|
--carousel-hover-header-text-decoration: none;
|
||||||
|
|
||||||
/** Drawer */
|
/** Drawer */
|
||||||
--drawer-background-color: white;
|
--drawer-background-color: white; // TODO: Remove this for bg
|
||||||
|
--drawer-bg-color: white;
|
||||||
|
--drawer-text-color: black;
|
||||||
|
|
||||||
/* Pagination */
|
/* Pagination */
|
||||||
--pagination-active-link-border-color: var(--primary-color);
|
--pagination-active-link-border-color: var(--primary-color);
|
||||||
|
@ -3,10 +3,12 @@
|
|||||||
--color-scheme: light;
|
--color-scheme: light;
|
||||||
--primary-color: #4ac694;
|
--primary-color: #4ac694;
|
||||||
--primary-color-dark-shade: #3B9E76;
|
--primary-color-dark-shade: #3B9E76;
|
||||||
|
--primary-color-darker-shade: #338A67;
|
||||||
|
--primary-color-darkest-shade: #25624A;
|
||||||
--error-color: #ff4136;
|
--error-color: #ff4136;
|
||||||
--bs-body-bg: #fff;
|
--bs-body-bg: #fff;
|
||||||
--body-text-color: #333;
|
--body-text-color: #333;
|
||||||
--btn-icon-filter: invert(1) grayscale(100%) brightness(200%);
|
--btn-icon-filter: none;
|
||||||
|
|
||||||
/* Navbar */
|
/* Navbar */
|
||||||
--navbar-bg-color: black;
|
--navbar-bg-color: black;
|
||||||
@ -20,6 +22,8 @@
|
|||||||
--input-bg-readonly-color: unset;
|
--input-bg-readonly-color: unset;
|
||||||
--input-placeholder-color: #aeaeae;
|
--input-placeholder-color: #aeaeae;
|
||||||
--input-border-color: #ccc;
|
--input-border-color: #ccc;
|
||||||
|
--input-range-color: var(--primary-color);
|
||||||
|
--input-range-active-color: var(--primary-color-darker-shade);
|
||||||
|
|
||||||
/* Buttons */
|
/* Buttons */
|
||||||
--btn-primary-text-color: black;
|
--btn-primary-text-color: black;
|
||||||
@ -91,8 +95,9 @@
|
|||||||
--ratingstar-star-filled: var(--primary-color);
|
--ratingstar-star-filled: var(--primary-color);
|
||||||
|
|
||||||
/* Global */
|
/* Global */
|
||||||
--accent-bg-color: rgba(206, 206, 206, 0.5);
|
--accent-bg-color: rgba(206, 206, 206, 0.5); // Drawer had: var(--bs-body-bg)
|
||||||
--accent-text-color: black;
|
--accent-text-color: grey;
|
||||||
|
--accent-text-size: 0.8rem;
|
||||||
--hr-color: rgba(239, 239, 239, 0.125);
|
--hr-color: rgba(239, 239, 239, 0.125);
|
||||||
--grid-breakpoints-xs: $grid-breakpoint-xs;
|
--grid-breakpoints-xs: $grid-breakpoint-xs;
|
||||||
--grid-breakpoints-sm: $grid-breakpoint-sm;
|
--grid-breakpoints-sm: $grid-breakpoint-sm;
|
||||||
@ -146,7 +151,9 @@
|
|||||||
--carousel-hover-header-text-decoration: underline;
|
--carousel-hover-header-text-decoration: underline;
|
||||||
|
|
||||||
/** Drawer */
|
/** Drawer */
|
||||||
--drawer-background-color: white;
|
--drawer-background-color: white; // TODO: Use bg
|
||||||
|
--drawer-bg-color: white;
|
||||||
|
--drawer-text-color: black;
|
||||||
|
|
||||||
/* Pagination */
|
/* Pagination */
|
||||||
--pagination-active-link-border-color: var(--primary-color);
|
--pagination-active-link-border-color: var(--primary-color);
|
||||||
@ -172,4 +179,17 @@
|
|||||||
--popover-arrow-color: lightgrey;
|
--popover-arrow-color: lightgrey;
|
||||||
--popover-bg-color: lightgrey;
|
--popover-bg-color: lightgrey;
|
||||||
--popover-border-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;
|
||||||
|
|
||||||
}
|
}
|
@ -20,6 +20,7 @@ hr {
|
|||||||
background-color: var(--accent-bg-color) !important;
|
background-color: var(--accent-bg-color) !important;
|
||||||
color: var(--accent-text-color) !important;
|
color: var(--accent-text-color) !important;
|
||||||
box-shadow: inset 0px 0px 8px 1px var(--accent-bg-color) !important;
|
box-shadow: inset 0px 0px 8px 1px var(--accent-bg-color) !important;
|
||||||
|
font-size: var(--accent-text-size) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-muted {
|
.text-muted {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user